pax_global_header00006660000000000000000000000064131344775370014530gustar00rootroot0000000000000052 comment=03ec423f83b5dc3c38962bb481557fcfce248448 forked-daapd-25.0/000077500000000000000000000000001313447753700137775ustar00rootroot00000000000000forked-daapd-25.0/.gitignore000066400000000000000000000006521313447753700157720ustar00rootroot00000000000000*~ *.swp Makefile.in Makefile *.o *.lo *.a *.la .dirstamp .deps/ .libs/ # autofoo stuff autom4te.cache aclocal.m4 compile config.guess config.h config.h.in config.log config.status config.sub configure depcomp install-sh libtool ltmain.sh missing stamp-h1 autotools-stamp build-stamp forked-daapd.spec forked-daapd.conf forked-daapd.service # ignore debian packaging for convenience debian/ /.settings /.cproject /.project forked-daapd-25.0/.travis.yml000066400000000000000000000022101313447753700161030ustar00rootroot00000000000000language: c sudo: required dist: trusty env: matrix: - CFG="--disable-verification" - CFG="--enable-lastfm --disable-verification" - CFG="--enable-spotify --disable-verification" - CFG="--enable-chromecast --disable-verification" - CFG="--with-pulseaudio --disable-verification" script: - autoreconf -fi - ./configure $CFG - make - make clean - scan-build --status-bugs -disable-checker deadcode.DeadStores make before_install: - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - - sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/jessie.list - sudo apt-get -qq update - sudo apt-get install -y build-essential clang git autotools-dev autoconf libtool gettext gawk gperf antlr3 libantlr3c-dev libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libmxml-dev libgcrypt11-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libcurl4-openssl-dev libjson-c-dev libspotify-dev libgnutls-dev libprotobuf-c0-dev libpulse-dev # Disable email notification notifications: email: false forked-daapd-25.0/AUTHORS000066400000000000000000000007461313447753700150560ustar00rootroot00000000000000forked-daapd is a fork/rewrite of mt-daapd (Firefly Media Server) written by Julien BLACHE . mt-daapd (Firefly Media Server) was originally written by Ron Pedde and a handful of contributors. Contributors to forked-daapd include: - Ace Jones - Dustin King - Kai Elwert - Espen Jürgensen - Christian Meffert forked-daapd-25.0/COPYING000066400000000000000000000431031313447753700150330ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. forked-daapd-25.0/ChangeLog000066400000000000000000000206441313447753700155570ustar00rootroot00000000000000ChangeLog for forked-daapd -------------------------- version 25.0: - improved playback resilience - substitute packet skipping (producing audio "clicks") with start/stop - support for MacOSX with macports and Bonjour mDNS - Airplay device verification for Apple TV 4 w/tvOS 10.2 (uses libsodium) - support for Spotify web api (saved tracks/albums + Discover Weekly) - automatic playback of pipes + support for Shairport metadata pipes - added pipe audio output (fifo) - persistent queues (queue remains across server restart) - support for browser based clients - mpd sendmessage: set individual speaker volume, remote pairing - mpd add http://path-to-radiostream (i.e. add non-library items) - new options, e.g. exclude speakers from list - fix for shuffle mode - fix broken PNG artwork rescaling - use friendly Chromecast name in speaker list - support for libav/ffmpeg dual installs - refactoring - added the input and library interface - performance improvements - and other fixing up... version 24.2: - Pulseaudio support (can be used for Bluetooth speakers) - new pipe/"fifo" audio output - fix misc Chromecast audio bugs - fix for Apple tvOS10 disconnecting after some minutes - enable CORS headers - LastFM scrobbling during regular DAAP streaming - smart playlist support for file modification time - misc MPD improvements, e.g. new outputsvolume command - ignore ipv6 link-local addresses (announced by e.g. ATV4) - better inter-thread command handling - improved cache performance and use of gzip - fix possible segfault on http timeouts - fix possible segfault when adding items during playback version 24.1: - support for Monkey's audio - fix build problems on some platforms (e.g. OpenWrt) version 24.0: - support for Chromecast audio - support more idv3 tags (eg. date released) - support more DAAP tags (eg. datereleased, hasbeenplayed) - fix problem with playlists not shown in correct order in Remote - autoselect devices based on priorities + keep devices selected - fix multiple FreeBSD filescanner bugs (like too many open files) - ALSA audio refurbished (prevent underrun/overrun, better AirPlay sync) - improved mpd command support (eg. lsinfo, move, queue autoplay start) - better mpd performance - timing changes to keep input and output in sync with player timer - prevent cache bloat (unscaled artwork or artwork for deleted tracks) - more intelligent Spotify artwork retrieval - artwork handling refactored - add generic output interface - add systemd service file - support for OSS4 dropped - support for old ffmpeg dropped - misc minor bugfixing version 23.4: - fix freeze problem on network stream disconnects - support for mp3 streaming - better ipv6 handling - option to hide singles from album/artist lists - misc MPD improvements, incl. new commands and zeroconf support - queue handling and transcoding refactored - libavresample/libswresample dependency changed to libavfilter - improved pairinghelper.sh script version 23.3: - fix issue where volume gets set to -1 on startup of raop devices - plug various minor memleaks - audiobook improvements, eg resuming playback from saved position - live icy metadata - libevent 1 support removed - LastFM performance improvement - drop legacy ffmpeg stuff - drop legacy flac, musepack and wma scanner version 23.2: - fix db lock, m3u and Windows Phone bugs - improvements for Spotify and mpd - fixing bugs as always - sorting of genres and composers version 23.1: - support for more mpd commands version 23.0: - add support for the mpd protocol - add support for smart playlists - playlist and internet stream overhaul version 22.2: - fix for iTunes 12.1 - fix misc bugs version 22.1: - artwork cache - some Spotify fixing up version 22.0: - queue handling improvements - added DAAP cache, good for low-power devices like the RPi - support for LastFM scrobbling - support for .pls playlists - added compability with ffmpeg libswresample - added options to modify the operation of the SQLite database - stop init-rescan/full-rescan from running twice - fix misc local audio problems - fix some FreeBSD sound timing problems - fix segfault on invalid utf8 while sorting - fix misc bugs version 21.0: - filescanner performance enhancements (db transactions) - support for queue editing - support for showing history - compability with libevent 2 - compability with libav 10 - support for Spotify - fix for Seafile libraries - support for permission changes in a library (IN_ATTRIB) - some DAAP changes to align with iTunes - properly show media kind shares in iTunes - support for named pipes/fifos - natural sorting and better handling of capitalization - introduce .init-rescan and .full-rescan triggers - fix for XBMC (libshairplay) crash - fix missing songartistid (fixes some remotes) - fix bug in m3u scanner - ICY metadata fixes version 20.0: - includes patch against timeouts - configurable artwork file names - support for Remote 3 and 4 - preference for album artist if it is available - bug fix for certain Airport Express models - bug fix for AirFoil Speakers - added support for internet radio with m3u - fixed fatal error when stopping player - m3u and iTunes playlist scanner improvements - improved compability with Hyperfine Remote for Android - speaker selection and m3u bug fixes - Shoutcast (icy) metadata support for internet radio - fix for Apple TV update 6.0 - fix artwork memory leak - basic support for podcasts, compilations and audiobooks - user can tell file scanner to ignore certain file types - fix for metadata on non-Apple airplay devices - configurable library names - sorting changes, ignore The/A/An - fix for TunesRemote SE - force library rescan with .force-rescan - fix for the Retune remote on Android - fix for AirBubble speakers (shairport) - support for artwork in iTunes - fixed ffmpeg/libav logging - logging and documentation adjustments - autoselect AirPlay device if none selected - support for embedded artwork - config option to disable startup scan - config option for max volume for AirPlay devices - support for embedded platforms without hi-res timers - fix for WMA files - fixes for video playback - other fixes: non apple players, ffmpeg/libav updates... version 0.19: - more libav 0.7 updates. - database speedups. - fix for iTunes 30-minute timeout. - fixes, big and small. version 0.18: - add config knob for ALSA mixer channel name. - do not elevate privileges for reopening the log file; log file will now be owned by the user forked-daapd runs as. - fixes, big and small. version 0.17: - support for libav 0.7 - fixes, big and small. version 0.16: - fix issue with non-UTF-8 metadata while scanning. - use proper file size in HTTP streaming code. - fix DAAP songlist bug with sort tags. - small code fixes. version 0.15: - add support for sending metadata to AppleTV during AirTunes streaming. - support DOS-encoded Remote pairing files. - rework album_artist_sort handling. - enable RAOP to queue RTSP requests. - speedup DAAP & RSP filters processing. - speedup DAAP songlist generation. - artwork can handle and send out both PNG and JPEG. - fixes, big and small. version 0.14: - sort headers/tags handling improvements. - better handling of tags for TV shows. - better handling of DRM-afflicted files. - configurable IPv6 support. - fix scanning of URL files. - fixes, big and small. version 0.13: - add Remote v2 support; Remote v1 is not supported anymore. - add per-speaker volume support. - implement RAOP retransmission. - implement per-device quirks in RAOP. - improve compatibility with 802.11g AirPort Express. - improve mDNS address resolution, making IPv4 usable again. - fix Murmur Hash bug on 32bit platforms. - add support for JPEG artwork and alternative filenames. - disable session expiration that was causing issues. - FFmpeg 0.6 support. - fixes, big and small. version 0.12: - add AirTunes v2 streaming. - add Remote support. - add gzipped replies. - add IPv6 support. - check for UTF-8 correctness of metadata. - fixes, big and small. version 0.11: - support iTunes 9. - add iTunes XML playlist scanner. - add support for TV shows. - add FreeBSD and GNU/kFreeBSD support. - add support for DAAP groups. - add support for artwork. - rework metdata extraction, better support for ID3 tags. - database code rework. - preliminary support for Remote (pairing, browsing). - fixes, big and small. version 0.10: - initial release. forked-daapd-25.0/INSTALL000066400000000000000000000325541313447753700150410ustar00rootroot00000000000000Installation instructions for forked-daapd ------------------------------------------ This document contains instructions for installing forked-daapd from the git tree. The source for this version of forked-daapd can be found here: The original (now unmaintained) source can be found here: Quick version for Raspbian (Raspberry Pi) ----------------------------------------- See the instructions here: http://www.raspberrypi.org/phpBB3/viewtopic.php?t=49928 Quick version for Debian/Ubuntu users ------------------------------------- If you are the lucky kind, this should get you all the required tools and libraries: sudo apt-get install \ build-essential git autotools-dev autoconf libtool gettext gawk gperf \ antlr3 libantlr3c-dev libconfuse-dev libunistring-dev libsqlite3-dev \ libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev \ libasound2-dev libmxml-dev libgcrypt11-dev libavahi-client-dev zlib1g-dev \ libevent-dev libplist-dev libsodium-dev Optional packages: Feature | Configure argument | Packages --------------------|------------------------|--------------------------------------------- Chromecast | --enable-chromecast | libjson-c-dev libgnutls-dev libprotobuf-c-dev LastFM | --enable-lastfm | libcurl4-gnutls-dev OR libcurl4-openssl-dev iTunes XML | --disable-itunes | libplist-dev Device verification | --disable-verification | libplist-dev libsodium-dev Pulseaudio | --with-pulseaudio | libpulse-dev Note that while forked-daapd will work with versions of libevent between 2.0.0 and 2.1.3, it is recommended to use 2.1.4+. Otherwise you may not have support for Shoutcast metadata and simultaneous streaming to multiple clients. Then run the following (adding configure arguments for optional features): git clone https://github.com/ejurgensen/forked-daapd.git cd forked-daapd autoreconf -i ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var make sudo make install Finally, read the section 'Long version - after installation' in the bottom of this document, which describes configuration, setting up init scripts and adding a system user. Also see the README for usage information. Quick version for Fedora ------------------------ If you haven't already enabled the free RPM fusion packages do that, since you will need ffmpeg. You can google how to do that. Then run: sudo yum install \ git automake autoconf gettext-devel gperf gawk libtool \ sqlite-devel libconfuse-devel libunistring-devel mxml-devel libevent-devel \ avahi-devel libgcrypt-devel zlib-devel alsa-lib-devel ffmpeg-devel \ libplist-devel libsodium-devel Clone the forked-daapd repo: git clone https://github.com/ejurgensen/forked-daapd.git cd forked-daapd Now you need to install ANTLR3, but you probably can't use the version that comes with the package manager (but do try that first). Instead you can install it by running this script: scripts/antlr35_install.sh Then run the following: autoreconf -i ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var make sudo make install Finally, read the section 'Long version - after installation' in the bottom of this document, which describes configuration, setting up init scripts and adding a system user. Also see the README for usage information. Quick version for FreeBSD ------------------------- The build process for FreeBSD is rather complicated, but the good news is that there is a script in the 'scripts' folder that will at least attempt to do all the work for you. And should the script not work for you, you can still look through it and use it as an installation guide. "Quick" version for macOS (using macports) ------------------------------------------ Caution: macports requires many downloads and lots of time to install (and sometimes build) ports... you'll want a decent network connection and some patience! Install macports (which requires Xcode): https://www.macports.org/install.php Install Apple's Java (this enables java command on OSX 10.7+): https://support.apple.com/kb/DL1572?locale=en_US Afterwards, you can optionally install Oracle's newer version, and then choose it using the Java pref in the System Preferences: http://www.oracle.com/technetwork/java/javase/downloads/index.html sudo port install \ autoconf automake libtool pkgconfig git gperf libgcrypt \ libunistring libconfuse ffmpeg libevent Download, configure, build and install the Mini-XML library: http://www.msweet.org/projects.php/Mini-XML Download, configure, build and install the libinotify library: https://github.com/libinotify-kqueue/libinotify-kqueue Add the following to .bashrc: # add /usr/local to pkg-config path export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/local/lib/pkgconfig # libunistring doesn't support pkg-config, set overrides export LIBUNISTRING_CFLAGS=-I/opt/local/include export LIBUNISTRING_LIBS="-L/opt/local/lib -lunistring" Optional features require the following additional ports: Feature | Configure argument | Ports --------------------|------------------------|-------------------------------------------- Chromecast | --enable-chromecast | json-c gnutls protobuf-c LastFM | --enable-lastfm | curl iTunes XML | --disable-itunes | libplist Device verification | --disable-verification | libplist libsodium Pulseaudio | --with-pulseaudio | pulseaudio Clone the forked-daapd repo: git clone https://github.com/ejurgensen/forked-daapd.git cd forked-daapd Install antlr3 and library using the included script: scripts/antlr35_install.sh -p /usr/local Finally, configure, build and install, adding configure arguments for optional features: autoreconf -i ./configure make sudo make install Note: if for some reason you've installed the avahi port, you need to add '--without-avahi' to configure above. Edit /usr/local/etc/forked-daapd.conf and change the 'uid' to a nice system daemon (eg: unknown), and run the following: sudo mkdir -p /usr/local/var/run sudo mkdir -p /usr/local/var/log # or change logfile in conf sudo chown unknown /usr/local/var/cache/forked-daapd # or change conf Run forked-daapd: sudo /usr/local/sbin/forked-daapd Verify it's running (you need to Ctrl-C to stop dns-sd): dns-sd -B _daap._tcp Long version - requirements --------------------------- Required tools: - ANTLR v3 is required to build forked-daapd, along with its C runtime (libantlr3c). Use a version between 3.1.3 and 3.5 of ANTLR v3 and the matching C runtime version. Get it from - Java runtime: ANTLR is written in Java and as such a JRE is required to run the tool. The JRE is enough, you don't need a full JDK. - autotools: autoconf 2.63+, automake 1.10+, libtool 2.2. Run autoreconf -i at the top of the source tree to generate the build system. - gettext: libunistring requires iconv and gettext provides the autotools macro definitions for iconv. - gperf Libraries: - libantlr3c (ANTLR3 C runtime, use the same version as antlr3) from - Avahi client libraries (avahi-client), 0.6.24 minimum from - sqlite3 3.5.0+ with unlock notify API enabled (read below) from - libav 9+ or ffmpeg 0.11+ from or - libconfuse from - libevent 2.0+ (best with 2.1.4+) from - MiniXML (aka mxml or libmxml) from - gcrypt 1.2.0+ from - zlib from - libunistring 0.9.3+ from - libasound (optional - ALSA local audio) often already installed as part of your distro - libpulse (optional - Pulseaudio local audio) from - libplist 0.16+ (optional - iTunes XML support and Apple TV device verification) from - libsodium (optional - Apple TV device verification) from - libspotify (optional - Spotify support) from - libcurl (optional - LastFM support) from - libjson-c (optional - Chromecast support) from - libgnutls (optional - Chromecast support) from - libprotobuf-c (optional - Chromecast support) from If using binary packages, remember that you need the development packages to build forked-daapd (usually named -dev or -devel). sqlite3 needs to be built with support for the unlock notify API; this isn't always the case in binary packages, so you may need to rebuild sqlite3 to enable the unlock notify API (you can check for the presence of the sqlite3_unlock_notify symbol in the sqlite3 library). Refer to the sqlite3 documentation, look for SQLITE_ENABLE_UNLOCK_NOTIFY. libav (or ffmpeg) is a central piece of forked-daapd and most other FLOSS multimedia applications. The version of libav you use will potentially have a great influence on your experience with forked-daapd. Long version - building and installing -------------------------------------- Start by generating the build system by running autoreconf -i. This will generate the configure script and Makefile.in. The configure script will look for a wrapper called antlr3 in the PATH to invoke ANTLR3. If your installation of ANTLR3 does not come with such a wrapper, create one as follows: #!/bin/sh CLASSPATH=... exec /path/to/java -cp $CLASSPATH org.antlr.Tool "@" Adjust the CLASSPATH as needed so that Java will find all the jars needed by ANTLR3. The parsers will be generated during the build, no manual intervention is needed. To display the configure options run ./configure --help Support for Spotify is optional. Use --enable-spotify to enable this feature. If you enable this feature libspotify/api.h is required at compile time. Forked-daapd uses runtime dynamic linking to the libspotify library, so even though you compiled with --enable-spotify, the executable will still be able to run on systems without libspotify (the Spotify features will then be disabled). Support for LastFM scrobbling is optional. Use --enable-lastfm to enable this feature. Support for iTunes Music Library XML format is optional. Use --disable-itunes to disable this feature. Support for the MPD protocol is optional. Use --disable-mpd to disable this feature. Support for Apple TV device verification is optional. Use --disable-verification to disable this feature. Support for Chromecast devices is optional. Use --enable-chromecast to enable this feature. Building with Pulseaudio is optional. Use --with-pulseaudio to enable. Recommended build settings: ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var After configure run the usual make, and if that went well, 'sudo make install' Long version - after installation --------------------------------- After installation, edit the configuration file, /etc/forked-daapd.conf. Note that 'sudo make install' will not install any system files to start the service after boot, and it will not setup a system user. forked-daapd will drop privileges to any user you'll specify in the configuration file if it's started as root. This user must have read permission on your library (you can create a group for this and make the user a member of the group, for instance) and read/write permissions on the database location ($localstatedir/cache/forked-daapd by default). If your system uses systemd then you might be able to use the service file included, see forked-daapd.service. Otherwise you might need an init script to start forked-daapd at boot. A simple init script will do, forked-daapd daemonizes all by itself and creates a pidfile under /var/run. Different distributions have different standards for init scripts and some do not use init scripts anymore; check the documentation for your distribution. For dependency-based boot systems, here are the forked-daapd dependencies: - local filesystems - network filesystems, if needed in your setup (library on NFS, ...) - networking - NTP - Avahi daemon The LSB header below sums it up: ### BEGIN INIT INFO # Provides: forked-daapd # Required-Start: $local_fs $remote_fs $network $time # Required-Stop: $local_fs $remote_fs $network $time # Should-Start: avahi # Should-Stop: avahi # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: DAAP/DACP (iTunes) server, support for AirPlay and Spotify # Description: forked-daapd is an iTunes-compatible media server for # sharing your media library over the local network with DAAP # clients like iTunes. Like iTunes, it can be controlled by # Apple Remote (and compatibles) and stream music directly to # AirPlay devices. It also supports streaming to RSP clients # (Roku devices) and streaming from Spotify. ### END INIT INFO forked-daapd-25.0/Makefile.am000066400000000000000000000022231313447753700160320ustar00rootroot00000000000000ACLOCAL_AMFLAGS = -I m4 RPM_SPEC_FILE = forked-daapd.spec CONF_FILE = forked-daapd.conf SYSTEMD_SERVICE_FILE = forked-daapd.service sysconf_DATA = $(CONF_FILE) BUILT_SOURCES = $(CONF_FILE) $(SYSTEMD_SERVICE_FILE) SUBDIRS = sqlext src dist_man_MANS = forked-daapd.8 nobase_dist_doc_DATA = \ UPGRADING \ README.md \ README_PULSE.md \ README_SMARTPL.md \ scripts/pairinghelper.sh EXTRA_DIST = \ $(CONF_FILE).in \ $(SYSTEMD_SERVICE_FILE).in \ $(RPM_SPEC_FILE) install-data-hook: $(MKDIR_P) "$(DESTDIR)$(localstatedir)/cache/$(PACKAGE)/libspotify" $(MKDIR_P) "$(DESTDIR)$(localstatedir)/log" CLEANFILES = $(BUILT_SOURCES) do_subst = $(SED) -e 's|@sbindir[@]|$(sbindir)|g' \ -e 's|@localstatedir[@]|$(localstatedir)|g' \ -e 's|@PACKAGE[@]|$(PACKAGE)|g' \ -e 's|@DAAPD_USER[@]|$(DAAPD_USER)|g' # these files use $prefix, which is determined at build (not configure) time $(CONF_FILE) $(SYSTEMD_SERVICE_FILE): Makefile $(AM_V_at)rm -f $@ $@-t $(AM_V_GEN)$(do_subst) "$(srcdir)/$@.in" > $@-t $(AM_V_at)mv $@-t $@ $(CONF_FILE): $(srcdir)/$(CONF_FILE).in $(SYSTEMD_SERVICE_FILE): $(srcdir)/$(SYSTEMD_SERVICE_FILE).in forked-daapd-25.0/NEWS000066400000000000000000000001401313447753700144710ustar00rootroot00000000000000Sorry, you'll have to find your news elsewhere. This is just a boilerplate to satisfy automake. forked-daapd-25.0/README.md000066400000000000000000000604671313447753700152730ustar00rootroot00000000000000# forked-daapd forked-daapd is a Linux/FreeBSD DAAP (iTunes), MPD (Music Player Daemon) and RSP (Roku) media server. It has support for AirPlay devices/speakers, Apple Remote (and compatibles), MPD clients, Chromecast, network streaming, internet radio, Spotify and LastFM. It does not support streaming video by AirPlay nor Chromecast. DAAP stands for Digital Audio Access Protocol, and is the protocol used by iTunes and friends to share/stream media libraries over the network. forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server). ## Contents of this README - [Getting started](#getting-started) - [Supported clients](#supported-clients) - [Using Remote](#using-remote) - [AirPlay devices/speakers](#airplay-devicesspeakers) - [Chromecast](#chromecast) - [Local audio through ALSA](#local-audio-through-alsa) - [Local audio, Bluetooth and more through Pulseaudio](#local-audio-bluetooth-and-more-through-pulseaudio) - [MP3 network streaming (streaming to iOS)](#mp3-network-streaming-streaming-to-ios) - [Supported formats](#supported-formats) - [Playlists and internet radio](#playlists-and-internet-radio) - [Artwork](#artwork) - [Library](#library) - [Command line and web interface](#command-line-and-web-interface) - [Spotify](#spotify) - [LastFM](#lastfm) - [MPD clients](#mpd-clients) - [References](#references) ## Getting started After installation (see [INSTALL](https://github.com/ejurgensen/forked-daapd/blob/master/INSTALL)) do the following: 1. Edit the configuration file (usually `/etc/forked-daapd.conf`) to suit your needs 2. Start or restart the server (usually `/etc/init.d/forked-daapd restart`) 3. Wait for the library scan to complete. You can follow the progress with `tail -f /var/log/forked-daapd.log` 4. If you are going to use a remote app, pair it following the procedure [described below](#using-remote) ## Supported clients forked-daapd supports these kinds of clients: - DAAP clients, like iTunes or Rhythmbox - Remote clients, like Apple Remote or compatibles for Android/Windows Phone - AirPlay devices, like AirPort Express, Shairport and various AirPlay speakers - Chromecast devices - MPD clients, like mpc (see [mpd-clients](#mpd-clients)) - MP3 network stream clients, like VLC and almost any other music player - RSP clients, like Roku Soundbridge Like iTunes, you can control forked-daapd with Remote and stream your music to AirPlay devices. A single forked-daapd instance can handle several clients concurrently, regardless of the protocol. Here is a list of working and non-working DAAP and Remote clients. The list is probably obsolete when you read it :-) | Client | Developer | Type | Platform | Working (vers.) | | ------------------------ | ---------- | ------ | ------------- | --------------- | | iTunes | Apple | DAAP | Win, OSX | Yes (12.4) | | Rhythmbox | Gnome | DAAP | Linux | Yes | | Diapente | diapente | DAAP | Android | Yes | | WinAmp DAAPClient | WardFamily | DAAP | WinAmp | Yes | | Amarok w/DAAP plugin | KDE | DAAP | Linux/Win | Yes (2.8.0) | | Banshee | | DAAP | Linux/Win/OSX | No (2.6.2) | | jtunes4 | | DAAP | Java | No | | Firefly Client | | (DAAP) | Java | No | | Remote | Apple | Remote | iOS | Yes (4.3) | | Retune | SquallyDoc | Remote | Android | Yes (3.5.23) | | TunesRemote+ | Melloware | Remote | Android | Yes (2.5.3) | | Remote for iTunes | Hyperfine | Remote | Android | Yes | | Remote for Windows Phone | Komodex | Remote | Windows Phone | Yes (2.2.1.0) | | TunesRemote SE | | Remote | Java | Yes (r108) | ## Using Remote If you plan to use Remote for iPod/iPhone/iPad with forked-daapd, read the following sections. The pairing process described is similar for other controllers/remotes (e.g. Retune), but some do not require pairing. ### Pairing with Remote on iPod/iPhone NOTE: These are the instructions for the current version of forked-daapd - for versions 24.2 and earlier see [here](https://github.com/ejurgensen/forked-daapd/blob/24.2/README.md#using-remote) If you just started forked-daapd for the first time, then wait til the library scan completes before pairing with Remote (see [library](#library)). Otherwise you risk timeouts. Then do the following. The Quick Way: 1. Download and run the helper script from [here](https://raw.githubusercontent.com/ejurgensen/forked-daapd/master/scripts/pairinghelper.sh) Another option is to use mpc (MPD command line client): 1. `mpc sendmessage pairing 5387` (where 5387 is the 4-digit pairing code displayed by Remote) Or, if that doesn't work: 1. Start forked-daapd 2. Start Remote, go to Settings, Add Library 3. Look in the log file for a message saying: ``` "Discovered remote 'Foobar' (id 71624..." ``` This tells you the name of your device (Foobar in this example). If you cannot find this message, it means that forked-daapd did not receive a mDNS announcement from your Remote. You have a network issue and mDNS doesn't work properly on your network. 4. Prepare a text file with a filename ending with .remote; the filename doesn't matter, only the .remote ending does. This first line in the file must contain the 4-digit pairing code displayed by Remote. 5. Move this file somewhere in your library At this point, you should be done with the pairing process and Remote should display the name of your forked-daapd library. You should delete the .remote file once the pairing process is done. If Remote doesn't display the name of your forked-daapd library at this point, the pairing process failed. Here are some common reasons: #### Your library is a network mount forked-daapd does not get notified about new files on network mounts, so the .remote file was not detected. You will see no log file messages about the file. Solution: Set two library paths in the config, and add the .remote file to the local path. See [Libraries on network mounts](#libraries-on-network-mounts). #### You did not enter the correct pairing code You will see an error in the log about pairing failure with a HTTP response code that is *not* 0. Solution: Try again. You can also try the pairinghelper script located in the scripts-folder of the source or the mpc method described above. #### No response from Remote, possibly a network issue If you see an error in the log with either: - a HTTP response code that is 0 - "Empty pairing request callback" it means that forked-daapd could not establish a connection to Remote. This might be a network issue, your router may not be allowing multicast between the Remote device and the host forked-daapd is running on. Solution 1: Sometimes it resolves the issue if you force Remote to quit, restart it and do the pairing proces again. Another trick is to establish some other connection (eg SSH) from the iPod/iPhone/iPad to the host. Solution 2: Check your router settings if you can whitelist multicast addresses under IGMP settings. For Apple Bonjour, setting a multicast address of 224.0.0.251 and a netmask of 255.255.255.255 should work. Otherwise try using avahi-browse for troubleshooting: - in a terminal, run `avahi-browse -r -k _touch-remote._tcp` - start Remote, goto Settings, Add Library - after a couple seconds at most, you should get something similar to this: ``` + ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3 _touch-remote._tcp local = ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3 _touch-remote._tcp local hostname = [Foobar.local] address = [192.168.1.1] port = [49160] txt = ["DvTy=iPod touch" "RemN=Remote" "txtvers=1" "RemV=10000" "Pair=FAEA410630AEC05E" "DvNm=Foobar"] ``` Hit Ctrl-C to terminate avahi-browse. To check for network issues you can try to connect to address and port with telnet. ### Selecting output devices Remote gets a list of output devices from the server; this list includes any and all devices on the network we know of that advertise AirPlay: AirPort Express, Apple TV, ... It also includes the local audio output, that is, the sound card on the server (even if there is no soundcard). If no output is selected when playback starts, forked-daapd will try to autoselect a device. forked-daapd remembers your selection and the individual volume for each output device; selected devices will be automatically re-selected, except if they return online during playback. ## AirPlay devices/speakers forked-daapd will discover the AirPlay devices available on your network. For devices that are password-protected, the device's AirPlay name and password must be given in the configuration file. See the sample configuration file for the syntax. If your Apple TV requires device verification (always required by Apple TV4 with tvOS 10.2) then you must select the device for playback, whereafter a PIN will be displayed by the Apple TV. The do either of the following: Alternative 1: Create a file ending with .verification in your music library, input the PIN, and save the file. Forked-daapd will now pair with the device, and if you select the device again, playback should start. Alternative 2: Run "mpc sendmessage verification [PIN]" (requires the mpc tool), and then select the device again. Playback should start. For troubleshooting, see [using Remote](#using-remote). ## Chromecast forked-daapd will discover Chromecast devices available on your network. There is no configuration to be done. This feature relies on streaming the audio in mp3 to your Chromecast device, which means that mp3 encoding must be supported by your ffmpeg/libav. See [MP3 network streaming](#mp3-network-streaming-streaming-to-ios). It is also required that forked-daapd is built with "--enable-chromecast". ## Local audio through ALSA In the config file, you can select ALSA for local audio. This is the default. When using ALSA, the server will try to syncronize playback with AirPlay. You can adjust the syncronization in the config file. ## Local audio, Bluetooth and more through Pulseaudio In the config file, you can select Pulseaudio for local audio. In addition to local audio, Pulseaudio also supports an array of other targets, e.g. Bluetooth or DLNA. However, Pulseaudio does require some setup, so here is a separate page with some help on that: [README_PULSE.md](https://github.com/ejurgensen/forked-daapd/blob/master/README_PULSE.md) Note that if you select Pulseaudio the "card" setting in the config file has no effect. Instead all soundcards detected by Pulseaudio will be listed as speakers by forked-daapd. You can adjust the latency of Pulseaudio playback in the config file. ## MP3 network streaming (streaming to iOS) You can listen to audio being played by forked-daapd by opening this network stream address in pretty much any music player: http://[your hostname/ip address]:3689/stream.mp3 This is currently the only way of listening to your audio on iOS devices, since Apple does not allow AirPlay receiver apps, and because Apple Home Sharing cannot be supported by forked-daapd. So what you can do instead is install a music player app like VLC, connect to the stream and control playback with Remote. You can also use MPoD in "On the go"-mode, where control and playback is integrated in one app (see (#mpd-clients)). Note that MP3 encoding must be supported by ffmpeg/libav for this to work. If it is not available you will see a message in the log file. In Debian/Ubuntu you get MP3 encoding support by installing the package "libavcodec-extra". ## Supported formats forked-daapd should support pretty much all media formats. It relies on libav (or ffmpeg) to extract metadata and decode the files on the fly when the client doesn't support the format. Formats are attributed a code, so any new format will need to be explicitely added. Currently supported: - MPEG4: mp4a, mp4v - AAC: alac - MP3 (and friends): mpeg - FLAC: flac - OGG VORBIS: ogg - Musepack: mpc - WMA: wma (WMA Pro), wmal (WMA Lossless), wmav (WMA video) - AIFF: aif - WAV: wav - Monkey's audio: ape ## Playlists and internet radio forked-daapd supports M3U and PLS playlists. Just drop your playlist somewhere in your library with an .m3u or .pls extension and it will pick it up. If the playlist contains an http URL it will be added as an internet radio station, and the URL will be probed for Shoutcast (ICY) metadata. If the radio station provides artwork, forked-daapd will download it during playback and send it to any remotes or AirPlay devices requesting it. Instead of downloading M3U's from your radio stations, you can also make an empty M3U file and in it insert links to the M3U's of your radio stations. Support for iTunes Music Library XML format is available as a compile-time option. By default, metadata from our parsers is preferred over what's in the iTunes DB; use itunes_overrides = true if you prefer iTunes' metadata. forked-daapd has support for smart playlists. How to create a smart playlist is documented in [README_SMARTPL.md](https://github.com/ejurgensen/forked-daapd/blob/master/README_SMARTPL.md). ## Artwork forked-daapd has support for PNG and JPEG artwork which is either: - embedded in the media files - placed as separate image files in the library - made available online by the radio station For media in your library, forked-daapd will try to locate album and artist artwork (group artwork) by the following procedure: - if a file {artwork,cover,Folder}.{png,jpg} is found in one of the directories containing files that are part of the group, it is used as the artwork. The first file found is used, ordering is not guaranteed; - failing that, if [directory name].{png,jpg} is found in one of the directories containing files that are part of the group, it is used as the artwork. The first file found is used, ordering is not guaranteed; - failing that, individual files are examined and the first file found with an embedded artwork is used. Here again, ordering is not guaranteed. {artwork,cover,Folder} are the default, you can add other base names in the configuration file. Here you can also enable/disable support for individual file artwork (instead of using the same artwork for all tracks in an entire album). For playlists in your library, say /foo/bar.m3u, then for any http streams in the list, forked-daapd will look for /foo/bar.{jpg,png}. You can use symlinks for the artwork files. forked-daapd caches artwork in a separate cache file. The default path is `/var/cache/forked-daapd/cache.db` and can be configured in the configuration file. The cache.db file can be deleted without losing the library and pairing informations. ## Library The library is scanned in bulk mode at startup, but the server will be available even while this scan is in progress. You can follow the progress of the scan in the log file. When the scan is complete you will see the log message: "Bulk library scan completed in X sec". The very first scan will take longer than subsequent startup scans, since every file gets analyzed. At the following startups the server looks for changed files and only analyzis those. Changes to the library are reflected in real time after the initial scan. The directories are monitored for changes and rescanned on the fly. Note that if you have your library on a network mount then real time updating may not work. Read below about what to do in that case. If you change any of the directory settings in the library section of the configuration file a rescan is required before the new setting will take effect. Currently, this will not be done automatically, so you need to trigger the rescan as described below. Symlinks are supported and dereferenced, but it is best to use them for directories only. ### Pipes (for e.g. multiroom with Shairport-sync) Some programs, like for instance Shairport-sync, can be configured to output audio to a named pipe. If this pipe is placed in the library, forked-daapd will automatically detect that it is there, and when there is audio being written to it, playback of the audio will be autostarted (and stopped). Using this feature, forked-daapd can act as an AirPlay multiroom "router": You can have an AirPlay source (e.g. your iPhone) send audio Shairport-sync, which forwards it to forked-daapd through the pipe, which then plays it on whatever speakers you have selected (through Remote). The format of the audio being written to the pipe must be PCM16. You can also start playback of pipes manually. You will find them in remotes listed under "Unknown artist" and "Unknown album". The track title will be the name of the pipe. Shairport-sync can write metadata to a pipe, and forked-daapd can read this. This requires that the metadata pipe has the same filename as the audio pipe plus a ".metadata" suffix. Say Shairport-sync is configured to write audio to "/foo/bar/pipe", then the metadata pipe should be "/foo/bar/pipe.metadata". ### Libraries on network mounts Most network filesharing protocols do not offer notifications when the library is changed. So that means forked-daapd cannot update its database in real time. Instead you can schedule a cron job to update the database. The first step in doing this is to add two entries to the 'directories' configuration item in forked-daapd.conf: ``` directories = { "/some/local/dir", "/your/network/mount/library" } ``` Now you can make a cron job that runs this command: ``` touch /some/local/dir/trigger.init-rescan ``` When forked-daapd detects a file with filename ending .init-rescan it will perform a bulk scan similar to the startup scan. ### Troubleshooting library issues If you place a file with the filename ending .full-rescan in your library, you can trigger a full rescan of your library. This will clear all music and playlists from forked-daapd's database and initiate a fresh bulk scan. Pairing and speaker information will be kept. Only use this for troubleshooting, it is not necessary during normal operation. ## Command line and web interface forked-daapd is meant to be used with the clients mentioned above, so it does not have a command line interface nor does it have a web interface. You can, however, to some extent control forked-daapd with [MPD clients](#mpd-clients) or from the command line by issuing DAAP/DACP commands with a program like curl. Here is an example of how to do that. Say you have a playlist with a radio station, and you want to make a script that starts playback of that station: 1. Run 'sqlite3 [your forked-daapd db]'. Use 'select id,title from files' to get the id of the radio station, and use 'select id,title from playlists' to get the id of the playlist. 2. Convert the two ids to hex. 3. Put the following lines in the script with the relevant ids inserted (also observe that you must use a session-id < 100, and that you must login and logout): ``` curl "http://localhost:3689/login?pairing-guid=0x1&request-session-id=50" curl "http://localhost:3689/ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x[PLAYLIST-ID]'&container-item-spec='dmap.containeritemid:0x[FILE ID]'&session-id=50" curl "http://localhost:3689/logout?session-id=50" ``` ## Spotify forked-daapd has support for playback of the tracks in your Spotify library. It must have been compiled with the `--enable-spotify` option (see [INSTALL](https://github.com/ejurgensen/forked-daapd/blob/master/INSTALL)). You must also have libspotify installed, otherwise Spotify integration will not be available. Unfortunately the library is no longer available from Spotify, and at the time of writing they have not provided an alternative. You can, however, still get libspotify here: - Debian package (libspotify-dev), see https://apt.mopidy.com You must also have a Spotify premium account. If you normally log into Spotify with your Facebook account you must first go to Spotify's web site where you can get the Spotify username and password that matches your account. With forked-daapd you cannot login into Spotify with your Facebook username/password. The procedure for logging in to Spotify is a two-step procedure due to the current state of libspotify: 1. Put a file in your forked-daapd library containing two lines, the first being your Spotify user name, and the second your password. The filename must have the ending ".spotify" 2. Delete the file again - forked-daapd will have read it. 3. forked-daapd will log in and add all music in your Spotify playlists to its database. Wait until completed (follow progress in the log file). 4. In a browser, go to http://forked-daapd.local:3689/oauth and click the link to authorize forked-daapd with Spotify. Spotify will automatically notify forked-daapd about playlist updates, so you should not need to restart forked-daapd to syncronize with Spotify. However, Spotify only notifies about playlist updates, not new saved tracks/albums, so you need to repeat step 4 above to load those. Forked-daapd will not store your password, but will still be able to log you in automatically afterwards, because libspotify saves a login token. You can configure the location of your Spotify user data in the configuration file. To permanently logout and remove credentials, delete the contents of `/var/cache/forked-daapd/libspotify` (while forked-daapd is stopped). Limitations: You will not be able to do any playlist management through forked-daapd - use a Spotify client for that. You also can only listen to your music by letting forked-daapd do the playback - so that means you can't stream from forked-daapd to iTunes. ## LastFM If forked-daapd was built with LastFM scrobbling enabled (see the [INSTALL](https://github.com/ejurgensen/forked-daapd/blob/master/INSTALL) file) you can have it scrobble the music you listen to. To set up scrobbling you must create a text file with the file name ending ".lastfm". The file must have two lines: The first is your LastFM user name, and the second is your password. Move the file to your forked-daapd library. Forked-daapd will then log in and get a permanent session key. You should delete the .lastfm file immediately after completing the first login. For safety, forked-daapd will not store your LastFM username/password, only the session key. The session key does not expire. To stop scrobbling from forked-daapd, add an empty ".lastfm" file to your library. ## MPD clients If forked-daapd was built with support for the [Music Player Deamon](http://musicpd.org/) protocol (see the [INSTALL](https://github.com/ejurgensen/forked-daapd/blob/master/INSTALL) file) you can - to some extent - use clients for MPD to control forked-daapd. By default forked-daapd listens on port 6600 for MPD clients. You can change this in the configuration file. Currently only a subset of the commands offered by MPD (see [MPD protocol documentation](http://www.musicpd.org/doc/protocol/)) are supported by forked-daapd. Due to some differences between forked-daapd and MPD not all commands will act the same way they would running MPD: - crossfade, mixrampdb, mixrampdelay and replaygain will have no effect - single, repeat: unlike MPD forked-daapd does not support setting single and repeat separately on/off, instead repeat off, repeat all and repeat single are supported. Thus setting single on will result in repeat single, repeat on results in repeat all. Following table shows what is working for a selection of MPD clients: | Client | Type | Status | | --------------------------------------------- | ------ | --------------- | | [mpc](http://www.musicpd.org/clients/mpc/) | CLI | Working commands: mpc, add, crop, current, del (ranges are not yet supported), play, next, prev (behaves like cdprev), pause, toggle, cdprev, seek, clear, outputs, enable, disable, playlist, ls, load, volume, repeat, random, single, search, find, list, update (initiates an init-rescan, the path argument is not supported) | | [ympd](http://www.ympd.org/) | Web | Everything except "add stream" should work | ## References The source for this version of forked-daapd can be found here: https://github.com/ejurgensen/forked-daapd.git The original (now unmaintained) source can be found here: http://git.debian.org/?p=users/jblache/forked-daapd.git forked-daapd-25.0/README_PULSE.md000066400000000000000000000053141313447753700162310ustar00rootroot00000000000000# forked-daapd and Pulseaudio Credit: [Rob Pope](http://robpope.co.uk/blog/post/setting-up-forked-daapd-with-bluetooth) This guide was written based on headless Debian Jessie platforms. Most of the instructions will require that you are root. ## Step 1: Setting up Pulseaudio in system mode with Bluetooth support If you see a "Connection refused" error when starting forked-daapd, then you will probably need to setup Pulseaudio to run in system mode [1]. This means that the Pulseaudio daemon will be started during boot and be available to all users. How to start Pulseaudio depends on your distribution, but in many cases you will need to add a pulseaudio.service file to /etc/systemd/system with the following content: ``` # systemd service file for Pulseaudio running in system mode [Unit] Description=Pulseaudio sound server Before=sound.target [Service] ExecStart=/usr/bin/pulseaudio --system --disallow-exit [Install] WantedBy=multi-user.target ``` If you want Bluetooth support, you must also configure Pulseaudio to load the Bluetooth module. First install it (Debian: `apt install pulseaudio-module-bluetooth`) and then add the following to /etc/pulse/system.pa: ``` ### Enable Bluetooth .ifexists module-bluetooth-discover.so load-module module-bluetooth-discover .endif ``` Now you need to make sure that Pulseaudio can communicate with the Bluetooth daemon through D-Bus. On Raspbian this is already enabled, and you can skip this step. Otherwise do one of the following: 1. Add the pulse user to the bluetooth group: `adduser pulse bluetooth` 2. Edit /etc/dbus-1/system.d/bluetooth.conf and change the policy for \ to "allow" Phew, almost done with Pulseaudio! Now you should: 1. enable system mode on boot with `systemctl enable pulseaudio` 2. reboot (or at least restart dbus and pulseaudio) 3. check that the Bluetooth module is loaded with `pactl list modules short` ## Step 2: Setting up forked-daapd Add the user forked-daapd is running as (typically "daapd") to the "pulse-access" group: ``` adduser daapd pulse-access ``` Now (re)start forked-daapd. ## Step 3: Adding a Bluetooth device To connect with the device, run `bluetoothctl` and then: ``` power on agent on scan on **Note MAC address of BT Speaker** pair [MAC address] **Type Pin if prompted** trust [MAC address] connect [MAC address] ``` Now the speaker should appear in forked-daapd. You can also verify that Pulseaudio has detected the speaker with `pactl list sinks short`. --- [1] Note that Pulseaudio will warn against system mode. However, in this use case it is actually the solution recommended by the [Pulseaudio folks themselves](https://lists.freedesktop.org/archives/pulseaudio-discuss/2016-August/026823.html). forked-daapd-25.0/README_SMARTPL.md000066400000000000000000000071451313447753700164670ustar00rootroot00000000000000# forked-daapd smart playlists To add a smart playlist to forked-daapd, create a new text file with a filename ending with .smartpl; the filename doesn't matter, only the .smartpl ending does. The file must be placed somewhere in your library folder. ## Syntax The contents of a smart playlist must follow the syntax: ``` "Playlist Name" { expression } ``` There is exactly one smart playlist allowed for a .smartpl file. An expression consists of: ``` field-name operator operand ``` Where valid field-names (with there types) are: * artist (string) * album_artist (string) * album (string) * title (string) * genre (string) * composer (string) * path (string) * type (string) * grouping (string) * data_kind (enumeration) * media_kind (enumeration) * play_count (integer) * rating (integer) * year (integer) * compilation (integer) * time_added (date) * time_modified (date) * time_played (date) Valid operators include: * is, includes (string) * >, <, <=, >=, = (int) * after, before (date) * is (enumeration) The "is" operator must exactly match the field value, while the "includes" operator matches a substring. Both matches are case-insensitive. Valid operands include: * "string value" (string) * integer (int) Valid operands for the enumeration "data_kind" are: * file * url * spotify * pipe Valid operands for the enumeration "media_kind" are: * music * movie * podcast * audiobook * tvshow Multiple expressions can be anded or ored together, using the keywords OR and AND. The unary not operator is also supported using the keyword NOT. Examples: ``` "techno" { genre includes "techno" and artist includes "zombie" } ``` This would match songs by "Rob Zombie" or "White Zombie", as well as those with a genre of "Techno-Industrial" or "Trance/Techno", for example. ``` "techno 2015" { genre includes "techno" and artist includes "zombie" and not genre includes "industrial" } ``` This would exclude e. g. songs with the genre "Techno-Industrial". ``` "Local music" { data_kind is file and media_kind is music } ``` This would match all songs added as files to the library that are not placed under the folders for podcasts, audiobooks. ``` "Unplayed podcasts and audiobooks" { play_count = 0 and (media_kind is podcast or media_kind is audiobook) } ``` This would match any podcast and audiobook file that was never played with forked-daapd. ## Date operand syntax One example of a valid date is a date in yyyy-mm-dd format: ``` "Files added after January 1, 2004" { time_added after 2004-01-01 } ``` There are also some special date keywords: * "today", "yesterday", "last week", "last month", "last year" A valid date can also be made by applying an interval to a date. Intervals can be defined as "days", "weeks", "months", "years". As an example, a valid date might be: ```3 weeks before today``` or ```3 weeks ago``` Examples: ``` "Recently Added" { time_added after 2 weeks ago } ``` This matches all songs added in the last 2 weeks. ``` "Recently played audiobooks" { time_played after last week and media_kind is audiobook } ``` This matches all audiobooks played in the last week. ## Differences to mt-daapd smart playlists The syntax is really close to the mt-daapd smart playlist syntax (see http://sourceforge.net/p/mt-daapd/code/HEAD/tree/tags/release-0.2.4.2/contrib/mt-daapd.playlist). Even this documentation is based on the file linked above. Some differences are: * only one smart playlist per file * the not operator must be placed before an expression and not before the operator * "||", "&&", "!" are not supported (use "or", "and", "not") * comments are not supported forked-daapd-25.0/UPGRADING000066400000000000000000000016241313447753700152450ustar00rootroot00000000000000Upgrading forked-daapd ---------------------- From time to time, newer versions of forked-daapd may need to perform a database upgrade. This upgrade is handled by forked-daapd upon startup if required. Before upgrading forked-daapd, it is always a good idea to backup your database, just in case. The database upgrade procedure is built into forked-daapd; there is no external upgrade script to run. Depending on the changes done to the database structure, the upgrade process will take more or less time and may need some space in /tmp for temporary data. The upgrade can also require some more space in the directory containing the database file. Before running the new forked-daapd version, make sure you have done your backups and checked your disk space. Some upgrades can also trigger a full rescan to rebuild parts of the database, so startup will be a bit slower and more resource-intensive than usual. forked-daapd-25.0/_config.yml000066400000000000000000000000311313447753700161200ustar00rootroot00000000000000theme: jekyll-theme-slateforked-daapd-25.0/build-aux/000077500000000000000000000000001313447753700156715ustar00rootroot00000000000000forked-daapd-25.0/build-aux/config.rpath000066400000000000000000000000001313447753700201640ustar00rootroot00000000000000forked-daapd-25.0/configure.ac000066400000000000000000000352021313447753700162670ustar00rootroot00000000000000dnl Process this file with autoconf to produce a configure script. AC_PREREQ([2.60]) AC_INIT([forked-daapd], [25.0]) AC_CONFIG_SRCDIR([config.h.in]) AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_AUX_DIR([build-aux]) AC_CONFIG_HEADERS([config.h]) AM_INIT_AUTOMAKE([foreign subdir-objects 1.11]) AM_SILENT_RULES([no]) dnl Requires autoconf 2.60 AC_USE_SYSTEM_EXTENSIONS dnl Checks for programs. AC_PROG_CC AC_PROG_CC_C_O AC_PROG_SED AC_PROG_MKDIR_P LT_INIT([disable-static]) AC_PATH_PROG([GPERF], [[gperf]]) AS_IF([[test -z "$GPERF"]], [AS_IF([[test -f "$srcdir/src/dmap_fields_hash.h"]], [AM_MISSING_PROG([GPERF], [[gperf]]) AC_MSG_NOTICE([[ GNU gperf not found, but it's output appears to be present. If you modify any gperf or ANTLR grammar files (.g), you will need to install it.]])], [AC_MSG_ERROR([[GNU gperf required, please install it.]])]) ]) GPERF_TEST="$(echo foo,bar | ${GPERF} -L ANSI-C)" AC_COMPILE_IFELSE( [AC_LANG_PROGRAM([ #include const char * in_word_set(const char *, size_t); $GPERF_TEST] )], [GPERF_LEN_TYPE=size_t], [AC_COMPILE_IFELSE( [AC_LANG_PROGRAM([ #include const char * in_word_set(const char *, unsigned); $GPERF_TEST] )], [GPERF_LEN_TYPE=unsigned], [AC_MSG_ERROR([** unable to determine gperf len type])] )] ) AC_DEFINE_UNQUOTED([GPERF_LEN_TYPE], [$GPERF_LEN_TYPE], [gperf len type]) AC_PATH_PROG([ANTLR], [[antlr3]]) AS_IF([[test -z "$ANTLR"]], [AS_IF([[test -f "$srcdir/src/SMARTPLLexer.h"]], [AM_MISSING_PROG([ANTLR], [[antlr3]]) AC_MSG_NOTICE([[ antlr3 not found, but it's output appears to be present. If you modify any ANTLR grammar files (.g), you will need to install it.]])], [AC_MSG_ERROR([[antlr3 wrapper required, please install it.]])]) ]) dnl Enable all warnings by default. AM_CPPFLAGS="-Wall" AC_SUBST([AM_CPPFLAGS]) dnl Checks for header files. AC_CHECK_HEADERS_ONCE([regex.h pthread_np.h]) AC_CHECK_HEADERS([sys/wait.h sys/param.h dirent.h getopt.h stdint.h], [], [AC_MSG_ERROR([[Missing header required to build forked-daapd]])]) AC_CHECK_HEADERS([time.h], [], [AC_MSG_ERROR([[Missing header required to build forked-daapd]])]) AC_CHECK_FUNCS_ONCE([posix_fadvise euidaccess pipe2]) AC_CHECK_FUNCS([strptime strtok_r], [], [AC_MSG_ERROR([[Missing function required to build forked-daapd]])]) dnl check for clock_gettime or replace it AC_SEARCH_LIBS([clock_gettime], [rt], [AC_DEFINE([HAVE_CLOCK_GETTIME], 1, [Define to 1 if have clock_gettime function])], [AC_CHECK_HEADER([mach/mach_time.h], [AC_DEFINE([HAVE_MACH_CLOCK], 1, [Define to 1 if mach kernel clock replacement available])], [AC_MSG_ERROR([[Missing clock_gettime and any replacement]])])]) dnl check for timer_settime or replace it AC_SEARCH_LIBS([timer_settime], [rt], [AC_DEFINE([HAVE_TIMER_SETTIME], 1, [Define to 1 if have timer_settime function])], [AC_CHECK_HEADER([mach/mach_time.h], [AC_DEFINE([HAVE_MACH_TIMER], 1, [Define to 1 if mach kernel clock replacement available])], [AC_MSG_ERROR([[Missing timer_settime and any replacement]])])]) AC_SEARCH_LIBS([pthread_exit], [pthread], [], [AC_MSG_ERROR([[pthreads library is required]])]) AC_SEARCH_LIBS([pthread_setname_np], [pthread], [dnl Validate pthread_setname_np with 2 args (some have 1) AC_MSG_CHECKING([[for two-parameter pthread_setname_np]]) AC_TRY_LINK([@%:@include ], [pthread_setname_np(pthread_self(), "name");], [AC_MSG_RESULT([yes]) AC_DEFINE([HAVE_PTHREAD_SETNAME_NP], 1, [Define to 1 if you have pthread_setname_np])], [AC_MSG_RESULT([[no]])])], [AC_SEARCH_LIBS([pthread_set_name_np], [pthread], [AC_CHECK_FUNCS([pthread_set_name_np])])]) dnl Large File Support (LFS) AC_SYS_LARGEFILE AC_TYPE_OFF_T dnl Checks for libraries. AC_SUBST([COMMON_LIBS]) AC_SUBST([COMMON_CPPFLAGS]) AC_SUBST([FORKED_LIBS]) AC_SUBST([FORKED_CPPFLAGS]) AC_SUBST([FORKED_OPTS_LIBS]) AC_SUBST([FORKED_OPTS_CPPFLAGS]) AM_ICONV dnl All FORK_ macros defined in m4/fork_checks.m4 FORK_FUNC_REQUIRE([COMMON], [GNU libunistring], [LIBUNISTRING], [unistring], [u8_strconv_from_locale], [uniconv.h], [], [dnl Retry test with iconv library FORK_VARS_PREPEND([COMMON], [LIBICONV], [INCICONV]) FORK_FUNC_REQUIRE([COMMON], [GNU libunistring], [LIBUNISTRING], [unistring], [u8_strconv_from_locale], [uniconv.h])]) FORK_MODULES_CHECK([FORKED], [ZLIB], [zlib], [deflate], [zlib.h]) FORK_MODULES_CHECK([FORKED], [CONFUSE], [libconfuse], [cfg_init], [confuse.h]) FORK_MODULES_CHECK([FORKED], [MINIXML], [mxml], [mxmlNewElement], [mxml.h], [AC_CHECK_FUNCS([mxmlGetOpaque] [mxmlGetText] [mxmlGetType] [mxmlGetFirstChild])]) dnl SQLite3 requires extra checks FORK_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0], [sqlite3_initialize], [sqlite3.h], [dnl Check that SQLite3 has the unlock notify API built-in AC_CHECK_FUNC([[sqlite3_unlock_notify]], [], [AC_MSG_ERROR([[SQLite3 was built without unlock notify support]])]) dnl Check that SQLite3 has been built with threadsafe operations AC_MSG_CHECKING([[if SQLite3 was built with threadsafe operations support]]) AC_RUN_IFELSE([AC_LANG_PROGRAM([[#include ]], [[ int ret = sqlite3_config(SQLITE_CONFIG_MULTITHREAD); if (ret != SQLITE_OK) return 1;]])], [AC_MSG_RESULT([[yes]])], [AC_MSG_RESULT([[no]]) AC_MSG_ERROR([[SQLite3 was not built with threadsafe operations support]])], [AC_MSG_RESULT([[runtime will tell]])]) ]) dnl libevent2 requires version checks FORK_MODULES_CHECK([FORKED], [LIBEVENT], [libevent >= 2], [event_base_new], [event2/event.h], [dnl check for old version PKG_CHECK_EXISTS([libevent >= 2.1.4], [], [AC_DEFINE([HAVE_LIBEVENT2_OLD], 1, [Define to 1 if you have libevent 2 (<2.1.4)])]) ]) dnl antlr version checks FORK_FUNC_REQUIRE([FORKED], [ANTLR3 C runtime], [ANTLR3C], [antlr3c], [antlr3BaseRecognizerNew], [antlr3.h], [AC_CHECK_FUNC([[antlr3NewAsciiStringInPlaceStream]], [AC_DEFINE([ANTLR3C_NEW_INPUT], 0, [define if antlr3 C runtime uses new input routines])], [AC_DEFINE([ANTLR3C_NEW_INPUT], 1, [define if antlr3 C runtime uses new input routines])]) ]) AM_PATH_LIBGCRYPT([1:1.2.0]) FORK_FUNC_REQUIRE([FORKED], [GNU Crypt Library], [LIBGCRYPT], [gcrypt], [gcry_control], [gcrypt.h]) AM_PATH_GPG_ERROR([1.6]) FORK_FUNC_REQUIRE([FORKED], [GNUPG Error Values], [GPG_ERROR_MT], [gpg-error], [gpg_err_init], [gpg-error.h]) AC_CHECK_HEADER([sys/eventfd.h], [AC_CHECK_FUNCS([eventfd])]) AC_CHECK_HEADER([sys/timerfd.h], [AC_CHECK_FUNC([timerfd_create], [AC_DEFINE([HAVE_TIMERFD], 1, [Define to 1 if you have timerfd])])]) FORK_FUNC_REQUIRE([FORKED], [inotify], [INOTIFY], [inotify], [inotify_add_watch], [sys/inotify.h]) have_signal=no AC_CHECK_HEADER([sys/signalfd.h], [AC_CHECK_FUNCS([signalfd], [have_signal=yes])]) AC_CHECK_HEADER([sys/event.h], [AC_CHECK_FUNCS([kqueue], [have_signal=yes])]) AS_IF([[test "$have_signal" = "no"]], [AC_MSG_ERROR([[Either signalfd or kqueue are required]])]) AC_CHECK_HEADERS_ONCE([endian.h sys/endian.h]) AC_CHECK_DECL([htobe16], [], [AC_CHECK_HEADERS([libkern/OSByteOrder.h], [], [AC_MSG_ERROR([[Missing functions to swap byte order]])])], [AC_INCLUDES_DEFAULT[ #ifdef HAVE_ENDIAN_H # include #elif defined(HAVE_SYS_ENDIAN_H) # include #endif ]]) dnl libav checks should be last, as they are sometimes both installed dnl and the CPPFLAGS/LIBS needs to be at the front of the search list. dnl Handle alternative package names for libav PKG_CHECK_EXISTS([libavcodec-libav], [LIBAV=-libav], [LIBAV=]) dnl Preference for ffmpeg if we have both (this could be an option...) PKG_CHECK_EXISTS([libavcodec], [LIBAV=]) dnl Option to choose libav even if ffmpeg is detected first AC_ARG_WITH([libav], [AS_HELP_STRING([--with-libav], [choose libav even if ffmpeg present (default=no)])], [[LIBAV=-libav]], [[LIBAV=]]) dnl libav/ffmpeg requires many feature checks FORK_MODULES_CHECK([FORKED], [LIBAV], [libavformat$LIBAV libavcodec$LIBAV libswscale$LIBAV libavutil$LIBAV libavfilter$LIBAV], [av_init_packet], [libavcodec/avcodec.h], [dnl Checks for misc libav and ffmpeg API differences AC_MSG_CHECKING([whether libav libraries are ffmpeg]) AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include ]], [[ #if LIBAVCODEC_VERSION_MICRO >= 100 /* ffmpeg uses 100+ as its micro version */ #else #error libav provider is not ffmpeg #endif ]])], [ [is_ffmpeg=yes] AC_DEFINE([HAVE_FFMPEG], 1, [Define to 1 if you have ffmpeg (not libav)])], [[is_ffmpeg=no]]) AC_MSG_RESULT([$is_ffmpeg]) FORK_CHECK_DECLS([av_buffersrc_add_frame_flags], [libavfilter/buffersrc.h]) FORK_CHECK_DECLS([av_buffersink_get_frame], [libavfilter/buffersink.h]) FORK_CHECK_DECLS([avfilter_graph_parse_ptr], [libavfilter/avfilter.h]) FORK_CHECK_DECLS([av_packet_unref], [libavcodec/avcodec.h]) FORK_CHECK_DECLS([av_packet_rescale_ts], [libavcodec/avcodec.h]) FORK_CHECK_DECLS([avformat_alloc_output_context2], [libavformat/avformat.h]) FORK_CHECK_DECLS([av_frame_alloc], [libavutil/frame.h]) FORK_CHECK_DECLS([av_frame_get_best_effort_timestamp], [libavutil/frame.h]) FORK_CHECK_DECLS([av_image_fill_arrays], [libavutil/imgutils.h]) FORK_CHECK_DECLS([av_image_get_buffer_size], [libavutil/imgutils.h]) AC_CHECK_HEADERS([libavutil/channel_layout.h libavutil/mathematics.h]) ]) AC_CHECK_SIZEOF([void *]) dnl --- Begin configuring the options --- dnl ALSA FORK_ARG_WITH_CHECK([FORKED_OPTS], [ALSA support], [alsa], [ALSA], [alsa], [snd_mixer_open], [asoundlib.h]) AM_CONDITIONAL([COND_ALSA], [[test "x$with_alsa" = "xyes"]]) dnl PULSEAUDIO FORK_ARG_WITH_CHECK([FORKED_OPTS], [Pulseaudio support], [pulseaudio], [LIBPULSE], [libpulse], [pa_stream_get_state], [pulse/pulseaudio.h], [AC_CHECK_FUNCS([pa_threaded_mainloop_set_name])]) AM_CONDITIONAL([COND_PULSEAUDIO], [[test "x$with_pulseaudio" = "xyes"]]) dnl Build with libcurl FORK_ARG_WITH_CHECK([FORKED_OPTS], [libcurl support], [libcurl], [LIBCURL], [libcurl], [curl_global_init], [curl/curl.h]) dnl Build with libsodium FORK_ARG_WITH_CHECK([FORKED_OPTS], [libsodium support], [libsodium], [LIBSODIUM], [libsodium], [sodium_init], [sodium.h]) dnl Build with libplist FORK_ARG_WITH_CHECK([FORKED_OPTS], [libplist support], [libplist], [LIBPLIST], [libplist >= 0.16], [plist_dict_get_item], [plist/plist.h]) dnl Build with libevent_pthreads FORK_ARG_WITH_CHECK([FORKED_OPTS], [libevent_pthreads support], [libevent_pthreads], [LIBEVENT_PTHREADS], [libevent_pthreads], [evthread_use_pthreads], [event2/thread.h]) dnl Build with json-c FORK_ARG_WITH_CHECK([FORKED_OPTS], [json-c support], [json], [JSON_C], [json-c >= 0.11], [json_tokener_parse], [json.h], [], [FORK_MODULES_CHECK([FORKED_OPTS], [JSON_C], [json], [json_tokener_parse], [json.h], [[with_json=yes] AC_DEFINE([HAVE_JSON_C_OLD], 1, [Define to 1 if you have json-c < 0.11])], [AS_IF([[test "x$with_json" != "xcheck"]], [AC_MSG_FAILURE([[--with-json was given, but test for json-c failed]])]) [with_json=no]] )]) dnl Build with Avahi (or Bonjour if not) FORK_ARG_WITH_CHECK([FORKED_OPTS], [Avahi mDNS], [avahi], [AVAHI], [avahi-client >= 0.6.24], [avahi_client_new], [avahi-client/client.h]) AS_IF([[test "x$with_avahi" = "xno"]], [FORK_FUNC_REQUIRE([FORKED_OPTS], [Bonjour DNS_SD], [DNSSD], [dns_sd], [DNSServiceGetAddrInfo], [dns_sd.h], [], [AC_MSG_ERROR([[Avahi client or Bonjour DNS_SD required, please install one.]])])]) AM_CONDITIONAL([COND_AVAHI], [[test "x$with_avahi" = "xyes"]]) dnl Spotify with dynamic linking to libspotify FORK_ARG_ENABLE([Spotify support], [spotify], [SPOTIFY], [AS_IF([[test "x$with_json" = "xno"]], [AC_MSG_ERROR([[Spotify support requires json-c]])]) AS_IF([[test "x$with_libevent_pthreads" = "xno"]], [AC_MSG_ERROR([[Spotify support requires libevent_pthreads]])]) FORK_MODULES_CHECK([SPOTIFY], [LIBSPOTIFY], [libspotify], [], [libspotify/api.h]) AC_DEFINE([HAVE_SPOTIFY_H], 1, [Define to 1 if you have the header file.]) dnl Don't link with libspotify, use dynamic linking AC_SEARCH_LIBS([dlopen], [dl], [], [AC_MSG_ERROR([[Spotify support requires dlopen]])]) FORK_VAR_PREPEND([FORKED_OPTS_CPPFLAGS], [$SPOTIFY_CPPFLAGS]) FORK_VAR_PREPEND([FORKED_OPTS_LIBS], [-rdynamic]) ]) AM_CONDITIONAL([COND_SPOTIFY], [[test "x$enable_spotify" = "xyes"]]) dnl LastFM support with libcurl FORK_ARG_ENABLE([LastFM support], [lastfm], [LASTFM], [AS_IF([[test "x$with_libcurl" = "xno"]], [AC_MSG_ERROR([[LastFM support requires libcurl]])])]) AM_CONDITIONAL([COND_LASTFM], [[test "x$enable_lastfm" = "xyes"]]) dnl ChromeCast support with libprotobuf-c FORK_ARG_ENABLE([Chromecast support], [chromecast], [CHROMECAST], [AS_IF([[test "x$with_json" = "xno"]], [AC_MSG_ERROR([[Chromecast support requires json-c]])]) FORK_MODULES_CHECK([FORKED_OPTS], [LIBPROTOBUF_C], [libprotobuf-c >= 1.0.0], [protobuf_c_message_pack], [protobuf-c/protobuf-c.h], [], [FORK_FUNC_REQUIRE([FORKED_OPTS], [v0 libprotobuf-c], [LIBPROTOBUF_OLD], [protobuf-c], [protobuf_c_message_pack], [google/protobuf-c/protobuf-c.h], [AC_DEFINE([HAVE_PROTOBUF_OLD], 1, [Define to 1 if you have libprotobuf < 1.0.0]) [protobuf_old=yes]], [AC_MSG_ERROR([[Chromecast support requires protobuf-c]])]) ]) FORK_MODULES_CHECK([FORKED_OPTS], [GNUTLS], [gnutls], [gnutls_init], [gnutls/gnutls.h]) ]) AM_CONDITIONAL([COND_CHROMECAST], [[test "x$enable_chromecast" = "xyes"]]) AM_CONDITIONAL([COND_PROTOBUF_OLD], [[test "x$protobuf_old" = "xyes"]]) dnl iTunes playlists with libplist FORK_ARG_DISABLE([iTunes Music Library XML support], [itunes], [ITUNES], [AS_IF([[test "x$with_libplist" = "xno"]], [AC_MSG_ERROR([[iTunes Music Library XML support requires libplist]])]) ]) AM_CONDITIONAL([COND_ITUNES], [[test "x$enable_itunes" = "xyes"]]) dnl MPD support FORK_ARG_DISABLE([MPD client protocol support], [mpd], [MPD]) AM_CONDITIONAL([COND_MPD], [[test "x$enable_mpd" = "xyes"]]) dnl Apple device verification FORK_ARG_DISABLE([Apple TV device verification], [verification], [RAOP_VERIFICATION], [ AS_IF([[test "x$with_libsodium" = "xno"]], [AC_MSG_ERROR([[Apple TV device verification requires libsodium]])]) AS_IF([[test "x$with_libplist" = "xno"]], [AC_MSG_ERROR([[Apple TV device verification requires libplist]])]) ]) AM_CONDITIONAL([COND_RAOP_VERIFICATION], [[test "x$enable_verification" = "xyes"]]) dnl Defining users and groups AC_ARG_WITH([daapd_user], [AS_HELP_STRING([--with-daapd-user=USER], [User for running forked-daapd (default=daapd)])], [[test x"$withval" = xyes && withval=]], [[withval=]]) DAAPD_USER=${withval:-daapd} AC_SUBST([DAAPD_USER]) AC_ARG_WITH([daapd_group], [AS_HELP_STRING([--with-daapd-group=GROUP], [Group for daapd user (default=USER)])], [[test x"$withval" = xyes && withval=]], [[withval=]]) DAAPD_GROUP=${withval:-$DAAPD_USER} AC_SUBST([DAAPD_GROUP]) dnl --- End options --- AC_CONFIG_FILES([ src/Makefile sqlext/Makefile Makefile forked-daapd.spec ]) AC_OUTPUT forked-daapd-25.0/forked-daapd.8000066400000000000000000000026641313447753700164210ustar00rootroot00000000000000.\" -*- nroff -*- .TH FORKED-DAAPD "8" "2015-12-30" "forked-daapd" "DAAP, MPD & RSP media server" .SH NAME forked\-daapd \- iTunes\-compatible DAAP server with MPD and RSP support .SH SYNOPSIS .B forked-daapd [\fIoptions\fR] .SH DESCRIPTION \fBforked\-daapd\fP is a Linux/FreeBSD DAAP (iTunes) media server with support for AirPlay devices, Apple Remote (and compatibles), MPD, Spotify, mp3 streaming and internet radio. It allows you to share your music collection over the local network. .SH OPTIONS .TP \fB\-d\fR \fIlevel\fP Log level (0\-5). .TP \fB\-D\fR \fIdom,..,dom\fP Debug domains; available domains are: \fIconfig\fP, \fIdaap\fP, \fIdb\fP, \fIhttpd\fP, \fImain\fP, \fImdns\fP, \fImisc\fP, \fIrsp\fP, \fIscan\fP, \fIxcode\fP, \fIevent\fP, \fIhttp\fP, \fIremote\fP, \fIdacp\fP, \fIffmpeg\fP, \fIartwork\fP, \fIplayer\fP, \fIraop\fP, \fIlaudio\fP, \fIdmap\fP, \fIfdbperf\fP, \fIspotify\fP, \fIlastfm\fP, \fIcache\fP, \fImpd\fP, \fIstream\fP, \fIcast\fP, \fIfifo\fP, \fIlib\fP. .TP \fB\-c\fR \fIfile\fP Use \fIfile\fP as the configuration file. .TP \fB\-P\fR \fIfile\fP Write PID to \fIfile\fP. .TP \fB\-f\fR Run in the foreground. .TP \fB\-b\fR \fIffid\fP \fIffid\fP to be broadcast in mDNS records. .TP \fB\-v\fR Display version information. .SH FILES .nf \fI/etc/forked\-daapd.conf\fR \fI/var/cache/forked\-daapd\fR .fi .SH SEE ALSO See \fIhttp://ejurgensen.github.io/forked-daapd/\fR for more in-depth information about using \fBforked-daapd\fP. forked-daapd-25.0/forked-daapd.conf.in000066400000000000000000000254151313447753700176030ustar00rootroot00000000000000# A quick guide to configuring forked-daapd: # # For regular use, the most important setting to configure is "directories", # which should be the location of your media. Whatever user you have set as # "uid" must have read access to this location. If the location is a network # mount, please see the README. # # In all likelihood, that's all you need to do! general { # Username # Make sure the user has read access to the library directories you set # below, and full access to the databases, log and local audio uid = "@DAAPD_USER@" # Database location # db_path = "@localstatedir@/cache/@PACKAGE@/songs3.db" # Log file and level # Available levels: fatal, log, warning, info, debug, spam logfile = "@localstatedir@/log/@PACKAGE@.log" loglevel = log # Admin password for the non-existent web interface admin_password = "unused" # Enable/disable IPv6 ipv6 = yes # Location of cache database # cache_path = "@localstatedir@/cache/@PACKAGE@/cache.db" # DAAP requests that take longer than this threshold (in msec) get their # replies cached for next time. Set to 0 to disable caching. # cache_daap_threshold = 1000 # When starting playback, autoselect speaker (if none of the previously # selected speakers/outputs are available) # speaker_autoselect = yes # Most modern systems have a high-resolution clock, but if you are on an # unusual platform and experience audio drop-outs, you can try changing # this option # high_resolution_clock = yes } # Library configuration library { # Name of the library as displayed by the clients # %h: hostname, %v: version name = "My Music on %h" # TCP port to listen on. Default port is 3689 (daap) port = 3689 # Password for the library. Optional. # password = "" # Directories to index directories = { "/srv/music" } # Directories containing podcasts # For each directory that is indexed the path is matched against these # names. If there is a match all items in the directory are marked as # podcasts. Eg. if you index /srv/music, and your podcasts are in # /srv/music/Podcasts, you can set this to "/Podcasts". # (changing this setting only takes effect after rescan, see the README) podcasts = { "/Podcasts" } # Directories containing audiobooks # For each directory that is indexed the path is matched against these # names. If there is a match all items in the directory are marked as # audiobooks. # (changing this setting only takes effect after rescan, see the README) audiobooks = { "/Audiobooks" } # Directories containing compilations (eg soundtracks) # For each directory that is indexed the path is matched against these # names. If there is a match all items in the directory are marked as # compilations. # (changing this setting only takes effect after rescan, see the README) compilations = { "/Compilations" } # Compilations usually have many artists, and if you don't want every # artist to be listed when artist browsing in Remote, you can set # a single name which will be used for all music in the compilation dir # (changing this setting only takes effect after rescan, see the README) compilation_artist = "Various artists" # If your album and artist lists are cluttered, you can choose to hide # albums and artists with only one track. The tracks will still be # visible in other lists, e.g. songs and playlists. This setting # currently only works in some remotes. # hide_singles = false # Internet streams in your playlists will by default be shown in the # "Radio" library, like iTunes does. However, some clients (like # TunesRemote+) won't show the "Radio" library. If you would also like # to have them shown like normal playlists, you can enable this option. # radio_playlists = false # These are the default playlists. If you want them to have other names, # you can set it here. # name_library = "Library" # name_music = "Music" # name_movies = "Movies" # name_tvshows = "TV Shows" # name_podcasts = "Podcasts" # name_audiobooks = "Audiobooks" # name_radio = "Radio" # Artwork file names (without file type extension) # forked-daapd will look for jpg and png files with these base names # artwork_basenames = { "artwork", "cover", "Folder" } # Enable searching for artwork corresponding to each individual media # file instead of only looking for album artwork. This is disabled by # default to reduce cache size. # artwork_individual = false # File types the scanner should ignore # Non-audio files will never be added to the database, but here you # can prevent the scanner from even probing them. This might improve # scan time. By default .db, .ini, .db-journal, .pdf and .metadata are # ignored. # filetypes_ignore = { ".db", ".ini", ".db-journal", ".pdf", ".metadata" } # File paths the scanner should ignore # If you want to exclude files on a more advanced basis you can enter # one or more POSIX regular expressions, and any file with a matching # path will be ignored. # filepath_ignore = { "myregex" } # Disable startup file scanning # When forked-daapd starts it will do an initial file scan of your # library (and then watch it for changes). If you are sure your library # never changes while forked-daapd is not running, you can disable the # initial file scan and save some system ressources. Disabling this scan # may lead to forked-daapd's database coming out of sync with the # library. If that happens read the instructions in the README on how # to trigger a rescan. # filescan_disable = false # Should iTunes metadata override ours? # itunes_overrides = false # Should we import the content of iTunes smart playlists? # itunes_smartpl = false # Decoding options for DAAP clients # Since iTunes has native support for mpeg, mp4a, mp4v, alac and wav, # such files will be sent as they are. Any other formats will be decoded # to raw wav. If forked-daapd detects a non-iTunes DAAP client, it is # assumed to only support mpeg and wav, other formats will be decoded. # Here you can change when to decode. Note that these settings have no # effect on AirPlay. # Formats: mp4a, mp4v, mpeg, alac, flac, mpc, ogg, wma, wmal, wmav, aif, wav # Formats that should never be decoded # no_decode = { "format", "format" } # Formats that should always be decoded # force_decode = { "format", "format" } # Watch named pipes in the library for data and autostart playback when # there is data to be read. To exclude specific pipes from watching, # consider using the above _ignore options. # pipe_autostart = true } # Local audio output audio { # Name - used in the speaker list in Remote nickname = "Computer" # Type of the output (alsa, pulseaudio, dummy or disabled) # type = "alsa" # Audio PCM device name for local audio output - ALSA only # card = "default" # Mixer channel to use for volume control - ALSA only # If not set, PCM will be used if available, otherwise Master. # mixer = "" # Mixer device to use for volume control - ALSA only # If not set, the value for "card" will be used. # mixer_device = "" # Syncronization # If your local audio is out of sync with AirPlay, you can adjust this # value. Positive values correspond to moving local audio ahead, # negative correspond to delaying it. The unit is samples, where is # 44100 = 1 second. The offset must be between -44100 and 44100. # offset = 0 } # Pipe output # Allows forked-daapd to output audio data to a named pipe #fifo { # nickname = "fifo" # path = "/path/to/fifo" #} # AirPlay/Airport Express device settings # (make sure you get the capitalization of the device name right) #airplay "My AirPlay device" { # forked-daapd's volume goes to 11! If that's more than you can handle # you can set a lower value here # max_volume = 11 # Enable this option to exclude a particular AirPlay device from the # speaker list # exclude = false # AirPlay password # password = "s1kr3t" #} # Spotify settings (only have effect if Spotify enabled - see README/INSTALL) spotify { # Directory where user settings should be stored (credentials) # settings_dir = "@localstatedir@/cache/@PACKAGE@/libspotify" # Cache directory # cache_dir = "/tmp" # Set preferred bitrate for music streaming # 0: No preference (default), 1: 96kbps, 2: 160kbps, 3: 320kbps # bitrate = 0 # Your Spotify playlists will by default be put in a "Spotify" playlist # folder. If you would rather have them together with your other # playlists you can set this option to true. # base_playlist_disable = false # Spotify playlists usually have many artist, and if you don't want # every artist to be listed when artist browsing in Remote, you can set # the artist_override flag to true. This will use the compilation_artist # as album artist for Spotify items. # artist_override = false # Similar to the different artists in Spotify playlists, the playlist # items belong to different albums, and if you do not want every album # to be listed when browsing in Remote, you can set the album_override # flag to true. This will use the playlist name as album name for # Spotify items. Notice that if an item is in more than one playlist, # it will only appear in one album when browsing (in which album is # random). # album_override = false } # MPD configuration (only have effect if MPD enabled - see README/INSTALL) mpd { # TCP port to listen on for MPD client requests. # Default port is 6600, set to 0 to disable MPD support. # port = 6600 # HTTP port to listen for artwork requests (only supported by some MPD # clients and will need additional configuration in the MPD client to # work). Set to 0 to disable serving artwork over http. # http_port = 0 # By default forked-daapd will - like iTunes - clear the playqueue if # playback stops. Setting clear_queue_on_stop_disable to true will keep # the playlist like MPD does. Note that some dacp clients do not show # the playqueue if playback is stopped. # clear_queue_on_stop_disable = false } # SQLite configuration (allows to modify the operation of the SQLite databases) # Make sure to read the SQLite documentation for the corresponding PRAGMA # statements as changing them from the defaults may increase the possibility of # database corruptions! By default the SQLite default values are used. sqlite { # Cache size in number of db pages for the library database # (SQLite default page size is 1024 bytes and cache size is 2000 pages) # pragma_cache_size_library = 2000 # Cache size in number of db pages for the daap cache database # (SQLite default page size is 1024 bytes and cache size is 2000 pages) # pragma_cache_size_cache = 2000 # Sets the journal mode for the database # DELETE (default), TRUNCATE, PERSIST, MEMORY, WAL, OFF # pragma_journal_mode = DELETE # Change the setting of the "synchronous" flag # 0: OFF, 1: NORMAL, 2: FULL (default) # pragma_synchronous = 2 # Should the database be vacuumed on startup? (increases startup time, # but may reduce database size). Default is yes. # vacuum = yes } forked-daapd-25.0/forked-daapd.service.in000066400000000000000000000006321313447753700203100ustar00rootroot00000000000000[Unit] Description=DAAP/DACP (iTunes), RSP and MPD server, supports AirPlay and Remote Documentation=man:forked-daapd(8) After=network.target sound.target remote-fs.target pulseaudio.service avahi-daemon.service [Service] ExecStart=@sbindir@/forked-daapd -f # Restart, but not more than once every 10 minutes Restart=on-failure StartLimitBurst=2 StartLimitInterval=600 [Install] WantedBy=multi-user.target forked-daapd-25.0/forked-daapd.spec.in000066400000000000000000000111041313447753700175760ustar00rootroot00000000000000# -*- Mode:rpm-spec -*- # @configure_input@ %global username @DAAPD_USER@ %global groupname @DAAPD_GROUP@ %bcond_without alsa %bcond_with pulseaudio %bcond_without libcurl %bcond_without json %bcond_with itunes %bcond_with spotify %bcond_with lastfm %bcond_with chromecast %bcond_without mpd %global _hardened_build 1 Summary: iTunes-compatible DAAP server with MPD and RSP support Name: @PACKAGE_NAME@ Version: @PACKAGE_VERSION@ Release: 1%{?dist} License: GPLv2+ Group: Applications/Multimedia Url: https://github.com/ejurgensen/forked-daapd Source: https://github.com/ejurgensen/%{name}/archive/%{version}.tar.gz#/%{name}-%{version}.tar.gz %{?systemd_ordering} BuildRequires: systemd BuildRequires: pkgconfig BuildRequires: pkgconfig(sqlite3) >= 3.5.0 BuildRequires: pkgconfig(libconfuse) BuildRequires: libunistring-devel BuildRequires: pkgconfig(mxml) BuildRequires: pkgconfig(libevent) >= 2.0.0 BuildRequires: pkgconfig(avahi-client) >= 0.6.24 BuildRequires: libgcrypt-devel >= 1.2.0 BuildRequires: libgpg-error-devel >= 1.6 BuildRequires: pkgconfig(zlib) BuildRequires: antlr3-C-devel BuildRequires: pkgconfig(libavformat) BuildRequires: pkgconfig(libavcodec) BuildRequires: pkgconfig(libswscale) BuildRequires: pkgconfig(libavutil) BuildRequires: pkgconfig(libavfilter) Requires(pre): shadow-utils Requires: systemd-units %if %{with alsa} BuildRequires: pkgconfig(alsa) %endif %if %{with pulseaudio} BuildRequires: pkgconfig(libpulse) %endif %if %{with libcurl} BuildRequires: pkgconfig(libcurl) %endif %if %{with json} BuildRequires: pkgconfig(json-c) %endif %if %{with itunes} BuildRequires: pkgconfig(libplist) >= 0.16 %endif %if %{with spotify} BuildRequires: libspotify-devel %endif %if %{with chromecast} BuildRequires: pkgconfig(gnutls) BuildRequires: pkgconfig(libprotobuf-c) %endif %global homedir %{_localstatedir}/lib/%{name} %global gecos %{name} User %{!?_pkgdocdir: %global _pkgdocdir %{_docdir}/%{name}-%{version}} %description forked-daapd is a DAAP/DACP (iTunes), MPD (Music Player Daemon) and RSP (Roku) media server. It has support for AirPlay devices/speakers, Apple Remote (and compatibles), MPD clients, Chromecast, network streaming, internet radio, Spotify and LastFM. It does not support streaming video by AirPlay nor Chromecast. DAAP stands for Digital Audio Access Protocol, and is the protocol used by iTunes and friends to share/stream media libraries over the network. forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server). %prep %if %{with spotify} && %{without json} echo "ERROR: Option '-with spotify' cannot be used with '-without json'" >&2 && exit 1 %endif %if %{with lastfm} && %{without libcurl} echo "ERROR: Option '-with lastfm' cannot be used with '-without libcurl'" >&2 && exit 1 %endif %if %{with chromecast} && %{without json} echo "ERROR: Option '-with chromecast' cannot be used with '-without json'" >&2 && exit 1 %endif %setup -q %build %configure \ %if %{without alsa} --without-alsa \ %endif %if %{without pulseaudio} --without-pulseaudio \ %endif %if %{without libcurl} --without-libcurl \ %endif %if %{without json} --without-json \ %endif %if %{with itunes} --enable-itunes \ %endif %if %{with spotify} --enable-spotify \ %endif %if %{with lastfm} --enable-lastfm \ %endif %if %{with chromecast} --enable-chromecast \ %endif %if %{with mpd} --enable-mpd \ %endif --with-daapd-user=%{username} \ --with-daapd-group=%{groupname} make %{?_smp_mflags} %install make install DESTDIR=%{buildroot} docdir=%{_pkgdocdir} rm -f %{buildroot}%{_pkgdocdir}/INSTALL mkdir -p %{buildroot}%{homedir} mkdir -p %{buildroot}%{_localstatedir}/log touch %{buildroot}%{_localstatedir}/log/%{name}.log mkdir -p %{buildroot}%{_unitdir} install -m 0644 forked-daapd.service %{buildroot}%{_unitdir}/%{name}.service rm -f %{buildroot}%{_libdir}/%{name}/*.la %pre getent group %{groupname} >/dev/null || groupadd -r %{groupname} getent passwd %{username} >/dev/null || \ useradd -r -g %{groupname} -d %{homedir} -s /sbin/nologin \ -c '%{gecos}' %{username} exit 0 %post %systemd_post %{name}.service %preun %systemd_preun %{name}.service %postun %systemd_postun_with_restart %{name}.service %files %{!?_licensedir:%global license %%doc} %license COPYING %{_pkgdocdir} %config(noreplace) %{_sysconfdir}/forked-daapd.conf %{_sbindir}/forked-daapd %{_libdir}/* %{_unitdir}/%{name}.service %attr(0750,%{username},%{groupname}) %{_localstatedir}/cache/%{name} %attr(0750,%{username},%{groupname}) %{homedir} %ghost %{_localstatedir}/log/%{name}.log %{_mandir}/man?/* %changelog * Tue Dec 20 2016 Scott Shambarger - 24.2-1 - Initial RPM release candidate. forked-daapd-25.0/m4/000077500000000000000000000000001313447753700143175ustar00rootroot00000000000000forked-daapd-25.0/m4/.gitignore000066400000000000000000000000231313447753700163020ustar00rootroot00000000000000libtool.m4 lt*.m4 forked-daapd-25.0/m4/fork_checks.m4000066400000000000000000000215061313447753700170460ustar00rootroot00000000000000# fork_checks.m4 serial 2 dnl Copyright (c) Scott Shambarger dnl dnl Copying and distribution of this file, with or without modification, are dnl permitted in any medium without royalty provided the copyright notice dnl and this notice are preserved. This file is offered as-is, without any dnl warranty. dnl _FORK_FUNC_MERGE dnl ---------------- dnl Internal only. Defines function used by FORK_VAR_PREPEND AC_DEFUN([_FORK_FUNC_MERGE], [[ # fork_fn_merge(before, after) # create wordlist removing duplicates fork_fn_merge() { fork_fn_var_result=$][1 for element in $][2; do fork_fn_var_haveit= for x in $fork_fn_var_result; do if test "X$x" = "X$element"; then fork_fn_var_haveit=1 break fi done if test -z "$fork_fn_var_haveit"; then fork_fn_var_result="${fork_fn_var_result}${fork_fn_var_result:+ }$element" fi done echo "$fork_fn_var_result" unset fork_fn_var_haveit unset fork_fn_var_result }]]) dnl FORK_VAR_PREPEND(VARNAME, BEFORE) dnl --------------------------------- dnl Prepends words in BEFORE to the contents of VARNAME, skipping any dnl duplicate words. AC_DEFUN([FORK_VAR_PREPEND], [AC_REQUIRE([_FORK_FUNC_MERGE])dnl [ $1=$(fork_fn_merge "$2" "$$1")]]) dnl FORK_VARS_PREPEND(TARGET, LIBS_ENV, CFLAGS_ENV) dnl ----------------------------------------------- dnl Prepend LIBS_ENV to LIBS and TARGET_LIBS dnl Append CFLAGS_ENV to CPPFLAGS and TARGET_CPPFLAGS. AC_DEFUN([FORK_VARS_PREPEND], [[ LIBS="$$2 $LIBS" $1_LIBS="$$2 $$1_LIBS"] FORK_VAR_PREPEND([CPPFLAGS], [$$3]) FORK_VAR_PREPEND([$1_CPPFLAGS], [$$3]) ]) dnl _FORK_VARS_ADD_PREFIX(TARGET) dnl ----------------------------- dnl Internal use only. Add libdir prefix to {TARGET_}LIBS and dnl includedir prefix to {TARGET_}CPPFLAGS as fallback search paths dnl expanding all variables. AC_DEFUN([_FORK_VARS_ADD_PREFIX], [AC_REQUIRE([AC_LIB_PREPARE_PREFIX]) AC_LIB_WITH_FINAL_PREFIX([[ eval LIBS=\"-L$libdir $LIBS\" eval $1_LIBS=\"-L$libdir $$1_LIBS\" eval fork_tmp_cppflags=\"-I$includedir\"] FORK_VAR_PREPEND([CPPFLAGS], [$fork_tmp_cppflags]) FORK_VAR_PREPEND([$1_CPPFLAGS], [$fork_tmp_cppflags]) ]) ]) dnl FORK_CHECK_DECLS(SYMBOLS, INCLUDE, [ACTION-IF-FOUND], dnl [ACTION-IF-NOT-FOUND]) dnl ----------------------------------------------------- dnl Expands AC_CHECK_DECLS with SYMBOLS and INCLUDE appended to dnl AC_INCLUDES_DEFAULT. dnl NOTE: Remember that AC_CHECK_DECLS defines HAVE_* to 1 or 0 dnl (not 1 or undefined!) AC_DEFUN([FORK_CHECK_DECLS], [AC_CHECK_DECLS([$1], [$3], [$4], [AC_INCLUDES_DEFAULT [@%:@include <$2>]]) ]) dnl FORK_FUNC_REQUIRE(TARGET, DESCRIPTION, ENV, LIBRARY, FUNCTION, [HEADER], dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) dnl ------------------------------------------------------------------------ dnl Check for software which lacks pkg-config support, setting TARGET_CPPFLAGS dnl and TARGET_LIBS with working values if FUNCTION found, or failing if dnl it's not. If ENV_CFLAGS and ENV_LIBS overrides are set (ENV is prefix), dnl tries to link FUNCTION/include HEADER with them. Without overrides, dnl expands like AC_SEARCH_LIBS on FUNCTION (trying without and with LIBRARY), dnl adding $prefix paths if necessary. If FUNCTION found, verifies optional dnl HEADER can be included (or fails with error), and expands optional dnl ACTION-IF-FOUND with working CPPFLAGS/LIBS for additional checks. dnl DESCRIPTION used as friendly name in error messages to help user dnl identify software to install. If FUNCTION not found, either displays dnl error suggested use of ENV_* overrides, or if ENV_* were not set dnl expands optional ACTION-IF-NOT-FOUND in place of error. dnl Restores original CPPFLAGS and LIBS when done. AC_DEFUN([FORK_FUNC_REQUIRE], [AS_VAR_PUSHDEF([FORK_MSG], [fork_msg_$3]) AC_ARG_VAR([$3_CFLAGS], [C compiler flags for $2, overriding search]) AC_ARG_VAR([$3_LIBS], [linker flags for $2, overriding search]) [fork_save_$3_LIBS=$LIBS; fork_save_$3_CPPFLAGS=$CPPFLAGS fork_found_$3=yes] AS_IF([[test -n "$$3_CFLAGS" && test -n "$$3_LIBS"]], [dnl ENV variables provided, just verify they work AS_VAR_SET([FORK_MSG], [[" Library specific environment variables $3_LIBS and $3_CFLAGS were used, verify they are correct..."]]) FORK_VARS_PREPEND([$1], [$3_LIBS], [$3_CFLAGS]) AC_CHECK_FUNC([[$5]], [], [AC_MSG_FAILURE([[Unable to link function $5 with $2.$]FORK_MSG])])], [dnl Search w/o LIBRARY, w/ LIBRARY, and finally adding $prefix path AS_VAR_SET([FORK_MSG], [[" Install $2 in the default include path, or alternatively set library specific environment variables $3_CFLAGS and $3_LIBS."]]) AC_MSG_CHECKING([[for library containing $5...]]) AC_TRY_LINK_FUNC([[$5]], [AC_MSG_RESULT([[none required]])], [[LIBS="-l$4 $LIBS" $1_LIBS="-l$4 $$1_LIBS"] AC_TRY_LINK_FUNC([[$5]], [AC_MSG_RESULT([[-l$4]])], [_FORK_VARS_ADD_PREFIX([$1]) AC_TRY_LINK_FUNC([[$5]], [AC_MSG_RESULT([[-l$4]])], [AC_MSG_RESULT([[no]]) fork_found_$3=no])]) ]) ]) AS_IF([[test "$fork_found_$3" != "no"]], [dnl check HEADER, then expand FOUND m4_ifval([$6], [AC_CHECK_HEADER([[$6]], [], [AC_MSG_FAILURE([[Unable to find header $6 for $2.$]FORK_MSG])])]) $7]) [LIBS=$fork_save_$3_LIBS; CPPFLAGS=$fork_save_$3_CPPFLAGS] dnl Expand NOT-FOUND after restoring saved flags to allow recursive expansion AS_IF([[test "$fork_found_$3" = "no"]], [m4_default_nblank([$8], [AC_MSG_FAILURE([[Function $5 in lib$4 not found.$]FORK_MSG])])]) AS_VAR_POPDEF([FORK_MSG]) ]) dnl FORK_MODULES_CHECK(TARGET, ENV, MODULES, [FUNCTION], [HEADER], dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) dnl -------------------------------------------------------------- dnl Expands PKG_CHECK_MODULES, but when found also attempt to link dnl FUNCTION and include HEADER. Appends working package values to dnl TARGET_CPPFLAGS and TARGET_LIBS. Expands optional ACTION-IF-FOUND with dnl working CPPFLAGS/LIBS for additional checks. Expands dnl ACTION-IF-NOT-FOUND only if package not found (not link/include failures) dnl overriding default error. Restores original CPPFLAGS and LIBS when done. AC_DEFUN([FORK_MODULES_CHECK], [PKG_CHECK_MODULES([$2], [[$3]], [[fork_save_$2_LIBS=$LIBS; fork_save_$2_CPPFLAGS=$CPPFLAGS] FORK_VARS_PREPEND([$1], [$2_LIBS], [$2_CFLAGS]) m4_ifval([$4], [AC_CHECK_FUNC([[$4]], [], [AC_MSG_ERROR([[Unable to link function $4]])])]) m4_ifval([$5], [AC_CHECK_HEADER([[$5]], [], [AC_MSG_ERROR([[Unable to find header $5]])])]) $6 [LIBS=$fork_save_$2_LIBS; CPPFLAGS=$fork_save_$2_CPPFLAGS]], m4_default_nblank_quoted([$7])) ]) dnl FORK_ARG_WITH_CHECK(TARGET, DESCRIPTION, OPTION, ENV, MODULES, [FUNCTION], dnl [HEADER], [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) dnl -------------------------------------------------------------------------- dnl Create an --with-OPTION with a default of "check" (include MODULES dnl if they are available). Expands FORK_MODULES_CHECK with remaining dnl arguments. Defines HAVE_ENV to 1 if package found. DESCRIPTION is used dnl in option help. Shell variable with_OPTION set to yes before dnl ACTION-IF-FOUND. Default ACTION-IF-NOT-FOUND will fail dnl if --with-OPTION given and MODULES not found, or sets shell var dnl with_OPTION to no if option was check. A non-empty ACTION-IF-NOT-FOUND dnl overrides this behavior to allow alternate checks. AC_DEFUN([FORK_ARG_WITH_CHECK], [AC_ARG_WITH([[$3]], [AS_HELP_STRING([--with-$3], [with $2 (default=check)])], [], [[with_$3=check]]) AS_IF([[test "x$with_$3" != "xno"]], [FORK_MODULES_CHECK([$1], [$4], [$5], [$6], [$7], [[with_$3=yes] AC_DEFINE([HAVE_$4], 1, [Define to 1 to build with $2]) $8], [m4_default_nblank([$9], [AS_IF([[test "x$with_$3" != "xcheck"]], [AC_MSG_FAILURE([[--with-$3 was given, but test for $5 failed]])]) [with_$3=no]]) ]) ]) ]) dnl FORK_ARG_ENABLE(DESCRIPTION, OPTION, DEFINE, [ACTION-IF-ENABLE]) dnl ---------------------------------------------------------------- dnl Create an --enable-OPTION, setting shell variable enable_OPTION dnl to no by default. If feature is enabled, defines DEFINE to 1 dnl and expand ACTION-IF_ENABLE. DESCRIPTION is used in option help. AC_DEFUN([FORK_ARG_ENABLE], [AC_ARG_ENABLE([[$2]], [AS_HELP_STRING([--enable-$2], [enable $1 (default=no)])]) AS_IF([[test "x$enable_$2" = "xyes"]], [AC_DEFINE([$3], 1, [Define to 1 to enable $1]) $4], [[enable_$2=no]]) ]) dnl FORK_ARG_DISABLE(DESCRIPTION, OPTION, DEFINE, [ACTION-IF-ENABLE]) dnl ---------------------------------------------------------------- dnl Create an --disable-OPTION, setting shell variable enable_OPTION dnl to yes by default. If feature is enabled, defines DEFINE to 1 dnl and expand ACTION-IF_ENABLE. DESCRIPTION is used in option help. AC_DEFUN([FORK_ARG_DISABLE], [AC_ARG_ENABLE([[$2]], [AS_HELP_STRING([--disable-$2], [disable $1 (default=no)])]) AS_IF([[test "x$enable_$2" = "x" || test "x$enable_$2" = "xyes"]], [AC_DEFINE([$3], 1, [Define to 1 to enable $1]) [enable_$2=yes] $4], [[enable_$2=no]]) ]) forked-daapd-25.0/scripts/000077500000000000000000000000001313447753700154665ustar00rootroot00000000000000forked-daapd-25.0/scripts/antlr35_install.sh000077500000000000000000000152501313447753700210460ustar00rootroot00000000000000#!/bin/sh WORKDIR=~/antlr35.tmp # programs MAKE=${MAKE-make} DOWNLOAD="wget --no-check-certificate" ALTDOWNLOAD="curl -LO" SUDO=sudo # source ANTLR_VERSION=3.5 ANTLR3_SOURCE="https://github.com/antlr/website-antlr3/raw/gh-pages/download" ANTLR3_JAR="antlr-3.5.2-complete.jar" ANTLR3_URL="$ANTLR3_SOURCE/$ANTLR3_JAR" LIBANTLR3C="libantlr3c-3.4" LIBANTLR3C_SOURCE="https://github.com/antlr/website-antlr3/raw/gh-pages/download/C" LIBANTLR3C_TAR="${LIBANTLR3C}.tar.gz" LIBANTLR3C_URL="$LIBANTLR3C_SOURCE/$LIBANTLR3C_TAR" usage() { echo echo "This script will download, build and install antlr $ANTLR_VERSION" echo " (and matching libantlrc) on your computer." echo echo "Usage: ${0##*/} -h | [ -p ] [ ]" echo echo "Parameters:" echo " -h Show this help" echo " -p Install to prefix (default: choose /usr or /usr/local)" echo " Build directory (default: $WORKDIR)" exit 0 } GIVEN_PREFIX= case $1 in -h|--help) usage;; -p) shift [ -n "$1" ] || { echo "Option -p requires a argument (try -h for usage)" exit 1 } GIVEN_PREFIX=$1 shift ;; -*) echo "Unrecognized option $1 (try -h for usage)" exit 1 ;; esac # override build directory? (support ~ expansion) [ -n "$1" ] && WORKDIR=$1 ORIG_DIR=`pwd` err() { echo "$*" if [ -n "$FILES_EXIST" ]; then echo "Files remain in $WORKDIR..." else cd "$ORIG_DIR" rmdir "$WORKDIR" fi exit 1 } is_yes() { case "$1" in [N]*|[n]*) return 1;; *) ;; esac return 0 } prog_install() { read -p "Would you like to install into $PREFIX now? [Y/n] " yn if ! is_yes "$yn"; then echo "Build left ready to install from $WORKDIR" echo "You can re-run the script (eg. as root) to install into" echo " $PREFIX later." exit fi if [ `id -u` -ne 0 ]; then echo "Would you like to install with sudo?" read -p "NOTE: You WILL be asked for your password! [Y/n] " yn if ! is_yes "$yn"; then SUDO= read -p "Continue to install as non-root user? [Y/n] " yn is_yes "$yn" || err "Install cancelled" fi else SUDO= fi cd $LIBANTLR3C || err "Unable to cd to build libantlr3c build directory!" echo "Installing libantlr3c to $PREFIX" $SUDO $MAKE install || err "Install of libantlr3c to $PREFIX failed!" cd "$ORIG_DIR" cd $WORKDIR echo "Installing antlr3 to $PREFIX" $SUDO mkdir -p "$PREFIX_JAVA" || err "Unable to create $PREFIX_JAVA" $SUDO install "$ANTLR3_JAR" "$PREFIX_JAVA" || \ err "Failed to install antlr3 jar to $PREFIX_JAVA" $SUDO mkdir -p "$PREFIX/bin" || err "Unable to create $PREFIX/bin" $SUDO install -m 755 antlr3 "$PREFIX/bin" || \ err "Failed to install antlr3 to $PREFIX/bin" echo "Install complete (build remains in $WORKDIR)" } echo "This script will download, build and install antlr $ANTLR_VERSION" echo " (and matching libantlrc) on your computer." echo # check if make works ISGNU=`$MAKE --version 2>/dev/null | grep "GNU Make"` if [ -z "$ISGNU" ]; then MAKE=gmake ISGNU=`$MAKE --version 2>/dev/null | grep "GNU Make"` fi [ -z "$ISGNU" ] && err "Unable to locate GNU Make, set \$MAKE to it's location and re-run" if [ -f "$WORKDIR/install_env" ]; then echo "Existing build found in $WORKDIR" FILES_EXIST=1 cd $WORKDIR || err "Unable to cd to '$WORKDIR'" . install_env [ -n "$PREFIX" ] || err "PREFIX is missing in file 'install_env'" if [ -n "$GIVEN_PREFIX" ] && [ "$GIVEN_PREFIX" != "$PREFIX" ]; then echo "You must rebuild to install into $GIVEN_PREFIX (current build for $PREFIX)" read -p "Would you like to rebuild for ${GIVEN_PREFIX}? [Y/n] " yn if is_yes "$yn"; then rm -f install_env PREFIX= else read -p "Would you like to install to ${PREFIX}? [Y/n] " yn ! is_yes "$yn" && err "Install cancelled" fi fi if [ -n "$PREFIX" ]; then PREFIX_JAVA=$PREFIX/share/java prog_install exit 0 fi fi if [ ! -d "$WORKDIR" ]; then read -p "Should the script create $WORKDIR and use it for building? [Y/n] " yn is_yes "$yn" || exit fi if [ -n "$GIVEN_PREFIX" ]; then PREFIX=$GIVEN_PREFIX else read -p "Should the script install with prefix /usr or /usr/local? [U/l] " yn if [ "$yn" = "l" ]; then PREFIX=/usr/local else PREFIX=/usr fi fi PREFIX_JAVA=$PREFIX/share/java MACHBITS=`getconf LONG_BIT 2>/dev/null` [ "$MACHBITS" = "64" ] && DEF_AN="[Y/n]" || DEF_AN="[y/N]" read -p "Should the script build libantlr3c for 64 bit? $DEF_AN " yn [ -z "$yn" -a "$MACHBITS" != "64" ] && yn=n is_yes "$yn" && ENABLE64BIT="--enable-64bit" mkdir -p "$WORKDIR" || err "Error creating $WORKDIR" # don't quote WORKDIR to catch a WORKDIR that will break the build (eg spaces) cd $WORKDIR || err "Unable to cd to '$WORKDIR' (does it include spaces?)" REMOVE_ON_CANCEL= cancel_download() { echo "removing $REMOVE_ON_CANCEL" [ -n "$REMOVE_ON_CANCEL" ] && rm -f "$REMOVE_ON_CANCEL" err "Cancelling download..." } antlr_download() { trap cancel_download SIGINT $DOWNLOAD --help >/dev/null 2>&1 || DOWNLOAD=$ALTDOWNLOAD $DOWNLOAD --help >/dev/null 2>&1 || { echo "Unable to find wget or curl commands to download source," echo " please install either one and re-try." exit 1 } [ "x$1" = "xreset" ] && rm "$ANTLR3_JAR" "$LIBANTLR3C_TAR" if [ ! -f "$ANTLR3_JAR" ]; then echo echo "Downloading antlr from $ANTLR3_URL" echo "Ctrl-C to abort..." REMOVE_ON_CANCEL=$ANTLR3_JAR $DOWNLOAD "$ANTLR3_URL" || err "Download of $ANTLR3_JAR failed!" FILES_EXIST=1 fi if [ ! -f "$LIBANTLR3C_TAR" ]; then echo echo "Downloading libantlr3c from $LIBANTLR3C_URL" echo "Ctrl-C to abort..." REMOVE_ON_CANCEL=$LIBANTLR3C_TAR $DOWNLOAD "$LIBANTLR3C_URL" || err "Download of $LIBANTLR3C_TAR failed!" FILES_EXIST=1 fi trap - SIGINT } # retrieve the source if [ -f "$ANTLR3_JAR" -a -f "$LIBANTLR3C_TAR" ]; then FILES_EXIST=1 read -p "Files appear to already be downloaded, use them? [Y/n] " yn ! is_yes "$yn" && antlr_download reset else read -p "Should the script download and build antlr and libantlr3c? [Y/n] " yn is_yes "$yn" || exit antlr_download fi # build/install libantlr3c [ -d "$LIBANTLR3C" ] && rm -rf "$LIBANTLR3C" tar xzf "$LIBANTLR3C_TAR" || err "Uncompress of $LIBANTLR3C_TAR failed!" cd $LIBANTLR3C || err "Unable to cd to build $LIBANTLR3C build directory!" ./configure $ENABLE64BIT --prefix=$PREFIX && $MAKE [ $? -ne 0 ] && err "Build of libantlr3c failed!" # install antlr3 jar and wrapper cd "$ORIG_DIR" cd $WORKDIR printf "#!/bin/sh export CLASSPATH CLASSPATH=\$CLASSPATH:$PREFIX_JAVA/${ANTLR3_JAR}:$PREFIX_JAVA /usr/bin/java org.antlr.Tool \$* " > antlr3 # save for later install attempts echo "PREFIX=$PREFIX" > install_env echo prog_install forked-daapd-25.0/scripts/freebsd_install_11.0.sh000066400000000000000000000105401313447753700216210ustar00rootroot00000000000000#!/bin/sh # Credit thorsteneckel who made the how-to that is the basis for this # script, see https://gist.github.com/thorsteneckel/c0610fb415c8d0486bce echo "This script will install forked-daapd in FreeBSD 10.1. The script is not" echo "very polished, so you might want to look through it before running it." read -p "Continue? [y/N] " yn if [ "$yn" != "y" ]; then exit fi DEPS="gmake autoconf automake libtool gettext gperf glib pkgconf wget git \ ffmpeg libconfuse libevent mxml libgcrypt libunistring libiconv \ libplist libinotify avahi sqlite3 alsa-lib libsodium" echo "The script can install the following dependency packages for you:" echo $DEPS read -p "Should the script install these packages? [y/N] " yn if [ "$yn" = "y" ]; then sudo pkg install $DEPS; fi JRE="openjdk8-jre" read -p "Should the script install $JRE for you? [y/N] " yn if [ "$yn" = "y" ]; then sudo pkg install $JRE; read -p "Should the script add the mount points to /etc/fstab that $JRE requests? [y/N] " yn if [ "$yn" = "y" ]; then sudo sh -c 'echo "fdesc /dev/fd fdescfs rw 0 0" >> /etc/fstab' sudo sh -c 'echo "proc /proc procfs rw 0 0" >> /etc/fstab' sudo mount /dev/fd sudo mount /proc fi fi WORKDIR=~/forked-daapd_build CONFIG=/usr/local/etc/forked-daapd.conf read -p "Should the script create $WORKDIR and use it for building? [Y/n] " yn if [ "$yn" = "n" ]; then exit fi mkdir -p $WORKDIR if [ ! -d $WORKDIR ]; then echo "Error creating $WORKDIR" exit fi cd $WORKDIR read -p "Should the script install antlr and libantlr3c? [y/N] " yn if [ "$yn" = "y" ]; then read -p "Should the script build libantlr3c for 64 bit? [Y/n] " yn if [ "$yn" != "n" ]; then ENABLE64BIT="--enable-64bit" fi wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/antlr-3.4-complete.jar wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/C/libantlr3c-3.4.tar.gz sudo install antlr-3.4-complete.jar /usr/local/share/java printf "#!/bin/sh export CLASSPATH CLASSPATH=\$CLASSPATH:/usr/local/share/java/antlr-3.4-complete.jar:/usr/local/share/java /usr/local/bin/java org.antlr.Tool \$* " > antlr3 sudo install -m 755 antlr3 /usr/local/bin tar xzf libantlr3c-3.4.tar.gz cd libantlr3c-3.4 ./configure $ENABLE64BIT && gmake && sudo gmake install cd $WORKDIR fi read -p "Should the script build forked-daapd? [y/N] " yn if [ "$yn" = "y" ]; then git clone https://github.com/ejurgensen/forked-daapd.git cd forked-daapd #Cleanup in case this is a re-run gmake clean git clean -f autoreconf -vi #These should no longer be required, but if you run into trouble you can try enabling them #export CC=cc #export LIBUNISTRING_CFLAGS=-I/usr/include #export LIBUNISTRING_LIBS="-L/usr/lib -lunistring" #export ZLIB_CFLAGS=-I/usr/include #export ZLIB_LIBS="-L/usr/lib -lz" export CFLAGS="-march=native -g -I/usr/local/include -I/usr/include" export LDFLAGS="-L/usr/local/lib -L/usr/lib" ./configure && gmake read -p "Should the script install forked-daapd and add service startup scripts? [y/N] " yn if [ "$yn" = "y" ]; then if [ -f $CONFIG ]; then echo "Backing up old config file to $CONFIG.bak" sudo cp "$CONFIG" "$CONFIG.bak" fi sudo gmake install sudo sed -i -- 's/\/var\/cache/\/usr\/local\/var\/cache/g' $CONFIG # Setup user and startup scripts echo "daapd::::::forked-daapd:/nonexistent:/usr/sbin/nologin:" | sudo adduser -w no -D -f - sudo chown -R daapd:daapd /usr/local/var/cache/forked-daapd if [ ! -f scripts/freebsd_start_10.1.sh ]; then echo "Could not find FreeBSD startup script" exit fi sudo install -m 755 scripts/freebsd_start_10.1.sh /usr/local/etc/rc.d/forked-daapd service forked-daapd enabled if [ $? -ne 0 ]; then sudo sh -c 'echo "forked_daapd_enable=\"YES\"" >> /etc/rc.conf' fi fi cd $WORKDIR fi read -p "Should the script enable and start dbus and avahi-daemon? [y/N] " yn if [ "$yn" = "y" ]; then service dbus enabled if [ $? -ne 0 ]; then sudo sh -c 'echo "dbus_enable=\"YES\"" >> /etc/rc.conf' fi sudo service dbus start service avahi-daemon enabled if [ $? -ne 0 ]; then sudo sh -c 'echo "avahi_daemon_enable=\"YES\"" >> /etc/rc.conf' fi sudo service avahi-daemon start fi read -p "Should the script (re)start forked-daapd and display the log output? [y/N] " yn if [ "$yn" = "y" ]; then sudo service forked-daapd restart tail -f /usr/local/var/log/forked-daapd.log fi forked-daapd-25.0/scripts/freebsd_start_10.1.sh000077500000000000000000000010431313447753700213110ustar00rootroot00000000000000#!/bin/sh # PROVIDE: forked-daapd # REQUIRE: avahi_daemon dbus # Add the following lines to /etc/rc.conf to enable `forked-daapd': # # forked_daapd_enable="YES" # forked_daapd_flags="" . /etc/rc.subr name="forked_daapd" rcvar=`set_rcvar` command="/usr/local/sbin/forked-daapd" command_args="-P /var/run/forked-daapd.pid" pidfile="/var/run/forked-daapd.pid" required_files="/usr/local/etc/forked-daapd.conf" # read configuration and set defaults load_rc_config "$name" : ${forked_daapd_enable="NO"} run_rc_command "$1" forked-daapd-25.0/scripts/pairinghelper.sh000077500000000000000000000065761313447753700206740ustar00rootroot00000000000000#!/bin/sh # Default config file conf_path="/etc/forked-daapd.conf" usage() { echo echo "Interactive script pair Remote with forked-daapd" echo echo "Usage: ${0##*/} -h | [ ]" echo echo "Parameters:" echo " -h Show this help" echo " Config file (default: $conf_path)" echo echo "NOTE: forked-daapd needs to be running..." exit 0 } case $1 in -h|--help) usage;; -*) echo "Unrecognized option $1 (try -h for usage)" exit 1 ;; esac [ -n "$1" ] && conf_path=$1 if [ ! -f "$conf_path" ]; then echo "Couldn't find config file '$conf_path' (try -h for usage)" exit 1 fi logfile=`awk '$1=="logfile"{print $3}' $conf_path` logfile="${logfile%\"}" logfile="${logfile#\"}" [ -z "$logfile" ] && logfile="/var/log/forked-daapd.log" if [ ! -r "$logfile" ]; then echo "Error: Couldn't read logfile '$logfile'" echo "Verify 'logfile' setting in config file '$conf_path'" exit 1 fi library_path=`awk '$1=="directories"{print}' $conf_path` library_path="${library_path#*\"}" library_path="${library_path%%\"*}" if [ -z "$library_path" ]; then echo "Couldn't find 'directories' setting in config file '$conf_path'" exit 1 fi if [ ! -d "$library_path" ]; then echo "Error: Couldn't find library '$library_path'" echo "Verify 'directories' setting in config file '$conf_path'" exit 1 fi rf="$library_path/pair.remote" [ -f "$rf" ] && rm -f "$rf" [ -f "$rf" ] && echo "Unable to remove existing pairing file '$rf'" && exit 1 echo "This script will help you pair Remote with forked-daapd" echo "Please verify that these paths are correct:" echo " Log file: '$logfile'" echo " Library: '$library_path'" read -p "Confirm? [Y/n] " yn case "$yn" in [N]*|[n]*) exit;; esac echo "Please start the pairing process in Remote by selecting Add library" read -p "Press ENTER when ready..." yn printf %s "Looking in $logfile for Remote announcement..." n=5 while [ $n -gt 0 ]; do n=`expr $n - 1` remote=`tail -50 "$logfile" | grep "Discovered remote" | tail -1 | grep -o "'.*' ("` remote="${remote%\'\ \(}" remote="${remote#\'}" [ -n "$remote" ] && break sleep 2 done if [ -z "$remote" ]; then echo "not found!" exit 1 fi echo "found" read -p "Ready to pair Remote '$remote', please enter PIN: " pin if [ -z "$pin" ]; then echo "Error: Invalid PIN" exit 1 fi echo "Writing pair.remote to $library_path..." printf "$pin" > "$rf" if [ ! -f "$rf" ]; then echo "Unable to create '$rf' - check directory permissions" exit 1 fi # leave enough time for deferred file processing on BSD n=20 echo "Waiting for pairing to complete (up to $n secs)..." while [ $n -gt 0 ]; do n=`expr $n - 1` result=`tail -1000 "$logfile" | sed -n "/.*remote:/ s,.*remote: ,,p" | awk '/^Discovered remote/{ f="" } /^Kickoff pairing with pin/ { f=$0; } END { print f }'` [ -n "$result" ] && break sleep 1 done if [ -z "$result" ]; then echo "forked-daap doesn't appear to be finding $rf..." echo "Check $logfile, removing pair.remote" rm "$rf" exit 1 fi echo "Pairing file pair.remote read, removing it" rm "$rf" n=5 while [ $n -gt 0 ]; do n=`expr $n - 1` result=`tail -1000 "$logfile" | sed -n "/.*remote:/ s,.*remote: ,,p" | awk '/^Discovered remote/{ f="" } /^Pairing succeeded/ { f=$0; } END { print f }'` if [ -n "$result" ]; then echo "All done" exit fi sleep 1 done echo "Pairing appears to have failed... check $rf for details" exit 1 forked-daapd-25.0/sqlext/000077500000000000000000000000001313447753700153175ustar00rootroot00000000000000forked-daapd-25.0/sqlext/Makefile.am000066400000000000000000000003571313447753700173600ustar00rootroot00000000000000pkglib_LTLIBRARIES = forked-daapd-sqlext.la forked_daapd_sqlext_la_SOURCES = sqlext.c forked_daapd_sqlext_la_LDFLAGS = -avoid-version -module -shared AM_CPPFLAGS += \ $(COMMON_CPPFLAGS) forked_daapd_sqlext_la_LIBADD = \ $(COMMON_LIBS) forked-daapd-25.0/sqlext/sqlext.c000066400000000000000000000135421313447753700170100ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * Copyright (C) 2010 Kai Elwert * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include SQLITE_EXTENSION_INIT1 /* * MurmurHash2, 64-bit versions, by Austin Appleby * * Code released under the public domain, as per * * as of 2010-01-03. */ #if SIZEOF_VOID_P == 8 /* 64bit platforms */ static uint64_t murmur_hash64(const void *key, int len, uint32_t seed) { const int r = 47; const uint64_t m = 0xc6a4a7935bd1e995; const uint64_t *data; const uint64_t *end; const unsigned char *data_tail; uint64_t h; uint64_t k; h = seed ^ (len * m); data = (const uint64_t *)key; end = data + (len / 8); while (data != end) { k = *data++; k *= m; k ^= k >> r; k *= m; h ^= k; h *= m; } data_tail = (const unsigned char *)data; switch (len & 7) { case 7: h ^= (uint64_t)(data_tail[6]) << 48; case 6: h ^= (uint64_t)(data_tail[5]) << 40; case 5: h ^= (uint64_t)(data_tail[4]) << 32; case 4: h ^= (uint64_t)(data_tail[3]) << 24; case 3: h ^= (uint64_t)(data_tail[2]) << 16; case 2: h ^= (uint64_t)(data_tail[1]) << 8; case 1: h ^= (uint64_t)(data_tail[0]); h *= m; } h ^= h >> r; h *= m; h ^= h >> r; return h; } #elif SIZEOF_VOID_P == 4 /* 32bit platforms */ static uint64_t murmur_hash64(const void *key, int len, uint32_t seed) { const int r = 24; const uint32_t m = 0x5bd1e995; const uint32_t *data; const unsigned char *data_tail; uint32_t k1; uint32_t h1; uint32_t k2; uint32_t h2; uint64_t h; h1 = seed ^ len; h2 = 0; data = (const uint32_t *)key; while (len >= 8) { k1 = *data++; k1 *= m; k1 ^= k1 >> r; k1 *= m; h1 *= m; h1 ^= k1; k2 = *data++; k2 *= m; k2 ^= k2 >> r; k2 *= m; h2 *= m; h2 ^= k2; len -= 8; } if (len >= 4) { k1 = *data++; k1 *= m; k1 ^= k1 >> r; k1 *= m; h1 *= m; h1 ^= k1; len -= 4; } data_tail = (const unsigned char *)data; switch(len) { case 3: h2 ^= (uint32_t)(data_tail[2]) << 16; case 2: h2 ^= (uint32_t)(data_tail[1]) << 8; case 1: h2 ^= (uint32_t)(data_tail[0]); h2 *= m; }; h1 ^= h2 >> 18; h1 *= m; h2 ^= h1 >> 22; h2 *= m; h1 ^= h2 >> 17; h1 *= m; h2 ^= h1 >> 19; h2 *= m; h = h1; h = (h << 32) | h2; return h; } #else # error Platform not supported #endif static void sqlext_daap_songalbumid_xfunc(sqlite3_context *pv, int n, sqlite3_value **ppv) { const char *album_artist; const char *album; char *hashbuf; sqlite3_int64 result; if (n != 2) { sqlite3_result_error(pv, "daap_songalbumid() requires 2 parameters, album_artist and album", -1); return; } if ((sqlite3_value_type(ppv[0]) != SQLITE_TEXT) || (sqlite3_value_type(ppv[1]) != SQLITE_TEXT)) { sqlite3_result_error(pv, "daap_songalbumid() requires 2 text parameters", -1); return; } album_artist = (const char *)sqlite3_value_text(ppv[0]); album = (const char *)sqlite3_value_text(ppv[1]); hashbuf = sqlite3_mprintf("%s==%s", (album_artist) ? album_artist : "", (album) ? album : ""); if (!hashbuf) { sqlite3_result_error(pv, "daap_songalbumid() out of memory for hashbuf", -1); return; } /* Limit hash length to 63 bits, due to signed type in sqlite */ result = murmur_hash64(hashbuf, strlen(hashbuf), 0) >> 1; sqlite3_free(hashbuf); sqlite3_result_int64(pv, result); } static int sqlext_daap_unicode_xcollation(void *notused, int llen, const void *left, int rlen, const void *right) { ucs4_t lch; ucs4_t rch; int lalpha; int ralpha; int rpp; int ret; /* Extract first utf-8 character */ ret = u8_mbtoucr(&lch, (const uint8_t *)left, llen); if (ret < 0) return 0; ret = u8_mbtoucr(&rch, (const uint8_t *)right, rlen); if (ret < 0) return 0; /* Ensure digits and other non-alphanum sort to tail */ lalpha = uc_is_alpha(lch); ralpha = uc_is_alpha(rch); if (!lalpha && ralpha) return 1; else if (lalpha && !ralpha) return -1; /* Compare case and normalization insensitive */ ret = u8_casecmp((const uint8_t *)left, llen, (const uint8_t*)right, rlen, NULL, UNINORM_NFD, &rpp); if (ret < 0) return 0; return rpp; } int sqlite3_extension_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) { SQLITE_EXTENSION_INIT2(pApi); int ret; ret = sqlite3_create_function(db, "daap_songalbumid", 2, SQLITE_UTF8, NULL, sqlext_daap_songalbumid_xfunc, NULL, NULL); if (ret != SQLITE_OK) { if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Could not create daap_songalbumid function: %s\n", sqlite3_errmsg(db)); return -1; } ret = sqlite3_create_collation(db, "DAAP", SQLITE_UTF8, NULL, sqlext_daap_unicode_xcollation); if (ret != SQLITE_OK) { if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Could not create sqlite3 custom collation DAAP: %s\n", sqlite3_errmsg(db)); return -1; } return 0; } forked-daapd-25.0/src/000077500000000000000000000000001313447753700145665ustar00rootroot00000000000000forked-daapd-25.0/src/.gitignore000066400000000000000000000002071313447753700165550ustar00rootroot00000000000000forked-daapd *.tokens *Lexer.[ch] *Parser.[ch] *2SQL.[ch] *.u daap_query_hash.h rsp_query_hash.h dacp_prop_hash.h dmap_fields_hash.h forked-daapd-25.0/src/DAAP.g000066400000000000000000000032731313447753700154500ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * 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 */ grammar DAAP; options { output = AST; ASTLabelType = pANTLR3_BASE_TREE; language = C; } query : expr NEWLINE? EOF -> expr ; expr : aexpr (OPOR^ aexpr)* ; aexpr : crit (OPAND^ crit)* ; crit : LPAR expr RPAR -> expr | STR ; QUOTE : '\''; LPAR : '('; RPAR : ')'; OPAND : '+' | ' '; OPOR : ','; NEWLINE : '\r'? '\n'; /* Unescaping adapted from (ported to the C runtime) */ STR @init{ pANTLR3_STRING unesc = GETTEXT()->factory->newRaw(GETTEXT()->factory); } : QUOTE ( reg = ~('\\' | '\'') { unesc->addc(unesc, reg); } | esc = ESCAPED { unesc->appendS(unesc, GETTEXT()); } )+ QUOTE { SETTEXT(unesc); }; fragment ESCAPED : '\\' ( '\\' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\\")); } | '\'' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\'")); } ) ; forked-daapd-25.0/src/DAAP2SQL.g000066400000000000000000000221021313447753700161020ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * 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 */ tree grammar DAAP2SQL; options { tokenVocab = DAAP; ASTLabelType = pANTLR3_BASE_TREE; language = C; } @header { #include #include #include #include #include #include "logger.h" #include "db.h" #include "daap_query.h" } @members { struct dmap_query_field_map { char *dmap_field; char *db_col; int as_int; }; /* gperf static hash, daap_query.gperf */ #include "daap_query_hash.h" } query returns [ pANTLR3_STRING result ] @init { $result = NULL; } : e = expr { if (!$e.valid) { $result = NULL; } else { $result = $e.result->factory->newRaw($e.result->factory); $result->append8($result, "("); $result->appendS($result, $e.result); $result->append8($result, ")"); } } ; expr returns [ pANTLR3_STRING result, int valid ] @init { $result = NULL; $valid = 1; } : ^(OPAND a = expr b = expr) { if ($a.valid && $b.valid) { $result = $a.result->factory->newRaw($a.result->factory); $result->append8($result, "("); $result->appendS($result, $a.result); $result->append8($result, " AND "); $result->appendS($result, $b.result); $result->append8($result, ")"); } else if ($a.valid) { $result = $a.result->factory->newRaw($a.result->factory); $result->appendS($result, $a.result); } else if ($b.valid) { $result = $b.result->factory->newRaw($b.result->factory); $result->appendS($result, $b.result); } else { $valid = 0; } } | ^(OPOR a = expr b = expr) { if ($a.valid && $b.valid) { $result = $a.result->factory->newRaw($a.result->factory); $result->append8($result, "("); $result->appendS($result, $a.result); $result->append8($result, " OR "); $result->appendS($result, $b.result); $result->append8($result, ")"); } else if ($a.valid) { $result = $a.result->factory->newRaw($a.result->factory); $result->appendS($result, $a.result); } else if ($b.valid) { $result = $b.result->factory->newRaw($b.result->factory); $result->appendS($result, $b.result); } else { $valid = 0; } } | STR { pANTLR3_STRING str; pANTLR3_UINT8 field; pANTLR3_UINT8 val; pANTLR3_UINT8 escaped; ANTLR3_UINT8 op; int neg_op; const struct dmap_query_field_map *dqfm; char *end; long long llval; escaped = NULL; $result = $STR.text->factory->newRaw($STR.text->factory); str = $STR.text->toUTF8($STR.text); /* NOTE: the lexer delivers the string without quotes which may not be obvious from the grammar due to embedded code */ /* Make daap.songalbumid:0 a no-op */ if (strcmp((char *)str->chars, "daap.songalbumid:0") == 0) { $result->append8($result, "1 = 1"); goto STR_out; } field = str->chars; val = field; while ((*val != '\0') && ((*val == '.') || (*val == '-') || ((*val >= 'a') && (*val <= 'z')) || ((*val >= 'A') && (*val <= 'Z')) || ((*val >= '0') && (*val <= '9')))) { val++; } if (*field == '\0') { DPRINTF(E_LOG, L_DAAP, "No field name found in clause '\%s'\n", field); $valid = 0; goto STR_result_valid_0; /* ABORT */ } if (*val == '\0') { DPRINTF(E_LOG, L_DAAP, "No operator found in clause '\%s'\n", field); $valid = 0; goto STR_result_valid_0; /* ABORT */ } op = *val; *val = '\0'; val++; if (op == '!') { if (*val == '\0') { DPRINTF(E_LOG, L_DAAP, "Negation found but operator missing in clause '\%s\%c'\n", field, op); $valid = 0; goto STR_result_valid_0; /* ABORT */ } neg_op = 1; op = *val; val++; } else neg_op = 0; /* Lookup DMAP field in the query field map */ dqfm = daap_query_field_lookup((char *)field, strlen((char *)field)); if (!dqfm) { DPRINTF(E_LOG, L_DAAP, "DMAP field '\%s' is not a valid field in queries\n", field); $valid = 0; goto STR_result_valid_0; /* ABORT */ } /* Empty values OK for string fields, NOK for integer */ if (*val == '\0') { if (dqfm->as_int) { DPRINTF(E_LOG, L_DAAP, "No value given in clause '\%s\%s\%c'\n", field, (neg_op) ? "!" : "", op); $valid = 0; goto STR_result_valid_0; /* ABORT */ } /* No need to exclude empty artist and album, as forked-daapd makes sure there always exists an artist/album. */ if (neg_op && op == ':' && (strcmp((char *)field, "daap.songalbumartist") == 0 || strcmp((char *)field, "daap.songartist") == 0 || strcmp((char *)field, "daap.songalbum") == 0)) { DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c'\n", field, (neg_op) ? "!" : "", op); $valid = 0; goto STR_result_valid_0; } /* Need to check against NULL too */ if (op == ':') $result->append8($result, "("); } /* Int field: check integer conversion */ if (dqfm->as_int) { errno = 0; llval = strtoll((const char *)val, &end, 10); if (((errno == ERANGE) && ((llval == LLONG_MAX) || (llval == LLONG_MIN))) || ((errno != 0) && (llval == 0))) { DPRINTF(E_LOG, L_DAAP, "Value '\%s' in clause '\%s\%s\%c\%s' does not convert to an integer type\n", val, field, (neg_op) ? "!" : "", op, val); $valid = 0; goto STR_result_valid_0; /* ABORT */ } if (end == (char *)val) { DPRINTF(E_LOG, L_DAAP, "Value '\%s' in clause '\%s\%s\%c\%s' does not represent an integer value\n", val, field, (neg_op) ? "!" : "", op, val); $valid = 0; goto STR_result_valid_0; /* ABORT */ } *end = '\0'; /* Cut out potential garbage - we're being kind */ /* forked-daapd only has media_kind = 1 for music - so remove media_kind = 32 to imporve select query performance. */ if (llval == 32 && (strcmp((char *)field, "com.apple.itunes.mediakind") == 0 || strcmp((char *)field, "com.apple.itunes.extended-media-kind") == 0)) { DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c\%s'\n", field, (neg_op) ? "!" : "", op, val); if (neg_op) $result->append8($result, "1 = 1"); else $result->append8($result, "1 = 0"); goto STR_out; } } /* String field: escape string, check for '*' */ else { if (op != ':') { DPRINTF(E_LOG, L_DAAP, "Operation '\%c' not valid for string values\n", op); $valid = 0; goto STR_result_valid_0; /* ABORT */ } escaped = (pANTLR3_UINT8)db_escape_string((char *)val); if (!escaped) { DPRINTF(E_LOG, L_DAAP, "Could not escape value\n"); $valid = 0; goto STR_result_valid_0; /* ABORT */ } val = escaped; if (val[0] == '*') { op = '\%'; val[0] = '\%'; } if (val[0] && val[1] && val[strlen((char *)val) - 1] == '*') { op = '\%'; val[strlen((char *)val) - 1] = '\%'; } } $result->append8($result, dqfm->db_col); switch(op) { case ':': if (neg_op) $result->append8($result, " <> "); else $result->append8($result, " = "); break; case '+': if (neg_op) $result->append8($result, " <= "); else $result->append8($result, " > "); break; case '-': if (neg_op) $result->append8($result, " >= "); else $result->append8($result, " < "); break; case '\%': $result->append8($result, " LIKE "); break; default: if (neg_op) DPRINTF(E_LOG, L_DAAP, "Missing or unknown operator '\%c' in clause '\%s!\%c\%s'\n", op, field, op, val); else DPRINTF(E_LOG, L_DAAP, "Unknown operator '\%c' in clause '\%s\%c\%s'\n", op, field, op, val); $valid = 0; goto STR_result_valid_0; /* ABORT */ break; } if (!dqfm->as_int) $result->append8($result, "'"); $result->append8($result, (const char *)val); if (!dqfm->as_int) $result->append8($result, "'"); /* For empty string value, we need to check against NULL too */ if ((*val == '\0') && (op == ':')) { if (neg_op) $result->append8($result, " AND "); else $result->append8($result, " OR "); $result->append8($result, dqfm->db_col); if (neg_op) $result->append8($result, " IS NOT NULL"); else $result->append8($result, " IS NULL"); $result->append8($result, ")"); } STR_result_valid_0: /* bail out label */ ; if (escaped) free(escaped); STR_out: /* get out of here */ ; } ; forked-daapd-25.0/src/Makefile.am000066400000000000000000000075231313447753700166310ustar00rootroot00000000000000 sbin_PROGRAMS = forked-daapd if COND_ITUNES ITUNES_SRC=library/filescanner_itunes.c endif if COND_SPOTIFY SPOTIFY_SRC=spotify.c spotify.h spotify_webapi.c spotify_webapi.h inputs/spotify.c endif if COND_LASTFM LASTFM_SRC=lastfm.c lastfm.h endif if COND_CHROMECAST if COND_PROTOBUF_OLD CHROMECAST_SRC=outputs/cast.c outputs/cast_channel.v0.pb-c.h outputs/cast_channel.v0.pb-c.c else CHROMECAST_SRC=outputs/cast.c outputs/cast_channel.pb-c.h outputs/cast_channel.pb-c.c endif endif if COND_MPD MPD_SRC=mpd.c mpd.h endif if COND_RAOP_VERIFICATION RAOP_VERIFICATION_SRC=outputs/raop_verification.c outputs/raop_verification.h endif if COND_ALSA ALSA_SRC=outputs/alsa.c endif if COND_PULSEAUDIO PULSEAUDIO_SRC=outputs/pulse.c endif if COND_AVAHI MDNS_SRC=mdns_avahi.c else MDNS_SRC=mdns_dnssd.c endif GPERF_FILES = \ daap_query.gperf \ rsp_query.gperf \ dacp_prop.gperf \ dmap_fields.gperf GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h) ANTLR_GRAMMARS = \ RSP.g RSP2SQL.g \ DAAP.g DAAP2SQL.g \ SMARTPL.g SMARTPL2SQL.g ANTLR_TOKENS = $(ANTLR_GRAMMARS:.g=.tokens) ANTLR_DEPS = $(ANTLR_GRAMMARS:%.g=$(srcdir)/%.u) ANTLR_SRC = \ RSPLexer.c RSPLexer.h RSPParser.c RSPParser.h \ RSP2SQL.c RSP2SQL.h \ DAAPLexer.c DAAPLexer.h DAAPParser.c DAAPParser.h \ DAAP2SQL.c DAAP2SQL.h \ SMARTPLLexer.c SMARTPLLexer.h SMARTPLParser.c SMARTPLParser.h \ SMARTPL2SQL.c SMARTPL2SQL.h AM_CPPFLAGS += \ $(FORKED_CPPFLAGS) \ $(FORKED_OPTS_CPPFLAGS) \ $(COMMON_CPPFLAGS) \ \ -D_GNU_SOURCE \ -DDATADIR=\"$(pkgdatadir)\" \ -DCONFDIR=\"$(sysconfdir)\" \ -DSTATEDIR=\"$(localstatedir)\" \ -DPKGLIBDIR=\"$(pkglibdir)\" forked_daapd_LDADD = \ $(FORKED_LIBS) \ $(FORKED_OPTS_LIBS) \ $(COMMON_LIBS) forked_daapd_SOURCES = main.c \ db.c db.h \ db_init.c db_init.h \ db_upgrade.c db_upgrade.h \ logger.c logger.h \ conffile.c conffile.h \ cache.c cache.h \ library/filescanner.c library/filescanner.h \ library/filescanner_ffmpeg.c library/filescanner_playlist.c \ library/filescanner_smartpl.c $(ITUNES_SRC) \ library.c library.h \ $(MDNS_SRC) mdns.h \ remote_pairing.c remote_pairing.h \ avio_evbuffer.c avio_evbuffer.h \ httpd.c httpd.h \ httpd_rsp.c httpd_rsp.h \ httpd_daap.c httpd_daap.h \ httpd_dacp.c httpd_dacp.h \ httpd_streaming.c httpd_streaming.h \ http.c http.h \ dmap_common.c dmap_common.h \ transcode.c transcode.h \ artwork.c artwork.h \ misc.c misc.h \ rng.c rng.h \ rsp_query.c rsp_query.h \ daap_query.c daap_query.h \ player.c player.h \ worker.c worker.h \ input.h input.c \ inputs/file_http.c inputs/pipe.c \ outputs.h outputs.c \ outputs/raop.c $(RAOP_VERIFICATION_SRC) \ outputs/streaming.c outputs/dummy.c outputs/fifo.c \ $(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \ evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \ $(SPOTIFY_SRC) \ $(LASTFM_SRC) \ $(MPD_SRC) \ listener.c listener.h \ commands.c commands.h \ ffmpeg-compat.h mxml-compat.h \ $(GPERF_SRC) \ $(ANTLR_SRC) # built by maintainers, and distributed. Clean with maintainer-clean BUILT_SOURCES = \ $(GPERF_SRC) \ $(ANTLR_SRC) \ $(ANTLR_TOKENS) \ $(ANTLR_DEPS) EXTRA_DIST = \ $(GPERF_FILES) \ $(ANTLR_GRAMMARS) \ $(ANTLR_TOKENS) \ $(ANTLR_DEPS) # gperf construction rules %_hash.h: %.gperf $(AM_V_GEN)$(GPERF) --output-file=$@ $< # silent rules for antlr antlr_verbose = $(antlr_verbose_@AM_V@) antlr_verbose_ = $(antlr_verbose_@AM_DEFAULT_V@) antlr_verbose_0 = @echo " GEN " $< "products"; # ANTLR grammar products %.tokens %.c %Lexer.c %Parser.c %Lexer.h %Parser.h %.h: %.g $(antlr_verbose)$(ANTLR) -Xconversiontimeout 30000 $(ANTLR_OPTIONS) -fo . $< # ANTLR dependency files (bypass circular dependency of .g on .tokens) %.u: %.g $(AM_V_GEN)$(ANTLR) -depend -fo . $< > $@ $(AM_V_at)$(SED) -n -e '/^.*\.g[ ]*:\(.*\)/ { s//\1/;h;d; }' -e '/\.tokens.*:/ { p;d; }' -e '/:/ { G;s/\n/ /;p; }' $@ > $@-t $(AM_V_at)mv $@-t $@ -include $(ANTLR_DEPS) forked-daapd-25.0/src/RSP.g000066400000000000000000000060171313447753700154060ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * 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 */ grammar RSP; options { output = AST; ASTLabelType = pANTLR3_BASE_TREE; language = C; } query : expr NEWLINE? EOF -> expr ; expr : aexpr (OR^ aexpr)* ; aexpr : crit (AND^ crit)* ; crit : LPAR expr RPAR -> expr | strcrit | intcrit | datecrit ; strcrit : FIELD strop STR -> ^(strop FIELD STR) | FIELD NOT strop STR -> ^(NOT ^(strop FIELD STR)) ; strop : equal=EQUAL | includes=INCLUDES | startsw=STARTSW | endsw=ENDSW ; intcrit : FIELD intop INT -> ^(intop FIELD INT) | FIELD NOT intop INT -> ^(NOT ^(intop FIELD INT)) ; intop : equal=EQUAL | less=LESS | greater=GREATER | lte=LTE | gte=GTE ; datecrit: FIELD dateop datespec -> ^(dateop FIELD datespec) ; dateop : before=BEFORE | after=AFTER ; datespec: dateref | INT dateintval dateop dateref -> ^(dateop dateref INT dateintval) ; dateref : date=DATE | today=TODAY ; dateintval : day=DAY | week=WEEK | month=MONTH | year=YEAR ; QUOTE : '"'; LPAR : '('; RPAR : ')'; AND : 'and'; OR : 'or'; NOT : '!'; /* Both string & int */ EQUAL : '='; /* String */ INCLUDES: 'includes'; STARTSW : 'startswith'; ENDSW : 'endswith'; /* Int */ GREATER : '>'; LESS : '<'; GTE : '>='; LTE : '<='; /* Date */ BEFORE : 'before'; AFTER : 'after'; DAY : 'day' | 'days'; WEEK : 'week' | 'weeks'; MONTH : 'month' | 'months'; YEAR : 'year' | 'years'; TODAY : 'today'; NEWLINE : '\r'? '\n'; WS : (' ' | '\t') { $channel = HIDDEN; }; FIELD : 'a'..'z' ('a'..'z' | '_')* 'a'..'z'; INT : DIGIT19 DIGIT09*; /* YYYY-MM-DD */ DATE : DIGIT19 DIGIT09 DIGIT09 DIGIT09 '-' ('0' DIGIT19 | '1' '0'..'2') '-' ('0' DIGIT19 | '1'..'2' DIGIT09 | '3' '0'..'1'); /* Unescaping adapted from (ported to the C runtime) */ STR @init{ pANTLR3_STRING unesc = GETTEXT()->factory->newRaw(GETTEXT()->factory); } : QUOTE ( reg = ~('\\' | '"') { unesc->addc(unesc, reg); } | esc = ESCAPED { unesc->appendS(unesc, GETTEXT()); } )+ QUOTE { SETTEXT(unesc); } ; fragment ESCAPED : '\\' ( '\\' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\\")); } | '"' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\"")); } ) ; fragment DIGIT09 : '0'..'9'; fragment DIGIT19 : '1'..'9'; forked-daapd-25.0/src/RSP2SQL.g000066400000000000000000000240061313447753700160460ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * 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 */ tree grammar RSP2SQL; options { tokenVocab = RSP; ASTLabelType = pANTLR3_BASE_TREE; language = C; } @header { /* Needs #define _GNU_SOURCE for strptime() */ #include #include #include #include #include "logger.h" #include "db.h" #include "misc.h" #include "rsp_query.h" } @members { #define RSP_TYPE_STRING 0 #define RSP_TYPE_INT 1 #define RSP_TYPE_DATE 2 struct rsp_query_field_map { char *rsp_field; int field_type; /* RSP fields are named after the DB columns - or vice versa */ }; /* gperf static hash, rsp_query.gperf */ #include "rsp_query_hash.h" } query returns [ pANTLR3_STRING result ] @init { $result = NULL; } : e = expr { if (!$e.valid) { $result = NULL; } else { $result = $e.result->factory->newRaw($e.result->factory); $result->append8($result, "("); $result->appendS($result, $e.result); $result->append8($result, ")"); } } ; expr returns [ pANTLR3_STRING result, int valid ] @init { $result = NULL; $valid = 1; } : ^(AND a = expr b = expr) { if (!$a.valid || !$b.valid) { $valid = 0; } else { $result = $a.result->factory->newRaw($a.result->factory); $result->append8($result, "("); $result->appendS($result, $a.result); $result->append8($result, " AND "); $result->appendS($result, $b.result); $result->append8($result, ")"); } } | ^(OR a = expr b = expr) { if (!$a.valid || !$b.valid) { $valid = 0; } else { $result = $a.result->factory->newRaw($a.result->factory); $result->append8($result, "("); $result->appendS($result, $a.result); $result->append8($result, " OR "); $result->appendS($result, $b.result); $result->append8($result, ")"); } } | c = strcrit { $valid = $c.valid; $result = $c.result; } | ^(NOT c = strcrit) { if (!$c.valid || !$c.result) { $valid = 0; } else { $result = $c.result->factory->newRaw($c.result->factory); $result->append8($result, "(NOT "); $result->appendS($result, $c.result); $result->append8($result, ")"); } } | i = intcrit { $valid = $i.valid; $result = $i.result; } | ^(NOT i = intcrit) { if (!$i.valid || !$i.result) { $valid = 0; } else { $result = $i.result->factory->newRaw($i.result->factory); $result->append8($result, "(NOT "); $result->appendS($result, $i.result); $result->append8($result, ")"); } } | d = datecrit { $valid = $d.valid; $result = $d.result; } ; strcrit returns [ pANTLR3_STRING result, int valid ] @init { $result = NULL; $valid = 1; } : ^(o = strop f = FIELD s = STR) { char *op; const struct rsp_query_field_map *rqfp; pANTLR3_STRING field; char *escaped; ANTLR3_UINT32 optok; escaped = NULL; op = NULL; optok = $o.op->getType($o.op); switch (optok) { case EQUAL: op = " = "; break; case INCLUDES: case STARTSW: case ENDSW: op = " LIKE "; break; } field = $f->getText($f); /* Field lookup */ rqfp = rsp_query_field_lookup((char *)field->chars, strlen((char *)field->chars)); if (!rqfp) { DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a valid field in queries\n", field->chars); $valid = 0; goto strcrit_valid_0; /* ABORT */ } /* Check field type */ if (rqfp->field_type != RSP_TYPE_STRING) { DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a string field\n", field->chars); $valid = 0; goto strcrit_valid_0; /* ABORT */ } escaped = db_escape_string((char *)$s->getText($s)->chars); if (!escaped) { DPRINTF(E_LOG, L_RSP, "Could not escape value\n"); $valid = 0; goto strcrit_valid_0; /* ABORT */ } $result = field->factory->newRaw(field->factory); $result->append8($result, "f."); $result->appendS($result, field); $result->append8($result, op); $result->append8($result, "'"); if ((optok == INCLUDES) || (optok == STARTSW)) $result->append8($result, "\%"); $result->append8($result, escaped); if ((optok == INCLUDES) || (optok == ENDSW)) $result->append8($result, "\%"); $result->append8($result, "'"); strcrit_valid_0: ; if (escaped) free(escaped); } ; strop returns [ pANTLR3_COMMON_TOKEN op ] @init { $op = NULL; } : n = EQUAL { $op = $n->getToken($n); } | n = INCLUDES { $op = $n->getToken($n); } | n = STARTSW { $op = $n->getToken($n); } | n = ENDSW { $op = $n->getToken($n); } ; intcrit returns [ pANTLR3_STRING result, int valid ] @init { $result = NULL; $valid = 1; } : ^(o = intop f = FIELD i = INT) { char *op; const struct rsp_query_field_map *rqfp; pANTLR3_STRING field; op = NULL; switch ($o.op->getType($o.op)) { case EQUAL: op = " = "; break; case LESS: op = " < "; break; case GREATER: op = " > "; break; case LTE: op = " <= "; break; case GTE: op = " >= "; break; } field = $f->getText($f); /* Field lookup */ rqfp = rsp_query_field_lookup((char *)field->chars, strlen((char *)field->chars)); if (!rqfp) { DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a valid field in queries\n", field->chars); $valid = 0; goto intcrit_valid_0; /* ABORT */ } /* Check field type */ if (rqfp->field_type != RSP_TYPE_INT) { DPRINTF(E_LOG, L_RSP, "Field '\%s' is not an integer field\n", field->chars); $valid = 0; goto intcrit_valid_0; /* ABORT */ } $result = field->factory->newRaw(field->factory); $result->append8($result, "f."); $result->appendS($result, field); $result->append8($result, op); $result->appendS($result, $i->getText($i)); intcrit_valid_0: ; } ; intop returns [ pANTLR3_COMMON_TOKEN op ] @init { $op = NULL; } : n = EQUAL { $op = $n->getToken($n); } | n = LESS { $op = $n->getToken($n); } | n = GREATER { $op = $n->getToken($n); } | n = LTE { $op = $n->getToken($n); } | n = GTE { $op = $n->getToken($n); } ; datecrit returns [ pANTLR3_STRING result, int valid ] @init { $result = NULL; $valid = 1; } : ^(o = dateop f = FIELD d = datespec) { char *op; const struct rsp_query_field_map *rqfp; pANTLR3_STRING field; char buf[32]; int ret; op = NULL; switch ($o.op->getType($o.op)) { case BEFORE: op = " < "; break; case AFTER: op = " > "; break; } field = $f->getText($f); /* Field lookup */ rqfp = rsp_query_field_lookup((char *)field->chars, strlen((char *)field->chars)); if (!rqfp) { DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a valid field in queries\n", field->chars); $valid = 0; goto datecrit_valid_0; /* ABORT */ } /* Check field type */ if (rqfp->field_type != RSP_TYPE_DATE) { DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a date field\n", field->chars); $valid = 0; goto datecrit_valid_0; /* ABORT */ } ret = snprintf(buf, sizeof(buf), "\%ld", $d.date); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_RSP, "Date \%ld too large for buffer, oops!\n", $d.date); $valid = 0; goto datecrit_valid_0; /* ABORT */ } $result = field->factory->newRaw(field->factory); $result->append8($result, "f."); $result->appendS($result, field); $result->append8($result, op); $result->append8($result, buf); datecrit_valid_0: ; } ; dateop returns [ pANTLR3_COMMON_TOKEN op ] @init { $op = NULL; } : n = BEFORE { $op = $n->getToken($n); } | n = AFTER { $op = $n->getToken($n); } ; datespec returns [ time_t date, int valid ] @init { $date = 0; $valid = 1; } : r = dateref { if (!$r.valid) $valid = 0; else $date = $r.date; } | ^(o = dateop r = dateref m = INT i = dateintval) { int32_t val; int ret; if (!$r.valid || !$i.valid) { $valid = 0; goto datespec_valid_0; /* ABORT */ } ret = safe_atoi32((char *)$m->getText($m)->chars, &val); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not convert '\%s' to integer\n", (char *)$m->getText($m)); $valid = 0; goto datespec_valid_0; /* ABORT */ } switch ($o.op->getType($o.op)) { case BEFORE: $date = $r.date - (val * $i.period); break; case AFTER: $date = $r.date + (val * $i.period); break; } datespec_valid_0: ; } ; dateref returns [ time_t date, int valid ] @init { $date = 0; $valid = 1; } : n = DATE { struct tm tm; char *ret; ret = strptime((char *)$n->getText($n), "\%Y-\%m-\%d", &tm); if (!ret) { DPRINTF(E_LOG, L_RSP, "Date '\%s' could not be interpreted\n", (char *)$n->getText($n)); $valid = 0; goto dateref_valid_0; /* ABORT */ } else { if (*ret != '\0') DPRINTF(E_LOG, L_RSP, "Garbage at end of date '\%s' ?!\n", (char *)$n->getText($n)); $date = mktime(&tm); if ($date == (time_t) -1) { DPRINTF(E_LOG, L_RSP, "Date '\%s' could not be converted to an epoch\n", (char *)$n->getText($n)); $valid = 0; goto dateref_valid_0; /* ABORT */ } } dateref_valid_0: ; } | TODAY { $date = time(NULL); } ; dateintval returns [ time_t period, int valid ] @init { $period = 0; $valid = 1; } : DAY { $period = 24 * 60 * 60; } | WEEK { $period = 7 * 24 * 60 * 60; } | MONTH { $period = 30 * 24 * 60 * 60; } | YEAR { $period = 365 * 24 * 60 * 60; } ; forked-daapd-25.0/src/SMARTPL.g000066400000000000000000000051061313447753700160620ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ grammar SMARTPL; options { output = AST; ASTLabelType = pANTLR3_BASE_TREE; language = C; } playlist : STR '{' expression '}' EOF ; expression : aexpr (OR^ aexpr)* ; aexpr : nexpr (AND^ nexpr)* ; nexpr : NOT^ crit | crit ; crit : LPAR expression RPAR -> expression | STRTAG (INCLUDES|IS) STR | INTTAG INTBOOL INT | DATETAG (AFTER|BEFORE) dateval | ENUMTAG IS ENUMVAL ; dateval : DATE | interval BEFORE DATE | interval AFTER DATE | interval AGO ; interval : INT DATINTERVAL ; STRTAG : 'artist' | 'album_artist' | 'album' | 'title' | 'genre' | 'composer' | 'path' | 'type' | 'grouping' ; INTTAG : 'play_count' | 'rating' | 'year' | 'compilation' ; DATETAG : 'time_added' | 'time_modified' | 'time_played' ; ENUMTAG : 'data_kind' | 'media_kind' ; INCLUDES : 'includes' ; IS : 'is' ; INTBOOL : (GREATER|GREATEREQUAL|LESS|LESSEQUAL|EQUAL) ; fragment GREATER : '>' ; fragment GREATEREQUAL: '>=' ; fragment LESS : '<' ; fragment LESSEQUAL : '<=' ; fragment EQUAL : '=' ; AFTER : 'after' ; BEFORE : 'before' ; AGO : 'ago' ; AND : 'AND' | 'and' ; OR : 'OR' | 'or' ; NOT : 'NOT' | 'not' ; LPAR : '(' ; RPAR : ')' ; DATE : ('0'..'9')('0'..'9')('0'..'9')('0'..'9')'-'('0'..'1')('0'..'9')'-'('0'..'3')('0'..'9') | 'today' | 'yesterday' | 'last week' | 'last month' | 'last year' ; DATINTERVAL : 'days' | 'weeks' | 'months' | 'years' ; ENUMVAL : 'music' | 'movie' | 'podcast' | 'audiobook' | 'tvshow' | 'file' | 'url' | 'spotify' | 'pipe' ; STR : '"' ~('"')+ '"' ; INT : ('0'..'9')+ ; WHITESPACE : ('\t'|' '|'\r'|'\n'|'\u000C') { $channel = HIDDEN; } ; forked-daapd-25.0/src/SMARTPL2SQL.g000066400000000000000000000216411313447753700165260ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ tree grammar SMARTPL2SQL; options { tokenVocab = SMARTPL; ASTLabelType = pANTLR3_BASE_TREE; language = C; } @header { #include #include #include #include #include #include #include #include "logger.h" #include "db.h" } @members { } playlist returns [ pANTLR3_STRING title, pANTLR3_STRING query ] @init { $title = NULL; $query = NULL; } : STR '{' e = expression '}' { pANTLR3_UINT8 val; val = $STR.text->toUTF8($STR.text)->chars; val++; val[strlen((const char *)val) - 1] = '\0'; $title = $STR.text->factory->newRaw($STR.text->factory); $title->append8($title, (const char *)val); $query = $e.result->factory->newRaw($e.result->factory); $query->append8($query, "("); $query->appendS($query, $e.result); $query->append8($query, ")"); } ; expression returns [ pANTLR3_STRING result ] @init { $result = NULL; } : ^(NOT a = expression) { $result = $a.result->factory->newRaw($a.result->factory); $result->append8($result, "NOT("); $result->appendS($result, $a.result); $result->append8($result, ")"); } | ^(AND a = expression b = expression) { $result = $a.result->factory->newRaw($a.result->factory); $result->append8($result, "("); $result->appendS($result, $a.result); $result->append8($result, " AND "); $result->appendS($result, $b.result); $result->append8($result, ")"); } | ^(OR a = expression b = expression) { $result = $a.result->factory->newRaw($a.result->factory); $result->append8($result, "("); $result->appendS($result, $a.result); $result->append8($result, " OR "); $result->appendS($result, $b.result); $result->append8($result, ")"); } | STRTAG INCLUDES STR { pANTLR3_UINT8 val; val = $STR.text->toUTF8($STR.text)->chars; val++; val[strlen((const char *)val) - 1] = '\0'; $result = $STR.text->factory->newRaw($STR.text->factory); $result->append8($result, "f."); $result->appendS($result, $STRTAG.text->toUTF8($STRTAG.text)); $result->append8($result, " LIKE '\%"); $result->append8($result, sqlite3_mprintf("\%q", (const char *)val)); $result->append8($result, "\%'"); } | STRTAG IS STR { pANTLR3_UINT8 val; val = $STR.text->toUTF8($STR.text)->chars; val++; val[strlen((const char *)val) - 1] = '\0'; $result = $STR.text->factory->newRaw($STR.text->factory); $result->append8($result, "f."); $result->appendS($result, $STRTAG.text->toUTF8($STRTAG.text)); $result->append8($result, " LIKE '"); $result->append8($result, sqlite3_mprintf("\%q", (const char *)val)); $result->append8($result, "'"); } | INTTAG INTBOOL INT { $result = $INTTAG.text->factory->newRaw($INTTAG.text->factory); $result->append8($result, "f."); $result->appendS($result, $INTTAG.text->toUTF8($INTTAG.text)); $result->append8($result, " "); $result->appendS($result, $INTBOOL.text->toUTF8($INTBOOL.text)); $result->append8($result, " "); $result->appendS($result, $INT.text->toUTF8($INT.text)); } | DATETAG AFTER dateval { char str[15]; sprintf(str, "\%d", $dateval.result); $result = $DATETAG.text->factory->newRaw($DATETAG.text->factory); $result->append8($result, "f."); $result->appendS($result, $DATETAG.text->toUTF8($DATETAG.text)); $result->append8($result, " > "); $result->append8($result, str); } | DATETAG BEFORE dateval { char str[15]; sprintf(str, "\%d", $dateval.result); $result = $DATETAG.text->factory->newRaw($DATETAG.text->factory); $result->append8($result, "f."); $result->appendS($result, $DATETAG.text->toUTF8($DATETAG.text)); $result->append8($result, " > "); $result->append8($result, str); } | ENUMTAG IS ENUMVAL { pANTLR3_UINT8 tag; pANTLR3_UINT8 val; char str[20]; sprintf(str, "1=1"); tag = $ENUMTAG.text->chars; val = $ENUMVAL.text->chars; if (strcmp((char *)tag, "media_kind") == 0) { if (strcmp((char *)val, "music") == 0) { sprintf(str, "f.media_kind = \%d", MEDIA_KIND_MUSIC); } else if (strcmp((char *)val, "movie") == 0) { sprintf(str, "f.media_kind = \%d", MEDIA_KIND_MOVIE); } else if (strcmp((char *)val, "podcast") == 0) { sprintf(str, "f.media_kind = \%d", MEDIA_KIND_PODCAST); } else if (strcmp((char *)val, "audiobook") == 0) { sprintf(str, "f.media_kind = \%d", MEDIA_KIND_AUDIOBOOK); } else if (strcmp((char *)val, "tvshow") == 0) { sprintf(str, "f.media_kind = \%d", MEDIA_KIND_TVSHOW); } } else if (strcmp((char *)tag, "data_kind") == 0) { if (strcmp((char *)val, "file") == 0) { sprintf(str, "f.data_kind = \%d", DATA_KIND_FILE); } else if (strcmp((char *)val, "url") == 0) { sprintf(str, "f.data_kind = \%d", DATA_KIND_HTTP); } else if (strcmp((char *)val, "spotify") == 0) { sprintf(str, "f.data_kind = \%d", DATA_KIND_SPOTIFY); } else if (strcmp((char *)val, "pipe") == 0) { sprintf(str, "f.data_kind = \%d", DATA_KIND_PIPE); } } $result = $ENUMTAG.text->factory->newRaw($ENUMTAG.text->factory); $result->append8($result, str); } ; dateval returns [ int result ] @init { $result = 0; } : DATE { pANTLR3_UINT8 datval; datval = $DATE.text->chars; if (strcmp((char *)datval, "today") == 0) { $result = time(NULL); } else if (strcmp((char *)datval, "yesterday") == 0) { $result = time(NULL) - 24 * 3600; } else if (strcmp((char *)datval, "last week") == 0) { $result = time(NULL) - 24 * 3600 * 7; } else if (strcmp((char *)datval, "last month") == 0) { $result = time(NULL) - 24 * 3600 * 30; } else if (strcmp((char *)datval, "last year") == 0) { $result = time(NULL) - 24 * 3600 * 365; } else { struct tm tm; char year[5]; char month[3]; char day[3]; memset((void*)&tm,0,sizeof(tm)); memset(year, 0, sizeof(year)); memset(month, 0, sizeof(month)); memset(day, 0, sizeof(day)); strncpy(year, (const char *)datval, 4); strncpy(month, (const char *)datval + 5, 2); strncpy(day, (const char *)datval + 8, 2); tm.tm_year = atoi(year) - 1900; tm.tm_mon = atoi(month) - 1; tm.tm_mday = atoi(day); $result = mktime(&tm); } } | interval BEFORE DATE { pANTLR3_UINT8 datval; datval = $DATE.text->chars; if (strcmp((char *)datval, "yesterday") == 0) { $result = time(NULL) - 24 * 3600; } else if (strcmp((char *)datval, "last week") == 0) { $result = time(NULL) - 24 * 3600 * 7; } else if (strcmp((char *)datval, "last month") == 0) { $result = time(NULL) - 24 * 3600 * 30; } else if (strcmp((char *)datval, "last year") == 0) { $result = time(NULL) - 24 * 3600 * 365; } else { $result = time(NULL); } $result = $result - $interval.result; } | interval AFTER DATE { pANTLR3_UINT8 datval; datval = $DATE.text->chars; if (strcmp((char *)datval, "yesterday") == 0) { $result = time(NULL) - 24 * 3600; } else if (strcmp((char *)datval, "last week") == 0) { $result = time(NULL) - 24 * 3600 * 7; } else if (strcmp((char *)datval, "last month") == 0) { $result = time(NULL) - 24 * 3600 * 30; } else if (strcmp((char *)datval, "last year") == 0) { $result = time(NULL) - 24 * 3600 * 365; } else { $result = time(NULL); } $result = $result + $interval.result; } | interval AGO { $result = time(NULL) - $interval.result; } ; interval returns [ int result ] @init { $result = 0; } : INT DATINTERVAL { pANTLR3_UINT8 interval; $result = atoi((const char *)$INT.text->chars); interval = $DATINTERVAL.text->chars; if (strcmp((char *)interval, "days") == 0) { $result = $result * 24 * 3600; } else if (strcmp((char *)interval, "weeks") == 0) { $result = $result * 24 * 3600 * 7; } else if (strcmp((char *)interval, "months") == 0) { $result = $result * 24 * 3600 * 30; } else if (strcmp((char *)interval, "weeks") == 0) { $result = $result * 24 * 3600 * 365; } else { $result = 0; } } ; forked-daapd-25.0/src/artwork.c000066400000000000000000001176331313447753700164360ustar00rootroot00000000000000/* * Copyright (C) 2015-2016 Espen Jürgensen * Copyright (C) 2010-2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "db.h" #include "misc.h" #include "logger.h" #include "conffile.h" #include "cache.h" #include "http.h" #include "avio_evbuffer.h" #include "artwork.h" #ifdef HAVE_SPOTIFY_H # include "spotify.h" #endif #include "ffmpeg-compat.h" /* This artwork module will look for artwork by consulting a set of sources one * at a time. A source is for instance the local library, the cache or a cover * art database. For each source there is a handler function, which will do the * actual work of getting the artwork. * * There are two types of handlers: item and group. Item handlers are capable of * finding artwork for a single item (a dbmfi), while group handlers can get for * an album or artist (a persistentid). * * An artwork source handler must return one of the following: * * ART_FMT_JPEG (positive) Found a jpeg * ART_FMT_PNG (positive) Found a png * ART_E_NONE (zero) No artwork found * ART_E_ERROR (negative) An error occurred while searching for artwork * ART_E_ABORT (negative) Caller should abort artwork search (may be returned by cache) */ #define ART_E_NONE 0 #define ART_E_ERROR -1 #define ART_E_ABORT -2 enum artwork_cache { NEVER = 0, // No caching of any results ON_SUCCESS = 1, // Cache if artwork found ON_FAILURE = 2, // Cache if artwork not found (so we don't keep asking) }; /* This struct contains the data available to the handler, as well as a char * buffer where the handler should output the path to the artwork (if it is * local - otherwise the buffer can be left empty). The purpose of supplying the * path is that the filescanner can then clear the cache in case the file * changes. */ struct artwork_ctx { // Handler should output path here if artwork is local char path[PATH_MAX]; // Handler should output artwork data to this evbuffer struct evbuffer *evbuf; // Input data to handler, requested width and height int max_w; int max_h; // Input data to handler, did user configure to look for individual artwork int individual; // Input data for item handlers struct db_media_file_info *dbmfi; int id; // Input data for group handlers int64_t persistentid; // Not to be used by handler - query for item or group struct query_params qp; // Not to be used by handler - should the result be cached enum artwork_cache cache; }; /* Definition of an artwork source. Covers both item and group sources. */ struct artwork_source { // Name of the source, e.g. "cache" const char *name; // The handler int (*handler)(struct artwork_ctx *ctx); // What data_kinds the handler can work with, combined with (1 << A) | (1 << B) int data_kinds; // When should results from the source be cached? enum artwork_cache cache; }; /* File extensions that we look for or accept */ static const char *cover_extension[] = { "jpg", "png", }; /* ----------------- DECLARE AND CONFIGURE SOURCE HANDLERS ----------------- */ /* Forward - group handlers */ static int source_group_cache_get(struct artwork_ctx *ctx); static int source_group_dir_get(struct artwork_ctx *ctx); /* Forward - item handlers */ static int source_item_cache_get(struct artwork_ctx *ctx); static int source_item_embedded_get(struct artwork_ctx *ctx); static int source_item_own_get(struct artwork_ctx *ctx); static int source_item_stream_get(struct artwork_ctx *ctx); static int source_item_spotify_get(struct artwork_ctx *ctx); static int source_item_ownpl_get(struct artwork_ctx *ctx); /* List of sources that can provide artwork for a group (i.e. usually an album * identified by a persistentid). The source handlers will be called in the * order of this list. Must be terminated by a NULL struct. */ static struct artwork_source artwork_group_source[] = { { .name = "cache", .handler = source_group_cache_get, .cache = ON_FAILURE, }, { .name = "directory", .handler = source_group_dir_get, .cache = ON_SUCCESS | ON_FAILURE, }, { .name = NULL, .handler = NULL, .cache = 0, } }; /* List of sources that can provide artwork for an item (a track characterized * by a dbmfi). The source handlers will be called in the order of this list. * The handler will only be called if the data_kind matches. Must be terminated * by a NULL struct. */ static struct artwork_source artwork_item_source[] = { { .name = "cache", .handler = source_item_cache_get, .data_kinds = (1 << DATA_KIND_FILE) | (1 << DATA_KIND_SPOTIFY), .cache = ON_FAILURE, }, { .name = "embedded", .handler = source_item_embedded_get, .data_kinds = (1 << DATA_KIND_FILE), .cache = ON_SUCCESS | ON_FAILURE, }, { .name = "own", .handler = source_item_own_get, .data_kinds = (1 << DATA_KIND_FILE), .cache = ON_SUCCESS | ON_FAILURE, }, { .name = "stream", .handler = source_item_stream_get, .data_kinds = (1 << DATA_KIND_HTTP), .cache = NEVER, }, { .name = "Spotify", .handler = source_item_spotify_get, .data_kinds = (1 << DATA_KIND_SPOTIFY), .cache = ON_SUCCESS, }, { .name = "playlist own", .handler = source_item_ownpl_get, .data_kinds = (1 << DATA_KIND_HTTP), .cache = ON_SUCCESS | ON_FAILURE, }, { .name = NULL, .handler = NULL, .data_kinds = 0, .cache = 0, } }; /* -------------------------------- HELPERS -------------------------------- */ /* Reads an artwork file from the filesystem straight into an evbuf * TODO Use evbuffer_add_file or evbuffer_read? * * @out evbuf Image data * @in path Path to the artwork * @return 0 on success, -1 on error */ static int artwork_read(struct evbuffer *evbuf, char *path) { uint8_t buf[4096]; struct stat sb; int fd; int ret; fd = open(path, O_RDONLY); if (fd < 0) { DPRINTF(E_WARN, L_ART, "Could not open artwork file '%s': %s\n", path, strerror(errno)); return -1; } ret = fstat(fd, &sb); if (ret < 0) { DPRINTF(E_WARN, L_ART, "Could not stat() artwork file '%s': %s\n", path, strerror(errno)); goto out_fail; } ret = evbuffer_expand(evbuf, sb.st_size); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Out of memory for artwork\n"); goto out_fail; } while ((ret = read(fd, buf, sizeof(buf))) > 0) evbuffer_add(evbuf, buf, ret); close(fd); return 0; out_fail: close(fd); return -1; } /* Will the source image fit inside requested size. If not, what size should it * be rescaled to to maintain aspect ratio. * * @in src Image source * @in max_w Requested width * @in max_h Requested height * @out target_w Rescaled width * @out target_h Rescaled height * @return 0 no rescaling needed, 1 rescaling needed */ static int rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *target_h) { DPRINTF(E_DBG, L_ART, "Original image dimensions: w %d h %d\n", src->width, src->height); *target_w = src->width; *target_h = src->height; if ((src->width == 0) || (src->height == 0)) /* Unknown source size, can't rescale */ return 0; if ((max_w <= 0) || (max_h <= 0)) /* No valid target dimensions, use original */ return 0; if ((src->width <= max_w) && (src->height <= max_h)) /* Smaller than target */ return 0; if (src->width * max_h > src->height * max_w) /* Wider aspect ratio than target */ { *target_w = max_w; *target_h = (double)max_w * ((double)src->height / (double)src->width); } else /* Taller or equal aspect ratio */ { *target_w = (double)max_h * ((double)src->width / (double)src->height); *target_h = max_h; } DPRINTF(E_DBG, L_ART, "Raw destination width %d height %d\n", *target_w, *target_h); if ((*target_h > max_h) && (max_h > 0)) *target_h = max_h; /* PNG prefers even row count */ *target_w += *target_w % 2; if ((*target_w > max_w) && (max_w > 0)) *target_w = max_w - (max_w % 2); DPRINTF(E_DBG, L_ART, "Destination width %d height %d\n", *target_w, *target_h); return 1; } /* Rescale an image * * @out evbuf Rescaled image data * @in src_ctx Image source * @in s Index of stream containing image * @in out_w Rescaled width * @in out_h Rescaled height * @return ART_FMT_* on success, -1 on error */ static int artwork_rescale(struct evbuffer *evbuf, AVFormatContext *src_ctx, int s, int out_w, int out_h) { uint8_t *buf; AVCodecContext *src; AVFormatContext *dst_ctx; AVCodecContext *dst; AVOutputFormat *dst_fmt; AVStream *dst_st; AVCodec *img_decoder; AVCodec *img_encoder; AVFrame *i_frame; AVFrame *o_frame; struct SwsContext *swsctx; AVPacket pkt; int have_frame; int ret; src = src_ctx->streams[s]->codec; // Avoids threading issue in both ffmpeg and libav that prevents decoding embedded png's src->thread_count = 1; img_decoder = avcodec_find_decoder(src->codec_id); if (!img_decoder) { DPRINTF(E_LOG, L_ART, "No suitable decoder found for artwork %s\n", src_ctx->filename); return -1; } ret = avcodec_open2(src, img_decoder, NULL); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not open codec for decoding: %s\n", strerror(AVUNERROR(ret))); return -1; } if (src->pix_fmt < 0) { DPRINTF(E_LOG, L_ART, "Unknown pixel format for artwork %s\n", src_ctx->filename); ret = -1; goto out_close_src; } /* Set up output */ dst_fmt = av_guess_format("image2", NULL, NULL); if (!dst_fmt) { DPRINTF(E_LOG, L_ART, "ffmpeg image2 muxer not available\n"); ret = -1; goto out_close_src; } dst_fmt->video_codec = AV_CODEC_ID_NONE; /* Try to keep same codec if possible */ if (src->codec_id == AV_CODEC_ID_PNG) dst_fmt->video_codec = AV_CODEC_ID_PNG; else if (src->codec_id == AV_CODEC_ID_MJPEG) dst_fmt->video_codec = AV_CODEC_ID_MJPEG; /* If not possible, select new codec */ if (dst_fmt->video_codec == AV_CODEC_ID_NONE) { dst_fmt->video_codec = AV_CODEC_ID_PNG; } img_encoder = avcodec_find_encoder(dst_fmt->video_codec); if (!img_encoder) { DPRINTF(E_LOG, L_ART, "No suitable encoder found for codec ID %d\n", dst_fmt->video_codec); ret = -1; goto out_close_src; } dst_ctx = avformat_alloc_context(); if (!dst_ctx) { DPRINTF(E_LOG, L_ART, "Out of memory for format context\n"); ret = -1; goto out_close_src; } dst_ctx->oformat = dst_fmt; dst_fmt->flags &= ~AVFMT_NOFILE; dst_st = avformat_new_stream(dst_ctx, NULL); if (!dst_st) { DPRINTF(E_LOG, L_ART, "Out of memory for new output stream\n"); ret = -1; goto out_free_dst_ctx; } dst = dst_st->codec; avcodec_get_context_defaults3(dst, NULL); if (dst_fmt->flags & AVFMT_GLOBALHEADER) dst->flags |= CODEC_FLAG_GLOBAL_HEADER; dst->codec_id = dst_fmt->video_codec; dst->codec_type = AVMEDIA_TYPE_VIDEO; dst->pix_fmt = avcodec_default_get_format(dst, img_encoder->pix_fmts); if (dst->pix_fmt < 0) { DPRINTF(E_LOG, L_ART, "Could not determine best pixel format\n"); ret = -1; goto out_free_dst_ctx; } dst->time_base.num = 1; dst->time_base.den = 25; dst->width = out_w; dst->height = out_h; /* Open encoder */ ret = avcodec_open2(dst, img_encoder, NULL); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not open codec for encoding: %s\n", strerror(AVUNERROR(ret))); ret = -1; goto out_free_dst_ctx; } i_frame = av_frame_alloc(); o_frame = av_frame_alloc(); if (!i_frame || !o_frame) { DPRINTF(E_LOG, L_ART, "Could not allocate input/output frame\n"); ret = -1; goto out_free_frames; } ret = av_image_get_buffer_size(dst->pix_fmt, src->width, src->height, 1); DPRINTF(E_DBG, L_ART, "Artwork buffer size: %d\n", ret); buf = (uint8_t *)av_malloc(ret); if (!buf) { DPRINTF(E_LOG, L_ART, "Out of memory for artwork buffer\n"); ret = -1; goto out_free_frames; } #if HAVE_DECL_AV_IMAGE_FILL_ARRAYS av_image_fill_arrays(o_frame->data, o_frame->linesize, buf, dst->pix_fmt, src->width, src->height, 1); #else avpicture_fill((AVPicture *)o_frame, buf, dst->pix_fmt, src->width, src->height); #endif o_frame->height = dst->height; o_frame->width = dst->width; o_frame->format = dst->pix_fmt; swsctx = sws_getContext(src->width, src->height, src->pix_fmt, dst->width, dst->height, dst->pix_fmt, SWS_BICUBIC, NULL, NULL, NULL); if (!swsctx) { DPRINTF(E_LOG, L_ART, "Could not get SWS context\n"); ret = -1; goto out_free_buf; } /* Get frame */ have_frame = 0; while (av_read_frame(src_ctx, &pkt) == 0) { if (pkt.stream_index != s) { av_packet_unref(&pkt); continue; } avcodec_decode_video2(src, i_frame, &have_frame, &pkt); break; } if (!have_frame) { DPRINTF(E_LOG, L_ART, "Could not decode artwork\n"); av_packet_unref(&pkt); sws_freeContext(swsctx); ret = -1; goto out_free_buf; } /* Scale */ sws_scale(swsctx, (const uint8_t * const *)i_frame->data, i_frame->linesize, 0, src->height, o_frame->data, o_frame->linesize); sws_freeContext(swsctx); av_packet_unref(&pkt); /* Open output file */ dst_ctx->pb = avio_output_evbuffer_open(evbuf); if (!dst_ctx->pb) { DPRINTF(E_LOG, L_ART, "Could not open artwork destination buffer\n"); ret = -1; goto out_free_buf; } /* Encode frame */ av_init_packet(&pkt); pkt.data = NULL; pkt.size = 0; ret = avcodec_encode_video2(dst, &pkt, o_frame, &have_frame); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not encode artwork\n"); ret = -1; goto out_fclose_dst; } ret = avformat_write_header(dst_ctx, NULL); if (ret != 0) { DPRINTF(E_LOG, L_ART, "Could not write artwork header: %s\n", strerror(AVUNERROR(ret))); ret = -1; goto out_fclose_dst; } ret = av_interleaved_write_frame(dst_ctx, &pkt); if (ret != 0) { DPRINTF(E_LOG, L_ART, "Error writing artwork\n"); ret = -1; goto out_fclose_dst; } ret = av_write_trailer(dst_ctx); if (ret != 0) { DPRINTF(E_LOG, L_ART, "Could not write artwork trailer: %s\n", strerror(AVUNERROR(ret))); ret = -1; goto out_fclose_dst; } switch (dst_fmt->video_codec) { case AV_CODEC_ID_PNG: ret = ART_FMT_PNG; break; case AV_CODEC_ID_MJPEG: ret = ART_FMT_JPEG; break; default: DPRINTF(E_LOG, L_ART, "Unhandled rescale output format\n"); ret = -1; break; } out_fclose_dst: avio_evbuffer_close(dst_ctx->pb); av_packet_unref(&pkt); out_free_buf: av_free(buf); out_free_frames: if (i_frame) av_frame_free(&i_frame); if (o_frame) av_frame_free(&o_frame); avcodec_close(dst); out_free_dst_ctx: avformat_free_context(dst_ctx); out_close_src: avcodec_close(src); return ret; } /* Get an artwork file from the filesystem. Will rescale if needed. * * @out evbuf Image data * @in path Path to the artwork * @in max_w Requested width * @in max_h Requested height * @return ART_FMT_* on success, ART_E_ERROR on error */ static int artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) { AVFormatContext *src_ctx; int s; int target_w; int target_h; int format_ok; int ret; DPRINTF(E_SPAM, L_ART, "Getting artwork (max destination width %d height %d)\n", max_w, max_h); src_ctx = NULL; ret = avformat_open_input(&src_ctx, path, NULL, NULL); if (ret < 0) { DPRINTF(E_WARN, L_ART, "Cannot open artwork file '%s': %s\n", path, strerror(AVUNERROR(ret))); return ART_E_ERROR; } ret = avformat_find_stream_info(src_ctx, NULL); if (ret < 0) { DPRINTF(E_WARN, L_ART, "Cannot get stream info: %s\n", strerror(AVUNERROR(ret))); avformat_close_input(&src_ctx); return ART_E_ERROR; } format_ok = 0; for (s = 0; s < src_ctx->nb_streams; s++) { if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG) { format_ok = ART_FMT_PNG; break; } else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG) { format_ok = ART_FMT_JPEG; break; } } if (s == src_ctx->nb_streams) { DPRINTF(E_LOG, L_ART, "Artwork file '%s' not a PNG or JPEG file\n", path); avformat_close_input(&src_ctx); return ART_E_ERROR; } ret = rescale_needed(src_ctx->streams[s]->codec, max_w, max_h, &target_w, &target_h); /* Fastpath */ if (!ret && format_ok) { ret = artwork_read(evbuf, path); if (ret == 0) ret = format_ok; } else ret = artwork_rescale(evbuf, src_ctx, s, target_w, target_h); avformat_close_input(&src_ctx); if (ret < 0) { if (evbuffer_get_length(evbuf) > 0) evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); ret = ART_E_ERROR; } return ret; } /* Looks for an artwork file in a directory. Will rescale if needed. * * @out evbuf Image data * @in dir Directory to search * @in max_w Requested width * @in max_h Requested height * @out out_path Path to the artwork file if found, must be a char[PATH_MAX] buffer * @return ART_FMT_* on success, ART_E_NONE on nothing found, ART_E_ERROR on error */ static int artwork_get_dir_image(struct evbuffer *evbuf, char *dir, int max_w, int max_h, char *out_path) { char path[PATH_MAX]; char parentdir[PATH_MAX]; int i; int j; int len; int ret; cfg_t *lib; int nbasenames; int nextensions; char *ptr; ret = snprintf(path, sizeof(path), "%s", dir); if ((ret < 0) || (ret >= sizeof(path))) { DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", dir); return ART_E_ERROR; } len = strlen(path); lib = cfg_getsec(cfg, "library"); nbasenames = cfg_size(lib, "artwork_basenames"); if (nbasenames == 0) return ART_E_NONE; nextensions = sizeof(cover_extension) / sizeof(cover_extension[0]); for (i = 0; i < nbasenames; i++) { for (j = 0; j < nextensions; j++) { ret = snprintf(path + len, sizeof(path) - len, "/%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]); if ((ret < 0) || (ret >= sizeof(path) - len)) { DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s/%s)\n", dir, cfg_getnstr(lib, "artwork_basenames", i)); continue; } DPRINTF(E_SPAM, L_ART, "Trying directory artwork file %s\n", path); ret = access(path, F_OK); if (ret < 0) continue; // If artwork file exists (ret == 0), exit the loop break; } // In case the previous loop exited early, we found an existing artwork file and exit the outer loop if (j < nextensions) break; } // If the loop for directory artwork did not exit early, look for parent directory artwork if (i == nbasenames) { ptr = strrchr(path, '/'); if (ptr) *ptr = '\0'; ptr = strrchr(path, '/'); if ((!ptr) || (strlen(ptr) <= 1)) { DPRINTF(E_LOG, L_ART, "Could not find parent dir name (%s)\n", path); return ART_E_ERROR; } strcpy(parentdir, ptr + 1); len = strlen(path); for (i = 0; i < nextensions; i++) { ret = snprintf(path + len, sizeof(path) - len, "/%s.%s", parentdir, cover_extension[i]); if ((ret < 0) || (ret >= sizeof(path) - len)) { DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", parentdir); continue; } DPRINTF(E_SPAM, L_ART, "Trying parent directory artwork file %s\n", path); ret = access(path, F_OK); if (ret < 0) continue; break; } if (i == nextensions) return ART_E_NONE; } snprintf(out_path, PATH_MAX, "%s", path); return artwork_get(evbuf, path, max_w, max_h); } /* ---------------------- SOURCE HANDLER IMPLEMENTATION -------------------- */ /* Looks in the cache for group artwork */ static int source_group_cache_get(struct artwork_ctx *ctx) { int format; int cached; int ret; ret = cache_artwork_get(CACHE_ARTWORK_GROUP, ctx->persistentid, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf); if (ret < 0) return ART_E_ERROR; if (!cached) return ART_E_NONE; if (!format) return ART_E_ABORT; return format; } /* Looks for cover files in a directory, so if dir is /foo/bar and the user has * configured the cover file names "cover" and "artwork" it will look for * /foo/bar/cover.{png,jpg}, /foo/bar/artwork.{png,jpg} and also * /foo/bar/bar.{png,jpg} (so-called parentdir artwork) */ static int source_group_dir_get(struct artwork_ctx *ctx) { struct query_params qp; char *dir; int ret; /* Image is not in the artwork cache. Try directory artwork first */ memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_GROUP_DIRS; qp.persistentid = ctx->persistentid; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not start Q_GROUP_DIRS query\n"); return ART_E_ERROR; } while (((ret = db_query_fetch_string(&qp, &dir)) == 0) && (dir)) { /* The db query may return non-directories (eg if item is an internet stream or Spotify) */ if (access(dir, F_OK) < 0) continue; ret = artwork_get_dir_image(ctx->evbuf, dir, ctx->max_w, ctx->max_h, ctx->path); if (ret > 0) { db_query_end(&qp); return ret; } } db_query_end(&qp); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_DIRS results\n"); return ART_E_ERROR; } return ART_E_NONE; } /* Looks in the cache for item artwork. Only relevant if configured to look for * individual artwork. */ static int source_item_cache_get(struct artwork_ctx *ctx) { int format; int cached; int ret; if (!ctx->individual) return ART_E_NONE; ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, ctx->id, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf); if (ret < 0) return ART_E_ERROR; if (!cached) return ART_E_NONE; if (!format) return ART_E_ABORT; return format; } /* Get an embedded artwork file from a media file. Will rescale if needed. */ static int source_item_embedded_get(struct artwork_ctx *ctx) { AVFormatContext *src_ctx; AVStream *src_st; int s; int target_w; int target_h; int format; int ret; DPRINTF(E_SPAM, L_ART, "Trying embedded artwork in %s\n", ctx->dbmfi->path); src_ctx = NULL; ret = avformat_open_input(&src_ctx, ctx->dbmfi->path, NULL, NULL); if (ret < 0) { DPRINTF(E_WARN, L_ART, "Cannot open media file '%s': %s\n", ctx->dbmfi->path, strerror(AVUNERROR(ret))); return ART_E_ERROR; } ret = avformat_find_stream_info(src_ctx, NULL); if (ret < 0) { DPRINTF(E_WARN, L_ART, "Cannot get stream info: %s\n", strerror(AVUNERROR(ret))); avformat_close_input(&src_ctx); return ART_E_ERROR; } format = 0; for (s = 0; s < src_ctx->nb_streams; s++) { if (src_ctx->streams[s]->disposition & AV_DISPOSITION_ATTACHED_PIC) { if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG) { format = ART_FMT_PNG; break; } else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG) { format = ART_FMT_JPEG; break; } } } if (s == src_ctx->nb_streams) { avformat_close_input(&src_ctx); return ART_E_NONE; } src_st = src_ctx->streams[s]; ret = rescale_needed(src_st->codec, ctx->max_w, ctx->max_h, &target_w, &target_h); /* Fastpath */ if (!ret && format) { DPRINTF(E_SPAM, L_ART, "Artwork not too large, using original image\n"); ret = evbuffer_add(ctx->evbuf, src_st->attached_pic.data, src_st->attached_pic.size); if (ret < 0) DPRINTF(E_LOG, L_ART, "Could not add embedded image to event buffer\n"); else ret = format; } else { DPRINTF(E_SPAM, L_ART, "Artwork too large, rescaling image\n"); ret = artwork_rescale(ctx->evbuf, src_ctx, s, target_w, target_h); } avformat_close_input(&src_ctx); if (ret < 0) { if (evbuffer_get_length(ctx->evbuf) > 0) evbuffer_drain(ctx->evbuf, evbuffer_get_length(ctx->evbuf)); ret = ART_E_ERROR; } else snprintf(ctx->path, sizeof(ctx->path), "%s", ctx->dbmfi->path); return ret; } /* Looks for basename(in_path).{png,jpg}, so if in_path is /foo/bar.mp3 it * will look for /foo/bar.png and /foo/bar.jpg */ static int source_item_own_get(struct artwork_ctx *ctx) { char path[PATH_MAX]; char *ptr; int len; int nextensions; int i; int ret; ret = snprintf(path, sizeof(path), "%s", ctx->dbmfi->path); if ((ret < 0) || (ret >= sizeof(path))) { DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", ctx->dbmfi->path); return ART_E_ERROR; } ptr = strrchr(path, '.'); if (ptr) *ptr = '\0'; len = strlen(path); nextensions = sizeof(cover_extension) / sizeof(cover_extension[0]); for (i = 0; i < nextensions; i++) { ret = snprintf(path + len, sizeof(path) - len, ".%s", cover_extension[i]); if ((ret < 0) || (ret >= sizeof(path) - len)) { DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", ctx->dbmfi->path); continue; } DPRINTF(E_SPAM, L_ART, "Trying own artwork file %s\n", path); ret = access(path, F_OK); if (ret < 0) continue; break; } if (i == nextensions) return ART_E_NONE; snprintf(ctx->path, sizeof(ctx->path), "%s", path); return artwork_get(ctx->evbuf, path, ctx->max_w, ctx->max_h); } /* * Downloads the artwork pointed to by the ICY metadata tag in an internet radio * stream (the StreamUrl tag). The path will be converted back to the id, which * is given to the player. If the id is currently being played, and there is a * valid ICY metadata artwork URL available, it will be returned to this * function, which will then use the http client to get the artwork. Notice: No * rescaling is done. */ static int source_item_stream_get(struct artwork_ctx *ctx) { struct http_client_ctx client; struct db_queue_item *queue_item; struct keyval *kv; const char *content_type; char *url; char *ext; int len; int ret; DPRINTF(E_SPAM, L_ART, "Trying internet stream artwork in %s\n", ctx->dbmfi->path); ret = ART_E_NONE; queue_item = db_queue_fetch_byfileid(ctx->id); if (!queue_item || !queue_item->artwork_url) { free_queue_item(queue_item, 0); return ART_E_NONE; } url = strdup(queue_item->artwork_url); free_queue_item(queue_item, 0); len = strlen(url); if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg goto out_url; ext = strrchr(url, '.'); if (!ext) goto out_url; if ((strcmp(ext, ".jpg") != 0) && (strcmp(ext, ".png") != 0)) goto out_url; cache_artwork_read(ctx->evbuf, url, &ret); if (ret > 0) goto out_url; kv = keyval_alloc(); if (!kv) goto out_url; memset(&client, 0, sizeof(struct http_client_ctx)); client.url = url; client.input_headers = kv; client.input_body = ctx->evbuf; if (http_client_request(&client) < 0) goto out_kv; content_type = keyval_get(kv, "Content-Type"); if (content_type && (strcmp(content_type, "image/jpeg") == 0)) ret = ART_FMT_JPEG; else if (content_type && (strcmp(content_type, "image/png") == 0)) ret = ART_FMT_PNG; if (ret > 0) { DPRINTF(E_SPAM, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type); cache_artwork_stash(ctx->evbuf, url, ret); } out_kv: keyval_clear(kv); free(kv); out_url: free(url); return ret; } #ifdef HAVE_SPOTIFY_H static int source_item_spotify_get(struct artwork_ctx *ctx) { AVFormatContext *src_ctx; AVIOContext *avio; AVInputFormat *ifmt; struct evbuffer *raw; struct evbuffer *evbuf; int target_w; int target_h; int ret; raw = evbuffer_new(); evbuf = evbuffer_new(); if (!raw || !evbuf) { DPRINTF(E_LOG, L_ART, "Out of memory for Spotify evbuf\n"); return ART_E_ERROR; } ret = spotify_artwork_get(raw, ctx->dbmfi->path, ctx->max_w, ctx->max_h); if (ret < 0) { DPRINTF(E_WARN, L_ART, "No artwork from Spotify for %s\n", ctx->dbmfi->path); evbuffer_free(raw); evbuffer_free(evbuf); return ART_E_NONE; } // Make a refbuf of raw for ffmpeg image size probing and possibly rescaling. // We keep raw around in case rescaling is not necessary. #ifdef HAVE_LIBEVENT2_OLD uint8_t *buf = evbuffer_pullup(raw, -1); if (!buf) { DPRINTF(E_LOG, L_ART, "Could not pullup raw artwork\n"); goto out_free_evbuf; } ret = evbuffer_add_reference(evbuf, buf, evbuffer_get_length(raw), NULL, NULL); #else ret = evbuffer_add_buffer_reference(evbuf, raw); #endif if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not copy/ref raw image for ffmpeg\n"); goto out_free_evbuf; } // Now evbuf will be processed by ffmpeg, since it probably needs to be rescaled src_ctx = avformat_alloc_context(); if (!src_ctx) { DPRINTF(E_LOG, L_ART, "Out of memory for source context\n"); goto out_free_evbuf; } avio = avio_input_evbuffer_open(evbuf); if (!avio) { DPRINTF(E_LOG, L_ART, "Could not alloc input evbuffer\n"); goto out_free_ctx; } src_ctx->pb = avio; ifmt = av_find_input_format("mjpeg"); if (!ifmt) { DPRINTF(E_LOG, L_ART, "Could not find mjpeg input format\n"); goto out_close_avio; } ret = avformat_open_input(&src_ctx, NULL, ifmt, NULL); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not open input\n"); goto out_close_avio; } ret = avformat_find_stream_info(src_ctx, NULL); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not find stream info\n"); goto out_close_input; } ret = rescale_needed(src_ctx->streams[0]->codec, ctx->max_w, ctx->max_h, &target_w, &target_h); if (!ret) ret = evbuffer_add_buffer(ctx->evbuf, raw); else ret = artwork_rescale(ctx->evbuf, src_ctx, 0, target_w, target_h); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not add or rescale image to output evbuf\n"); goto out_close_input; } avformat_close_input(&src_ctx); avio_evbuffer_close(avio); evbuffer_free(evbuf); evbuffer_free(raw); return ART_FMT_JPEG; out_close_input: avformat_close_input(&src_ctx); out_close_avio: avio_evbuffer_close(avio); out_free_ctx: if (src_ctx) avformat_free_context(src_ctx); out_free_evbuf: evbuffer_free(evbuf); evbuffer_free(raw); return ART_E_ERROR; } #else static int source_item_spotify_get(struct artwork_ctx *ctx) { return ART_E_ERROR; } #endif /* First looks of the mfi->path is in any playlist, and if so looks in the dir * of the playlist file (m3u et al) to see if there is any artwork. So if the * playlist is /foo/bar.m3u it will look for /foo/bar.png and /foo/bar.jpg. */ static int source_item_ownpl_get(struct artwork_ctx *ctx) { struct query_params qp; struct db_playlist_info dbpli; char filter[PATH_MAX + 64]; char *mfi_path; int format; int ret; ret = snprintf(filter, sizeof(filter), "(filepath = '%s')", ctx->dbmfi->path); if ((ret < 0) || (ret >= sizeof(filter))) { DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", ctx->dbmfi->path); return ART_E_ERROR; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_FIND_PL; qp.filter = filter; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not start ownpl query\n"); return ART_E_ERROR; } mfi_path = ctx->dbmfi->path; format = ART_E_NONE; while (((ret = db_query_fetch_pl(&qp, &dbpli, 0)) == 0) && (dbpli.id) && (format == ART_E_NONE)) { if (!dbpli.path) continue; ctx->dbmfi->path = dbpli.path; format = source_item_own_get(ctx); } ctx->dbmfi->path = mfi_path; if ((ret < 0) || (format < 0)) format = ART_E_ERROR; db_query_end(&qp); return format; } /* --------------------------- SOURCE PROCESSING --------------------------- */ static int process_items(struct artwork_ctx *ctx, int item_mode) { struct db_media_file_info dbmfi; uint32_t data_kind; int i; int ret; ret = db_query_start(&ctx->qp); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Could not start query (type=%d)\n", ctx->qp.type); ctx->cache = NEVER; return -1; } while (((ret = db_query_fetch_file(&ctx->qp, &dbmfi)) == 0) && (dbmfi.id)) { // Save the first songalbumid, might need it for process_group() if this search doesn't give anything if (!ctx->persistentid) safe_atoi64(dbmfi.songalbumid, &ctx->persistentid); if (item_mode && !ctx->individual) goto no_artwork; ret = (safe_atoi32(dbmfi.id, &ctx->id) < 0) || (safe_atou32(dbmfi.data_kind, &data_kind) < 0) || (data_kind > 30); if (ret) { DPRINTF(E_LOG, L_ART, "Error converting dbmfi id or data_kind to number\n"); continue; } for (i = 0; artwork_item_source[i].handler; i++) { if ((artwork_item_source[i].data_kinds & (1 << data_kind)) == 0) continue; // If just one handler says we should not cache a negative result then we obey that if ((artwork_item_source[i].cache & ON_FAILURE) == 0) ctx->cache = NEVER; DPRINTF(E_SPAM, L_ART, "Checking item source '%s'\n", artwork_item_source[i].name); ctx->dbmfi = &dbmfi; ret = artwork_item_source[i].handler(ctx); ctx->dbmfi = NULL; if (ret > 0) { DPRINTF(E_DBG, L_ART, "Artwork for '%s' found in source '%s'\n", dbmfi.title, artwork_item_source[i].name); ctx->cache = (artwork_item_source[i].cache & ON_SUCCESS); db_query_end(&ctx->qp); return ret; } else if (ret == ART_E_ABORT) { DPRINTF(E_DBG, L_ART, "Source '%s' stopped search for artwork for '%s'\n", artwork_item_source[i].name, dbmfi.title); ctx->cache = NEVER; break; } else if (ret == ART_E_ERROR) { DPRINTF(E_LOG, L_ART, "Source '%s' returned an error for '%s'\n", artwork_item_source[i].name, dbmfi.title); ctx->cache = NEVER; } } } if (ret < 0) { DPRINTF(E_LOG, L_ART, "Error fetching results\n"); ctx->cache = NEVER; } no_artwork: db_query_end(&ctx->qp); return -1; } static int process_group(struct artwork_ctx *ctx) { int i; int ret; if (!ctx->persistentid) { DPRINTF(E_LOG, L_ART, "Bug! No persistentid in call to process_group()\n"); ctx->cache = NEVER; return -1; } for (i = 0; artwork_group_source[i].handler; i++) { // If just one handler says we should not cache a negative result then we obey that if ((artwork_group_source[i].cache & ON_FAILURE) == 0) ctx->cache = NEVER; DPRINTF(E_SPAM, L_ART, "Checking group source '%s'\n", artwork_group_source[i].name); ret = artwork_group_source[i].handler(ctx); if (ret > 0) { DPRINTF(E_DBG, L_ART, "Artwork for group %" PRIi64 " found in source '%s'\n", ctx->persistentid, artwork_group_source[i].name); ctx->cache = (artwork_group_source[i].cache & ON_SUCCESS); return ret; } else if (ret == ART_E_ABORT) { DPRINTF(E_DBG, L_ART, "Source '%s' stopped search for artwork for group %" PRIi64 "\n", artwork_group_source[i].name, ctx->persistentid); ctx->cache = NEVER; return -1; } else if (ret == ART_E_ERROR) { DPRINTF(E_LOG, L_ART, "Source '%s' returned an error for group %" PRIi64 "\n", artwork_group_source[i].name, ctx->persistentid); ctx->cache = NEVER; } } ret = process_items(ctx, 0); return ret; } /* ------------------------------ ARTWORK API ------------------------------ */ int artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h) { struct artwork_ctx ctx; char filter[32]; int ret; DPRINTF(E_DBG, L_ART, "Artwork request for item %d\n", id); memset(&ctx, 0, sizeof(struct artwork_ctx)); ctx.qp.type = Q_ITEMS; ctx.qp.filter = filter; ctx.evbuf = evbuf; ctx.max_w = max_w; ctx.max_h = max_h; ctx.cache = ON_FAILURE; ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"); ret = snprintf(filter, sizeof(filter), "id = %d", id); if ((ret < 0) || (ret >= sizeof(filter))) { DPRINTF(E_LOG, L_ART, "Could not build filter for file id %d; no artwork will be sent\n", id); return -1; } // Note: process_items will set ctx.persistentid for the following process_group() // - and do nothing else if artwork_individual is not configured by user ret = process_items(&ctx, 1); if (ret > 0) { if (ctx.cache == ON_SUCCESS) cache_artwork_add(CACHE_ARTWORK_INDIVIDUAL, id, max_w, max_h, ret, ctx.path, evbuf); return ret; } ctx.qp.type = Q_GROUP_ITEMS; ctx.qp.persistentid = ctx.persistentid; ret = process_group(&ctx); if (ret > 0) { if (ctx.cache == ON_SUCCESS) cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, ret, ctx.path, evbuf); return ret; } DPRINTF(E_DBG, L_ART, "No artwork found for item %d\n", id); if (ctx.cache == ON_FAILURE) cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, 0, "", evbuf); return -1; } int artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h) { struct artwork_ctx ctx; int ret; DPRINTF(E_DBG, L_ART, "Artwork request for group %d\n", id); memset(&ctx, 0, sizeof(struct artwork_ctx)); /* Get the persistent id for the given group id */ ret = db_group_persistentid_byid(id, &ctx.persistentid); if (ret < 0) { DPRINTF(E_LOG, L_ART, "Error fetching persistent id for group id %d\n", id); return -1; } ctx.qp.type = Q_GROUP_ITEMS; ctx.qp.persistentid = ctx.persistentid; ctx.evbuf = evbuf; ctx.max_w = max_w; ctx.max_h = max_h; ctx.cache = ON_FAILURE; ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"); ret = process_group(&ctx); if (ret > 0) { if (ctx.cache == ON_SUCCESS) cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, ret, ctx.path, evbuf); return ret; } DPRINTF(E_DBG, L_ART, "No artwork found for group %d\n", id); if (ctx.cache == ON_FAILURE) cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, 0, "", evbuf); return -1; } /* Checks if the file is an artwork file */ int artwork_file_is_artwork(const char *filename) { cfg_t *lib; int n; int i; int j; int ret; char artwork[PATH_MAX]; lib = cfg_getsec(cfg, "library"); n = cfg_size(lib, "artwork_basenames"); for (i = 0; i < n; i++) { for (j = 0; j < (sizeof(cover_extension) / sizeof(cover_extension[0])); j++) { ret = snprintf(artwork, sizeof(artwork), "%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]); if ((ret < 0) || (ret >= sizeof(artwork))) { DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX (%s.%s)\n", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]); continue; } if (strcmp(artwork, filename) == 0) return 1; } if (j < (sizeof(cover_extension) / sizeof(cover_extension[0]))) break; } return 0; } forked-daapd-25.0/src/artwork.h000066400000000000000000000023761313447753700164400ustar00rootroot00000000000000 #ifndef __ARTWORK_H__ #define __ARTWORK_H__ #define ART_FMT_PNG 1 #define ART_FMT_JPEG 2 #include /* * Get the artwork image for an individual item (track) * * @out evbuf Event buffer that will contain the (scaled) image * @in id The mfi item id * @in max_w Requested maximum image width (may not be obeyed) * @in max_h Requested maximum image height (may not be obeyed) * @return ART_FMT_* on success, -1 on error or no artwork found */ int artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h); /* * Get the artwork image for a group (an album or an artist) * * @out evbuf Event buffer that will contain the (scaled) image * @in id The group id (not the persistentid) * @in max_w Requested maximum image width (may not be obeyed) * @in max_h Requested maximum image height (may not be obeyed) * @return ART_FMT_* on success, -1 on error or no artwork found */ int artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h); /* * Checks if the file is an artwork file (based on user config) * * @in filename Name of the file * @return 1 if true, 0 if false */ int artwork_file_is_artwork(const char *filename); #endif /* !__ARTWORK_H__ */ forked-daapd-25.0/src/avio_evbuffer.c000066400000000000000000000054131313447753700175570ustar00rootroot00000000000000/* * Copyright (C) 2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include "logger.h" #include "avio_evbuffer.h" /* * libav AVIO interface for evbuffers */ #define BUFFER_SIZE 4096 struct avio_evbuffer { struct evbuffer *evbuf; uint8_t *buffer; }; static int avio_evbuffer_read(void *opaque, uint8_t *buf, int size) { struct avio_evbuffer *ae; int ret; ae = (struct avio_evbuffer *)opaque; ret = evbuffer_remove(ae->evbuf, buf, size); return ret; } static int avio_evbuffer_write(void *opaque, uint8_t *buf, int size) { struct avio_evbuffer *ae; int ret; ae = (struct avio_evbuffer *)opaque; ret = evbuffer_add(ae->evbuf, buf, size); return (ret == 0) ? size : -1; } static AVIOContext * avio_evbuffer_open(struct evbuffer *evbuf, int is_output) { struct avio_evbuffer *ae; AVIOContext *s; ae = (struct avio_evbuffer *)malloc(sizeof(struct avio_evbuffer)); if (!ae) { DPRINTF(E_LOG, L_FFMPEG, "Out of memory for avio_evbuffer\n"); return NULL; } ae->buffer = av_mallocz(BUFFER_SIZE); if (!ae->buffer) { DPRINTF(E_LOG, L_FFMPEG, "Out of memory for avio buffer\n"); free(ae); return NULL; } ae->evbuf = evbuf; if (is_output) s = avio_alloc_context(ae->buffer, BUFFER_SIZE, 1, ae, NULL, avio_evbuffer_write, NULL); else s = avio_alloc_context(ae->buffer, BUFFER_SIZE, 0, ae, avio_evbuffer_read, NULL, NULL); if (!s) { DPRINTF(E_LOG, L_FFMPEG, "Could not allocate AVIOContext\n"); av_free(ae->buffer); free(ae); return NULL; } s->seekable = 0; return s; } AVIOContext * avio_input_evbuffer_open(struct evbuffer *evbuf) { return avio_evbuffer_open(evbuf, 0); } AVIOContext * avio_output_evbuffer_open(struct evbuffer *evbuf) { return avio_evbuffer_open(evbuf, 1); } void avio_evbuffer_close(AVIOContext *s) { struct avio_evbuffer *ae; ae = (struct avio_evbuffer *)s->opaque; avio_flush(s); av_free(s->buffer); free(ae); av_free(s); } forked-daapd-25.0/src/avio_evbuffer.h000066400000000000000000000004461313447753700175650ustar00rootroot00000000000000 #ifndef __AVIO_EVBUFFER_H__ #define __AVIO_EVBUFFER_H__ #include AVIOContext * avio_input_evbuffer_open(struct evbuffer *evbuf); AVIOContext * avio_output_evbuffer_open(struct evbuffer *evbuf); void avio_evbuffer_close(AVIOContext *s); #endif /* !__AVIO_EVBUFFER_H__ */ forked-daapd-25.0/src/cache.c000066400000000000000000001234231313447753700160020ustar00rootroot00000000000000/* * Copyright (C) 2014 Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include #include "conffile.h" #include "logger.h" #include "httpd.h" #include "httpd_daap.h" #include "db.h" #include "cache.h" #include "listener.h" #include "commands.h" #define CACHE_VERSION 2 struct cache_arg { char *query; // daap query char *ua; // user agent int msec; char *path; // artwork path int type; // individual or group artwork int64_t persistentid; int max_w; int max_h; int format; time_t mtime; int cached; int del; struct evbuffer *evbuf; }; /* --- Globals --- */ // cache thread static pthread_t tid_cache; // Event base, pipes and events struct event_base *evbase_cache; static struct commands_base *cmdbase; static struct event *cache_daap_updateev; static int g_initialized; // Global cache database handle static sqlite3 *g_db_hdl; static char *g_db_path; // Global artwork stash struct stash { char *path; int format; size_t size; uint8_t *data; } g_stash; static int g_suspended; // The user may configure a threshold (in msec), and queries slower than // that will have their reply cached static int g_cfg_threshold; /* --------------------------------- HELPERS ------------------------------- */ /* The purpose of this function is to remove transient tags from a request * url (query), eg remove session-id=xxx */ static void remove_tag(char *in, const char *tag) { char *s; char *e; s = strstr(in, tag); if (!s) return; e = strchr(s, '&'); if (e) memmove(s, (e + 1), strlen(e + 1) + 1); else if (s > in) *(s - 1) = '\0'; } /* --------------------------------- MAIN --------------------------------- */ /* Thread: cache */ static int cache_create_tables(void) { #define T_REPLIES \ "CREATE TABLE IF NOT EXISTS replies (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " query VARCHAR(4096) NOT NULL," \ " reply BLOB" \ ");" #define T_QUERIES \ "CREATE TABLE IF NOT EXISTS queries (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " query VARCHAR(4096) UNIQUE NOT NULL," \ " user_agent VARCHAR(1024)," \ " msec INTEGER DEFAULT 0," \ " timestamp INTEGER DEFAULT 0" \ ");" #define I_QUERY \ "CREATE INDEX IF NOT EXISTS idx_query ON replies (query);" #define T_ARTWORK \ "CREATE TABLE IF NOT EXISTS artwork (" \ " id INTEGER PRIMARY KEY NOT NULL,"\ " type INTEGER NOT NULL DEFAULT 0," \ " persistentid INTEGER NOT NULL," \ " max_w INTEGER NOT NULL," \ " max_h INTEGER NOT NULL," \ " format INTEGER NOT NULL," \ " filepath VARCHAR(4096) NOT NULL," \ " db_timestamp INTEGER DEFAULT 0," \ " data BLOB" \ ");" #define I_ARTWORK_ID \ "CREATE INDEX IF NOT EXISTS idx_persistentidwh ON artwork(type, persistentid, max_w, max_h);" #define I_ARTWORK_PATH \ "CREATE INDEX IF NOT EXISTS idx_pathtime ON artwork(filepath, db_timestamp);" #define T_ADMIN_CACHE \ "CREATE TABLE IF NOT EXISTS admin_cache(" \ " key VARCHAR(32) PRIMARY KEY NOT NULL," \ " value VARCHAR(32) NOT NULL" \ ");" #define Q_CACHE_VERSION \ "INSERT INTO admin_cache (key, value) VALUES ('cache_version', '%d');" char *query; char *errmsg; int ret; // Create reply cache table ret = sqlite3_exec(g_db_hdl, T_REPLIES, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error creating cache table 'replies': %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Create query table (the queries for which we will generate and cache replies) ret = sqlite3_exec(g_db_hdl, T_QUERIES, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error creating cache table 'queries': %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Create index ret = sqlite3_exec(g_db_hdl, I_QUERY, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error creating index on replies(query): %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Create artwork table ret = sqlite3_exec(g_db_hdl, T_ARTWORK, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error creating cache table 'artwork': %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Create index ret = sqlite3_exec(g_db_hdl, I_ARTWORK_ID, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error creating index on artwork(type, persistentid, max_w, max_h): %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } ret = sqlite3_exec(g_db_hdl, I_ARTWORK_PATH, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error creating index on artwork(filepath, db_timestamp): %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Create admin cache table ret = sqlite3_exec(g_db_hdl, T_ADMIN_CACHE, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error creating cache table 'admin_cache': %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } query = sqlite3_mprintf(Q_CACHE_VERSION, CACHE_VERSION); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error inserting cache version: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } DPRINTF(E_DBG, L_CACHE, "Cache tables created\n"); return 0; #undef T_REPLIES #undef T_QUERIES #undef I_QUERY #undef T_ARTWORK #undef I_ARTWORK_ID #undef I_ARTWORK_PATH #undef T_ADMIN_CACHE #undef Q_CACHE_VERSION } static int cache_drop_tables(void) { #define D_REPLIES "DROP TABLE IF EXISTS replies;" #define D_QUERIES "DROP TABLE IF EXISTS queries;" #define D_QUERY "DROP INDEX IF EXISTS idx_query;" #define D_ARTWORK "DROP TABLE IF EXISTS artwork;" #define D_ARTWORK_ID "DROP INDEX IF EXISTS idx_persistentidwh;" #define D_ARTWORK_PATH "DROP INDEX IF EXISTS idx_pathtime;" #define D_ADMIN_CACHE "DROP TABLE IF EXISTS admin_cache;" #define Q_VACUUM "VACUUM;" char *errmsg; int ret; // Drop reply cache table ret = sqlite3_exec(g_db_hdl, D_REPLIES, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error dropping reply cache table: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Drop query table ret = sqlite3_exec(g_db_hdl, D_QUERIES, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error dropping query table: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Drop index ret = sqlite3_exec(g_db_hdl, D_QUERY, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error dropping query index: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Drop artwork table ret = sqlite3_exec(g_db_hdl, D_ARTWORK, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error dropping artwork table: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Drop index ret = sqlite3_exec(g_db_hdl, D_ARTWORK_ID, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error dropping artwork id index: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } ret = sqlite3_exec(g_db_hdl, D_ARTWORK_PATH, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error dropping artwork path index: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Drop admin cache table ret = sqlite3_exec(g_db_hdl, D_ADMIN_CACHE, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_CACHE, "Error dropping admin cache table: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } // Vacuum ret = sqlite3_exec(g_db_hdl, Q_VACUUM, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error vacuuming cache database: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } DPRINTF(E_DBG, L_CACHE, "Cache tables dropped\n"); return 0; #undef D_REPLIES #undef D_QUERIES #undef D_QUERY #undef D_ARTWORK #undef D_ARTWORK_ID #undef D_ARTWORK_PATH #undef D_ADMIN_CACHE #undef Q_VACUUM } /* * Compares the CACHE_VERSION against the version stored in the cache admin table. * Drops the tables and indexes if the versions are different. * * @return 0 if versions are equal, 1 if versions are different or the admin table does not exist, -1 if an error occurred */ static int cache_check_version(void) { #define Q_VER "SELECT value FROM admin_cache WHERE key = 'cache_version';" sqlite3_stmt *stmt; int cur_ver; int ret; DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", Q_VER); ret = sqlite3_prepare_v2(g_db_hdl, Q_VER, strlen(Q_VER) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_WARN, L_CACHE, "Could not prepare statement: %s\n", sqlite3_errmsg(g_db_hdl)); return 1; } ret = sqlite3_step(stmt); if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_CACHE, "Could not step: %s\n", sqlite3_errmsg(g_db_hdl)); sqlite3_finalize(stmt); return -1; } cur_ver = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); if (cur_ver != CACHE_VERSION) { DPRINTF(E_LOG, L_CACHE, "Database schema outdated, deleting cache v%d -> v%d\n", cur_ver, CACHE_VERSION); ret = cache_drop_tables(); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Error deleting database tables\n"); return -1; } return 1; } return 0; #undef Q_VER } static int cache_create(void) { #define Q_PRAGMA_CACHE_SIZE "PRAGMA cache_size=%d;" #define Q_PRAGMA_JOURNAL_MODE "PRAGMA journal_mode=%s;" #define Q_PRAGMA_SYNCHRONOUS "PRAGMA synchronous=%d;" char *errmsg; int ret; int cache_size; char *journal_mode; int synchronous; char *query; // Open db ret = sqlite3_open(g_db_path, &g_db_hdl); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Could not open cache database: %s\n", sqlite3_errmsg(g_db_hdl)); sqlite3_close(g_db_hdl); return -1; } // Check cache version ret = cache_check_version(); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Could not check cache database version\n"); sqlite3_close(g_db_hdl); return -1; } else if (ret > 0) { ret = cache_create_tables(); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Could not create cache database tables\n"); sqlite3_close(g_db_hdl); return -1; } } // Set page cache size in number of pages cache_size = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_cache_size_cache"); if (cache_size > -1) { query = sqlite3_mprintf(Q_PRAGMA_CACHE_SIZE, cache_size); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error setting pragma_cache_size_cache: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } } // Set journal mode journal_mode = cfg_getstr(cfg_getsec(cfg, "sqlite"), "pragma_journal_mode"); if (journal_mode) { query = sqlite3_mprintf(Q_PRAGMA_JOURNAL_MODE, journal_mode); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error setting pragma_journal_mode: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } } // Set synchronous flag synchronous = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_synchronous"); if (synchronous > -1) { query = sqlite3_mprintf(Q_PRAGMA_SYNCHRONOUS, synchronous); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error setting pragma_synchronous: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_close(g_db_hdl); return -1; } } DPRINTF(E_DBG, L_CACHE, "Cache created\n"); return 0; #undef Q_PRAGMA_CACHE_SIZE #undef Q_PRAGMA_JOURNAL_MODE #undef Q_PRAGMA_SYNCHRONOUS } static void cache_close(void) { sqlite3_stmt *stmt; if (!g_db_hdl) return; /* Tear down anything that's in flight */ while ((stmt = sqlite3_next_stmt(g_db_hdl, 0))) sqlite3_finalize(stmt); sqlite3_close(g_db_hdl); DPRINTF(E_DBG, L_CACHE, "Cache closed\n"); } /* Adds the reply (stored in evbuf) to the cache */ static int cache_daap_reply_add(const char *query, struct evbuffer *evbuf) { #define Q_TMPL "INSERT INTO replies (query, reply) VALUES (?, ?);" sqlite3_stmt *stmt; unsigned char *data; size_t datalen; int ret; datalen = evbuffer_get_length(evbuf); data = evbuffer_pullup(evbuf, -1); ret = sqlite3_prepare_v2(g_db_hdl, Q_TMPL, -1, &stmt, 0); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error preparing query for cache update: %s\n", sqlite3_errmsg(g_db_hdl)); return -1; } sqlite3_bind_text(stmt, 1, query, -1, SQLITE_STATIC); sqlite3_bind_blob(stmt, 2, data, datalen, SQLITE_STATIC); ret = sqlite3_step(stmt); if (ret != SQLITE_DONE) { DPRINTF(E_LOG, L_CACHE, "Error stepping query for cache update: %s\n", sqlite3_errmsg(g_db_hdl)); sqlite3_finalize(stmt); return -1; } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error finalizing query for cache update: %s\n", sqlite3_errmsg(g_db_hdl)); return -1; } //DPRINTF(E_DBG, L_CACHE, "Wrote cache reply, size %d\n", datalen); return 0; #undef Q_TMPL } /* Adds the query to the list of queries for which we will build and cache a reply */ static enum command_state cache_daap_query_add(void *arg, int *retval) { #define Q_TMPL "INSERT OR REPLACE INTO queries (user_agent, query, msec, timestamp) VALUES ('%q', '%q', %d, %" PRIi64 ");" #define Q_CLEANUP "DELETE FROM queries WHERE id NOT IN (SELECT id FROM queries ORDER BY timestamp DESC LIMIT 20);" struct cache_arg *cmdarg; struct timeval delay = { 60, 0 }; char *query; char *errmsg; int ret; cmdarg = arg; if (!cmdarg->ua) { DPRINTF(E_LOG, L_CACHE, "Couldn't add slow query to cache, unknown user-agent\n"); goto error_add; } // Currently we are only able to pre-build and cache these reply types if ( (strncmp(cmdarg->query, "/databases/1/containers/", strlen("/databases/1/containers/")) != 0) && (strncmp(cmdarg->query, "/databases/1/groups?", strlen("/databases/1/groups?")) != 0) && (strncmp(cmdarg->query, "/databases/1/items?", strlen("/databases/1/items?")) != 0) && (strncmp(cmdarg->query, "/databases/1/browse/", strlen("/databases/1/browse/")) != 0) ) goto error_add; remove_tag(cmdarg->query, "session-id"); remove_tag(cmdarg->query, "revision-number"); query = sqlite3_mprintf(Q_TMPL, cmdarg->ua, cmdarg->query, cmdarg->msec, (int64_t)time(NULL)); if (!query) { DPRINTF(E_LOG, L_CACHE, "Out of memory making query string.\n"); goto error_add; } ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error adding query to query list: %s\n", errmsg); sqlite3_free(errmsg); goto error_add; } DPRINTF(E_INFO, L_CACHE, "Slow query (%d ms) added to cache: '%s' (user-agent: '%s')\n", cmdarg->msec, cmdarg->query, cmdarg->ua); free(cmdarg->ua); free(cmdarg->query); // Limits the size of the cache to only contain replies for 20 most recent queries ret = sqlite3_exec(g_db_hdl, Q_CLEANUP, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error cleaning up query list before update: %s\n", errmsg); sqlite3_free(errmsg); *retval = -1; return COMMAND_END; } // Will set of cache regeneration after waiting a bit (so there is less risk // of disturbing the user) evtimer_add(cache_daap_updateev, &delay); *retval = 0; return COMMAND_END; error_add: if (cmdarg->ua) free(cmdarg->ua); if (cmdarg->query) free(cmdarg->query); *retval = -1; return COMMAND_END; #undef Q_CLEANUP #undef Q_TMPL } // Gets a reply from the cache. // cmdarg->evbuf will be filled with the reply (gzipped) static enum command_state cache_daap_query_get(void *arg, int *retval) { #define Q_TMPL "SELECT reply FROM replies WHERE query = ?;" struct cache_arg *cmdarg; sqlite3_stmt *stmt; char *query; int datalen; int ret; cmdarg = arg; query = cmdarg->query; remove_tag(query, "session-id"); remove_tag(query, "revision-number"); // Look in the DB ret = sqlite3_prepare_v2(g_db_hdl, Q_TMPL, -1, &stmt, 0); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error preparing query for cache update: %s\n", sqlite3_errmsg(g_db_hdl)); free(query); *retval = -1; return COMMAND_END; } sqlite3_bind_text(stmt, 1, query, -1, SQLITE_STATIC); ret = sqlite3_step(stmt); if (ret != SQLITE_ROW) { if (ret != SQLITE_DONE) DPRINTF(E_LOG, L_CACHE, "Error stepping query for cache update: %s\n", sqlite3_errmsg(g_db_hdl)); goto error_get; } datalen = sqlite3_column_bytes(stmt, 0); if (!cmdarg->evbuf) { DPRINTF(E_LOG, L_CACHE, "Error: DAAP reply evbuffer is NULL\n"); goto error_get; } ret = evbuffer_add(cmdarg->evbuf, sqlite3_column_blob(stmt, 0), datalen); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Out of memory for DAAP reply evbuffer\n"); goto error_get; } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) DPRINTF(E_LOG, L_CACHE, "Error finalizing query for getting cache: %s\n", sqlite3_errmsg(g_db_hdl)); DPRINTF(E_INFO, L_CACHE, "Cache hit: %s\n", query); free(query); *retval = 0; return COMMAND_END; error_get: sqlite3_finalize(stmt); free(query); *retval = -1; return COMMAND_END; #undef Q_TMPL } /* Removes the query from the cache */ static int cache_daap_query_delete(const int id) { #define Q_TMPL "DELETE FROM queries WHERE id = %d;" char *query; char *errmsg; int ret; query = sqlite3_mprintf(Q_TMPL, id); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error deleting query from cache: %s\n", errmsg); sqlite3_free(errmsg); return -1; } return 0; #undef Q_TMPL } /* Here we actually update the cache by asking httpd_daap for responses * to the queries set for caching */ static void cache_daap_update_cb(int fd, short what, void *arg) { sqlite3_stmt *stmt; struct evbuffer *evbuf; struct evbuffer *gzbuf; char *errmsg; char *query; int ret; if (g_suspended) { DPRINTF(E_DBG, L_CACHE, "Got a request to update DAAP cache while suspended\n"); return; } DPRINTF(E_LOG, L_CACHE, "Beginning DAAP cache update\n"); ret = sqlite3_exec(g_db_hdl, "DELETE FROM replies;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error clearing reply cache before update: %s\n", errmsg); sqlite3_free(errmsg); return; } ret = sqlite3_prepare_v2(g_db_hdl, "SELECT id, user_agent, query FROM queries;", -1, &stmt, 0); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error preparing for cache update: %s\n", sqlite3_errmsg(g_db_hdl)); return; } while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) { query = strdup((char *)sqlite3_column_text(stmt, 2)); evbuf = daap_reply_build(query, (char *)sqlite3_column_text(stmt, 1)); if (!evbuf) { DPRINTF(E_LOG, L_CACHE, "Error building DAAP reply for query: %s\n", query); cache_daap_query_delete(sqlite3_column_int(stmt, 0)); free(query); continue; } gzbuf = httpd_gzip_deflate(evbuf); if (!gzbuf) { DPRINTF(E_LOG, L_CACHE, "Error gzipping DAAP reply for query: %s\n", query); cache_daap_query_delete(sqlite3_column_int(stmt, 0)); free(query); evbuffer_free(evbuf); continue; } evbuffer_free(evbuf); cache_daap_reply_add(query, gzbuf); free(query); evbuffer_free(gzbuf); } if (ret != SQLITE_DONE) DPRINTF(E_LOG, L_CACHE, "Could not step: %s\n", sqlite3_errmsg(g_db_hdl)); sqlite3_finalize(stmt); DPRINTF(E_LOG, L_CACHE, "DAAP cache updated\n"); } /* Sets off an update by activating the event. The delay is because we are low * priority compared to other listeners of database updates. */ static enum command_state cache_daap_update(void *arg, int *retval) { struct timeval delay = { 10, 0 }; *retval = event_add(cache_daap_updateev, &delay); return COMMAND_END; } /* Callback from filescanner thread */ static void cache_daap_listener_cb(enum listener_event_type type) { commands_exec_async(cmdbase, cache_daap_update, NULL); } /* * Updates cached timestamps to current time for all cache entries for the given path, if the file was not modfied * after the cached timestamp. All cache entries for the given path are deleted, if the file was * modified after the cached timestamp. * * @param cmdarg->path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) * @param cmdarg->mtime modified timestamp of the artwork file * @return 0 if successful, -1 if an error occurred */ static enum command_state cache_artwork_ping_impl(void *arg, int *retval) { #define Q_TMPL_PING "UPDATE artwork SET db_timestamp = %" PRIi64 " WHERE filepath = '%q' AND db_timestamp >= %" PRIi64 ";" #define Q_TMPL_DEL "DELETE FROM artwork WHERE filepath = '%q' AND db_timestamp < %" PRIi64 ";" struct cache_arg *cmdarg; char *query; char *errmsg; int ret; cmdarg = arg; query = sqlite3_mprintf(Q_TMPL_PING, (int64_t)time(NULL), cmdarg->path, (int64_t)cmdarg->mtime); DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg); goto error_ping; } if (cmdarg->del > 0) { query = sqlite3_mprintf(Q_TMPL_DEL, cmdarg->path, (int64_t)cmdarg->mtime); DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg); goto error_ping; } } free(cmdarg->path); *retval = 0; return COMMAND_END; error_ping: sqlite3_free(errmsg); free(cmdarg->path); *retval = -1; return COMMAND_END; #undef Q_TMPL_PING #undef Q_TMPL_DEL } /* * Removes all cache entries for the given path * * @param cmdarg->path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) * @return 0 if successful, -1 if an error occurred */ static enum command_state cache_artwork_delete_by_path_impl(void *arg, int *retval) { #define Q_TMPL_DEL "DELETE FROM artwork WHERE filepath = '%q';" struct cache_arg *cmdarg; char *query; char *errmsg; int ret; cmdarg = arg; query = sqlite3_mprintf(Q_TMPL_DEL, cmdarg->path); DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg); sqlite3_free(errmsg); *retval = -1; return COMMAND_END; } DPRINTF(E_DBG, L_CACHE, "Deleted %d rows\n", sqlite3_changes(g_db_hdl)); *retval = 0; return COMMAND_END; #undef Q_TMPL_DEL } /* * Removes all cache entries with cached timestamp older than the given reference timestamp * * @param cmdarg->mtime reference timestamp * @return 0 if successful, -1 if an error occurred */ static enum command_state cache_artwork_purge_cruft_impl(void *arg, int *retval) { #define Q_TMPL "DELETE FROM artwork WHERE db_timestamp < %" PRIi64 ";" struct cache_arg *cmdarg; char *query; char *errmsg; int ret; cmdarg = arg; query = sqlite3_mprintf(Q_TMPL, (int64_t)cmdarg->mtime); DPRINTF(E_DBG, L_CACHE, "Running purge query '%s'\n", query); ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg); sqlite3_free(errmsg); *retval = -1; return COMMAND_END; } DPRINTF(E_DBG, L_CACHE, "Purged %d rows\n", sqlite3_changes(g_db_hdl)); *retval = 0; return COMMAND_END; #undef Q_TMPL } /* * Adds the given (scaled) artwork image to the artwork cache * * @param cmdarg->persistentid persistent songalbumid or songartistid * @param cmdarg->max_w maximum image width * @param cmdarg->max_h maximum image height * @param cmdarg->format ART_FMT_PNG for png, ART_FMT_JPEG for jpeg or 0 if no artwork available * @param cmdarg->filename the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) or empty if no artwork available * @param cmdarg->evbuf event buffer containing the (scaled) image * @return 0 if successful, -1 if an error occurred */ static enum command_state cache_artwork_add_impl(void *arg, int *retval) { struct cache_arg *cmdarg; sqlite3_stmt *stmt; char *query; uint8_t *data; int datalen; int ret; cmdarg = arg; query = "INSERT INTO artwork (id, persistentid, max_w, max_h, format, filepath, db_timestamp, data, type) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?);"; ret = sqlite3_prepare_v2(g_db_hdl, query, -1, &stmt, 0); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Could not prepare statement: %s\n", sqlite3_errmsg(g_db_hdl)); *retval = -1; return COMMAND_END; } datalen = evbuffer_get_length(cmdarg->evbuf); data = evbuffer_pullup(cmdarg->evbuf, -1); sqlite3_bind_int64(stmt, 1, cmdarg->persistentid); sqlite3_bind_int(stmt, 2, cmdarg->max_w); sqlite3_bind_int(stmt, 3, cmdarg->max_h); sqlite3_bind_int(stmt, 4, cmdarg->format); sqlite3_bind_text(stmt, 5, cmdarg->path, -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 6, (uint64_t)time(NULL)); sqlite3_bind_blob(stmt, 7, data, datalen, SQLITE_STATIC); sqlite3_bind_int(stmt, 8, cmdarg->type); ret = sqlite3_step(stmt); if (ret != SQLITE_DONE) { DPRINTF(E_LOG, L_CACHE, "Error stepping query for artwork add: %s\n", sqlite3_errmsg(g_db_hdl)); sqlite3_finalize(stmt); *retval = -1; return COMMAND_END; } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Error finalizing query for artwork add: %s\n", sqlite3_errmsg(g_db_hdl)); *retval = -1; return COMMAND_END; } *retval = 0; return COMMAND_END; } /* * Get the cached artwork image for the given persistentid and maximum width/height * * If there is a cached entry for the given id and width/height, the parameter cached is set to 1. * In this case format and data contain the cached values. * * @param cmdarg->type individual or group artwork * @param cmdarg->persistentid persistent itemid, songalbumid or songartistid * @param cmdarg->max_w maximum image width * @param cmdarg->max_h maximum image height * @param cmdarg->cached set by this function to 0 if no cache entry exists, otherwise 1 * @param cmdarg->format set by this function to the format of the cache entry * @param cmdarg->evbuf event buffer filled by this function with the scaled image * @return 0 if successful, -1 if an error occurred */ static enum command_state cache_artwork_get_impl(void *arg, int *retval) { #define Q_TMPL "SELECT a.format, a.data FROM artwork a WHERE a.type = %d AND a.persistentid = %" PRIi64 " AND a.max_w = %d AND a.max_h = %d;" struct cache_arg *cmdarg; sqlite3_stmt *stmt; char *query; int datalen; int ret; cmdarg = arg; query = sqlite3_mprintf(Q_TMPL, cmdarg->type, cmdarg->persistentid, cmdarg->max_w, cmdarg->max_h); if (!query) { DPRINTF(E_LOG, L_CACHE, "Out of memory for query string\n"); *retval = -1; return COMMAND_END; } DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query); ret = sqlite3_prepare_v2(g_db_hdl, query, -1, &stmt, 0); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_CACHE, "Could not prepare statement: %s\n", sqlite3_errmsg(g_db_hdl)); ret = -1; goto error_get; } ret = sqlite3_step(stmt); if (ret != SQLITE_ROW) { cmdarg->cached = 0; if (ret == SQLITE_DONE) { ret = 0; DPRINTF(E_DBG, L_CACHE, "No results\n"); } else { ret = -1; DPRINTF(E_LOG, L_CACHE, "Could not step: %s\n", sqlite3_errmsg(g_db_hdl)); } goto error_get; } cmdarg->format = sqlite3_column_int(stmt, 0); datalen = sqlite3_column_bytes(stmt, 1); if (!cmdarg->evbuf) { DPRINTF(E_LOG, L_CACHE, "Error: Artwork evbuffer is NULL\n"); ret = -1; goto error_get; } ret = evbuffer_add(cmdarg->evbuf, sqlite3_column_blob(stmt, 1), datalen); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Out of memory for artwork evbuffer\n"); ret = -1; goto error_get; } cmdarg->cached = 1; ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) DPRINTF(E_LOG, L_CACHE, "Error finalizing query for getting cache: %s\n", sqlite3_errmsg(g_db_hdl)); DPRINTF(E_DBG, L_CACHE, "Cache hit: %s\n", query); sqlite3_free(query); *retval = 0; return COMMAND_END; error_get: sqlite3_finalize(stmt); sqlite3_free(query); *retval = ret; return COMMAND_END; #undef Q_TMPL } static enum command_state cache_artwork_stash_impl(void *arg, int *retval) { struct cache_arg *cmdarg; cmdarg = arg; /* Clear current stash */ if (g_stash.path) { free(g_stash.path); free(g_stash.data); memset(&g_stash, 0, sizeof(struct stash)); } g_stash.size = evbuffer_get_length(cmdarg->evbuf); g_stash.data = malloc(g_stash.size); if (!g_stash.data) { DPRINTF(E_LOG, L_CACHE, "Out of memory for artwork stash data\n"); *retval = -1; return COMMAND_END; } g_stash.path = strdup(cmdarg->path); if (!g_stash.path) { DPRINTF(E_LOG, L_CACHE, "Out of memory for artwork stash path\n"); free(g_stash.data); *retval = -1; return COMMAND_END; } g_stash.format = cmdarg->format; *retval = evbuffer_copyout(cmdarg->evbuf, g_stash.data, g_stash.size); return COMMAND_END; } static enum command_state cache_artwork_read_impl(void *arg, int *retval) { struct cache_arg *cmdarg; cmdarg = arg; cmdarg->format = 0; if (!g_stash.path || !g_stash.data || (strcmp(g_stash.path, cmdarg->path) != 0)) { *retval = -1; return COMMAND_END; } cmdarg->format = g_stash.format; DPRINTF(E_DBG, L_CACHE, "Stash hit (format %d, size %zu): %s\n", g_stash.format, g_stash.size, g_stash.path); *retval = evbuffer_add(cmdarg->evbuf, g_stash.data, g_stash.size); return COMMAND_END; } static void * cache(void *arg) { int ret; ret = cache_create(); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Error: Cache create failed. Cache will be disabled.\n"); pthread_exit(NULL); } /* The thread needs a connection with the main db, so it can generate DAAP * replies through httpd_daap.c */ ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Error: DB init failed. Cache will be disabled.\n"); cache_close(); pthread_exit(NULL); } g_initialized = 1; event_base_dispatch(evbase_cache); if (g_initialized) { DPRINTF(E_LOG, L_CACHE, "Cache event loop terminated ahead of time!\n"); g_initialized = 0; } db_perthread_deinit(); cache_close(); pthread_exit(NULL); } /* ---------------------------- DAAP cache API --------------------------- */ /* The DAAP cache will cache raw daap replies for queries added with * cache_daap_add(). Only some query types are supported. * You can't add queries where the canonical reply is not HTTP_OK, because * daap_request will use that as default for cache replies. * */ void cache_daap_suspend(void) { g_suspended = 1; } void cache_daap_resume(void) { g_suspended = 0; } int cache_daap_get(const char *query, struct evbuffer *evbuf) { struct cache_arg cmdarg; if (!g_initialized) return -1; cmdarg.query = strdup(query); cmdarg.evbuf = evbuf; return commands_exec_sync(cmdbase, cache_daap_query_get, NULL, &cmdarg); } void cache_daap_add(const char *query, const char *ua, int msec) { struct cache_arg *cmdarg; if (!g_initialized) return; cmdarg = calloc(1, sizeof(struct cache_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_CACHE, "Could not allocate cache_arg\n"); return; } cmdarg->query = strdup(query); cmdarg->ua = strdup(ua); cmdarg->msec = msec; commands_exec_async(cmdbase, cache_daap_query_add, cmdarg); } int cache_daap_threshold(void) { return g_cfg_threshold; } /* --------------------------- Artwork cache API -------------------------- */ /* * Updates cached timestamps to current time for all cache entries for the given path, if the file was not modfied * after the cached timestamp. * * If the parameter "del" is greater than 0, all cache entries for the given path are deleted, if the file was * modified after the cached timestamp. * * @param path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) * @param mtime modified timestamp of the artwork file * @param del if > 0 cached entries for the given path are deleted if the cached timestamp (db_timestamp) is older than mtime * @return 0 if successful, -1 if an error occurred */ void cache_artwork_ping(const char *path, time_t mtime, int del) { struct cache_arg *cmdarg; if (!g_initialized) return; cmdarg = calloc(1, sizeof(struct cache_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_CACHE, "Could not allocate cache_arg\n"); return; } cmdarg->path = strdup(path); cmdarg->mtime = mtime; cmdarg->del = del; commands_exec_async(cmdbase, cache_artwork_ping_impl, cmdarg); } /* * Removes all cache entries for the given path * * @param path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) * @return 0 if successful, -1 if an error occurred */ int cache_artwork_delete_by_path(char *path) { struct cache_arg cmdarg; if (!g_initialized) return -1; cmdarg.path = path; return commands_exec_sync(cmdbase, cache_artwork_delete_by_path_impl, NULL, &cmdarg); } /* * Removes all cache entries with cached timestamp older than the given reference timestamp * * @param ref reference timestamp * @return 0 if successful, -1 if an error occurred */ int cache_artwork_purge_cruft(time_t ref) { struct cache_arg cmdarg; if (!g_initialized) return -1; cmdarg.mtime = ref; return commands_exec_sync(cmdbase, cache_artwork_purge_cruft_impl, NULL, &cmdarg); } /* * Adds the given (scaled) artwork image to the artwork cache * * @param type individual or group artwork * @param persistentid persistent itemid, songalbumid or songartistid * @param max_w maximum image width * @param max_h maximum image height * @param format ART_FMT_PNG for png, ART_FMT_JPEG for jpeg or 0 if no artwork available * @param filename the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) or empty if no artwork available * @param evbuf event buffer containing the (scaled) image * @return 0 if successful, -1 if an error occurred */ int cache_artwork_add(int type, int64_t persistentid, int max_w, int max_h, int format, char *filename, struct evbuffer *evbuf) { struct cache_arg cmdarg; if (!g_initialized) return -1; cmdarg.type = type; cmdarg.persistentid = persistentid; cmdarg.max_w = max_w; cmdarg.max_h = max_h; cmdarg.format = format; cmdarg.path = filename; cmdarg.evbuf = evbuf; return commands_exec_sync(cmdbase, cache_artwork_add_impl, NULL, &cmdarg); } /* * Get the cached artwork image for the given persistentid and maximum width/height * * If there is a cached entry for the given id and width/height, the parameter cached is set to 1. * In this case format and data contain the cached values. * * @param persistentid persistent songalbumid or songartistid * @param max_w maximum image width * @param max_h maximum image height * @param cached set by this function to 0 if no cache entry exists, otherwise 1 * @param format set by this function to the format of the cache entry * @param evbuf event buffer filled by this function with the scaled image * @return 0 if successful, -1 if an error occurred */ int cache_artwork_get(int type, int64_t persistentid, int max_w, int max_h, int *cached, int *format, struct evbuffer *evbuf) { struct cache_arg cmdarg; int ret; if (!g_initialized) { *cached = 0; *format = 0; return 0; } cmdarg.type = type; cmdarg.persistentid = persistentid; cmdarg.max_w = max_w; cmdarg.max_h = max_h; cmdarg.evbuf = evbuf; ret = commands_exec_sync(cmdbase, cache_artwork_get_impl, NULL, &cmdarg); *format = cmdarg.format; *cached = cmdarg.cached; return ret; } /* * Put an artwork image in the in-memory stash (the previous will be deleted) * * @param evbuf event buffer with the cached image to cache * @param path the source (url) of the image to stash * @param format the format of the image * @return 0 if successful, -1 if an error occurred */ int cache_artwork_stash(struct evbuffer *evbuf, char *path, int format) { struct cache_arg cmdarg; if (!g_initialized) return -1; cmdarg.evbuf = evbuf; cmdarg.path = path; cmdarg.format = format; return commands_exec_sync(cmdbase, cache_artwork_stash_impl, NULL, &cmdarg); } /* * Read the cached artwork image in the in-memory stash into evbuffer * * @param evbuf event buffer filled by this function with the cached image * @param path this function will check that the path matches the cached image's path * @param format set by this function to the format of the image * @return 0 if successful, -1 if an error occurred */ int cache_artwork_read(struct evbuffer *evbuf, char *path, int *format) { struct cache_arg cmdarg; int ret; if (!g_initialized) return -1; cmdarg.evbuf = evbuf; cmdarg.path = path; ret = commands_exec_sync(cmdbase, cache_artwork_read_impl, NULL, &cmdarg); *format = cmdarg.format; return ret; } /* -------------------------- Cache general API --------------------------- */ int cache_init(void) { int ret; g_initialized = 0; g_db_path = cfg_getstr(cfg_getsec(cfg, "general"), "cache_path"); if (!g_db_path || (strlen(g_db_path) == 0)) { DPRINTF(E_LOG, L_CACHE, "Cache path invalid, disabling cache\n"); return 0; } g_cfg_threshold = cfg_getint(cfg_getsec(cfg, "general"), "cache_daap_threshold"); if (g_cfg_threshold == 0) { DPRINTF(E_LOG, L_CACHE, "Cache threshold set to 0, disabling cache\n"); return 0; } evbase_cache = event_base_new(); if (!evbase_cache) { DPRINTF(E_LOG, L_CACHE, "Could not create an event base\n"); goto evbase_fail; } cache_daap_updateev = evtimer_new(evbase_cache, cache_daap_update_cb, NULL); if (!cache_daap_updateev) { DPRINTF(E_LOG, L_CACHE, "Could not create cache event\n"); goto evnew_fail; } cmdbase = commands_base_new(evbase_cache, NULL); ret = listener_add(cache_daap_listener_cb, LISTENER_DATABASE); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Could not create listener event\n"); goto listener_fail; } DPRINTF(E_INFO, L_CACHE, "cache thread init\n"); ret = pthread_create(&tid_cache, NULL, cache, NULL); if (ret < 0) { DPRINTF(E_LOG, L_CACHE, "Could not spawn cache thread: %s\n", strerror(errno)); goto thread_fail; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_cache, "cache"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_cache, "cache"); #endif return 0; thread_fail: listener_remove(cache_daap_listener_cb); listener_fail: commands_base_free(cmdbase); evnew_fail: event_base_free(evbase_cache); evbase_cache = NULL; evbase_fail: return -1; } void cache_deinit(void) { int ret; if (!g_initialized) return; g_initialized = 0; listener_remove(cache_daap_listener_cb); commands_base_destroy(cmdbase); ret = pthread_join(tid_cache, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_CACHE, "Could not join cache thread: %s\n", strerror(errno)); return; } // Free event base (should free events too) event_base_free(evbase_cache); } forked-daapd-25.0/src/cache.h000066400000000000000000000023311313447753700160010ustar00rootroot00000000000000 #ifndef __CACHE_H__ #define __CACHE_H__ #include /* ---------------------------- DAAP cache API --------------------------- */ void cache_daap_suspend(void); void cache_daap_resume(void); int cache_daap_get(const char *query, struct evbuffer *evbuf); void cache_daap_add(const char *query, const char *ua, int msec); int cache_daap_threshold(void); /* ---------------------------- Artwork cache API --------------------------- */ #define CACHE_ARTWORK_GROUP 0 #define CACHE_ARTWORK_INDIVIDUAL 1 void cache_artwork_ping(const char *path, time_t mtime, int del); int cache_artwork_delete_by_path(char *path); int cache_artwork_purge_cruft(time_t ref); int cache_artwork_add(int type, int64_t persistentid, int max_w, int max_h, int format, char *filename, struct evbuffer *evbuf); int cache_artwork_get(int type, int64_t persistentid, int max_w, int max_h, int *cached, int *format, struct evbuffer *evbuf); int cache_artwork_stash(struct evbuffer *evbuf, char *path, int format); int cache_artwork_read(struct evbuffer *evbuf, char *path, int *format); /* ---------------------------- Cache API --------------------------- */ int cache_init(void); void cache_deinit(void); #endif /* !__CACHE_H__ */ forked-daapd-25.0/src/commands.c000066400000000000000000000254001313447753700165340ustar00rootroot00000000000000/* * Copyright (C) 2016 Christian Meffert * * 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 */ #include "commands.h" #include #include #include #include #include #include #include "logger.h" #include "misc.h" struct command { pthread_mutex_t lck; pthread_cond_t cond; command_function func; command_function func_bh; void *arg; int nonblock; int ret; int pending; }; struct commands_base { struct event_base *evbase; command_exit_cb exit_cb; int command_pipe[2]; struct event *command_event; struct command *current_cmd; }; /* * Asynchronous execution of the command function */ static void command_cb_async(struct commands_base *cmdbase, struct command *cmd) { enum command_state cmdstate; // Command is executed asynchronously cmdstate = cmd->func(cmd->arg, &cmd->ret); // Only free arg if there are no pending events (used in worker.c) if (cmdstate != COMMAND_PENDING && cmd->arg) free(cmd->arg); free(cmd); event_add(cmdbase->command_event, NULL); } /* * Synchronous execution of the command function */ static void command_cb_sync(struct commands_base *cmdbase, struct command *cmd) { enum command_state cmdstate; CHECK_ERR(L_MAIN, pthread_mutex_lock(&cmd->lck)); cmdstate = cmd->func(cmd->arg, &cmd->ret); if (cmdstate == COMMAND_PENDING) { // Command execution is waiting for pending events before returning to the caller cmdbase->current_cmd = cmd; cmd->pending = cmd->ret; } else { // Command execution finished, execute the bottom half function if (cmd->ret == 0 && cmd->func_bh) cmd->func_bh(cmd->arg, &cmd->ret); event_add(cmdbase->command_event, NULL); // Signal the calling thread that the command execution finished CHECK_ERR(L_MAIN, pthread_cond_signal(&cmd->cond)); CHECK_ERR(L_MAIN, pthread_mutex_unlock(&cmd->lck)); // Note if cmd->func was cmdloop_exit then cmdbase may be invalid now, // because commands_base_destroy() may have freed it } } /* * Event callback function * * Function is triggered by libevent if there is data to read on the command pipe (writing to the command pipe happens through * the send_command function). */ static void command_cb(int fd, short what, void *arg) { struct commands_base *cmdbase; struct command *cmd; int ret; cmdbase = arg; // Get the command to execute from the pipe ret = read(cmdbase->command_pipe[0], &cmd, sizeof(cmd)); if (ret != sizeof(cmd)) { DPRINTF(E_LOG, L_MAIN, "Error reading command from command pipe: expected %zu bytes, read %d bytes\n", sizeof(cmd), ret); event_add(cmdbase->command_event, NULL); return; } // Execute the command function if (cmd->nonblock) { // Command is executed asynchronously command_cb_async(cmdbase, cmd); } else { // Command is executed synchronously, caller is waiting until signaled that the execution finished command_cb_sync(cmdbase, cmd); } } /* * Writes the given command to the command pipe */ static int send_command(struct commands_base *cmdbase, struct command *cmd) { int ret; if (!cmd->func) { DPRINTF(E_LOG, L_MAIN, "Programming error: send_command called with command->func NULL!\n"); return -1; } ret = write(cmdbase->command_pipe[1], &cmd, sizeof(cmd)); if (ret != sizeof(cmd)) { DPRINTF(E_LOG, L_MAIN, "Bad write to command pipe (write incomplete)\n"); return -1; } return 0; } /* * Creates a new command base, needs to be freed by commands_base_destroy or commands_base_free. * * @param evbase The libevent base to use for command handling * @param exit_cb Optional callback function to be called during commands_base_destroy */ struct commands_base * commands_base_new(struct event_base *evbase, command_exit_cb exit_cb) { struct commands_base *cmdbase; int ret; cmdbase = calloc(1, sizeof(struct commands_base)); if (!cmdbase) { DPRINTF(E_LOG, L_MAIN, "Out of memory for cmdbase\n"); return NULL; } #ifdef HAVE_PIPE2 ret = pipe2(cmdbase->command_pipe, O_CLOEXEC); #else ret = pipe(cmdbase->command_pipe); #endif if (ret < 0) { DPRINTF(E_LOG, L_MAIN, "Could not create command pipe: %s\n", strerror(errno)); free(cmdbase); return NULL; } cmdbase->command_event = event_new(evbase, cmdbase->command_pipe[0], EV_READ, command_cb, cmdbase); if (!cmdbase->command_event) { DPRINTF(E_LOG, L_MAIN, "Could not create cmd event\n"); close(cmdbase->command_pipe[0]); close(cmdbase->command_pipe[1]); free(cmdbase); return NULL; } ret = event_add(cmdbase->command_event, NULL); if (ret != 0) { DPRINTF(E_LOG, L_MAIN, "Could not add cmd event\n"); close(cmdbase->command_pipe[0]); close(cmdbase->command_pipe[1]); free(cmdbase); return NULL; } cmdbase->evbase = evbase; cmdbase->exit_cb = exit_cb; return cmdbase; } /* * Frees the command base and closes the (internally used) pipes */ int commands_base_free(struct commands_base *cmdbase) { event_free(cmdbase->command_event); close(cmdbase->command_pipe[0]); close(cmdbase->command_pipe[1]); free(cmdbase); return 0; } /* * Gets the current return value for the current pending command. * * If a command has more than one pending event, each event can access the previous set return value * if it depends on it. * * @param cmdbase The command base * @return The current return value */ int commands_exec_returnvalue(struct commands_base *cmdbase) { if (cmdbase->current_cmd == NULL) return 0; return cmdbase->current_cmd->ret; } /* * If a command function returned COMMAND_PENDING, each event triggered by this command needs to * call command_exec_end, passing it the return value of the event execution. * * If a command function is waiting for multiple events, each event needs to call command_exec_end. * The command base keeps track of the number of still pending events and only returns to the caller * if there are no pending events left. * * @param cmdbase The command base (holds the current pending command) * @param retvalue The return value for the calling thread */ void commands_exec_end(struct commands_base *cmdbase, int retvalue) { struct command *current_cmd = cmdbase->current_cmd; if (!current_cmd) return; // A pending event finished, decrease the number of pending events and update the return value current_cmd->pending--; current_cmd->ret = retvalue; DPRINTF(E_DBG, L_MAIN, "Command has %d pending events\n", current_cmd->pending); // If there are still pending events return if (current_cmd->pending > 0) return; // All pending events have finished, execute the bottom half and signal the caller that the command execution finished if (current_cmd->func_bh) current_cmd->func_bh(current_cmd->arg, ¤t_cmd->ret); cmdbase->current_cmd = NULL; /* Process commands again */ event_add(cmdbase->command_event, NULL); CHECK_ERR(L_MAIN, pthread_cond_signal(¤t_cmd->cond)); CHECK_ERR(L_MAIN, pthread_mutex_unlock(¤t_cmd->lck)); } /* * Execute the function 'func' with the given argument 'arg' in the event loop thread. * Blocks the caller (thread) until the function returned. * * If a function 'func_bh' ("bottom half") is given, it is executed after 'func' has successfully * finished. * * @param cmdbase The command base * @param func The function to be executed * @param func_bh The bottom half function to be executed after all pending events from func are processed * @param arg Argument passed to func (and func_bh) * @return Return value of func (or func_bh if func_bh is not NULL) */ int commands_exec_sync(struct commands_base *cmdbase, command_function func, command_function func_bh, void *arg) { struct command cmd; int ret; memset(&cmd, 0, sizeof(struct command)); cmd.func = func; cmd.func_bh = func_bh; cmd.arg = arg; cmd.nonblock = 0; CHECK_ERR(L_MAIN, mutex_init(&cmd.lck)); CHECK_ERR(L_MAIN, pthread_cond_init(&cmd.cond, NULL)); CHECK_ERR(L_MAIN, pthread_mutex_lock(&cmd.lck)); ret = send_command(cmdbase, &cmd); if (ret < 0) { DPRINTF(E_LOG, L_MAIN, "Error sending command\n"); cmd.ret = -1; } else { CHECK_ERR(L_MAIN, pthread_cond_wait(&cmd.cond, &cmd.lck)); } CHECK_ERR(L_MAIN, pthread_mutex_unlock(&cmd.lck)); CHECK_ERR(L_MAIN, pthread_cond_destroy(&cmd.cond)); CHECK_ERR(L_MAIN, pthread_mutex_destroy(&cmd.lck)); return cmd.ret; } /* * Execute the function 'func' with the given argument 'arg' in the event loop thread. * Triggers the function execution and immediately returns (does not wait for func to finish). * * The pointer passed as argument is freed in the event loop thread after func returned. * * @param cmdbase The command base * @param func The function to be executed * @param arg Argument passed to func * @return 0 if triggering the function execution succeeded, -1 on failure. */ int commands_exec_async(struct commands_base *cmdbase, command_function func, void *arg) { struct command *cmd; int ret; cmd = calloc(1, sizeof(struct command)); cmd->func = func; cmd->func_bh = NULL; cmd->arg = arg; cmd->nonblock = 1; ret = send_command(cmdbase, cmd); if (ret < 0) { free(cmd); return -1; } return 0; } /* * Command to break the libevent loop * * If the command base was created with an exit_cb function, exit_cb is called before breaking the * libevent loop. * * @param arg The command base * @param retval Always set to COMMAND_END */ static enum command_state cmdloop_exit(void *arg, int *retval) { struct commands_base *cmdbase = arg; *retval = 0; if (cmdbase->exit_cb) cmdbase->exit_cb(); event_base_loopbreak(cmdbase->evbase); return COMMAND_END; } /* * Break the libevent loop for the given command base, closes the internally used pipes * and frees the command base. * * @param cmdbase The command base */ void commands_base_destroy(struct commands_base *cmdbase) { commands_exec_sync(cmdbase, cmdloop_exit, NULL, cmdbase); commands_base_free(cmdbase); } forked-daapd-25.0/src/commands.h000066400000000000000000000030341313447753700165400ustar00rootroot00000000000000 #ifndef SRC_COMMANDS_H_ #define SRC_COMMANDS_H_ #include enum command_state { COMMAND_END = 0, COMMAND_PENDING = 1, }; /* * Function that will be executed in the event loop thread. * * If the function has pending events to complete, it needs to return * COMMAND_PENDING with 'ret' set to the number of pending events to wait for. * * If the function returns with COMMAND_END, command execution will proceed * with the "bottem half" function (if passed to the command_exec function) only * if 'ret' is 0. * * @param arg Opaque pointer passed by command_exec_sync or command_exec_async * @param ret Pointer to the return value for the caller of the command * @return COMMAND_END if there are no pending events (function execution is * complete) or COMMAND_PENDING if there are pending events */ typedef enum command_state (*command_function)(void *arg, int *ret); typedef void (*command_exit_cb)(void); struct commands_base; struct commands_base * commands_base_new(struct event_base *evbase, command_exit_cb exit_cb); int commands_base_free(struct commands_base *cmdbase); int commands_exec_returnvalue(struct commands_base *cmdbase); void commands_exec_end(struct commands_base *cmdbase, int retvalue); int commands_exec_sync(struct commands_base *cmdbase, command_function func, command_function func_bh, void *arg); int commands_exec_async(struct commands_base *cmdbase, command_function func, void *arg); void commands_base_destroy(struct commands_base *cmdbase); #endif /* SRC_COMMANDS_H_ */ forked-daapd-25.0/src/conffile.c000066400000000000000000000225111313447753700165200ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include "logger.h" #include "misc.h" #include "conffile.h" /* Forward */ static int cb_loglevel(cfg_t *cfg, cfg_opt_t *opt, const char *value, void *result); /* general section structure */ static cfg_opt_t sec_general[] = { CFG_STR("uid", "nobody", CFGF_NONE), CFG_STR("admin_password", NULL, CFGF_NONE), CFG_STR("logfile", STATEDIR "/log/" PACKAGE ".log", CFGF_NONE), CFG_STR("db_path", STATEDIR "/cache/" PACKAGE "/songs3.db", CFGF_NONE), CFG_INT("db_pragma_cache_size", -1, CFGF_NONE), CFG_STR("db_pragma_journal_mode", NULL, CFGF_NONE), CFG_INT("db_pragma_synchronous", -1, CFGF_NONE), CFG_INT_CB("loglevel", E_LOG, CFGF_NONE, &cb_loglevel), CFG_BOOL("ipv6", cfg_true, CFGF_NONE), CFG_STR("cache_path", STATEDIR "/cache/" PACKAGE "/cache.db", CFGF_NONE), CFG_INT("cache_daap_threshold", 1000, CFGF_NONE), CFG_BOOL("speaker_autoselect", cfg_true, CFGF_NONE), #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) CFG_BOOL("high_resolution_clock", cfg_false, CFGF_NONE), #else CFG_BOOL("high_resolution_clock", cfg_true, CFGF_NONE), #endif CFG_STR("allow_origin", "*", CFGF_NONE), CFG_END() }; /* library section structure */ static cfg_opt_t sec_library[] = { CFG_STR("name", "My Music on %h", CFGF_NONE), CFG_INT("port", 3689, CFGF_NONE), CFG_STR("password", NULL, CFGF_NONE), CFG_STR_LIST("directories", NULL, CFGF_NONE), CFG_STR_LIST("podcasts", NULL, CFGF_NONE), CFG_STR_LIST("audiobooks", NULL, CFGF_NONE), CFG_STR_LIST("compilations", NULL, CFGF_NONE), CFG_STR("compilation_artist", NULL, CFGF_NONE), CFG_BOOL("hide_singles", cfg_false, CFGF_NONE), CFG_BOOL("radio_playlists", cfg_false, CFGF_NONE), CFG_STR("name_library", "Library", CFGF_NONE), CFG_STR("name_music", "Music", CFGF_NONE), CFG_STR("name_movies", "Movies", CFGF_NONE), CFG_STR("name_tvshows", "TV Shows", CFGF_NONE), CFG_STR("name_podcasts", "Podcasts", CFGF_NONE), CFG_STR("name_audiobooks", "Audiobooks", CFGF_NONE), CFG_STR("name_radio", "Radio", CFGF_NONE), CFG_STR_LIST("artwork_basenames", "{artwork,cover,Folder}", CFGF_NONE), CFG_BOOL("artwork_individual", cfg_false, CFGF_NONE), CFG_STR_LIST("filetypes_ignore", "{.db,.ini,.db-journal,.pdf,.metadata}", CFGF_NONE), CFG_STR_LIST("filepath_ignore", NULL, CFGF_NONE), CFG_BOOL("filescan_disable", cfg_false, CFGF_NONE), CFG_BOOL("itunes_overrides", cfg_false, CFGF_NONE), CFG_BOOL("itunes_smartpl", cfg_false, CFGF_NONE), CFG_STR_LIST("no_decode", NULL, CFGF_NONE), CFG_STR_LIST("force_decode", NULL, CFGF_NONE), CFG_BOOL("pipe_autostart", cfg_true, CFGF_NONE), CFG_END() }; /* local audio section structure */ static cfg_opt_t sec_audio[] = { CFG_STR("nickname", "Computer", CFGF_NONE), CFG_STR("type", NULL, CFGF_NONE), CFG_STR("card", "default", CFGF_NONE), CFG_STR("mixer", NULL, CFGF_NONE), CFG_STR("mixer_device", NULL, CFGF_NONE), CFG_INT("offset", 0, CFGF_NONE), CFG_END() }; /* AirPlay/ApEx device section structure */ static cfg_opt_t sec_airplay[] = { CFG_INT("max_volume", 11, CFGF_NONE), CFG_BOOL("exclude", cfg_false, CFGF_NONE), CFG_STR("password", NULL, CFGF_NONE), CFG_END() }; /* FIFO section structure */ static cfg_opt_t sec_fifo[] = { CFG_STR("nickname", "fifo", CFGF_NONE), CFG_STR("path", NULL, CFGF_NONE), CFG_END() }; /* Spotify section structure */ static cfg_opt_t sec_spotify[] = { CFG_STR("settings_dir", STATEDIR "/cache/" PACKAGE "/libspotify", CFGF_NONE), CFG_STR("cache_dir", "/tmp", CFGF_NONE), CFG_INT("bitrate", 0, CFGF_NONE), CFG_BOOL("base_playlist_disable", cfg_false, CFGF_NONE), CFG_BOOL("artist_override", cfg_false, CFGF_NONE), CFG_BOOL("album_override", cfg_false, CFGF_NONE), CFG_END() }; /* SQLite section structure */ static cfg_opt_t sec_sqlite[] = { CFG_INT("pragma_cache_size_library", -1, CFGF_NONE), CFG_INT("pragma_cache_size_cache", -1, CFGF_NONE), CFG_STR("pragma_journal_mode", NULL, CFGF_NONE), CFG_INT("pragma_synchronous", -1, CFGF_NONE), CFG_BOOL("vacuum", cfg_true, CFGF_NONE), CFG_END() }; /* MPD section structure */ static cfg_opt_t sec_mpd[] = { CFG_INT("port", 6600, CFGF_NONE), CFG_INT("http_port", 0, CFGF_NONE), CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NONE), CFG_END() }; /* Config file structure */ static cfg_opt_t toplvl_cfg[] = { CFG_SEC("general", sec_general, CFGF_NONE), CFG_SEC("library", sec_library, CFGF_NONE), CFG_SEC("audio", sec_audio, CFGF_NONE), CFG_SEC("airplay", sec_airplay, CFGF_MULTI | CFGF_TITLE), CFG_SEC("fifo", sec_fifo, CFGF_NONE), CFG_SEC("spotify", sec_spotify, CFGF_NONE), CFG_SEC("sqlite", sec_sqlite, CFGF_NONE), CFG_SEC("mpd", sec_mpd, CFGF_NONE), CFG_END() }; cfg_t *cfg; uint64_t libhash; uid_t runas_uid; gid_t runas_gid; static int cb_loglevel(cfg_t *cfg, cfg_opt_t *opt, const char *value, void *result) { if (strcasecmp(value, "fatal") == 0) *(long int *)result = E_FATAL; else if (strcasecmp(value, "log") == 0) *(long int *)result = E_LOG; else if (strcasecmp(value, "warning") == 0) *(long int *)result = E_WARN; else if (strcasecmp(value, "info") == 0) *(long int *)result = E_INFO; else if (strcasecmp(value, "debug") == 0) *(long int *)result = E_DBG; else if (strcasecmp(value, "spam") == 0) *(long int *)result = E_SPAM; else { DPRINTF(E_WARN, L_CONF, "Unrecognised loglevel '%s'\n", value); /* Default to warning */ *(long int *)result = 1; } return 0; } static int conffile_expand_libname(cfg_t *lib) { char *libname; char *hostname; char *s; char *d; char *expanded; struct utsname sysinfo; size_t len; size_t olen; size_t hostlen; size_t verlen; int ret; libname = cfg_getstr(lib, "name"); olen = strlen(libname); /* Fast path */ s = strchr(libname, '%'); if (!s) { libhash = murmur_hash64(libname, olen, 0); return 0; } /* Grab what we need */ ret = uname(&sysinfo); if (ret != 0) { DPRINTF(E_WARN, L_CONF, "Could not get system name: %s\n", strerror(errno)); hostname = "Unknown host"; } else hostname = sysinfo.nodename; hostlen = strlen(hostname); verlen = strlen(VERSION); /* Compute expanded size */ len = olen; s = libname; while (*s) { if (*s == '%') { s++; switch (*s) { case 'h': len += hostlen; break; case 'v': len += verlen; break; } } s++; } expanded = (char *)malloc(len + 1); if (!expanded) { DPRINTF(E_FATAL, L_CONF, "Out of memory\n"); return -1; } memset(expanded, 0, len + 1); /* Do the actual expansion */ s = libname; d = expanded; while (*s) { if (*s == '%') { s++; switch (*s) { case 'h': strcat(d, hostname); d += hostlen; break; case 'v': strcat(d, VERSION); d += verlen; break; } s++; } else { *d = *s; s++; d++; } } cfg_setstr(lib, "name", expanded); libhash = murmur_hash64(expanded, strlen(expanded), 0); free(expanded); return 0; } int conffile_load(char *file) { cfg_t *lib; struct passwd *pw; char *runas; int ret; cfg = cfg_init(toplvl_cfg, CFGF_NONE); ret = cfg_parse(cfg, file); if (ret == CFG_FILE_ERROR) { DPRINTF(E_FATAL, L_CONF, "Could not open config file %s\n", file); goto out_fail; } else if (ret == CFG_PARSE_ERROR) { DPRINTF(E_FATAL, L_CONF, "Parse error in config file %s\n", file); goto out_fail; } /* Resolve runas username */ runas = cfg_getstr(cfg_getsec(cfg, "general"), "uid"); pw = getpwnam(runas); if (!pw) { DPRINTF(E_FATAL, L_CONF, "Could not lookup user %s: %s\n", runas, strerror(errno)); goto out_fail; } runas_uid = pw->pw_uid; runas_gid = pw->pw_gid; lib = cfg_getsec(cfg, "library"); if (cfg_size(lib, "directories") == 0) { DPRINTF(E_FATAL, L_CONF, "No directories specified for library\n"); goto out_fail; } /* Do keyword expansion on library names */ ret = conffile_expand_libname(lib); if (ret != 0) { DPRINTF(E_FATAL, L_CONF, "Could not expand library name\n"); goto out_fail; } return 0; out_fail: cfg_free(cfg); return -1; } void conffile_unload(void) { cfg_free(cfg); } forked-daapd-25.0/src/conffile.h000066400000000000000000000005061313447753700165250ustar00rootroot00000000000000 #ifndef __CONFFILE_H__ #define __CONFFILE_H__ #include #include #define CONFFILE CONFDIR "/forked-daapd.conf" extern cfg_t *cfg; extern uint64_t libhash; extern uid_t runas_uid; extern gid_t runas_gid; int conffile_load(char *file); void conffile_unload(void); #endif /* !__CONFFILE_H__ */ forked-daapd-25.0/src/daap_query.c000066400000000000000000000100171313447753700170630ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include "logger.h" #include "misc.h" #include "daap_query.h" #include "DAAPLexer.h" #include "DAAPParser.h" #include "DAAP2SQL.h" char * daap_query_parse_sql(const char *daap_query) { /* Input DAAP query, fed to the lexer */ pANTLR3_INPUT_STREAM query; /* Lexer and the resulting token stream, fed to the parser */ pDAAPLexer lxr; pANTLR3_COMMON_TOKEN_STREAM tkstream; /* Parser and the resulting AST, fed to the tree parser */ pDAAPParser psr; DAAPParser_query_return qtree; pANTLR3_COMMON_TREE_NODE_STREAM nodes; /* Tree parser and the resulting SQL query string */ pDAAP2SQL sqlconv; pANTLR3_STRING sql; char *ret = NULL; if (!daap_query) { DPRINTF(E_LOG, L_DAAP, "DAAP query is null\n"); return NULL; } DPRINTF(E_DBG, L_DAAP, "Trying DAAP query -%s-\n", daap_query); #if ANTLR3C_NEW_INPUT query = antlr3StringStreamNew ((pANTLR3_UINT8)daap_query, ANTLR3_ENC_8BIT, (ANTLR3_UINT64)strlen(daap_query), (pANTLR3_UINT8)"DAAP query"); #else query = antlr3NewAsciiStringInPlaceStream ((pANTLR3_UINT8)daap_query, (ANTLR3_UINT64)strlen(daap_query), (pANTLR3_UINT8)"DAAP query"); #endif if (!query) { DPRINTF(E_DBG, L_DAAP, "Could not create input stream\n"); return NULL; } lxr = DAAPLexerNew(query); if (!lxr) { DPRINTF(E_DBG, L_DAAP, "Could not create DAAP lexer\n"); goto lxr_fail; } tkstream = antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lxr)); if (!tkstream) { DPRINTF(E_DBG, L_DAAP, "Could not create DAAP token stream\n"); goto tkstream_fail; } psr = DAAPParserNew(tkstream); if (!psr) { DPRINTF(E_DBG, L_DAAP, "Could not create DAAP parser\n"); goto psr_fail; } qtree = psr->query(psr); /* Check for parser errors */ if (psr->pParser->rec->state->errorCount > 0) { DPRINTF(E_LOG, L_DAAP, "DAAP query parser terminated with %d errors\n", psr->pParser->rec->state->errorCount); goto psr_error; } DPRINTF(E_SPAM, L_DAAP, "DAAP query AST:\n\t%s\n", qtree.tree->toStringTree(qtree.tree)->chars); nodes = antlr3CommonTreeNodeStreamNewTree(qtree.tree, ANTLR3_SIZE_HINT); if (!nodes) { DPRINTF(E_DBG, L_DAAP, "Could not create node stream\n"); goto psr_error; } sqlconv = DAAP2SQLNew(nodes); if (!sqlconv) { DPRINTF(E_DBG, L_DAAP, "Could not create SQL converter\n"); goto sql_fail; } sql = sqlconv->query(sqlconv); /* Check for tree parser errors */ if (sqlconv->pTreeParser->rec->state->errorCount > 0) { DPRINTF(E_LOG, L_DAAP, "DAAP query tree parser terminated with %d errors\n", sqlconv->pTreeParser->rec->state->errorCount); goto sql_error; } if (sql) { DPRINTF(E_DBG, L_DAAP, "DAAP SQL query: -%s-\n", sql->chars); ret = strdup((char *)sql->chars); } else { DPRINTF(E_LOG, L_DAAP, "Invalid DAAP query -%s-\n", daap_query); ret = NULL; } sql_error: sqlconv->free(sqlconv); sql_fail: nodes->free(nodes); psr_error: psr->free(psr); psr_fail: tkstream->free(tkstream); tkstream_fail: lxr->free(lxr); lxr_fail: query->close(query); return ret; } forked-daapd-25.0/src/daap_query.gperf000066400000000000000000000037221313447753700177510ustar00rootroot00000000000000%language=ANSI-C %readonly-tables %enum %switch=1 %compare-lengths %define hash-function-name daap_query_field_hash %define lookup-function-name daap_query_field_lookup %define slot-name dmap_field %struct-type %omit-struct-type struct dmap_query_field_map; %% "dmap.itemname", "f.title", 0 "dmap.itemid", "f.id", 1 "dmap.containeritemid", "f.id", 1 "daap.songalbum", "f.album", 0 "daap.songalbumid", "f.songalbumid", 1 "daap.songartist", "f.album_artist", 0 "daap.songartistid", "f.songartistid", 1 "daap.songalbumartist", "f.album_artist", 0 "daap.songbitrate", "f.bitrate", 1 "daap.songcomment", "f.comment", 0 "daap.songcompilation", "f.compilation", 1 "daap.songcomposer", "f.composer", 0 "daap.songdatakind", "f.data_kind", 1 "daap.songdataurl", "f.url", 0 "daap.songdateadded", "f.time_added", 1 "daap.songdatemodified", "f.time_modified", 1 "daap.songdatereleased", "f.date_released", 1 "daap.songdescription", "f.description", 0 "daap.songdisccount", "f.total_discs", 1 "daap.songdiscnumber", "f.disc", 1 "daap.songformat", "f.type", 0 "daap.songgenre", "f.genre", 0 "daap.songsamplerate", "f.samplerate", 1 "daap.songsize", "f.file_size", 1 "daap.songstoptime", "f.song_length", 1 "daap.songtime", "f.song_length", 1 "daap.songtrackcount", "f.total_tracks", 1 "daap.songtracknumber", "f.track", 1 "daap.songuserplaycount", "f.play_count", 1 "daap.songyear", "f.year", 1 "com.apple.itunes.mediakind", "f.media_kind", 1 "com.apple.itunes.extended-media-kind", "f.media_kind", 1 forked-daapd-25.0/src/daap_query.h000066400000000000000000000002611313447753700170700ustar00rootroot00000000000000 #ifndef __DAAP_QUERY_H__ #define __DAAP_QUERY_H__ #include "logger.h" #include "misc.h" char * daap_query_parse_sql(const char *daap_query); #endif /* !__DAAP_QUERY_H__ */ forked-daapd-25.0/src/dacp_prop.gperf000066400000000000000000000034131313447753700175630ustar00rootroot00000000000000%language=ANSI-C %readonly-tables %enum %switch=1 %compare-lengths %define hash-function-name dacp_hash_prop %define lookup-function-name dacp_find_prop %define slot-name desc %struct-type %omit-struct-type struct dacp_prop_map; %% "dmcp.volume", dacp_propget_volume, dacp_propset_volume "dacp.playerstate", dacp_propget_playerstate, NULL "dacp.nowplaying", dacp_propget_nowplaying, NULL "dacp.playingtime", dacp_propget_playingtime, dacp_propset_playingtime "dacp.volumecontrollable", dacp_propget_volumecontrollable, NULL "dacp.availableshufflestates", dacp_propget_availableshufflestates, NULL "dacp.availablerepeatstates", dacp_propget_availablerepeatstates, NULL "dacp.shufflestate", dacp_propget_shufflestate, dacp_propset_shufflestate "dacp.repeatstate", dacp_propget_repeatstate, dacp_propset_repeatstate "dacp.userrating", NULL, dacp_propset_userrating "dacp.fullscreenenabled", dacp_propget_fullscreenenabled, NULL "dacp.fullscreen", dacp_propget_fullscreen, NULL "dacp.visualizerenabled", dacp_propget_visualizerenabled, NULL "dacp.visualizer", dacp_propget_visualizer, NULL "com.apple.itunes.itms-songid", dacp_propget_itms_songid, NULL "com.apple.itunes.has-chapter-data", dacp_propget_haschapterdata, NULL "com.apple.itunes.mediakind", dacp_propget_mediakind, NULL "com.apple.itunes.extended-media-kind", dacp_propget_extendedmediakind, NULL forked-daapd-25.0/src/db.c000066400000000000000000004373301313447753700153310ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * Copyright (C) 2010 Kai Elwert * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "conffile.h" #include "logger.h" #include "cache.h" #include "listener.h" #include "library.h" #include "misc.h" #include "db.h" #include "db_init.h" #include "db_upgrade.h" #include "rng.h" #define STR(x) ((x) ? (x) : "") /* Inotify cookies are uint32_t */ #define INOTIFY_FAKE_COOKIE ((int64_t)1 << 32) enum group_type { G_ALBUMS = 1, G_ARTISTS = 2, }; struct db_unlock { int proceed; pthread_cond_t cond; pthread_mutex_t lck; }; #define DB_TYPE_CHAR 1 #define DB_TYPE_INT 2 #define DB_TYPE_INT64 3 #define DB_TYPE_STRING 4 struct col_type_map { ssize_t offset; short type; }; struct query_clause { char *where; const char *order; char *index; }; /* This list must be kept in sync with * - the order of the columns in the files table * - the type and name of the fields in struct media_file_info */ static const struct col_type_map mfi_cols_map[] = { { mfi_offsetof(id), DB_TYPE_INT }, { mfi_offsetof(path), DB_TYPE_STRING }, { mfi_offsetof(fname), DB_TYPE_STRING }, { mfi_offsetof(title), DB_TYPE_STRING }, { mfi_offsetof(artist), DB_TYPE_STRING }, { mfi_offsetof(album), DB_TYPE_STRING }, { mfi_offsetof(genre), DB_TYPE_STRING }, { mfi_offsetof(comment), DB_TYPE_STRING }, { mfi_offsetof(type), DB_TYPE_STRING }, { mfi_offsetof(composer), DB_TYPE_STRING }, { mfi_offsetof(orchestra), DB_TYPE_STRING }, { mfi_offsetof(conductor), DB_TYPE_STRING }, { mfi_offsetof(grouping), DB_TYPE_STRING }, { mfi_offsetof(url), DB_TYPE_STRING }, { mfi_offsetof(bitrate), DB_TYPE_INT }, { mfi_offsetof(samplerate), DB_TYPE_INT }, { mfi_offsetof(song_length), DB_TYPE_INT }, { mfi_offsetof(file_size), DB_TYPE_INT64 }, { mfi_offsetof(year), DB_TYPE_INT }, { mfi_offsetof(track), DB_TYPE_INT }, { mfi_offsetof(total_tracks), DB_TYPE_INT }, { mfi_offsetof(disc), DB_TYPE_INT }, { mfi_offsetof(total_discs), DB_TYPE_INT }, { mfi_offsetof(bpm), DB_TYPE_INT }, { mfi_offsetof(compilation), DB_TYPE_CHAR }, { mfi_offsetof(artwork), DB_TYPE_CHAR }, { mfi_offsetof(rating), DB_TYPE_INT }, { mfi_offsetof(play_count), DB_TYPE_INT }, { mfi_offsetof(seek), DB_TYPE_INT }, { mfi_offsetof(data_kind), DB_TYPE_INT }, { mfi_offsetof(item_kind), DB_TYPE_INT }, { mfi_offsetof(description), DB_TYPE_STRING }, { mfi_offsetof(time_added), DB_TYPE_INT }, { mfi_offsetof(time_modified), DB_TYPE_INT }, { mfi_offsetof(time_played), DB_TYPE_INT }, { mfi_offsetof(db_timestamp), DB_TYPE_INT }, { mfi_offsetof(disabled), DB_TYPE_INT }, { mfi_offsetof(sample_count), DB_TYPE_INT64 }, { mfi_offsetof(codectype), DB_TYPE_STRING }, { mfi_offsetof(index), DB_TYPE_INT }, { mfi_offsetof(has_video), DB_TYPE_INT }, { mfi_offsetof(contentrating), DB_TYPE_INT }, { mfi_offsetof(bits_per_sample), DB_TYPE_INT }, { mfi_offsetof(album_artist), DB_TYPE_STRING }, { mfi_offsetof(media_kind), DB_TYPE_INT }, { mfi_offsetof(tv_series_name), DB_TYPE_STRING }, { mfi_offsetof(tv_episode_num_str), DB_TYPE_STRING }, { mfi_offsetof(tv_network_name), DB_TYPE_STRING }, { mfi_offsetof(tv_episode_sort), DB_TYPE_INT }, { mfi_offsetof(tv_season_num), DB_TYPE_INT }, { mfi_offsetof(songartistid), DB_TYPE_INT64 }, { mfi_offsetof(songalbumid), DB_TYPE_INT64 }, { mfi_offsetof(title_sort), DB_TYPE_STRING }, { mfi_offsetof(artist_sort), DB_TYPE_STRING }, { mfi_offsetof(album_sort), DB_TYPE_STRING }, { mfi_offsetof(composer_sort), DB_TYPE_STRING }, { mfi_offsetof(album_artist_sort), DB_TYPE_STRING }, { mfi_offsetof(virtual_path), DB_TYPE_STRING }, { mfi_offsetof(directory_id), DB_TYPE_INT }, { mfi_offsetof(date_released), DB_TYPE_INT }, }; /* This list must be kept in sync with * - the order of the columns in the playlists table * - the type and name of the fields in struct playlist_info */ static const struct col_type_map pli_cols_map[] = { { pli_offsetof(id), DB_TYPE_INT }, { pli_offsetof(title), DB_TYPE_STRING }, { pli_offsetof(type), DB_TYPE_INT }, { pli_offsetof(query), DB_TYPE_STRING }, { pli_offsetof(db_timestamp), DB_TYPE_INT }, { pli_offsetof(disabled), DB_TYPE_INT }, { pli_offsetof(path), DB_TYPE_STRING }, { pli_offsetof(index), DB_TYPE_INT }, { pli_offsetof(special_id), DB_TYPE_INT }, { pli_offsetof(virtual_path), DB_TYPE_STRING }, { pli_offsetof(parent_id), DB_TYPE_INT }, { pli_offsetof(directory_id), DB_TYPE_INT }, /* items is computed on the fly */ }; /* This list must be kept in sync with * - the order of the columns in the files table * - the name of the fields in struct db_media_file_info */ static const ssize_t dbmfi_cols_map[] = { dbmfi_offsetof(id), dbmfi_offsetof(path), dbmfi_offsetof(fname), dbmfi_offsetof(title), dbmfi_offsetof(artist), dbmfi_offsetof(album), dbmfi_offsetof(genre), dbmfi_offsetof(comment), dbmfi_offsetof(type), dbmfi_offsetof(composer), dbmfi_offsetof(orchestra), dbmfi_offsetof(conductor), dbmfi_offsetof(grouping), dbmfi_offsetof(url), dbmfi_offsetof(bitrate), dbmfi_offsetof(samplerate), dbmfi_offsetof(song_length), dbmfi_offsetof(file_size), dbmfi_offsetof(year), dbmfi_offsetof(track), dbmfi_offsetof(total_tracks), dbmfi_offsetof(disc), dbmfi_offsetof(total_discs), dbmfi_offsetof(bpm), dbmfi_offsetof(compilation), dbmfi_offsetof(artwork), dbmfi_offsetof(rating), dbmfi_offsetof(play_count), dbmfi_offsetof(seek), dbmfi_offsetof(data_kind), dbmfi_offsetof(item_kind), dbmfi_offsetof(description), dbmfi_offsetof(time_added), dbmfi_offsetof(time_modified), dbmfi_offsetof(time_played), dbmfi_offsetof(db_timestamp), dbmfi_offsetof(disabled), dbmfi_offsetof(sample_count), dbmfi_offsetof(codectype), dbmfi_offsetof(idx), dbmfi_offsetof(has_video), dbmfi_offsetof(contentrating), dbmfi_offsetof(bits_per_sample), dbmfi_offsetof(album_artist), dbmfi_offsetof(media_kind), dbmfi_offsetof(tv_series_name), dbmfi_offsetof(tv_episode_num_str), dbmfi_offsetof(tv_network_name), dbmfi_offsetof(tv_episode_sort), dbmfi_offsetof(tv_season_num), dbmfi_offsetof(songartistid), dbmfi_offsetof(songalbumid), dbmfi_offsetof(title_sort), dbmfi_offsetof(artist_sort), dbmfi_offsetof(album_sort), dbmfi_offsetof(composer_sort), dbmfi_offsetof(album_artist_sort), dbmfi_offsetof(virtual_path), dbmfi_offsetof(directory_id), dbmfi_offsetof(date_released), }; /* This list must be kept in sync with * - the order of the columns in the playlists table * - the name of the fields in struct playlist_info */ static const ssize_t dbpli_cols_map[] = { dbpli_offsetof(id), dbpli_offsetof(title), dbpli_offsetof(type), dbpli_offsetof(query), dbpli_offsetof(db_timestamp), dbpli_offsetof(disabled), dbpli_offsetof(path), dbpli_offsetof(index), dbpli_offsetof(special_id), dbpli_offsetof(virtual_path), dbpli_offsetof(parent_id), dbpli_offsetof(directory_id), /* items is computed on the fly */ }; /* This list must be kept in sync with * - the order of fields in the Q_GROUP_ALBUMS and Q_GROUP_ARTISTS query * - the name of the fields in struct group_info */ static const ssize_t dbgri_cols_map[] = { dbgri_offsetof(id), dbgri_offsetof(persistentid), dbgri_offsetof(itemname), dbgri_offsetof(itemname_sort), dbgri_offsetof(itemcount), dbgri_offsetof(groupalbumcount), dbgri_offsetof(songalbumartist), dbgri_offsetof(songartistid), dbgri_offsetof(song_length), }; /* This list must be kept in sync with * - the order of the columns in the inotify table * - the name and type of the fields in struct watch_info */ static const struct col_type_map wi_cols_map[] = { { wi_offsetof(wd), DB_TYPE_INT }, { wi_offsetof(cookie), DB_TYPE_INT }, { wi_offsetof(path), DB_TYPE_STRING }, }; /* Sort clauses */ /* Keep in sync with enum sort_type */ static const char *sort_clause[] = { "", "ORDER BY f.title_sort ASC", "ORDER BY f.album_sort ASC, f.disc ASC, f.track ASC", "ORDER BY f.album_artist_sort ASC, f.album_sort ASC, f.disc ASC, f.track ASC", "ORDER BY f.type ASC, f.parent_id ASC, f.special_id ASC, f.title ASC", "ORDER BY f.year ASC", "ORDER BY f.genre ASC", "ORDER BY f.composer_sort ASC", "ORDER BY f.disc ASC", "ORDER BY f.track ASC", "ORDER BY f.virtual_path ASC", "ORDER BY pos ASC", "ORDER BY shuffle_pos ASC", }; /* Shuffle RNG state */ struct rng_ctx shuffle_rng; static char *db_path; static __thread sqlite3 *hdl; /* Forward */ static int db_pl_count_items(int id, int streams_only); static int db_smartpl_count_items(const char *smartpl_query); struct playlist_info * db_pl_fetch_byid(int id); static enum group_type db_group_type_bypersistentid(int64_t persistentid); static int db_query_run(char *query, int free, int cache_update); char * db_escape_string(const char *str) { char *escaped; char *ret; escaped = sqlite3_mprintf("%q", str); if (!escaped) { DPRINTF(E_LOG, L_DB, "Out of memory for escaped string\n"); return NULL; } ret = strdup(escaped); sqlite3_free(escaped); return ret; } void free_pi(struct pairing_info *pi, int content_only) { if (!pi) return; free(pi->remote_id); free(pi->name); free(pi->guid); if (!content_only) free(pi); else memset(pi, 0, sizeof(struct pairing_info)); } void free_mfi(struct media_file_info *mfi, int content_only) { if (!mfi) return; free(mfi->path); free(mfi->fname); free(mfi->title); free(mfi->artist); free(mfi->album); free(mfi->genre); free(mfi->comment); free(mfi->type); free(mfi->composer); free(mfi->orchestra); free(mfi->conductor); free(mfi->grouping); free(mfi->description); free(mfi->codectype); free(mfi->album_artist); free(mfi->tv_series_name); free(mfi->tv_episode_num_str); free(mfi->tv_network_name); free(mfi->title_sort); free(mfi->artist_sort); free(mfi->album_sort); free(mfi->composer_sort); free(mfi->album_artist_sort); free(mfi->virtual_path); if (!content_only) free(mfi); else memset(mfi, 0, sizeof(struct media_file_info)); } void unicode_fixup_mfi(struct media_file_info *mfi) { char *ret; char **field; int i; for (i = 0; i < (sizeof(mfi_cols_map) / sizeof(mfi_cols_map[0])); i++) { if (mfi_cols_map[i].type != DB_TYPE_STRING) continue; switch (mfi_cols_map[i].offset) { case mfi_offsetof(path): case mfi_offsetof(fname): case mfi_offsetof(codectype): continue; } field = (char **) ((char *)mfi + mfi_cols_map[i].offset); if (!*field) continue; ret = unicode_fixup_string(*field, "ascii"); if (ret != *field) { free(*field); *field = ret; } } } void free_pli(struct playlist_info *pli, int content_only) { if (!pli) return; free(pli->title); free(pli->query); free(pli->path); free(pli->virtual_path); if (!content_only) free(pli); else memset(pli, 0, sizeof(struct playlist_info)); } void free_di(struct directory_info *di, int content_only) { if (!di) return; free(di->virtual_path); if (!content_only) free(di); else memset(di, 0, sizeof(struct directory_info)); } /* Unlock notification support */ static void unlock_notify_cb(void **args, int nargs) { struct db_unlock *u; int i; for (i = 0; i < nargs; i++) { u = (struct db_unlock *)args[i]; CHECK_ERR(L_DB, pthread_mutex_lock(&u->lck)); u->proceed = 1; CHECK_ERR(L_DB, pthread_cond_signal(&u->cond)); CHECK_ERR(L_DB, pthread_mutex_unlock(&u->lck)); } } static int db_wait_unlock(void) { struct db_unlock u; int ret; u.proceed = 0; CHECK_ERR(L_DB, mutex_init(&u.lck)); CHECK_ERR(L_DB, pthread_cond_init(&u.cond, NULL)); ret = sqlite3_unlock_notify(hdl, unlock_notify_cb, &u); if (ret == SQLITE_OK) { CHECK_ERR(L_DB, pthread_mutex_lock(&u.lck)); if (!u.proceed) { DPRINTF(E_INFO, L_DB, "Waiting for database unlock\n"); CHECK_ERR(L_DB, pthread_cond_wait(&u.cond, &u.lck)); } CHECK_ERR(L_DB, pthread_mutex_unlock(&u.lck)); } CHECK_ERR(L_DB, pthread_cond_destroy(&u.cond)); CHECK_ERR(L_DB, pthread_mutex_destroy(&u.lck)); return ret; } static int db_blocking_step(sqlite3_stmt *stmt) { int ret; while ((ret = sqlite3_step(stmt)) == SQLITE_LOCKED) { ret = db_wait_unlock(); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Database deadlocked!\n"); break; } sqlite3_reset(stmt); } return ret; } static int db_blocking_prepare_v2(const char *query, int len, sqlite3_stmt **stmt, const char **end) { int ret; while ((ret = sqlite3_prepare_v2(hdl, query, len, stmt, end)) == SQLITE_LOCKED) { ret = db_wait_unlock(); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Database deadlocked!\n"); break; } } return ret; } /* Modelled after sqlite3_exec() */ static int db_exec(const char *query, char **errmsg) { sqlite3_stmt *stmt; int try; int ret; *errmsg = NULL; for (try = 0; try < 5; try++) { ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { *errmsg = sqlite3_mprintf("prepare failed: %s", sqlite3_errmsg(hdl)); return ret; } while ((ret = db_blocking_step(stmt)) == SQLITE_ROW) ; /* EMPTY */ sqlite3_finalize(stmt); if (ret != SQLITE_SCHEMA) break; } if (ret != SQLITE_DONE) { *errmsg = sqlite3_mprintf("step failed: %s", sqlite3_errmsg(hdl)); return ret; } return SQLITE_OK; } /* Maintenance and DB hygiene */ static void db_analyze(void) { char *query = "ANALYZE;"; char *errmsg; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "ANALYZE failed: %s\n", errmsg); sqlite3_free(errmsg); } } /* Set names of default playlists according to config */ static void db_set_cfg_names(void) { #define Q_TMPL "UPDATE playlists SET title = '%q' WHERE type = %d AND special_id = %d;" char *cfg_item[6] = { "name_library", "name_music", "name_movies", "name_tvshows", "name_podcasts", "name_audiobooks" }; char special_id[6] = { 0, 6, 4, 5, 1, 7 }; cfg_t *lib; char *query; char *title; char *errmsg; int ret; int i; lib = cfg_getsec(cfg, "library"); for (i = 0; i < (sizeof(cfg_item) / sizeof(cfg_item[0])); i++) { title = cfg_getstr(lib, cfg_item[i]); if (!title) { DPRINTF(E_LOG, L_DB, "Internal error, unknown config item '%s'\n", cfg_item[i]); continue; } query = sqlite3_mprintf(Q_TMPL, title, PL_SPECIAL, special_id[i]); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return; } ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error setting playlist title, query %s, error: %s\n", query, errmsg); sqlite3_free(errmsg); } else DPRINTF(E_DBG, L_DB, "Playlist title for config item '%s' set with query '%s'\n", cfg_item[i], query); sqlite3_free(query); } #undef Q_TMPL } void db_hook_post_scan(void) { DPRINTF(E_DBG, L_DB, "Running post-scan DB maintenance tasks...\n"); db_analyze(); DPRINTF(E_DBG, L_DB, "Done with post-scan DB maintenance\n"); } void db_purge_cruft(time_t ref) { #define Q_TMPL "DELETE FROM directories WHERE id >= %d AND db_timestamp < %" PRIi64 ";" int i; int ret; char *query; char *queries_tmpl[3] = { "DELETE FROM playlistitems WHERE playlistid IN (SELECT id FROM playlists p WHERE p.type <> %d AND p.db_timestamp < %" PRIi64 ");", "DELETE FROM playlists WHERE type <> %d AND db_timestamp < %" PRIi64 ";", "DELETE FROM files WHERE -1 <> %d AND db_timestamp < %" PRIi64 ";", }; db_transaction_begin(); for (i = 0; i < (sizeof(queries_tmpl) / sizeof(queries_tmpl[0])); i++) { query = sqlite3_mprintf(queries_tmpl[i], PL_SPECIAL, (int64_t)ref); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); db_transaction_end(); return; } DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query); ret = db_query_run(query, 1, 0); if (ret == 0) DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl)); } query = sqlite3_mprintf(Q_TMPL, DIR_MAX, (int64_t)ref); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); db_transaction_end(); return; } DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query); ret = db_query_run(query, 1, 1); if (ret == 0) DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl)); db_transaction_end(); #undef Q_TMPL } void db_purge_all(void) { #define Q_TMPL_PL "DELETE FROM playlists WHERE type <> %d;" #define Q_TMPL_DIR "DELETE FROM directories WHERE id >= %d;" char *queries[4] = { "DELETE FROM inotify;", "DELETE FROM playlistitems;", "DELETE FROM files;", "DELETE FROM groups;", }; char *errmsg; char *query; int i; int ret; for (i = 0; i < (sizeof(queries) / sizeof(queries[0])); i++) { DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", queries[i]); ret = db_exec(queries[i], &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Purge query %d error: %s\n", i, errmsg); sqlite3_free(errmsg); } else DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl)); } // Purge playlists query = sqlite3_mprintf(Q_TMPL_PL, PL_SPECIAL); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return; } DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Purge query '%s' error: %s\n", query, errmsg); sqlite3_free(errmsg); } else DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl)); sqlite3_free(query); // Purge directories query = sqlite3_mprintf(Q_TMPL_DIR, DIR_MAX); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return; } DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Purge query '%s' error: %s\n", query, errmsg); sqlite3_free(errmsg); } else DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl)); sqlite3_free(query); #undef Q_TMPL_PL #undef Q_TMPL_DIR } static int db_get_one_int(const char *query) { sqlite3_stmt *stmt; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); return -1; } ret = sqlite3_column_int(stmt, 0); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); return ret; } /* Transactions */ void db_transaction_begin(void) { char *query = "BEGIN TRANSACTION;"; char *errmsg; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "SQL error running '%s': %s\n", query, errmsg); sqlite3_free(errmsg); } } void db_transaction_end(void) { char *query = "END TRANSACTION;"; char *errmsg; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "SQL error running '%s': %s\n", query, errmsg); sqlite3_free(errmsg); } } void db_transaction_rollback(void) { char *query = "ROLLBACK TRANSACTION;"; char *errmsg; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "SQL error running '%s': %s\n", query, errmsg); sqlite3_free(errmsg); } } static void db_free_query_clause(struct query_clause *qc) { if (!qc) return; sqlite3_free(qc->where); sqlite3_free(qc->index); free(qc); } static struct query_clause * db_build_query_clause(struct query_params *qp) { struct query_clause *qc; qc = calloc(1, sizeof(struct query_clause)); if (!qc) goto error; if (qp->filter) qc->where = sqlite3_mprintf("WHERE f.disabled = 0 AND %s", qp->filter); else qc->where = sqlite3_mprintf("WHERE f.disabled = 0"); if (qp->sort) qc->order = sort_clause[qp->sort]; else qc->order = ""; switch (qp->idx_type) { case I_FIRST: qc->index = sqlite3_mprintf("LIMIT %d", qp->limit); break; case I_LAST: qc->index = sqlite3_mprintf("LIMIT -1 OFFSET %d", qp->results - qp->limit); break; case I_SUB: qc->index = sqlite3_mprintf("LIMIT %d OFFSET %d", qp->limit, qp->offset); break; case I_NONE: qc->index = sqlite3_mprintf(""); break; } if (!qc->where || !qc->index) goto error; return qc; error: DPRINTF(E_LOG, L_DB, "Error building query clause\n"); db_free_query_clause(qc); return NULL; } static char * db_build_query_check(struct query_params *qp, char *count, char *query) { if (!count || !query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); goto failed; } qp->results = db_get_one_int(count); if (qp->results < 0) goto failed; sqlite3_free(count); return query; failed: sqlite3_free(count); sqlite3_free(query); return NULL; } static char * db_build_query_items(struct query_params *qp) { struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s;", qc->where); query = sqlite3_mprintf("SELECT f.* FROM files f %s %s %s;", qc->where, qc->order, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_pls(struct query_params *qp) { struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; count = sqlite3_mprintf("SELECT COUNT(*) FROM playlists f %s;", qc->where); query = sqlite3_mprintf("SELECT f.* FROM playlists f %s %s %s;", qc->where, qc->order, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_find_pls(struct query_params *qp) { struct query_clause *qc; char *count; char *query; if (!qp->filter) { DPRINTF(E_LOG, L_DB, "Bug! Playlist find called without search criteria\n"); return NULL; } qc = db_build_query_clause(qp); if (!qc) return NULL; // Use qp->filter because qc->where has a f.disabled which is not a column in playlistitems count = sqlite3_mprintf("SELECT COUNT(*) FROM playlists f WHERE f.id IN (SELECT playlistid FROM playlistitems WHERE %s);", qp->filter); query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.id IN (SELECT playlistid FROM playlistitems WHERE %s) %s %s;", qp->filter, qc->order, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_plitems_plain(struct query_params *qp) { struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; count = sqlite3_mprintf("SELECT COUNT(*) FROM files f JOIN playlistitems pi ON f.path = pi.filepath %s AND pi.playlistid = %d;", qc->where, qp->id); query = sqlite3_mprintf("SELECT f.* FROM files f JOIN playlistitems pi ON f.path = pi.filepath %s AND pi.playlistid = %d ORDER BY pi.id ASC %s;", qc->where, qp->id, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_plitems_smart(struct query_params *qp, char *smartpl_query) { struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s AND %s;", qc->where, smartpl_query); query = sqlite3_mprintf("SELECT f.* FROM files f %s AND %s %s %s;", qc->where, smartpl_query, qc->order, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_plitems(struct query_params *qp) { struct playlist_info *pli; char *query; if (qp->id <= 0) { DPRINTF(E_LOG, L_DB, "No playlist id specified in playlist items query\n"); return NULL; } pli = db_pl_fetch_byid(qp->id); if (!pli) return NULL; switch (pli->type) { case PL_SPECIAL: case PL_SMART: query = db_build_query_plitems_smart(qp, pli->query); break; case PL_PLAIN: case PL_FOLDER: query = db_build_query_plitems_plain(qp); break; default: DPRINTF(E_LOG, L_DB, "Unknown playlist type %d in playlist items query\n", pli->type); query = NULL; break; } free_pli(pli, 0); return query; } static char * db_build_query_group_albums(struct query_params *qp) { struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.songalbumid) FROM files f WHERE f.disabled = 0;"); query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid, SUM(f.song_length) FROM files f JOIN groups g ON f.songalbumid = g.persistentid %s GROUP BY f.songalbumid %s %s;", qc->where, qc->order, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_group_artists(struct query_params *qp) { struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.songartistid) FROM files f %s;", qc->where); query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid, SUM(f.song_length) FROM files f JOIN groups g ON f.songartistid = g.persistentid %s GROUP BY f.songartistid %s %s;", qc->where, qc->order, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_group_items(struct query_params *qp) { enum group_type gt; struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; gt = db_group_type_bypersistentid(qp->persistentid); switch (gt) { case G_ALBUMS: count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s AND f.songalbumid = %" PRIi64 ";", qc->where, qp->persistentid); query = sqlite3_mprintf("SELECT f.* FROM files f %s AND f.songalbumid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index); break; case G_ARTISTS: count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s AND f.songartistid = %" PRIi64 ";", qc->where, qp->persistentid); query = sqlite3_mprintf("SELECT f.* FROM files f %s AND f.songartistid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index); break; default: DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %" PRIi64 "\n", gt, qp->persistentid); db_free_query_clause(qc); return NULL; } db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_group_dirs(struct query_params *qp) { enum group_type gt; struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; gt = db_group_type_bypersistentid(qp->persistentid); switch (gt) { case G_ALBUMS: count = sqlite3_mprintf("SELECT COUNT(DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1)))" " FROM files f %s AND f.songalbumid = %" PRIi64 ";", qc->where, qp->persistentid); query = sqlite3_mprintf("SELECT DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1))" " FROM files f %s AND f.songalbumid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index); break; case G_ARTISTS: count = sqlite3_mprintf("SELECT COUNT(DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1)))" " FROM files f %s AND f.songartistid = %" PRIi64 ";", qc->where, qp->persistentid); query = sqlite3_mprintf("SELECT DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1))" " FROM files f %s AND f.songartistid = %" PRIi64 " %s %s;", qc->where, qp->persistentid, qc->order, qc->index); break; default: DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %" PRIi64 "\n", gt, qp->persistentid); db_free_query_clause(qc); return NULL; } db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_browse(struct query_params *qp, const char *field, const char *group_field) { struct query_clause *qc; char *count; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.%s) FROM files f %s AND f.%s != '';", field, qc->where, field); query = sqlite3_mprintf("SELECT f.%s, f.%s FROM files f %s AND f.%s != '' GROUP BY f.%s %s %s;", field, group_field, qc->where, field, group_field, qc->order, qc->index); db_free_query_clause(qc); return db_build_query_check(qp, count, query); } static char * db_build_query_count_items(struct query_params *qp) { struct query_clause *qc; char *query; qc = db_build_query_clause(qp); if (!qc) return NULL; qp->results = 1; query = sqlite3_mprintf("SELECT COUNT(*), SUM(song_length) FROM files f %s;", qc->where); if (!query) DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); db_free_query_clause(qc); return query; } int db_query_start(struct query_params *qp) { char *query; int ret; qp->stmt = NULL; switch (qp->type) { case Q_ITEMS: query = db_build_query_items(qp); break; case Q_PL: query = db_build_query_pls(qp); break; case Q_FIND_PL: query = db_build_query_find_pls(qp); break; case Q_PLITEMS: query = db_build_query_plitems(qp); break; case Q_GROUP_ALBUMS: query = db_build_query_group_albums(qp); break; case Q_GROUP_ARTISTS: query = db_build_query_group_artists(qp); break; case Q_GROUP_ITEMS: query = db_build_query_group_items(qp); break; case Q_GROUP_DIRS: query = db_build_query_group_dirs(qp); break; case Q_BROWSE_ALBUMS: query = db_build_query_browse(qp, "album", "album_sort"); break; case Q_BROWSE_ARTISTS: query = db_build_query_browse(qp, "album_artist", "album_artist_sort"); break; case Q_BROWSE_GENRES: query = db_build_query_browse(qp, "genre", "genre"); break; case Q_BROWSE_COMPOSERS: query = db_build_query_browse(qp, "composer", "composer_sort"); break; case Q_BROWSE_YEARS: query = db_build_query_browse(qp, "year", "year"); break; case Q_BROWSE_DISCS: query = db_build_query_browse(qp, "disc", "disc"); break; case Q_BROWSE_TRACKS: query = db_build_query_browse(qp, "track", "track"); break; case Q_BROWSE_VPATH: query = db_build_query_browse(qp, "virtual_path", "virtual_path"); break; case Q_BROWSE_PATH: query = db_build_query_browse(qp, "path", "path"); break; case Q_COUNT_ITEMS: query = db_build_query_count_items(qp); break; default: DPRINTF(E_LOG, L_DB, "Unknown query type\n"); return -1; } if (!query) return -1; DPRINTF(E_DBG, L_DB, "Starting query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &qp->stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } sqlite3_free(query); return 0; } void db_query_end(struct query_params *qp) { if (!qp->stmt) return; qp->results = -1; sqlite3_finalize(qp->stmt); qp->stmt = NULL; } /* * Utility function for running write queries (INSERT, UPDATE, DELETE). If you * set free to non-zero, the function will free the query. If you set * library_update to non-zero it means that the update was not just of some * internal value (like a timestamp), but of something that requires clients * to update their cache of the library (and of course also of our own cache). */ static int db_query_run(char *query, int free, int library_update) { char *errmsg; int ret; if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); /* If the query will be long running we don't want the cache to start regenerating */ cache_daap_suspend(); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) DPRINTF(E_LOG, L_DB, "Error '%s' while runnning '%s'\n", errmsg, query); sqlite3_free(errmsg); if (free) sqlite3_free(query); cache_daap_resume(); if (library_update) library_update_trigger(); return ((ret != SQLITE_OK) ? -1 : 0); } int db_query_fetch_file(struct query_params *qp, struct db_media_file_info *dbmfi) { int ncols; char **strcol; int i; int ret; memset(dbmfi, 0, sizeof(struct db_media_file_info)); if (!qp->stmt) { DPRINTF(E_LOG, L_DB, "Query not started!\n"); return -1; } if ((qp->type != Q_ITEMS) && (qp->type != Q_PLITEMS) && (qp->type != Q_GROUP_ITEMS)) { DPRINTF(E_LOG, L_DB, "Not an items, playlist or group items query!\n"); return -1; } ret = db_blocking_step(qp->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); dbmfi->id = NULL; return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } ncols = sqlite3_column_count(qp->stmt); if (sizeof(dbmfi_cols_map) / sizeof(dbmfi_cols_map[0]) != ncols) { DPRINTF(E_LOG, L_DB, "BUG: dbmfi column map out of sync with schema\n"); return -1; } for (i = 0; i < ncols; i++) { strcol = (char **) ((char *)dbmfi + dbmfi_cols_map[i]); *strcol = (char *)sqlite3_column_text(qp->stmt, i); } return 0; } int db_query_fetch_pl(struct query_params *qp, struct db_playlist_info *dbpli, int with_itemcount) { int ncols; char **strcol; int id; int type; int nitems; int nstreams; int i; int ret; memset(dbpli, 0, sizeof(struct db_playlist_info)); if (!qp->stmt) { DPRINTF(E_LOG, L_DB, "Query not started!\n"); return -1; } if ((qp->type != Q_PL) && (qp->type != Q_FIND_PL)) { DPRINTF(E_LOG, L_DB, "Not a playlist query!\n"); return -1; } ret = db_blocking_step(qp->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); dbpli->id = NULL; return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } ncols = sqlite3_column_count(qp->stmt); if (sizeof(dbpli_cols_map) / sizeof(dbpli_cols_map[0]) != ncols) { DPRINTF(E_LOG, L_DB, "BUG: dbpli column map out of sync with schema\n"); return -1; } for (i = 0; i < ncols; i++) { strcol = (char **) ((char *)dbpli + dbpli_cols_map[i]); *strcol = (char *)sqlite3_column_text(qp->stmt, i); } if (with_itemcount) { type = sqlite3_column_int(qp->stmt, 2); switch (type) { case PL_PLAIN: case PL_FOLDER: id = sqlite3_column_int(qp->stmt, 0); nitems = db_pl_count_items(id, 0); nstreams = db_pl_count_items(id, 1); break; case PL_SPECIAL: case PL_SMART: nitems = db_smartpl_count_items(dbpli->query); nstreams = 0; break; default: DPRINTF(E_LOG, L_DB, "Unknown playlist type %d while fetching playlist\n", type); return -1; } dbpli->items = qp->buf1; ret = snprintf(qp->buf1, sizeof(qp->buf1), "%d", nitems); if ((ret < 0) || (ret >= sizeof(qp->buf1))) { DPRINTF(E_LOG, L_DB, "Could not convert item count, buffer too small\n"); strcpy(qp->buf1, "0"); } dbpli->streams = qp->buf2; ret = snprintf(qp->buf2, sizeof(qp->buf2), "%d", nstreams); if ((ret < 0) || (ret >= sizeof(qp->buf2))) { DPRINTF(E_LOG, L_DB, "Could not convert stream count, buffer too small\n"); strcpy(qp->buf2, "0"); } } return 0; } int db_query_fetch_group(struct query_params *qp, struct db_group_info *dbgri) { int ncols; char **strcol; int i; int ret; memset(dbgri, 0, sizeof(struct db_group_info)); if (!qp->stmt) { DPRINTF(E_LOG, L_DB, "Query not started!\n"); return -1; } if ((qp->type != Q_GROUP_ALBUMS) && (qp->type != Q_GROUP_ARTISTS)) { DPRINTF(E_LOG, L_DB, "Not a groups query!\n"); return -1; } ret = db_blocking_step(qp->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); return 1; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } ncols = sqlite3_column_count(qp->stmt); if (sizeof(dbgri_cols_map) / sizeof(dbgri_cols_map[0]) != ncols) { DPRINTF(E_LOG, L_DB, "BUG: dbgri column map out of sync with schema\n"); return -1; } for (i = 0; i < ncols; i++) { strcol = (char **) ((char *)dbgri + dbgri_cols_map[i]); *strcol = (char *)sqlite3_column_text(qp->stmt, i); } return 0; } int db_query_fetch_count(struct query_params *qp, struct filecount_info *fci) { int ret; memset(fci, 0, sizeof(struct filecount_info)); if (!qp->stmt) { DPRINTF(E_LOG, L_DB, "Query not started!\n"); return -1; } if (qp->type != Q_COUNT_ITEMS) { DPRINTF(E_LOG, L_DB, "Not a count query!\n"); return -1; } ret = db_blocking_step(qp->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results for count query\n"); return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } fci->count = sqlite3_column_int(qp->stmt, 0); fci->length = sqlite3_column_int64(qp->stmt, 1); return 0; } int db_query_fetch_string(struct query_params *qp, char **string) { int ret; *string = NULL; if (!qp->stmt) { DPRINTF(E_LOG, L_DB, "Query not started!\n"); return -1; } if (!(qp->type & Q_F_BROWSE)) { DPRINTF(E_LOG, L_DB, "Not a browse query!\n"); return -1; } ret = db_blocking_step(qp->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); *string = NULL; return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } *string = (char *)sqlite3_column_text(qp->stmt, 0); return 0; } int db_query_fetch_string_sort(struct query_params *qp, char **string, char **sortstring) { int ret; *string = NULL; if (!qp->stmt) { DPRINTF(E_LOG, L_DB, "Query not started!\n"); return -1; } if (!(qp->type & Q_F_BROWSE)) { DPRINTF(E_LOG, L_DB, "Not a browse query!\n"); return -1; } ret = db_blocking_step(qp->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); *string = NULL; return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } *string = (char *)sqlite3_column_text(qp->stmt, 0); *sortstring = (char *)sqlite3_column_text(qp->stmt, 1); return 0; } /* Files */ int db_files_get_count(void) { return db_get_one_int("SELECT COUNT(*) FROM files f WHERE f.disabled = 0;"); } int db_files_get_artist_count(void) { return db_get_one_int("SELECT COUNT(DISTINCT songartistid) FROM files f WHERE f.disabled = 0;"); } int db_files_get_album_count(void) { return db_get_one_int("SELECT COUNT(DISTINCT songalbumid) FROM files f WHERE f.disabled = 0;"); } int db_files_get_count_bymatch(char *path) { #define Q_TMPL "SELECT COUNT(*) FROM files f WHERE f.path LIKE '%%%q';" char *query; int count; query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory making count query string.\n"); return -1; } count = db_get_one_int(query); sqlite3_free(query); return count; #undef Q_TMPL } void db_file_inc_playcount(int id) { #define Q_TMPL "UPDATE files SET play_count = play_count + 1, time_played = %" PRIi64 ", seek = 0 WHERE id = %d;" char *query; query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return; } db_query_run(query, 1, 0); #undef Q_TMPL } void db_file_ping(int id) { #define Q_TMPL "UPDATE files SET db_timestamp = %" PRIi64 ", disabled = 0 WHERE id = %d;" char *query; query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id); db_query_run(query, 1, 0); #undef Q_TMPL } void db_file_ping_bymatch(char *path, int isdir) { #define Q_TMPL_DIR "UPDATE files SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';" #define Q_TMPL_NODIR "UPDATE files SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q%%';" char *query; if (isdir) query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), path); else query = sqlite3_mprintf(Q_TMPL_NODIR, (int64_t)time(NULL), path); db_query_run(query, 1, 0); #undef Q_TMPL_DIR #undef Q_TMPL_NODIR } char * db_file_path_byid(int id) { #define Q_TMPL "SELECT f.path FROM files f WHERE f.id = %d;" char *query; sqlite3_stmt *stmt; char *res; int ret; query = sqlite3_mprintf(Q_TMPL, id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return NULL; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); sqlite3_free(query); return NULL; } res = (char *)sqlite3_column_text(stmt, 0); if (res) res = strdup(res); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); return res; #undef Q_TMPL } static int db_file_id_byquery(char *query) { sqlite3_stmt *stmt; int ret; if (!query) return 0; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return 0; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); return 0; } ret = sqlite3_column_int(stmt, 0); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); return ret; } int db_file_id_bypath(char *path) { #define Q_TMPL "SELECT f.id FROM files f WHERE f.path = '%q';" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_file_id_byquery(query); sqlite3_free(query); return ret; #undef Q_TMPL } int db_file_id_bymatch(char *path) { #define Q_TMPL "SELECT f.id FROM files f WHERE f.path LIKE '%%%q';" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_file_id_byquery(query); sqlite3_free(query); return ret; #undef Q_TMPL } int db_file_id_byfile(char *filename) { #define Q_TMPL "SELECT f.id FROM files f WHERE f.fname = '%q';" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, filename); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_file_id_byquery(query); sqlite3_free(query); return ret; #undef Q_TMPL } int db_file_id_byurl(char *url) { #define Q_TMPL "SELECT f.id FROM files f WHERE f.url = '%q';" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, url); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_file_id_byquery(query); sqlite3_free(query); return ret; #undef Q_TMPL } int db_file_id_by_virtualpath_match(char *path) { #define Q_TMPL "SELECT f.id FROM files f WHERE f.virtual_path LIKE '%%%q%%';" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_file_id_byquery(query); sqlite3_free(query); return ret; #undef Q_TMPL } void db_file_stamp_bypath(const char *path, time_t *stamp, int *id) { #define Q_TMPL "SELECT f.id, f.db_timestamp FROM files f WHERE f.path = '%q';" char *query; sqlite3_stmt *stmt; int ret; *id = 0; *stamp = 0; query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); sqlite3_free(query); return; } *id = sqlite3_column_int(stmt, 0); *stamp = (time_t)sqlite3_column_int64(stmt, 1); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); #undef Q_TMPL } static struct media_file_info * db_file_fetch_byquery(char *query) { struct media_file_info *mfi; sqlite3_stmt *stmt; int ncols; char *cval; uint32_t *ival; uint64_t *i64val; char **strval; uint64_t disabled; int i; int ret; if (!query) return NULL; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); mfi = (struct media_file_info *)malloc(sizeof(struct media_file_info)); if (!mfi) { DPRINTF(E_LOG, L_DB, "Could not allocate struct media_file_info, out of memory\n"); return NULL; } memset(mfi, 0, sizeof(struct media_file_info)); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); free(mfi); return NULL; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); free(mfi); return NULL; } ncols = sqlite3_column_count(stmt); if (sizeof(mfi_cols_map) / sizeof(mfi_cols_map[0]) != ncols) { DPRINTF(E_LOG, L_DB, "BUG: mfi column map out of sync with schema\n"); sqlite3_finalize(stmt); free(mfi); return NULL; } for (i = 0; i < ncols; i++) { switch (mfi_cols_map[i].type) { case DB_TYPE_CHAR: cval = (char *)mfi + mfi_cols_map[i].offset; *cval = sqlite3_column_int(stmt, i); break; case DB_TYPE_INT: ival = (uint32_t *) ((char *)mfi + mfi_cols_map[i].offset); if (mfi_cols_map[i].offset == mfi_offsetof(disabled)) { disabled = sqlite3_column_int64(stmt, i); *ival = (disabled != 0); } else *ival = sqlite3_column_int(stmt, i); break; case DB_TYPE_INT64: i64val = (uint64_t *) ((char *)mfi + mfi_cols_map[i].offset); *i64val = sqlite3_column_int64(stmt, i); break; case DB_TYPE_STRING: strval = (char **) ((char *)mfi + mfi_cols_map[i].offset); cval = (char *)sqlite3_column_text(stmt, i); if (cval) *strval = strdup(cval); break; default: DPRINTF(E_LOG, L_DB, "BUG: Unknown type %d in mfi column map\n", mfi_cols_map[i].type); free_mfi(mfi, 0); sqlite3_finalize(stmt); return NULL; } } #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); return mfi; } struct media_file_info * db_file_fetch_byid(int id) { #define Q_TMPL "SELECT f.* FROM files f WHERE f.id = %d;" struct media_file_info *mfi; char *query; query = sqlite3_mprintf(Q_TMPL, id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } mfi = db_file_fetch_byquery(query); sqlite3_free(query); return mfi; #undef Q_TMPL } struct media_file_info * db_file_fetch_byvirtualpath(char *virtual_path) { #define Q_TMPL "SELECT f.* FROM files f WHERE f.virtual_path = %Q;" struct media_file_info *mfi; char *query; query = sqlite3_mprintf(Q_TMPL, virtual_path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } mfi = db_file_fetch_byquery(query); sqlite3_free(query); return mfi; #undef Q_TMPL } int db_file_add(struct media_file_info *mfi) { #define Q_TMPL "INSERT INTO files (id, path, fname, title, artist, album, genre, comment, type, composer," \ " orchestra, conductor, grouping, url, bitrate, samplerate, song_length, file_size, year, track," \ " total_tracks, disc, total_discs, bpm, compilation, artwork, rating, play_count, seek, data_kind, item_kind," \ " description, time_added, time_modified, time_played, db_timestamp, disabled, sample_count," \ " codectype, idx, has_video, contentrating, bits_per_sample, album_artist," \ " media_kind, tv_series_name, tv_episode_num_str, tv_network_name, tv_episode_sort, tv_season_num, " \ " songartistid, songalbumid, " \ " title_sort, artist_sort, album_sort, composer_sort, album_artist_sort, virtual_path," \ " directory_id, date_released) " \ " VALUES (NULL, '%q', '%q', TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), %Q, TRIM(%Q)," \ " TRIM(%Q), TRIM(%Q), TRIM(%Q), %Q, %d, %d, %d, %" PRIi64 ", %d, %d," \ " %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d," \ " %Q, %" PRIi64 ", %" PRIi64 ", %" PRIi64 ", %" PRIi64 ", %d, %" PRIi64 "," \ " %Q, %d, %d, %d, %d, TRIM(%Q)," \ " %d, TRIM(%Q), TRIM(%Q), TRIM(%Q), %d, %d," \ " daap_songalbumid(LOWER(TRIM(%Q)), ''), daap_songalbumid(LOWER(TRIM(%Q)), LOWER(TRIM(%Q))), " \ " TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), %d, %d);" char *query; char *errmsg; int ret; if (mfi->id != 0) { DPRINTF(E_WARN, L_DB, "Trying to add file with non-zero id; use db_file_update()?\n"); return -1; } mfi->db_timestamp = (uint64_t)time(NULL); if (mfi->time_added == 0) mfi->time_added = mfi->db_timestamp; if (mfi->time_modified == 0) mfi->time_modified = mfi->db_timestamp; query = sqlite3_mprintf(Q_TMPL, STR(mfi->path), STR(mfi->fname), mfi->title, mfi->artist, mfi->album, mfi->genre, mfi->comment, mfi->type, mfi->composer, mfi->orchestra, mfi->conductor, mfi->grouping, mfi->url, mfi->bitrate, mfi->samplerate, mfi->song_length, mfi->file_size, mfi->year, mfi->track, mfi->total_tracks, mfi->disc, mfi->total_discs, mfi->bpm, mfi->compilation, mfi->artwork, mfi->rating, mfi->play_count, mfi->seek, mfi->data_kind, mfi->item_kind, mfi->description, (int64_t)mfi->time_added, (int64_t)mfi->time_modified, (int64_t)mfi->time_played, (int64_t)mfi->db_timestamp, mfi->disabled, mfi->sample_count, mfi->codectype, mfi->index, mfi->has_video, mfi->contentrating, mfi->bits_per_sample, mfi->album_artist, mfi->media_kind, mfi->tv_series_name, mfi->tv_episode_num_str, mfi->tv_network_name, mfi->tv_episode_sort, mfi->tv_season_num, mfi->album_artist, mfi->album_artist, mfi->album, mfi->title_sort, mfi->artist_sort, mfi->album_sort, mfi->composer_sort, mfi->album_artist_sort, mfi->virtual_path, mfi->directory_id, mfi->date_released); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); return -1; } sqlite3_free(query); library_update_trigger(); return 0; #undef Q_TMPL } int db_file_update(struct media_file_info *mfi) { #define Q_TMPL "UPDATE files SET path = '%q', fname = '%q', title = TRIM(%Q), artist = TRIM(%Q), album = TRIM(%Q), genre = TRIM(%Q)," \ " comment = TRIM(%Q), type = %Q, composer = TRIM(%Q), orchestra = TRIM(%Q), conductor = TRIM(%Q), grouping = TRIM(%Q)," \ " url = %Q, bitrate = %d, samplerate = %d, song_length = %d, file_size = %" PRIi64 "," \ " year = %d, track = %d, total_tracks = %d, disc = %d, total_discs = %d, bpm = %d," \ " compilation = %d, artwork = %d, rating = %d, seek = %d, data_kind = %d, item_kind = %d," \ " description = %Q, time_modified = %" PRIi64 "," \ " db_timestamp = %" PRIi64 ", disabled = %" PRIi64 ", sample_count = %" PRIi64 "," \ " codectype = %Q, idx = %d, has_video = %d," \ " bits_per_sample = %d, album_artist = TRIM(%Q)," \ " media_kind = %d, tv_series_name = TRIM(%Q), tv_episode_num_str = TRIM(%Q)," \ " tv_network_name = TRIM(%Q), tv_episode_sort = %d, tv_season_num = %d," \ " songartistid = daap_songalbumid(LOWER(TRIM(%Q)), ''), songalbumid = daap_songalbumid(LOWER(TRIM(%Q)), LOWER(TRIM(%Q)))," \ " title_sort = TRIM(%Q), artist_sort = TRIM(%Q), album_sort = TRIM(%Q), composer_sort = TRIM(%Q), album_artist_sort = TRIM(%Q)," \ " virtual_path = TRIM(%Q), directory_id = %d, date_released = %d" \ " WHERE id = %d;" char *query; char *errmsg; int ret; if (mfi->id == 0) { DPRINTF(E_WARN, L_DB, "Trying to update file with id 0; use db_file_add()?\n"); return -1; } mfi->db_timestamp = (uint64_t)time(NULL); if (mfi->time_modified == 0) mfi->time_modified = mfi->db_timestamp; query = sqlite3_mprintf(Q_TMPL, STR(mfi->path), STR(mfi->fname), mfi->title, mfi->artist, mfi->album, mfi->genre, mfi->comment, mfi->type, mfi->composer, mfi->orchestra, mfi->conductor, mfi->grouping, mfi->url, mfi->bitrate, mfi->samplerate, mfi->song_length, mfi->file_size, mfi->year, mfi->track, mfi->total_tracks, mfi->disc, mfi->total_discs, mfi->bpm, mfi->compilation, mfi->artwork, mfi->rating, mfi->seek, mfi->data_kind, mfi->item_kind, mfi->description, (int64_t)mfi->time_modified, (int64_t)mfi->db_timestamp, (int64_t)mfi->disabled, mfi->sample_count, mfi->codectype, mfi->index, mfi->has_video, mfi->bits_per_sample, mfi->album_artist, mfi->media_kind, mfi->tv_series_name, mfi->tv_episode_num_str, mfi->tv_network_name, mfi->tv_episode_sort, mfi->tv_season_num, mfi->album_artist, mfi->album_artist, mfi->album, mfi->title_sort, mfi->artist_sort, mfi->album_sort, mfi->composer_sort, mfi->album_artist_sort, mfi->virtual_path, mfi->directory_id, mfi->date_released, mfi->id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); return -1; } sqlite3_free(query); library_update_trigger(); return 0; #undef Q_TMPL } void db_file_seek_update(int id, uint32_t seek) { #define Q_TMPL "UPDATE files SET seek = %d WHERE id = %d;" char *query; if (id == 0) return; query = sqlite3_mprintf(Q_TMPL, seek, id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return; } db_query_run(query, 1, 0); #undef Q_TMPL } void db_file_delete_bypath(char *path) { #define Q_TMPL "DELETE FROM files WHERE path = '%q';" char *query; query = sqlite3_mprintf(Q_TMPL, path); db_query_run(query, 1, 1); #undef Q_TMPL } void db_file_disable_bypath(char *path, char *strip, uint32_t cookie) { #define Q_TMPL "UPDATE files SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path = '%q';" char *query; int64_t disabled; int striplen; int striplenvpath; disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE; striplen = strlen(strip) + 1; if (strlen(strip) > 0) striplenvpath = strlen(strip) + strlen("/file:/"); else striplenvpath = 0; query = sqlite3_mprintf(Q_TMPL, striplen, striplenvpath, disabled, path); db_query_run(query, 1, 1); #undef Q_TMPL } void db_file_disable_bymatch(char *path, char *strip, uint32_t cookie) { #define Q_TMPL "UPDATE files SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path LIKE '%q/%%';" char *query; int64_t disabled; int striplen; int striplenvpath; disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE; striplen = strlen(strip) + 1; if (strlen(strip) > 0) striplenvpath = strlen(strip) + strlen("/file:/"); else striplenvpath = 0; query = sqlite3_mprintf(Q_TMPL, striplen, striplenvpath, disabled, path); db_query_run(query, 1, 1); #undef Q_TMPL } int db_file_enable_bycookie(uint32_t cookie, char *path) { #define Q_TMPL "UPDATE files SET path = '%q' || path, virtual_path = '/file:%q' || virtual_path, disabled = 0 WHERE disabled = %" PRIi64 ";" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, path, path, (int64_t)cookie); ret = db_query_run(query, 1, 1); return ((ret < 0) ? -1 : sqlite3_changes(hdl)); #undef Q_TMPL } int db_file_update_directoryid(char *path, int dir_id) { #define Q_TMPL "UPDATE files SET directory_id = %d WHERE path = %Q;" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, dir_id, path); ret = db_query_run(query, 1, 0); return ((ret < 0) ? -1 : sqlite3_changes(hdl)); #undef Q_TMPL } /* Playlists */ int db_pl_get_count(void) { return db_get_one_int("SELECT COUNT(*) FROM playlists p WHERE p.disabled = 0;"); } static int db_pl_count_items(int id, int streams_only) { #define Q_TMPL "SELECT COUNT(*) FROM playlistitems pi JOIN files f" \ " ON pi.filepath = f.path WHERE f.disabled = 0 AND pi.playlistid = %d;" #define Q_TMPL_STREAMS "SELECT COUNT(*) FROM playlistitems pi JOIN files f" \ " ON pi.filepath = f.path WHERE f.disabled = 0 AND f.data_kind = 1 AND pi.playlistid = %d;" char *query; int ret; if (!streams_only) query = sqlite3_mprintf(Q_TMPL, id); else query = sqlite3_mprintf(Q_TMPL_STREAMS, id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_get_one_int(query); sqlite3_free(query); return ret; #undef Q_TMPL_STREAMS #undef Q_TMPL } static int db_smartpl_count_items(const char *smartpl_query) { #define Q_TMPL "SELECT COUNT(*) FROM files f WHERE f.disabled = 0 AND %s;" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, smartpl_query); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_get_one_int(query); sqlite3_free(query); return ret; #undef Q_TMPL } void db_pl_ping(int id) { #define Q_TMPL "UPDATE playlists SET db_timestamp = %" PRIi64 ", disabled = 0 WHERE id = %d;" char *query; query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id); db_query_run(query, 1, 0); #undef Q_TMPL } void db_pl_ping_bymatch(char *path, int isdir) { #define Q_TMPL_DIR "UPDATE playlists SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';" #define Q_TMPL_NODIR "UPDATE playlists SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q%%';" char *query; if (isdir) query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), path); else query = sqlite3_mprintf(Q_TMPL_NODIR, (int64_t)time(NULL), path); db_query_run(query, 1, 0); #undef Q_TMPL_DIR #undef Q_TMPL_NODIR } static int db_pl_id_bypath(char *path, int *id) { #define Q_TMPL "SELECT p.id FROM playlists p WHERE p.path = '%q';" char *query; sqlite3_stmt *stmt; int ret; query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); sqlite3_free(query); return -1; } *id = sqlite3_column_int(stmt, 0); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); return 0; #undef Q_TMPL } static struct playlist_info * db_pl_fetch_byquery(char *query) { struct playlist_info *pli; sqlite3_stmt *stmt; int ncols; char *cval; uint32_t *ival; char **strval; uint64_t disabled; int i; int ret; if (!query) return NULL; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); pli = (struct playlist_info *)malloc(sizeof(struct playlist_info)); if (!pli) { DPRINTF(E_LOG, L_DB, "Could not allocate struct playlist_info, out of memory\n"); return NULL; } memset(pli, 0, sizeof(struct playlist_info)); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); free(pli); return NULL; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); free(pli); return NULL; } ncols = sqlite3_column_count(stmt); if (sizeof(pli_cols_map) / sizeof(pli_cols_map[0]) != ncols) { DPRINTF(E_LOG, L_DB, "BUG: pli column map out of sync with schema\n"); sqlite3_finalize(stmt); free(pli); return NULL; } for (i = 0; i < ncols; i++) { switch (pli_cols_map[i].type) { case DB_TYPE_INT: ival = (uint32_t *) ((char *)pli + pli_cols_map[i].offset); if (pli_cols_map[i].offset == pli_offsetof(disabled)) { disabled = sqlite3_column_int64(stmt, i); *ival = (disabled != 0); } else *ival = sqlite3_column_int(stmt, i); break; case DB_TYPE_STRING: strval = (char **) ((char *)pli + pli_cols_map[i].offset); cval = (char *)sqlite3_column_text(stmt, i); if (cval) *strval = strdup(cval); break; default: DPRINTF(E_LOG, L_DB, "BUG: Unknown type %d in pli column map\n", pli_cols_map[i].type); sqlite3_finalize(stmt); free_pli(pli, 0); return NULL; } } ret = db_blocking_step(stmt); sqlite3_finalize(stmt); if (ret != SQLITE_DONE) { DPRINTF(E_WARN, L_DB, "Query had more than a single result!\n"); free_pli(pli, 0); return NULL; } switch (pli->type) { case PL_PLAIN: case PL_FOLDER: pli->items = db_pl_count_items(pli->id, 0); pli->streams = db_pl_count_items(pli->id, 1); break; case PL_SPECIAL: case PL_SMART: pli->items = db_smartpl_count_items(pli->query); break; default: DPRINTF(E_LOG, L_DB, "Unknown playlist type %d while fetching playlist\n", pli->type); free_pli(pli, 0); return NULL; } return pli; } struct playlist_info * db_pl_fetch_bypath(const char *path) { #define Q_TMPL "SELECT p.* FROM playlists p WHERE p.path = '%q';" struct playlist_info *pli; char *query; query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } pli = db_pl_fetch_byquery(query); sqlite3_free(query); return pli; #undef Q_TMPL } struct playlist_info * db_pl_fetch_byvirtualpath(char *virtual_path) { #define Q_TMPL "SELECT p.* FROM playlists p WHERE p.virtual_path = '%q';" struct playlist_info *pli; char *query; query = sqlite3_mprintf(Q_TMPL, virtual_path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } pli = db_pl_fetch_byquery(query); sqlite3_free(query); return pli; #undef Q_TMPL } struct playlist_info * db_pl_fetch_byid(int id) { #define Q_TMPL "SELECT p.* FROM playlists p WHERE p.id = %d;" struct playlist_info *pli; char *query; query = sqlite3_mprintf(Q_TMPL, id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } pli = db_pl_fetch_byquery(query); sqlite3_free(query); return pli; #undef Q_TMPL } struct playlist_info * db_pl_fetch_bytitlepath(char *title, char *path) { #define Q_TMPL "SELECT p.* FROM playlists p WHERE p.title = '%q' AND p.path = '%q';" struct playlist_info *pli; char *query; query = sqlite3_mprintf(Q_TMPL, title, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } pli = db_pl_fetch_byquery(query); sqlite3_free(query); return pli; #undef Q_TMPL } int db_pl_add(struct playlist_info *pli, int *id) { #define QDUP_TMPL "SELECT COUNT(*) FROM playlists p WHERE p.title = TRIM(%Q) AND p.path = '%q';" #define QADD_TMPL "INSERT INTO playlists (title, type, query, db_timestamp, disabled, path, idx, special_id, parent_id, virtual_path, directory_id)" \ " VALUES (TRIM(%Q), %d, '%q', %" PRIi64 ", %d, '%q', %d, %d, %d, '%q', %d);" char *query; char *errmsg; int ret; /* Check duplicates */ query = sqlite3_mprintf(QDUP_TMPL, pli->title, STR(pli->path)); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } ret = db_get_one_int(query); sqlite3_free(query); if (ret > 0) { DPRINTF(E_WARN, L_DB, "Duplicate playlist with title '%s' path '%s'\n", pli->title, pli->path); return -1; } /* Add */ query = sqlite3_mprintf(QADD_TMPL, pli->title, pli->type, pli->query, (int64_t)time(NULL), pli->disabled, STR(pli->path), pli->index, pli->special_id, pli->parent_id, pli->virtual_path, pli->directory_id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); return -1; } sqlite3_free(query); *id = (int)sqlite3_last_insert_rowid(hdl); if (*id == 0) { DPRINTF(E_LOG, L_DB, "Successful insert but no last_insert_rowid!\n"); return -1; } DPRINTF(E_DBG, L_DB, "Added playlist %s (path %s) with id %d\n", pli->title, pli->path, *id); return 0; #undef QDUP_TMPL #undef QADD_TMPL } int db_pl_add_item_bypath(int plid, const char *path) { #define Q_TMPL "INSERT INTO playlistitems (playlistid, filepath) VALUES (%d, '%q');" char *query; query = sqlite3_mprintf(Q_TMPL, plid, path); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_pl_add_item_byid(int plid, int fileid) { #define Q_TMPL "INSERT INTO playlistitems (playlistid, filepath) VALUES (%d, (SELECT f.path FROM files f WHERE f.id = %d));" char *query; query = sqlite3_mprintf(Q_TMPL, plid, fileid); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_pl_update(struct playlist_info *pli) { #define Q_TMPL "UPDATE playlists SET title = TRIM(%Q), type = %d, query = '%q', db_timestamp = %" PRIi64 ", disabled = %d, " \ " path = '%q', idx = %d, special_id = %d, parent_id = %d, virtual_path = '%q', directory_id = %d " \ " WHERE id = %d;" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, pli->title, pli->type, pli->query, (int64_t)time(NULL), pli->disabled, STR(pli->path), pli->index, pli->special_id, pli->parent_id, pli->virtual_path, pli->directory_id, pli->id); ret = db_query_run(query, 1, 0); return ret; #undef Q_TMPL } void db_pl_clear_items(int id) { #define Q_TMPL "DELETE FROM playlistitems WHERE playlistid = %d;" char *query; query = sqlite3_mprintf(Q_TMPL, id); db_query_run(query, 1, 0); #undef Q_TMPL } void db_pl_delete(int id) { #define Q_TMPL "DELETE FROM playlists WHERE id = %d;" char *query; int ret; if (id == 1) return; query = sqlite3_mprintf(Q_TMPL, id); ret = db_query_run(query, 1, 0); if (ret == 0) db_pl_clear_items(id); #undef Q_TMPL } void db_pl_delete_bypath(char *path) { int id; int ret; ret = db_pl_id_bypath(path, &id); if (ret < 0) return; db_pl_delete(id); } void db_pl_disable_bypath(char *path, char *strip, uint32_t cookie) { #define Q_TMPL "UPDATE playlists SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path = '%q';" char *query; int64_t disabled; int striplen; int striplenvpath; disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE; striplen = strlen(strip) + 1; if (strlen(strip) > 0) striplenvpath = strlen(strip) + strlen("/file:/"); else striplenvpath = 0; query = sqlite3_mprintf(Q_TMPL, striplen, striplenvpath, disabled, path); db_query_run(query, 1, 0); #undef Q_TMPL } void db_pl_disable_bymatch(char *path, char *strip, uint32_t cookie) { #define Q_TMPL "UPDATE playlists SET path = substr(path, %d), virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE path LIKE '%q/%%';" char *query; int64_t disabled; int striplen; int striplenvpath; disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE; striplen = strlen(strip) + 1; if (strlen(strip) > 0) striplenvpath = strlen(strip) + strlen("/file:/"); else striplenvpath = 0; query = sqlite3_mprintf(Q_TMPL, striplen, striplenvpath, disabled, path); db_query_run(query, 1, 0); #undef Q_TMPL } int db_pl_enable_bycookie(uint32_t cookie, char *path) { #define Q_TMPL "UPDATE playlists SET path = '%q' || path, virtual_path = '/file:%q' || virtual_path, disabled = 0 WHERE disabled = %" PRIi64 ";" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, path, path, (int64_t)cookie); ret = db_query_run(query, 1, 0); return ((ret < 0) ? -1 : sqlite3_changes(hdl)); #undef Q_TMPL } /* Groups */ // Remove album and artist entries in the groups table that are not longer referenced from the files table int db_groups_cleanup() { #define Q_TMPL_ALBUM "DELETE FROM groups WHERE type = 1 AND NOT persistentid IN (SELECT songalbumid from files WHERE disabled = 0);" #define Q_TMPL_ARTIST "DELETE FROM groups WHERE type = 2 AND NOT persistentid IN (SELECT songartistid from files WHERE disabled = 0);" int ret; db_transaction_begin(); ret = db_query_run(Q_TMPL_ALBUM, 0, 1); if (ret < 0) { db_transaction_rollback(); return -1; } DPRINTF(E_DBG, L_DB, "Removed album group-entries: %d\n", sqlite3_changes(hdl)); ret = db_query_run(Q_TMPL_ARTIST, 0, 1); if (ret < 0) { db_transaction_rollback(); return -1; } DPRINTF(E_DBG, L_DB, "Removed artist group-entries: %d\n", sqlite3_changes(hdl)); db_transaction_end(); return 0; #undef Q_TMPL_ALBUM #undef Q_TMPL_ARTIST } static enum group_type db_group_type_bypersistentid(int64_t persistentid) { #define Q_TMPL "SELECT g.type FROM groups g WHERE g.persistentid = %" PRIi64 ";" char *query; sqlite3_stmt *stmt; int ret; query = sqlite3_mprintf(Q_TMPL, persistentid); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return 0; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); sqlite3_free(query); return 0; } ret = sqlite3_column_int(stmt, 0); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); return ret; #undef Q_TMPL } int db_group_persistentid_byid(int id, int64_t *persistentid) { #define Q_TMPL "SELECT g.persistentid FROM groups g WHERE g.id = %d;" char *query; sqlite3_stmt *stmt; int ret; query = sqlite3_mprintf(Q_TMPL, id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); sqlite3_free(query); return -1; } *persistentid = sqlite3_column_int64(stmt, 0); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); return 0; #undef Q_TMPL } /* Directories */ int db_directory_id_byvirtualpath(char *virtual_path) { #define Q_TMPL "SELECT d.id FROM directories d WHERE d.virtual_path = '%q';" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, virtual_path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_file_id_byquery(query); sqlite3_free(query); return ret; #undef Q_TMPL } int db_directory_enum_start(struct directory_enum *de) { #define Q_TMPL "SELECT * FROM directories WHERE disabled = 0 AND parent_id = %d ORDER BY virtual_path;" char *query; int ret; de->stmt = NULL; query = sqlite3_mprintf(Q_TMPL, de->parent_id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Starting enum '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &de->stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } sqlite3_free(query); return 0; #undef Q_TMPL } int db_directory_enum_fetch(struct directory_enum *de, struct directory_info *di) { uint64_t disabled; int ret; memset(di, 0, sizeof(struct directory_info)); if (!de->stmt) { DPRINTF(E_LOG, L_DB, "Directory enum not started!\n"); return -1; } ret = db_blocking_step(de->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of directory enum results\n"); return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } di->id = sqlite3_column_int(de->stmt, 0); di->virtual_path = (char *)sqlite3_column_text(de->stmt, 1); di->db_timestamp = sqlite3_column_int(de->stmt, 2); disabled = sqlite3_column_int64(de->stmt, 3); di->disabled = (disabled != 0); di->parent_id = sqlite3_column_int(de->stmt, 4); return 0; } void db_directory_enum_end(struct directory_enum *de) { if (!de->stmt) return; sqlite3_finalize(de->stmt); de->stmt = NULL; } static int db_directory_add(struct directory_info *di, int *id) { #define QADD_TMPL "INSERT INTO directories (virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (TRIM(%Q), %d, %d, %d);" char *query; char *errmsg; int ret; query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); return -1; } sqlite3_free(query); *id = (int)sqlite3_last_insert_rowid(hdl); if (*id == 0) { DPRINTF(E_LOG, L_DB, "Successful insert but no last_insert_rowid!\n"); return -1; } DPRINTF(E_DBG, L_DB, "Added directory %s with id %d\n", di->virtual_path, *id); return 0; #undef QADD_TMPL } static int db_directory_update(struct directory_info *di) { #define QADD_TMPL "UPDATE directories SET virtual_path = TRIM(%Q), db_timestamp = %d, disabled = %d, parent_id = %d" \ " WHERE id = %d;" char *query; char *errmsg; int ret; /* Add */ query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id, di->id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_exec(query, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); return -1; } sqlite3_free(query); DPRINTF(E_DBG, L_DB, "Updated directory %s with id %d\n", di->virtual_path, di->id); return 0; #undef QADD_TMPL } int db_directory_addorupdate(char *virtual_path, int disabled, int parent_id) { struct directory_info di; int id; int ret; id = db_directory_id_byvirtualpath(virtual_path); di.id = id; di.parent_id = parent_id; di.virtual_path = virtual_path; di.disabled = disabled; di.db_timestamp = (uint64_t)time(NULL); if (di.id == 0) ret = db_directory_add(&di, &id); else ret = db_directory_update(&di); if (ret < 0 || id <= 0) { DPRINTF(E_LOG, L_DB, "Insert or update of directory failed '%s'\n", virtual_path); return -1; } return id; } void db_directory_ping_bymatch(char *virtual_path) { #define Q_TMPL_DIR "UPDATE directories SET db_timestamp = %" PRIi64 " WHERE virtual_path = '%q' OR virtual_path LIKE '%q/%%';" char *query; query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), virtual_path, virtual_path); db_query_run(query, 1, 0); #undef Q_TMPL_DIR } void db_directory_disable_bymatch(char *path, char *strip, uint32_t cookie) { #define Q_TMPL "UPDATE directories SET virtual_path = substr(virtual_path, %d), disabled = %" PRIi64 " WHERE virtual_path = '/file:%q' OR virtual_path LIKE '/file:%q/%%';" char *query; int64_t disabled; int striplen; disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE; if (strlen(strip) > 0) striplen = strlen(strip) + strlen("/file:/"); else striplen = 0; query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path, path, path); db_query_run(query, 1, 1); #undef Q_TMPL } int db_directory_enable_bycookie(uint32_t cookie, char *path) { #define Q_TMPL "UPDATE directories SET virtual_path = '/file:%q' || virtual_path, disabled = 0 WHERE disabled = %" PRIi64 ";" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, path, (int64_t)cookie); ret = db_query_run(query, 1, 1); return ((ret < 0) ? -1 : sqlite3_changes(hdl)); #undef Q_TMPL } int db_directory_enable_bypath(char *path) { #define Q_TMPL "UPDATE directories SET disabled = 0 WHERE virtual_path = %Q;" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, path); ret = db_query_run(query, 1, 1); return ((ret < 0) ? -1 : sqlite3_changes(hdl)); #undef Q_TMPL } /* Remotes */ static int db_pairing_delete_byremote(char *remote_id) { #define Q_TMPL "DELETE FROM pairings WHERE remote = '%q';" char *query; query = sqlite3_mprintf(Q_TMPL, remote_id); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_pairing_add(struct pairing_info *pi) { #define Q_TMPL "INSERT INTO pairings (remote, name, guid) VALUES ('%q', '%q', '%q');" char *query; int ret; ret = db_pairing_delete_byremote(pi->remote_id); if (ret < 0) return ret; query = sqlite3_mprintf(Q_TMPL, pi->remote_id, pi->name, pi->guid); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_pairing_fetch_byguid(struct pairing_info *pi) { #define Q_TMPL "SELECT p.* FROM pairings p WHERE p.guid = '%q';" char *query; sqlite3_stmt *stmt; int ret; query = sqlite3_mprintf(Q_TMPL, pi->guid); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_INFO, L_DB, "Pairing GUID %s not found\n", pi->guid); else DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); sqlite3_free(query); return -1; } pi->remote_id = strdup((char *)sqlite3_column_text(stmt, 0)); pi->name = strdup((char *)sqlite3_column_text(stmt, 1)); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); return 0; #undef Q_TMPL } #ifdef HAVE_SPOTIFY_H /* Spotify */ void db_spotify_purge(void) { #define Q_TMPL "UPDATE directories SET disabled = %" PRIi64 " WHERE virtual_path = '/spotify:';" char *queries[4] = { "DELETE FROM files WHERE path LIKE 'spotify:%%';", "DELETE FROM playlistitems WHERE filepath LIKE 'spotify:%%';", "DELETE FROM playlists WHERE path LIKE 'spotify:%%';", "DELETE FROM directories WHERE virtual_path LIKE '/spotify:/%%';", }; char *query; int i; int ret; for (i = 0; i < (sizeof(queries) / sizeof(queries[0])); i++) { ret = db_query_run(queries[i], 0, 1); if (ret == 0) DPRINTF(E_DBG, L_DB, "Processed %d rows\n", sqlite3_changes(hdl)); } // Disable the spotify directory by setting 'disabled' to INOTIFY_FAKE_COOKIE value query = sqlite3_mprintf(Q_TMPL, INOTIFY_FAKE_COOKIE); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return; } ret = db_query_run(query, 1, 1); if (ret == 0) DPRINTF(E_DBG, L_DB, "Disabled spotify directory\n"); #undef Q_TMPL } /* Spotify */ void db_spotify_pl_delete(int id) { char *queries_tmpl[2] = { "DELETE FROM playlists WHERE id = %d;", "DELETE FROM playlistitems WHERE playlistid = %d;", }; char *query; int i; int ret; for (i = 0; i < (sizeof(queries_tmpl) / sizeof(queries_tmpl[0])); i++) { query = sqlite3_mprintf(queries_tmpl[i], id); ret = db_query_run(query, 1, 1); if (ret == 0) DPRINTF(E_DBG, L_DB, "Deleted %d rows\n", sqlite3_changes(hdl)); } } /* Spotify */ void db_spotify_files_delete(void) { #define Q_TMPL "DELETE FROM files WHERE path LIKE 'spotify:%%' AND NOT path IN (SELECT filepath FROM playlistitems);" char *query; int ret; query = sqlite3_mprintf(Q_TMPL); ret = db_query_run(query, 1, 1); if (ret == 0) DPRINTF(E_DBG, L_DB, "Deleted %d rows\n", sqlite3_changes(hdl)); #undef Q_TMPL } #endif /* Admin */ int db_admin_set(const char *key, const char *value) { #define Q_TMPL "INSERT OR REPLACE INTO admin (key, value) VALUES ('%q', '%q');" char *query; query = sqlite3_mprintf(Q_TMPL, key, value); return db_query_run(query, 1, 0); #undef Q_TMPL } char * db_admin_get(const char *key) { #define Q_TMPL "SELECT value FROM admin a WHERE a.key = '%q';" char *query; sqlite3_stmt *stmt; char *res; int ret; query = sqlite3_mprintf(Q_TMPL, key); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return NULL; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_WARN, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return NULL; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret == SQLITE_DONE) DPRINTF(E_DBG, L_DB, "No results\n"); else DPRINTF(E_WARN, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); sqlite3_free(query); return NULL; } res = (char *)sqlite3_column_text(stmt, 0); if (res) res = strdup(res); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); return res; #undef Q_TMPL } int db_admin_delete(const char *key) { #define Q_TMPL "DELETE FROM admin where key='%q';" char *query; query = sqlite3_mprintf(Q_TMPL, key); return db_query_run(query, 1, 0); #undef Q_TMPL } /* Speakers */ int db_speaker_save(struct output_device *device) { #define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name, auth_key) VALUES (%" PRIi64 ", %d, %d, %Q, %Q);" char *query; query = sqlite3_mprintf(Q_TMPL, device->id, device->selected, device->volume, device->name, device->auth_key); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_speaker_get(struct output_device *device, uint64_t id) { #define Q_TMPL "SELECT s.selected, s.volume, s.name, s.auth_key FROM speakers s WHERE s.id = %" PRIi64 ";" sqlite3_stmt *stmt; char *query; int ret; query = sqlite3_mprintf(Q_TMPL, id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); ret = -1; goto out; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { if (ret != SQLITE_DONE) DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); ret = -1; goto out; } device->id = id; device->selected = sqlite3_column_int(stmt, 0); device->volume = sqlite3_column_int(stmt, 1); free(device->name); device->name = safe_strdup((char *)sqlite3_column_text(stmt, 2)); free(device->auth_key); device->auth_key = safe_strdup((char *)sqlite3_column_text(stmt, 3)); #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); ret = 0; out: sqlite3_free(query); return ret; #undef Q_TMPL } void db_speaker_clear_all(void) { db_query_run("UPDATE speakers SET selected = 0;", 0, 0); } /* Queue */ void free_queue_item(struct db_queue_item *queue_item, int content_only) { if (!queue_item) return; free(queue_item->path); free(queue_item->virtual_path); free(queue_item->title); free(queue_item->artist); free(queue_item->album_artist); free(queue_item->album); free(queue_item->genre); free(queue_item->artist_sort); free(queue_item->album_sort); free(queue_item->album_artist_sort); free(queue_item->artwork_url); if (!content_only) free(queue_item); else memset(queue_item, 0, sizeof(struct db_queue_item)); } /* * Returns the queue version from the admin table * * @return queue version */ int db_queue_get_version() { char *version; int32_t queue_version; int ret; queue_version = 0; version = db_admin_get("queue_version"); if (version) { ret = safe_atoi32(version, &queue_version); free(version); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Could not get playlist version\n"); return -1; } } return queue_version; } /* * Increments the version of the queue in the admin table and notifies listener of LISTENER_QUEUE * about the change. * * This function must be called after successfully modifying the queue table in order to send * notification messages to the clients (e. g. dacp or mpd clients). */ static void queue_inc_version_and_notify() { int queue_version; char version[10]; int ret; db_transaction_begin(); queue_version = db_queue_get_version(); if (queue_version < 0) queue_version = 0; queue_version++; ret = snprintf(version, sizeof(version), "%d", queue_version); if (ret >= sizeof(version)) { DPRINTF(E_LOG, L_DB, "Error incrementing queue version. Could not convert version to string: %d\n", queue_version); db_transaction_rollback(); return; } ret = db_admin_set("queue_version", version); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Error incrementing queue version. Could not update version in admin table: %d\n", queue_version); db_transaction_rollback(); return; } db_transaction_end(); listener_notify(LISTENER_QUEUE); } static int queue_add_file(struct db_media_file_info *dbmfi, int pos, int shuffle_pos) { #define Q_TMPL "INSERT INTO queue " \ "(id, file_id, song_length, data_kind, media_kind, " \ "pos, shuffle_pos, path, virtual_path, title, " \ "artist, album_artist, album, genre, songalbumid, " \ "time_modified, artist_sort, album_sort, album_artist_sort, year, " \ "track, disc)" \ "VALUES" \ "(NULL, %s, %s, %s, %s, " \ "%d, %d, %Q, %Q, %Q, " \ "%Q, %Q, %Q, %Q, %s, " \ "%s, %Q, %Q, %Q, %s, " \ "%s, %s);" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, dbmfi->id, dbmfi->song_length, dbmfi->data_kind, dbmfi->media_kind, pos, pos, dbmfi->path, dbmfi->virtual_path, dbmfi->title, dbmfi->artist, dbmfi->album_artist, dbmfi->album, dbmfi->genre, dbmfi->songalbumid, dbmfi->time_modified, dbmfi->artist_sort, dbmfi->album_sort, dbmfi->album_artist_sort, dbmfi->year, dbmfi->track, dbmfi->disc); ret = db_query_run(query, 1, 0); return ret; #undef Q_TMPL } int db_queue_update_item(struct db_queue_item *qi) { #define Q_TMPL "UPDATE queue SET " \ "file_id = %d, song_length = %d, data_kind = %d, media_kind = %d, " \ "pos = %d, shuffle_pos = %d, path = '%q', virtual_path = %Q, " \ "title = %Q, artist = %Q, album_artist = %Q, album = %Q, " \ "genre = %Q, songalbumid = %" PRIi64 ", time_modified = %d, " \ "artist_sort = %Q, album_sort = %Q, album_artist_sort = %Q, " \ "year = %d, track = %d, disc = %d, artwork_url = %Q " \ "WHERE id = %d;" char *query; int ret; query = sqlite3_mprintf(Q_TMPL, qi->file_id, qi->song_length, qi->data_kind, qi->media_kind, qi->pos, qi->shuffle_pos, qi->path, qi->virtual_path, qi->title, qi->artist, qi->album_artist, qi->album, qi->genre, qi->songalbumid, qi->time_modified, qi->artist_sort, qi->album_sort, qi->album_artist_sort, qi->year, qi->track, qi->disc, qi->artwork_url, qi->id); ret = db_query_run(query, 1, 0); return ret; #undef Q_TMPL } /* * Adds the files matching the given query to the queue after the item with the given item id * * The files table is queried with the given parameters and all found files are added after the * item with the given item id to the "normal" queue. They are appended to end of the shuffled queue * (assuming that the shuffled queue will get reshuffled after adding new items). * * The function returns -1 on failure (e. g. error reading from database) and if the given item id * does not exist. It wraps all database access in a transaction and performs a rollback if an error * occurs, leaving the queue in a consistent state. * * @param qp Query parameters for the files table * @param item_id Files are added after item with this id * @return 0 on success, -1 on failure */ int db_queue_add_by_queryafteritemid(struct query_params *qp, uint32_t item_id) { struct db_media_file_info dbmfi; char *query; int shuffle_pos; int pos; int ret; db_transaction_begin(); // Position of the first new item pos = db_queue_get_pos(item_id, 0); if (pos < 0) { DPRINTF(E_LOG, L_DB, "Could not fetch queue item for item-id %d\n", item_id); db_transaction_rollback(); return -1; } pos++; // Shuffle position of the first new item shuffle_pos = db_queue_get_count(); if (shuffle_pos < 0) { DPRINTF(E_LOG, L_DB, "Could not get count from queue\n"); db_transaction_rollback(); return -1; } // Start query for new items from files table ret = db_query_start(qp); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Could not start query\n"); db_transaction_rollback(); return -1; } DPRINTF(E_DBG, L_DB, "Player queue query returned %d items\n", qp->results); // Update pos for all items after the item with item_id query = sqlite3_mprintf("UPDATE queue SET pos = pos + %d WHERE pos > %d;", qp->results, (pos - 1)); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } // Iterate over new items from files table and insert into queue while (((ret = db_query_fetch_file(qp, &dbmfi)) == 0) && (dbmfi.id)) { ret = queue_add_file(&dbmfi, pos, shuffle_pos); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Failed to add song with id %s (%s) to queue\n", dbmfi.id, dbmfi.title); break; } DPRINTF(E_DBG, L_DB, "Added song with id %s (%s) to queue\n", dbmfi.id, dbmfi.title); shuffle_pos++; pos++; } db_query_end(qp); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Error fetching results\n"); db_transaction_rollback(); return -1; } db_transaction_end(); queue_inc_version_and_notify(); return 0; } /* * Adds the files matching the given query to the queue * * The files table is queried with the given parameters and all found files are added to the end of the * "normal" queue and the shuffled queue. * * The function returns -1 on failure (e. g. error reading from database). It wraps all database access * in a transaction and performs a rollback if an error occurs, leaving the queue in a consistent state. * * @param qp Query parameters for the files table * @param reshuffle If 1 queue will be reshuffled after adding new items * @param item_id The base item id, all items after this will be reshuffled * @return Item id of the last inserted item on success, -1 on failure */ int db_queue_add_by_query(struct query_params *qp, char reshuffle, uint32_t item_id) { struct db_media_file_info dbmfi; int pos; int ret; db_transaction_begin(); pos = db_queue_get_count(); if (pos < 0) { DPRINTF(E_LOG, L_DB, "Could not get count from queue\n"); db_transaction_rollback(); return -1; } ret = db_query_start(qp); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Could not start query\n"); db_transaction_rollback(); return -1; } DPRINTF(E_DBG, L_DB, "Player queue query returned %d items\n", qp->results); if (qp->results == 0) { db_query_end(qp); db_transaction_end(); return 0; } while (((ret = db_query_fetch_file(qp, &dbmfi)) == 0) && (dbmfi.id)) { ret = queue_add_file(&dbmfi, pos, pos); if (ret < 0) { DPRINTF(E_DBG, L_DB, "Failed to add song id %s (%s)\n", dbmfi.id, dbmfi.title); break; } DPRINTF(E_DBG, L_DB, "Added song id %s (%s) to queue\n", dbmfi.id, dbmfi.title); pos++; } db_query_end(qp); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Error fetching results\n"); db_transaction_rollback(); return -1; } ret = (int) sqlite3_last_insert_rowid(hdl); db_transaction_end(); // Reshuffle after adding new items if (reshuffle) { db_queue_reshuffle(item_id); } else { queue_inc_version_and_notify(); } return ret; } /* * Adds the items of the stored playlist with the given id to the end of the queue * * @param plid Id of the stored playlist * @param reshuffle If 1 queue will be reshuffled after adding new items * @param item_id The base item id, all items after this will be reshuffled * @return 0 on success, -1 on failure */ int db_queue_add_by_playlistid(int plid, char reshuffle, uint32_t item_id) { struct query_params qp; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.id = plid; qp.type = Q_PLITEMS; ret = db_queue_add_by_query(&qp, reshuffle, item_id); return ret; } /* * Adds the file with the given id to the queue * * @param id Id of the file * @param reshuffle If 1 queue will be reshuffled after adding new items * @param item_id The base item id, all items after this will be reshuffled * @return 0 on success, -1 on failure */ int db_queue_add_by_fileid(int id, char reshuffle, uint32_t item_id) { struct query_params qp; char buf[124]; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; snprintf(buf, sizeof(buf), "f.id = %" PRIu32, id); qp.filter = buf; ret = db_queue_add_by_query(&qp, reshuffle, item_id); return ret; } int db_queue_add_item(struct db_queue_item *queue_item, char reshuffle, uint32_t item_id) { #define Q_TMPL "INSERT INTO queue " \ "(id, file_id, song_length, data_kind, media_kind, " \ "pos, shuffle_pos, path, virtual_path, title, " \ "artist, album_artist, album, genre, songalbumid, " \ "time_modified, artist_sort, album_sort, album_artist_sort, year, " \ "track, disc)" \ "VALUES" \ "(NULL, %d, %d, %d, %d, " \ "%d, %d, %Q, %Q, %Q, " \ "%Q, %Q, %Q, %Q, %d, " \ "%d, %Q, %Q, %Q, %d, " \ "%d, %d);" char *query; int pos; int ret; db_transaction_begin(); pos = db_queue_get_count(); if (pos < 0) { DPRINTF(E_LOG, L_DB, "Could not get count from queue\n"); db_transaction_rollback(); return -1; } query = sqlite3_mprintf(Q_TMPL, queue_item->file_id, queue_item->song_length, queue_item->data_kind, queue_item->media_kind, pos, pos, queue_item->path, queue_item->virtual_path, queue_item->title, queue_item->artist, queue_item->album_artist, queue_item->album, queue_item->genre, queue_item->songalbumid, queue_item->time_modified, queue_item->artist_sort, queue_item->album_sort, queue_item->album_artist_sort, queue_item->year, queue_item->track, queue_item->disc); ret = db_query_run(query, 1, 0); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Error adding queue item\n"); db_transaction_rollback(); return -1; } ret = (int) sqlite3_last_insert_rowid(hdl); db_transaction_end(); // Reshuffle after adding new items if (reshuffle) { db_queue_reshuffle(item_id); } else { queue_inc_version_and_notify(); } return ret; #undef Q_TMPL } static int queue_enum_start(struct query_params *qp) { #define Q_TMPL "SELECT * FROM queue WHERE %s %s;" char *query; const char *orderby; int ret; qp->stmt = NULL; if (qp->sort) orderby = sort_clause[qp->sort]; else orderby = sort_clause[S_POS]; if (qp->filter) query = sqlite3_mprintf(Q_TMPL, qp->filter, orderby); else query = sqlite3_mprintf(Q_TMPL, "1=1", orderby); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Starting enum '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &qp->stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } sqlite3_free(query); return 0; #undef Q_TMPL } static inline char * strdup_if(char *str, int cond) { if (str == NULL) return NULL; if (cond) return strdup(str); return str; } static int queue_enum_fetch(struct query_params *qp, struct db_queue_item *queue_item, int keep_item) { int ret; memset(queue_item, 0, sizeof(struct db_queue_item)); if (!qp->stmt) { DPRINTF(E_LOG, L_DB, "Queue enum not started!\n"); return -1; } ret = db_blocking_step(qp->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of queue enum results\n"); return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } queue_item->id = (uint32_t)sqlite3_column_int(qp->stmt, 0); queue_item->file_id = (uint32_t)sqlite3_column_int(qp->stmt, 1); queue_item->pos = (uint32_t)sqlite3_column_int(qp->stmt, 2); queue_item->shuffle_pos = (uint32_t)sqlite3_column_int(qp->stmt, 3); queue_item->data_kind = sqlite3_column_int(qp->stmt, 4); queue_item->media_kind = sqlite3_column_int(qp->stmt, 5); queue_item->song_length = (uint32_t)sqlite3_column_int(qp->stmt, 6); queue_item->path = strdup_if((char *)sqlite3_column_text(qp->stmt, 7), keep_item); queue_item->virtual_path = strdup_if((char *)sqlite3_column_text(qp->stmt, 8), keep_item); queue_item->title = strdup_if((char *)sqlite3_column_text(qp->stmt, 9), keep_item); queue_item->artist = strdup_if((char *)sqlite3_column_text(qp->stmt, 10), keep_item); queue_item->album_artist = strdup_if((char *)sqlite3_column_text(qp->stmt, 11), keep_item); queue_item->album = strdup_if((char *)sqlite3_column_text(qp->stmt, 12), keep_item); queue_item->genre = strdup_if((char *)sqlite3_column_text(qp->stmt, 13), keep_item); queue_item->songalbumid = sqlite3_column_int64(qp->stmt, 14); queue_item->time_modified = sqlite3_column_int(qp->stmt, 15); queue_item->artist_sort = strdup_if((char *)sqlite3_column_text(qp->stmt, 16), keep_item); queue_item->album_sort = strdup_if((char *)sqlite3_column_text(qp->stmt, 17), keep_item); queue_item->album_artist_sort = strdup_if((char *)sqlite3_column_text(qp->stmt, 18), keep_item); queue_item->year = sqlite3_column_int(qp->stmt, 19); queue_item->track = sqlite3_column_int(qp->stmt, 20); queue_item->disc = sqlite3_column_int(qp->stmt, 21); queue_item->artwork_url = strdup_if((char *)sqlite3_column_text(qp->stmt, 22), keep_item); return 0; } int db_queue_enum_start(struct query_params *qp) { int ret; db_transaction_begin(); ret = queue_enum_start(qp); if (ret < 0) db_transaction_rollback(); return ret; } void db_queue_enum_end(struct query_params *qp) { db_query_end(qp); db_transaction_end(); } int db_queue_enum_fetch(struct query_params *qp, struct db_queue_item *queue_item) { return queue_enum_fetch(qp, queue_item, 0); } int db_queue_get_pos(uint32_t item_id, char shuffle) { #define Q_TMPL "SELECT pos FROM queue WHERE id = %d;" #define Q_TMPL_SHUFFLE "SELECT shuffle_pos FROM queue WHERE id = %d;" char *query; int pos; if (shuffle) query = sqlite3_mprintf(Q_TMPL_SHUFFLE, item_id); else query = sqlite3_mprintf(Q_TMPL, item_id); pos = db_get_one_int(query); sqlite3_free(query); return pos; #undef Q_TMPL #undef Q_TMPL_SHUFFLE } int db_queue_get_pos_byfileid(uint32_t file_id, char shuffle) { #define Q_TMPL "SELECT pos FROM queue WHERE file_id = %d LIMIT 1;" #define Q_TMPL_SHUFFLE "SELECT shuffle_pos FROM queue WHERE file_id = %d LIMIT 1;" char *query; int pos; if (shuffle) query = sqlite3_mprintf(Q_TMPL_SHUFFLE, file_id); else query = sqlite3_mprintf(Q_TMPL, file_id); pos = db_get_one_int(query); sqlite3_free(query); return pos; #undef Q_TMPL #undef Q_TMPL_SHUFFLE } static int queue_fetch_byitemid(uint32_t item_id, struct db_queue_item *queue_item, int with_metadata) { struct query_params qp; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.filter = sqlite3_mprintf("id = %d", item_id); ret = queue_enum_start(&qp); if (ret < 0) { sqlite3_free(qp.filter); return -1; } ret = queue_enum_fetch(&qp, queue_item, with_metadata); db_query_end(&qp); sqlite3_free(qp.filter); return ret; } struct db_queue_item * db_queue_fetch_byitemid(uint32_t item_id) { struct db_queue_item *queue_item; int ret; queue_item = calloc(1, sizeof(struct db_queue_item)); if (!queue_item) { DPRINTF(E_LOG, L_DB, "Out of memory for queue_item\n"); return NULL; } db_transaction_begin(); ret = queue_fetch_byitemid(item_id, queue_item, 1); db_transaction_end(); if (ret < 0) { free_queue_item(queue_item, 0); DPRINTF(E_LOG, L_DB, "Error fetching queue item by item id\n"); return NULL; } else if (queue_item->id == 0) { // No item found free_queue_item(queue_item, 0); return NULL; } return queue_item; } struct db_queue_item * db_queue_fetch_byfileid(uint32_t file_id) { struct db_queue_item *queue_item; struct query_params qp; int ret; memset(&qp, 0, sizeof(struct query_params)); queue_item = calloc(1, sizeof(struct db_queue_item)); if (!queue_item) { DPRINTF(E_LOG, L_DB, "Out of memory for queue_item\n"); return NULL; } db_transaction_begin(); qp.filter = sqlite3_mprintf("file_id = %d", file_id); ret = queue_enum_start(&qp); if (ret < 0) { sqlite3_free(qp.filter); db_transaction_end(); free_queue_item(queue_item, 0); DPRINTF(E_LOG, L_DB, "Error fetching queue item by file id\n"); return NULL; } ret = queue_enum_fetch(&qp, queue_item, 1); db_query_end(&qp); sqlite3_free(qp.filter); db_transaction_end(); if (ret < 0) { free_queue_item(queue_item, 0); DPRINTF(E_LOG, L_DB, "Error fetching queue item by file id\n"); return NULL; } else if (queue_item->id == 0) { // No item found free_queue_item(queue_item, 0); return NULL; } return queue_item; } static int queue_fetch_bypos(uint32_t pos, char shuffle, struct db_queue_item *queue_item, int with_metadata) { struct query_params qp; int ret; memset(&qp, 0, sizeof(struct query_params)); if (shuffle) qp.filter = sqlite3_mprintf("shuffle_pos = %d", pos); else qp.filter = sqlite3_mprintf("pos = %d", pos); ret = queue_enum_start(&qp); if (ret < 0) { sqlite3_free(qp.filter); return -1; } ret = queue_enum_fetch(&qp, queue_item, with_metadata); db_query_end(&qp); sqlite3_free(qp.filter); return ret; } struct db_queue_item * db_queue_fetch_bypos(uint32_t pos, char shuffle) { struct db_queue_item *queue_item; int ret; queue_item = calloc(1, sizeof(struct db_queue_item)); if (!queue_item) { DPRINTF(E_LOG, L_MAIN, "Out of memory for queue_item\n"); return NULL; } db_transaction_begin(); ret = queue_fetch_bypos(pos, shuffle, queue_item, 1); db_transaction_end(); if (ret < 0) { free_queue_item(queue_item, 0); DPRINTF(E_LOG, L_DB, "Error fetching queue item by pos id\n"); return NULL; } else if (queue_item->id == 0) { // No item found free_queue_item(queue_item, 0); return NULL; } return queue_item; } static int queue_fetch_byposrelativetoitem(int pos, uint32_t item_id, char shuffle, struct db_queue_item *queue_item, int with_metadata) { int pos_absolute; int ret; DPRINTF(E_DBG, L_DB, "Fetch by pos: pos (%d) relative to item with id (%d)\n", pos, item_id); pos_absolute = db_queue_get_pos(item_id, shuffle); if (pos_absolute < 0) { return -1; } DPRINTF(E_DBG, L_DB, "Fetch by pos: item (%d) has absolute pos %d\n", item_id, pos_absolute); pos_absolute += pos; ret = queue_fetch_bypos(pos_absolute, shuffle, queue_item, with_metadata); if (ret < 0) DPRINTF(E_LOG, L_DB, "Error fetching item by pos: pos (%d) relative to item with id (%d)\n", pos, item_id); else DPRINTF(E_DBG, L_DB, "Fetch by pos: fetched item (id=%d, pos=%d, file-id=%d)\n", queue_item->id, queue_item->pos, queue_item->file_id); return ret; } struct db_queue_item * db_queue_fetch_byposrelativetoitem(int pos, uint32_t item_id, char shuffle) { struct db_queue_item *queue_item; int ret; DPRINTF(E_DBG, L_DB, "Fetch by pos: pos (%d) relative to item with id (%d)\n", pos, item_id); queue_item = calloc(1, sizeof(struct db_queue_item)); if (!queue_item) { DPRINTF(E_LOG, L_MAIN, "Out of memory for queue_item\n"); return NULL; } db_transaction_begin(); ret = queue_fetch_byposrelativetoitem(pos, item_id, shuffle, queue_item, 1); db_transaction_end(); if (ret < 0) { free_queue_item(queue_item, 0); DPRINTF(E_LOG, L_DB, "Error fetching queue item by pos relative to item id\n"); return NULL; } else if (queue_item->id == 0) { // No item found free_queue_item(queue_item, 0); return NULL; } DPRINTF(E_DBG, L_DB, "Fetch by pos: fetched item (id=%d, pos=%d, file-id=%d)\n", queue_item->id, queue_item->pos, queue_item->file_id); return queue_item; } struct db_queue_item * db_queue_fetch_next(uint32_t item_id, char shuffle) { return db_queue_fetch_byposrelativetoitem(1, item_id, shuffle); } struct db_queue_item * db_queue_fetch_prev(uint32_t item_id, char shuffle) { return db_queue_fetch_byposrelativetoitem(-1, item_id, shuffle); } static int queue_fix_pos(enum sort_type sort) { #define Q_TMPL "UPDATE queue SET %q = %d WHERE id = %d;" struct query_params qp; struct db_queue_item queue_item; char *query; int pos; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.sort = sort; ret = queue_enum_start(&qp); if (ret < 0) { return -1; } pos = 0; while ((ret = queue_enum_fetch(&qp, &queue_item, 0)) == 0 && (queue_item.id > 0)) { if (queue_item.pos != pos) { if (sort == S_SHUFFLE_POS) query = sqlite3_mprintf(Q_TMPL, "shuffle_pos", pos, queue_item.id); else query = sqlite3_mprintf(Q_TMPL, "pos", pos, queue_item.id); ret = db_query_run(query, 1, 0); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Failed to update item with item-id: %d\n", queue_item.id); break; } } pos++; } db_query_end(&qp); return ret; #undef Q_TMPL } /* * Remove files that are disabled or non existant in the library and repair ordering of * the queue (shuffle and normal) */ int db_queue_cleanup() { #define Q_TMPL "DELETE FROM queue WHERE NOT file_id IN (SELECT id from files WHERE disabled = 0);" int deleted; int ret; db_transaction_begin(); ret = db_query_run(Q_TMPL, 0, 0); if (ret < 0) { db_transaction_rollback(); return -1; } deleted = sqlite3_changes(hdl); if (deleted <= 0) { // Nothing to do db_transaction_end(); return 0; } // Update position of normal queue ret = queue_fix_pos(S_POS); if (ret < 0) { db_transaction_rollback(); return -1; } // Update position of shuffle queue ret = queue_fix_pos(S_SHUFFLE_POS); if (ret < 0) { db_transaction_rollback(); return -1; } db_transaction_end(); queue_inc_version_and_notify(); return 0; #undef Q_TMPL } /* * Removes all items from the queue except the item give by 'keep_item_id' (if 'keep_item_id' > 0). * * @param keep_item_id item-id (e. g. the now playing item) to be left in the queue */ int db_queue_clear(uint32_t keep_item_id) { char *query; int ret; query = sqlite3_mprintf("DELETE FROM queue where id <> %d;", keep_item_id); db_transaction_begin(); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return ret; } if (keep_item_id) { query = sqlite3_mprintf("UPDATE queue SET pos = 0 AND shuffle_pos = 0 where id = %d;", keep_item_id); ret = db_query_run(query, 1, 0); } if (ret < 0) { db_transaction_rollback(); return ret; } db_transaction_end(); queue_inc_version_and_notify(); return ret; } static int queue_delete_item(struct db_queue_item *queue_item) { char *query; int ret; // Remove item with the given item_id query = sqlite3_mprintf("DELETE FROM queue where id = %d;", queue_item->id); ret = db_query_run(query, 1, 0); if (ret < 0) { return -1; } // Update pos for all items after the item with given item_id query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1 WHERE pos > %d;", queue_item->pos); ret = db_query_run(query, 1, 0); if (ret < 0) { return -1; } // Update shuffle_pos for all items after the item with given item_id query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos - 1 WHERE shuffle_pos > %d;", queue_item->shuffle_pos); ret = db_query_run(query, 1, 0); if (ret < 0) { return -1; } return 0; } int db_queue_delete_byitemid(uint32_t item_id) { struct db_queue_item queue_item; int ret; db_transaction_begin(); ret = queue_fetch_byitemid(item_id, &queue_item, 0); if (ret < 0) { db_transaction_rollback(); return -1; } if (queue_item.id == 0) { db_transaction_end(); return 0; } ret = queue_delete_item(&queue_item); if (ret < 0) { db_transaction_rollback(); } else { db_transaction_end(); queue_inc_version_and_notify(); } return ret; } int db_queue_delete_bypos(uint32_t pos, int count) { char *query; int to_pos; int ret; db_transaction_begin(); // Remove item with the given item_id to_pos = pos + count; query = sqlite3_mprintf("DELETE FROM queue where pos >= %d AND pos < %d;", pos, to_pos); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } ret = queue_fix_pos(S_POS); if (ret < 0) { db_transaction_rollback(); return -1; } ret = queue_fix_pos(S_SHUFFLE_POS); if (ret < 0) { db_transaction_rollback(); return -1; } db_transaction_end(); queue_inc_version_and_notify(); return ret; } int db_queue_delete_byposrelativetoitem(uint32_t pos, uint32_t item_id, char shuffle) { struct db_queue_item queue_item; int ret; db_transaction_begin(); ret = queue_fetch_byposrelativetoitem(pos, item_id, shuffle, &queue_item, 0); if (ret < 0) { db_transaction_rollback(); return -1; } else if (queue_item.id == 0) { // No item found db_transaction_end(); return 0; } ret = queue_delete_item(&queue_item); if (ret < 0) { db_transaction_rollback(); } else { db_transaction_end(); queue_inc_version_and_notify(); } return ret; } /* * Moves the queue item with the given id to the given position (zero-based). * * @param item_id Queue item id * @param pos_to target position in the queue (zero-based) * @þaram shuffle If 1 move item in the shuffle queue * @return 0 on success, -1 on failure */ int db_queue_move_byitemid(uint32_t item_id, int pos_to, char shuffle) { char *query; int pos_from; int ret; db_transaction_begin(); // Find item with the given item_id pos_from = db_queue_get_pos(item_id, shuffle); if (pos_from < 0) { db_transaction_rollback(); return -1; } // Update pos for all items after the item with given item_id if (shuffle) query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos - 1 WHERE shuffle_pos > %d;", pos_from); else query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1 WHERE pos > %d;", pos_from); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } // Update pos for all items from the given pos_to if (shuffle) query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos + 1 WHERE shuffle_pos >= %d;", pos_to); else query = sqlite3_mprintf("UPDATE queue SET pos = pos + 1 WHERE pos >= %d;", pos_to); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } // Update item with the given item_id if (shuffle) query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = %d where id = %d;", pos_to, item_id); else query = sqlite3_mprintf("UPDATE queue SET pos = %d where id = %d;", pos_to, item_id); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } db_transaction_end(); queue_inc_version_and_notify(); return 0; } /* * Moves the queue item at the given position to the given position (zero-based). * * @param pos_from Position of the queue item to move * @param pos_to target position in the queue (zero-based) * @return 0 on success, -1 on failure */ int db_queue_move_bypos(int pos_from, int pos_to) { struct db_queue_item queue_item; char *query; int ret; db_transaction_begin(); // Find item to move ret = queue_fetch_bypos(pos_from, 0, &queue_item, 0); if (ret < 0) { db_transaction_rollback(); return -1; } if (queue_item.id == 0) { db_transaction_end(); return 0; } // Update pos for all items after the item with given position query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1 WHERE pos > %d;", queue_item.pos); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } // Update pos for all items from the given pos_to query = sqlite3_mprintf("UPDATE queue SET pos = pos + 1 WHERE pos >= %d;", pos_to); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } // Update item with the given item_id query = sqlite3_mprintf("UPDATE queue SET pos = %d where id = %d;", pos_to, queue_item.id); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } db_transaction_end(); queue_inc_version_and_notify(); return 0; } /* * Moves the queue item at the given position to the given target position. The positions * are relavtive to the given base item (item id). * * @param from_pos Relative position of the queue item to the base item * @param to_offset Target position relative to the base item * @param item_id The base item id (normaly the now playing item) * @return 0 on success, -1 on failure */ int db_queue_move_byposrelativetoitem(uint32_t from_pos, uint32_t to_offset, uint32_t item_id, char shuffle) { struct db_queue_item queue_item; char *query; int pos_move_from; int pos_move_to; int ret; db_transaction_begin(); DPRINTF(E_DBG, L_DB, "Move by pos: from %d offset %d relative to item (%d)\n", from_pos, to_offset, item_id); // Find item with the given item_id ret = queue_fetch_byitemid(item_id, &queue_item, 0); if (ret < 0) { db_transaction_rollback(); return -1; } DPRINTF(E_DBG, L_DB, "Move by pos: base item (id=%d, pos=%d, file-id=%d)\n", queue_item.id, queue_item.pos, queue_item.file_id); if (queue_item.id == 0) { db_transaction_end(); return 0; } // Calculate the position of the item to move if (shuffle) pos_move_from = queue_item.shuffle_pos + from_pos; else pos_move_from = queue_item.pos + from_pos; // Calculate the position where to move the item to if (shuffle) pos_move_to = queue_item.shuffle_pos + to_offset; else pos_move_to = queue_item.pos + to_offset; if (pos_move_to < pos_move_from) { /* * Moving an item to a previous position seems to send an offset incremented by one */ pos_move_to++; } DPRINTF(E_DBG, L_DB, "Move by pos: absolute pos: move from %d to %d\n", pos_move_from, pos_move_to); // Find item to move ret = queue_fetch_bypos(pos_move_from, shuffle, &queue_item, 0); if (ret < 0) { db_transaction_rollback(); return -1; } DPRINTF(E_DBG, L_DB, "Move by pos: move item (id=%d, pos=%d, file-id=%d)\n", queue_item.id, queue_item.pos, queue_item.file_id); if (queue_item.id == 0) { db_transaction_end(); return 0; } // Update pos for all items after the item with given position if (shuffle) query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos - 1 WHERE shuffle_pos > %d;", queue_item.shuffle_pos); else query = sqlite3_mprintf("UPDATE queue SET pos = pos - 1 WHERE pos > %d;", queue_item.pos); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } // Update pos for all items from the given pos_to if (shuffle) query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = shuffle_pos + 1 WHERE shuffle_pos >= %d;", pos_move_to); else query = sqlite3_mprintf("UPDATE queue SET pos = pos + 1 WHERE pos >= %d;", pos_move_to); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } // Update item with the given item_id if (shuffle) query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = %d where id = %d;", pos_move_to, queue_item.id); else query = sqlite3_mprintf("UPDATE queue SET pos = %d where id = %d;", pos_move_to, queue_item.id); ret = db_query_run(query, 1, 0); if (ret < 0) { db_transaction_rollback(); return -1; } db_transaction_end(); queue_inc_version_and_notify(); return 0; } /* * Reshuffles the shuffle queue * * If the given item_id is 0, the whole shuffle queue is reshuffled, otherwise the * queue is reshuffled after the item with the given id (excluding this item). * * @param item_id The base item, after this item the queue is reshuffled * @return 0 on success, -1 on failure */ int db_queue_reshuffle(uint32_t item_id) { char *query; int pos; int count; struct db_queue_item queue_item; int *shuffle_pos; int len; int i; struct query_params qp; int ret; db_transaction_begin(); DPRINTF(E_DBG, L_DB, "Reshuffle queue after item with item-id: %d\n", item_id); // Reset the shuffled order ret = db_query_run("UPDATE queue SET shuffle_pos = pos;", 0, 0); if (ret < 0) { db_transaction_rollback(); return -1; } pos = 0; if (item_id > 0) { pos = db_queue_get_pos(item_id, 0); if (pos < 0) { db_transaction_rollback(); return -1; } pos++; // Do not reshuffle the base item } count = db_queue_get_count(); len = count - pos; DPRINTF(E_DBG, L_DB, "Reshuffle %d items off %d total items, starting from pos %d\n", len, count, pos); shuffle_pos = malloc(len * sizeof(int)); for (i = 0; i < len; i++) { shuffle_pos[i] = i + pos; } shuffle_int(&shuffle_rng, shuffle_pos, len); memset(&qp, 0, sizeof(struct query_params)); qp.filter = sqlite3_mprintf("pos >= %d", pos); ret = queue_enum_start(&qp); if (ret < 0) { sqlite3_free(qp.filter); db_transaction_rollback(); return -1; } i = 0; while ((ret = queue_enum_fetch(&qp, &queue_item, 0)) == 0 && (queue_item.id > 0) && (i < len)) { query = sqlite3_mprintf("UPDATE queue SET shuffle_pos = %d where id = %d;", shuffle_pos[i], queue_item.id); ret = db_query_run(query, 1, 0); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Failed to delete item with item-id: %d\n", queue_item.id); break; } i++; } db_query_end(&qp); sqlite3_free(qp.filter); if (ret < 0) { db_transaction_rollback(); return -1; } db_transaction_end(); queue_inc_version_and_notify(); return 0; } int db_queue_get_count() { return db_get_one_int("SELECT COUNT(*) FROM queue;"); } /* Inotify */ int db_watch_clear(void) { return db_query_run("DELETE FROM inotify;", 0, 0); } int db_watch_add(struct watch_info *wi) { #define Q_TMPL "INSERT INTO inotify (wd, cookie, path) VALUES (%d, 0, '%q');" char *query; query = sqlite3_mprintf(Q_TMPL, wi->wd, wi->path); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_watch_delete_bywd(uint32_t wd) { #define Q_TMPL "DELETE FROM inotify WHERE wd = %d;" char *query; query = sqlite3_mprintf(Q_TMPL, wd); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_watch_delete_bypath(char *path) { #define Q_TMPL "DELETE FROM inotify WHERE path = '%q';" char *query; query = sqlite3_mprintf(Q_TMPL, path); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_watch_delete_bymatch(char *path) { #define Q_TMPL "DELETE FROM inotify WHERE path LIKE '%q/%%';" char *query; query = sqlite3_mprintf(Q_TMPL, path); return db_query_run(query, 1, 0); #undef Q_TMPL } int db_watch_delete_bycookie(uint32_t cookie) { #define Q_TMPL "DELETE FROM inotify WHERE cookie = %" PRIi64 ";" char *query; if (cookie == 0) return -1; query = sqlite3_mprintf(Q_TMPL, (int64_t)cookie); return db_query_run(query, 1, 0); #undef Q_TMPL } static int db_watch_get_byquery(struct watch_info *wi, char *query) { sqlite3_stmt *stmt; char **strval; char *cval; uint32_t *ival; int64_t cookie; int ncols; int i; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { DPRINTF(E_WARN, L_DB, "Watch not found: '%s'\n", query); sqlite3_finalize(stmt); sqlite3_free(query); return -1; } ncols = sqlite3_column_count(stmt); if (sizeof(wi_cols_map) / sizeof(wi_cols_map[0]) != ncols) { DPRINTF(E_LOG, L_DB, "BUG: wi column map out of sync with schema\n"); sqlite3_finalize(stmt); sqlite3_free(query); return -1; } for (i = 0; i < ncols; i++) { switch (wi_cols_map[i].type) { case DB_TYPE_INT: ival = (uint32_t *) ((char *)wi + wi_cols_map[i].offset); if (wi_cols_map[i].offset == wi_offsetof(cookie)) { cookie = sqlite3_column_int64(stmt, i); *ival = (cookie == INOTIFY_FAKE_COOKIE) ? 0 : cookie; } else *ival = sqlite3_column_int(stmt, i); break; case DB_TYPE_STRING: strval = (char **) ((char *)wi + wi_cols_map[i].offset); cval = (char *)sqlite3_column_text(stmt, i); if (cval) *strval = strdup(cval); break; default: DPRINTF(E_LOG, L_DB, "BUG: Unknown type %d in wi column map\n", wi_cols_map[i].type); sqlite3_finalize(stmt); sqlite3_free(query); return -1; } } #ifdef DB_PROFILE while (db_blocking_step(stmt) == SQLITE_ROW) ; /* EMPTY */ #endif sqlite3_finalize(stmt); sqlite3_free(query); return 0; } int db_watch_get_bywd(struct watch_info *wi) { #define Q_TMPL "SELECT * FROM inotify WHERE wd = %d;" char *query; query = sqlite3_mprintf(Q_TMPL, wi->wd); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } return db_watch_get_byquery(wi, query); #undef Q_TMPL } int db_watch_get_bypath(struct watch_info *wi) { #define Q_TMPL "SELECT * FROM inotify WHERE path = '%q';" char *query; query = sqlite3_mprintf(Q_TMPL, wi->path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } return db_watch_get_byquery(wi, query); #undef Q_TMPL } void db_watch_mark_bypath(char *path, char *strip, uint32_t cookie) { #define Q_TMPL "UPDATE inotify SET path = substr(path, %d), cookie = %" PRIi64 " WHERE path = '%q';" char *query; int64_t disabled; int striplen; disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE; striplen = strlen(strip) + 1; query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path); db_query_run(query, 1, 0); #undef Q_TMPL } void db_watch_mark_bymatch(char *path, char *strip, uint32_t cookie) { #define Q_TMPL "UPDATE inotify SET path = substr(path, %d), cookie = %" PRIi64 " WHERE path LIKE '%q/%%';" char *query; int64_t disabled; int striplen; disabled = (cookie != 0) ? cookie : INOTIFY_FAKE_COOKIE; striplen = strlen(strip) + 1; query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path); db_query_run(query, 1, 0); #undef Q_TMPL } void db_watch_move_bycookie(uint32_t cookie, char *path) { #define Q_TMPL "UPDATE inotify SET path = '%q' || path, cookie = 0 WHERE cookie = %" PRIi64 ";" char *query; if (cookie == 0) return; query = sqlite3_mprintf(Q_TMPL, path, (int64_t)cookie); db_query_run(query, 1, 0); #undef Q_TMPL } int db_watch_cookie_known(uint32_t cookie) { #define Q_TMPL "SELECT COUNT(*) FROM inotify WHERE cookie = %" PRIi64 ";" char *query; int ret; if (cookie == 0) return 0; query = sqlite3_mprintf(Q_TMPL, (int64_t)cookie); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return 0; } ret = db_get_one_int(query); sqlite3_free(query); return (ret > 0); #undef Q_TMPL } int db_watch_enum_start(struct watch_enum *we) { #define Q_MATCH_TMPL "SELECT wd FROM inotify WHERE path LIKE '%q/%%';" #define Q_COOKIE_TMPL "SELECT wd FROM inotify WHERE cookie = %" PRIi64 ";" char *query; int ret; we->stmt = NULL; if (we->match) query = sqlite3_mprintf(Q_MATCH_TMPL, we->match); else if (we->cookie != 0) query = sqlite3_mprintf(Q_COOKIE_TMPL, we->cookie); else { DPRINTF(E_LOG, L_DB, "Could not start enum, no parameter given\n"); return -1; } if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Starting enum '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &we->stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } sqlite3_free(query); return 0; #undef Q_MATCH_TMPL #undef Q_COOKIE_TMPL } void db_watch_enum_end(struct watch_enum *we) { if (!we->stmt) return; sqlite3_finalize(we->stmt); we->stmt = NULL; } int db_watch_enum_fetchwd(struct watch_enum *we, uint32_t *wd) { int ret; *wd = 0; if (!we->stmt) { DPRINTF(E_LOG, L_DB, "Watch enum not started!\n"); return -1; } ret = db_blocking_step(we->stmt); if (ret == SQLITE_DONE) { DPRINTF(E_INFO, L_DB, "End of watch enum results\n"); return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); return -1; } *wd = (uint32_t)sqlite3_column_int(we->stmt, 0); return 0; } #ifdef DB_PROFILE static void db_xprofile(void *notused, const char *pquery, sqlite3_uint64 ptime) { sqlite3_stmt *stmt; char *query; int ret; DPRINTF(E_DBG, L_DBPERF, "SQL PROFILE query: %s\n", pquery); DPRINTF(E_DBG, L_DBPERF, "SQL PROFILE time: %" PRIu64 " ms\n", ((uint64_t)ptime / 1000000)); if ((strncmp(pquery, "SELECT", 6) != 0) && (strncmp(pquery, "UPDATE", 6) != 0) && (strncmp(pquery, "DELETE", 6) != 0)) return; /* Disable profiling callback */ sqlite3_profile(hdl, NULL, NULL); query = sqlite3_mprintf("EXPLAIN QUERY PLAN %s", pquery); if (!query) { DPRINTF(E_DBG, L_DBPERF, "Query plan: Out of memory\n"); goto out; } ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_DBG, L_DBPERF, "Query plan: Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); goto out; } DPRINTF(E_DBG, L_DBPERF, "Query plan:\n"); while ((ret = db_blocking_step(stmt)) == SQLITE_ROW) { DPRINTF(E_DBG, L_DBPERF, "(%d,%d,%d) %s\n", sqlite3_column_int(stmt, 0), sqlite3_column_int(stmt, 1), sqlite3_column_int(stmt, 2), sqlite3_column_text(stmt, 3)); } if (ret != SQLITE_DONE) DPRINTF(E_DBG, L_DBPERF, "Query plan: Could not step: %s\n", sqlite3_errmsg(hdl)); DPRINTF(E_DBG, L_DBPERF, "---\n"); sqlite3_finalize(stmt); out: /* Reenable profiling callback */ sqlite3_profile(hdl, db_xprofile, NULL); } #endif static int db_pragma_get_cache_size() { sqlite3_stmt *stmt; char *query = "PRAGMA cache_size;"; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return 0; } ret = db_blocking_step(stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); sqlite3_free(query); return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } ret = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return ret; } static int db_pragma_set_cache_size(int pages) { #define Q_TMPL "PRAGMA cache_size=%d;" sqlite3_stmt *stmt; char *query; int ret; query = sqlite3_mprintf(Q_TMPL, pages); DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return 0; } sqlite3_finalize(stmt); sqlite3_free(query); return 0; #undef Q_TMPL } static char * db_pragma_set_journal_mode(char *mode) { #define Q_TMPL "PRAGMA journal_mode=%s;" sqlite3_stmt *stmt; char *query; int ret; char *new_mode; query = sqlite3_mprintf(Q_TMPL, mode); DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return NULL; } ret = db_blocking_step(stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); sqlite3_free(query); return NULL; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return NULL; } new_mode = (char *) sqlite3_column_text(stmt, 0); sqlite3_finalize(stmt); sqlite3_free(query); return new_mode; #undef Q_TMPL } static int db_pragma_get_synchronous() { sqlite3_stmt *stmt; char *query = "PRAGMA synchronous;"; int ret; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return 0; } ret = db_blocking_step(stmt); if (ret == SQLITE_DONE) { DPRINTF(E_DBG, L_DB, "End of query results\n"); sqlite3_free(query); return 0; } else if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return -1; } ret = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return ret; } static int db_pragma_set_synchronous(int synchronous) { #define Q_TMPL "PRAGMA synchronous=%d;" sqlite3_stmt *stmt; char *query; int ret; query = sqlite3_mprintf(Q_TMPL, synchronous); DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); sqlite3_free(query); return 0; } sqlite3_finalize(stmt); sqlite3_free(query); return 0; #undef Q_TMPL } int db_perthread_init(void) { char *errmsg; int ret; int cache_size; char *journal_mode; int synchronous; ret = sqlite3_open(db_path, &hdl); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not open database: %s\n", sqlite3_errmsg(hdl)); sqlite3_close(hdl); return -1; } ret = sqlite3_enable_load_extension(hdl, 1); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not enable extension loading\n"); sqlite3_close(hdl); return -1; } errmsg = NULL; ret = sqlite3_load_extension(hdl, PKGLIBDIR "/forked-daapd-sqlext.so", NULL, &errmsg); if (ret != SQLITE_OK) { if (errmsg) { DPRINTF(E_LOG, L_DB, "Could not load SQLite extension: %s\n", errmsg); sqlite3_free(errmsg); } else DPRINTF(E_LOG, L_DB, "Could not load SQLite extension: %s\n", sqlite3_errmsg(hdl)); sqlite3_close(hdl); return -1; } ret = sqlite3_enable_load_extension(hdl, 0); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not disable extension loading\n"); sqlite3_close(hdl); return -1; } #ifdef DB_PROFILE sqlite3_profile(hdl, db_xprofile, NULL); #endif cache_size = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_cache_size_library"); if (cache_size > -1) { db_pragma_set_cache_size(cache_size); cache_size = db_pragma_get_cache_size(); DPRINTF(E_DBG, L_DB, "Database cache size in pages: %d\n", cache_size); } journal_mode = cfg_getstr(cfg_getsec(cfg, "sqlite"), "pragma_journal_mode"); if (journal_mode) { journal_mode = db_pragma_set_journal_mode(journal_mode); DPRINTF(E_DBG, L_DB, "Database journal mode: %s\n", journal_mode); } synchronous = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_synchronous"); if (synchronous > -1) { db_pragma_set_synchronous(synchronous); synchronous = db_pragma_get_synchronous(); DPRINTF(E_DBG, L_DB, "Database synchronous: %d\n", synchronous); } return 0; } void db_perthread_deinit(void) { sqlite3_stmt *stmt; if (!hdl) return; /* Tear down anything that's in flight */ while ((stmt = sqlite3_next_stmt(hdl, 0))) sqlite3_finalize(stmt); sqlite3_close(hdl); } static int db_check_version(void) { #define Q_VACUUM "VACUUM;" char *buf; char *errmsg; int db_ver_major; int db_ver_minor; int db_ver; int vacuum; int ret; vacuum = cfg_getbool(cfg_getsec(cfg, "sqlite"), "vacuum"); buf = db_admin_get("schema_version_major"); if (!buf) buf = db_admin_get("schema_version"); // Pre schema v15.1 if (!buf) return 1; // Will create new database safe_atoi32(buf, &db_ver_major); free(buf); buf = db_admin_get("schema_version_minor"); if (buf) { safe_atoi32(buf, &db_ver_minor); free(buf); } else db_ver_minor = 0; db_ver = db_ver_major * 100 + db_ver_minor; if (db_ver_major < 10) { DPRINTF(E_FATAL, L_DB, "Database schema v%d too old, cannot upgrade\n", db_ver_major); return -1; } else if (db_ver_major > SCHEMA_VERSION_MAJOR) { DPRINTF(E_FATAL, L_DB, "Database schema v%d is newer than the supported version\n", db_ver_major); return -1; } else if (db_ver < (SCHEMA_VERSION_MAJOR * 100 + SCHEMA_VERSION_MINOR)) { DPRINTF(E_LOG, L_DB, "Database schema outdated, upgrading schema v%d.%d -> v%d.%d...\n", db_ver_major, db_ver_minor, SCHEMA_VERSION_MAJOR, SCHEMA_VERSION_MINOR); ret = sqlite3_exec(hdl, "BEGIN TRANSACTION;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "DB error while running 'BEGIN TRANSACTION': %s\n", errmsg); sqlite3_free(errmsg); return -1; } ret = db_upgrade(hdl, db_ver); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Database upgrade errored out, rolling back changes ...\n"); ret = sqlite3_exec(hdl, "ROLLBACK TRANSACTION;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "DB error while running 'ROLLBACK TRANSACTION': %s\n", errmsg); sqlite3_free(errmsg); } return -1; } ret = db_init_indices(hdl); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Database upgrade errored out, rolling back changes ...\n"); ret = sqlite3_exec(hdl, "ROLLBACK TRANSACTION;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "DB error while running 'ROLLBACK TRANSACTION': %s\n", errmsg); sqlite3_free(errmsg); } return -1; } ret = sqlite3_exec(hdl, "COMMIT TRANSACTION;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "DB error while running 'COMMIT TRANSACTION': %s\n", errmsg); sqlite3_free(errmsg); return -1; } DPRINTF(E_LOG, L_DB, "Upgrading schema to v%d.%d completed\n", SCHEMA_VERSION_MAJOR, SCHEMA_VERSION_MINOR); vacuum = 1; } if (vacuum) { DPRINTF(E_LOG, L_DB, "Now vacuuming database, this may take some time...\n"); ret = sqlite3_exec(hdl, Q_VACUUM, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not VACUUM database: %s\n", errmsg); sqlite3_free(errmsg); return -1; } } return 0; #undef Q_VACUUM } int db_init(void) { int files; int pls; int ret; db_path = cfg_getstr(cfg_getsec(cfg, "general"), "db_path"); ret = sqlite3_config(SQLITE_CONFIG_MULTITHREAD); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "Could not switch SQLite3 to multithread mode\n"); DPRINTF(E_FATAL, L_DB, "Check that SQLite3 has been configured for thread-safe operations\n"); return -1; } ret = sqlite3_enable_shared_cache(1); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "Could not enable SQLite3 shared-cache mode\n"); return -1; } ret = sqlite3_initialize(); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "SQLite3 failed to initialize\n"); return -1; } ret = db_perthread_init(); if (ret < 0) return ret; ret = db_check_version(); if (ret < 0) { DPRINTF(E_FATAL, L_DB, "Database version check errored out, incompatible database\n"); db_perthread_deinit(); return -1; } else if (ret > 0) { DPRINTF(E_LOG, L_DB, "Could not check database version, trying DB init\n"); ret = db_init_tables(hdl); if (ret < 0) { DPRINTF(E_FATAL, L_DB, "Could not create tables\n"); db_perthread_deinit(); return -1; } } db_analyze(); db_set_cfg_names(); files = db_files_get_count(); pls = db_pl_get_count(); db_perthread_deinit(); DPRINTF(E_LOG, L_DB, "Database OK with %d active files and %d active playlists\n", files, pls); rng_init(&shuffle_rng); return 0; } void db_deinit(void) { sqlite3_shutdown(); } forked-daapd-25.0/src/db.h000066400000000000000000000401641313447753700153310ustar00rootroot00000000000000 #ifndef __DB_H__ #define __DB_H__ #include #include #include #include #include "outputs.h" enum index_type { I_NONE, I_FIRST, I_LAST, I_SUB }; enum sort_type { S_NONE = 0, S_NAME, S_ALBUM, S_ARTIST, S_PLAYLIST, S_YEAR, S_GENRE, S_COMPOSER, S_DISC, S_TRACK, S_VPATH, S_POS, S_SHUFFLE_POS, }; #define Q_F_BROWSE (1 << 15) enum query_type { Q_ITEMS = 1, Q_PL = 2, Q_FIND_PL = 3, Q_PLITEMS = 4, Q_GROUP_ALBUMS = 5, Q_GROUP_ARTISTS = 6, Q_GROUP_ITEMS = 7, Q_COUNT_ITEMS = 8, Q_BROWSE_ARTISTS = Q_F_BROWSE | 1, Q_BROWSE_ALBUMS = Q_F_BROWSE | 2, Q_BROWSE_GENRES = Q_F_BROWSE | 3, Q_BROWSE_COMPOSERS = Q_F_BROWSE | 4, Q_GROUP_DIRS = Q_F_BROWSE | 5, Q_BROWSE_YEARS = Q_F_BROWSE | 6, Q_BROWSE_DISCS = Q_F_BROWSE | 7, Q_BROWSE_TRACKS = Q_F_BROWSE | 8, Q_BROWSE_VPATH = Q_F_BROWSE | 9, Q_BROWSE_PATH = Q_F_BROWSE | 10, }; #define ARTWORK_UNKNOWN 0 #define ARTWORK_NONE 1 #define ARTWORK_EMBEDDED 2 #define ARTWORK_OWN 3 #define ARTWORK_DIR 4 #define ARTWORK_PARENTDIR 5 #define ARTWORK_SPOTIFY 6 #define ARTWORK_HTTP 7 struct query_params { /* Query parameters, filled in by caller */ enum query_type type; enum index_type idx_type; enum sort_type sort; int id; int64_t persistentid; int offset; int limit; char *filter; /* Query results, filled in by query_start */ int results; /* Private query context, keep out */ sqlite3_stmt *stmt; char buf1[32]; char buf2[32]; }; struct pairing_info { char *remote_id; char *name; char *guid; }; enum media_kind { MEDIA_KIND_MUSIC = 1, MEDIA_KIND_MOVIE = 2, MEDIA_KIND_PODCAST = 4, MEDIA_KIND_AUDIOBOOK = 8, MEDIA_KIND_MUSICVIDEO = 32, MEDIA_KIND_TVSHOW = 64, }; enum data_kind { DATA_KIND_FILE = 0, /* normal file */ DATA_KIND_HTTP = 1, /* network stream (radio) */ DATA_KIND_SPOTIFY = 2, /* iTunes has no spotify data kind, but we use 2 */ DATA_KIND_PIPE = 3, /* iTunes has no pipe data kind, but we use 3 */ }; /* Note that fields marked as integers in the metadata map in filescanner_ffmpeg must be uint32_t here */ struct media_file_info { char *path; uint32_t index; char *fname; char *title; char *artist; char *album; char *genre; char *comment; char *type; /* daap.songformat */ char *composer; char *orchestra; char *conductor; char *grouping; char *url; /* daap.songdataurl (asul) */ uint32_t bitrate; uint32_t samplerate; uint32_t song_length; int64_t file_size; uint32_t year; /* TDRC */ uint32_t track; /* TRCK */ uint32_t total_tracks; uint32_t disc; /* TPOS */ uint32_t total_discs; uint32_t time_added; /* FIXME: time_t */ uint32_t time_modified; uint32_t time_played; uint32_t play_count; uint32_t seek; uint32_t rating; uint32_t db_timestamp; uint32_t disabled; uint32_t bpm; /* TBPM */ uint32_t id; char *description; /* daap.songdescription */ char *codectype; /* song.codectype, 4 chars max (32 bits) */ uint32_t item_kind; /* song or movie */ uint32_t data_kind; /* dmap.datakind (asdk) */ uint64_t sample_count; //TODO [unused] sample count is never set and therefor always 0 uint32_t compilation; char artwork; /* iTunes 5+ */ uint32_t contentrating; /* iTunes 6.0.2 */ uint32_t has_video; uint32_t bits_per_sample; uint32_t media_kind; uint32_t tv_episode_sort; uint32_t tv_season_num; char *tv_series_name; char *tv_episode_num_str; /* com.apple.itunes.episode-num-str, used as a unique episode identifier */ char *tv_network_name; char *album_artist; int64_t songartistid; int64_t songalbumid; char *title_sort; char *artist_sort; char *album_sort; char *composer_sort; char *album_artist_sort; char *virtual_path; uint32_t directory_id; /* Id of directory */ uint32_t date_released; }; #define mfi_offsetof(field) offsetof(struct media_file_info, field) /* PL_SPECIAL value must be in sync with type value in Q_PL* in db_init.c */ enum pl_type { PL_SPECIAL = 0, PL_FOLDER = 1, PL_SMART = 2, PL_PLAIN = 3, PL_MAX, }; struct playlist_info { uint32_t id; /* integer id (miid) */ char *title; /* playlist name as displayed in iTunes (minm) */ enum pl_type type; /* see PL_ types */ uint32_t items; /* number of items (mimc) */ uint32_t streams; /* number of internet streams */ char *query; /* where clause if type 1 (MSPS) */ uint32_t db_timestamp; /* time last updated */ uint32_t disabled; char *path; /* path of underlying playlist */ uint32_t index; /* index of playlist for paths with multiple playlists */ uint32_t special_id; /* iTunes identifies certain 'special' playlists with special meaning */ char *virtual_path; /* virtual path of underlying playlist */ uint32_t parent_id; /* Id of parent playlist if the playlist is nested */ uint32_t directory_id; /* Id of directory */ }; #define pli_offsetof(field) offsetof(struct playlist_info, field) struct db_playlist_info { char *id; char *title; char *type; char *items; char *streams; char *query; char *db_timestamp; char *disabled; char *path; char *index; char *special_id; char *virtual_path; char *parent_id; char *directory_id; }; #define dbpli_offsetof(field) offsetof(struct db_playlist_info, field) struct group_info { uint32_t id; /* integer id (miid) */ uint64_t persistentid; /* ulonglong id (mper) */ char *itemname; /* album or album_artist (minm) */ char *itemname_sort; /* album_sort or album_artist_sort (~mshc) */ uint32_t itemcount; /* number of items (mimc) */ uint32_t groupalbumcount; /* number of albums (agac) */ char *songalbumartist; /* song album artist (asaa) */ uint64_t songartistid; /* song artist id (asri) */ uint32_t song_length; }; #define gri_offsetof(field) offsetof(struct group_info, field) struct db_group_info { char *id; char *persistentid; char *itemname; char *itemname_sort; char *itemcount; char *groupalbumcount; char *songalbumartist; char *songartistid; char *song_length; }; #define dbgri_offsetof(field) offsetof(struct db_group_info, field) struct db_media_file_info { char *id; char *path; char *fname; char *title; char *artist; char *album; char *genre; char *comment; char *type; char *composer; char *orchestra; char *conductor; char *grouping; char *url; char *bitrate; char *samplerate; char *song_length; char *file_size; char *year; char *track; char *total_tracks; char *disc; char *total_discs; char *bpm; char *compilation; char *artwork; char *rating; char *play_count; char *seek; char *data_kind; char *item_kind; char *description; char *time_added; char *time_modified; char *time_played; char *db_timestamp; char *disabled; char *sample_count; char *codectype; char *idx; char *has_video; char *contentrating; char *bits_per_sample; char *album_artist; char *media_kind; char *tv_episode_sort; char *tv_season_num; char *tv_series_name; char *tv_episode_num_str; char *tv_network_name; char *songartistid; char *songalbumid; char *title_sort; char *artist_sort; char *album_sort; char *composer_sort; char *album_artist_sort; char *virtual_path; char *directory_id; char *date_released; }; #define dbmfi_offsetof(field) offsetof(struct db_media_file_info, field) struct watch_info { int wd; char *path; uint32_t cookie; }; #define wi_offsetof(field) offsetof(struct watch_info, field) struct watch_enum { uint32_t cookie; char *match; /* Private enum context, keep out */ sqlite3_stmt *stmt; }; struct filecount_info { uint32_t count; uint64_t length; }; /* Directory ids must be in sync with the ids in Q_DIR* in db_init.c */ enum directory_ids { DIR_ROOT = 1, DIR_FILE = 2, DIR_HTTP = 3, DIR_SPOTIFY = 4, DIR_MAX }; struct directory_info { uint32_t id; char *virtual_path; uint32_t db_timestamp; uint32_t disabled; uint32_t parent_id; }; struct directory_enum { int parent_id; /* Private enum context, keep out */ sqlite3_stmt *stmt; }; struct db_queue_item { /* A unique id for this queue item. If the same item appears multiple times in the queue each corresponding queue item has its own id. */ uint32_t id; /* Id of the file/item in the files database */ uint32_t file_id; /* Length of the item in ms */ uint32_t song_length; /* Data type of the item */ enum data_kind data_kind; /* Media type of the item */ enum media_kind media_kind; uint32_t seek; uint32_t pos; uint32_t shuffle_pos; char *path; char *virtual_path; char *title; char *artist; char *album_artist; char *album; char *genre; int64_t songalbumid; uint32_t time_modified; char *artist_sort; char *album_sort; char *album_artist_sort; uint32_t year; uint32_t track; uint32_t disc; char *artwork_url; }; char * db_escape_string(const char *str); void free_pi(struct pairing_info *pi, int content_only); void free_mfi(struct media_file_info *mfi, int content_only); void unicode_fixup_mfi(struct media_file_info *mfi); void free_pli(struct playlist_info *pli, int content_only); void free_di(struct directory_info *di, int content_only); void free_queue_item(struct db_queue_item *queue_item, int content_only); /* Maintenance and DB hygiene */ void db_hook_post_scan(void); void db_purge_cruft(time_t ref); void db_purge_all(void); /* Transactions */ void db_transaction_begin(void); void db_transaction_end(void); void db_transaction_rollback(void); /* Queries */ int db_query_start(struct query_params *qp); void db_query_end(struct query_params *qp); int db_query_fetch_file(struct query_params *qp, struct db_media_file_info *dbmfi); int db_query_fetch_pl(struct query_params *qp, struct db_playlist_info *dbpli, int with_itemcount); int db_query_fetch_group(struct query_params *qp, struct db_group_info *dbgri); int db_query_fetch_count(struct query_params *qp, struct filecount_info *fci); int db_query_fetch_string(struct query_params *qp, char **string); int db_query_fetch_string_sort(struct query_params *qp, char **string, char **sortstring); /* Files */ int db_files_get_count(void); int db_files_get_artist_count(void); int db_files_get_album_count(void); int db_files_get_count_bymatch(char *path); void db_file_inc_playcount(int id); void db_file_ping(int id); void db_file_ping_bymatch(char *path, int isdir); char * db_file_path_byid(int id); int db_file_id_bypath(char *path); int db_file_id_bymatch(char *path); int db_file_id_byfile(char *filename); int db_file_id_byurl(char *url); int db_file_id_by_virtualpath_match(char *path); void db_file_stamp_bypath(const char *path, time_t *stamp, int *id); struct media_file_info * db_file_fetch_byid(int id); struct media_file_info * db_file_fetch_byvirtualpath(char *path); int db_file_add(struct media_file_info *mfi); int db_file_update(struct media_file_info *mfi); void db_file_seek_update(int id, uint32_t seek); void db_file_delete_bypath(char *path); void db_file_disable_bypath(char *path, char *strip, uint32_t cookie); void db_file_disable_bymatch(char *path, char *strip, uint32_t cookie); int db_file_enable_bycookie(uint32_t cookie, char *path); int db_file_update_directoryid(char *path, int dir_id); /* Playlists */ int db_pl_get_count(void); void db_pl_ping(int id); void db_pl_ping_bymatch(char *path, int isdir); struct playlist_info * db_pl_fetch_bypath(const char *path); struct playlist_info * db_pl_fetch_byvirtualpath(char *virtual_path); struct playlist_info * db_pl_fetch_bytitlepath(char *title, char *path); int db_pl_add(struct playlist_info *pli, int *id); int db_pl_add_item_bypath(int plid, const char *path); int db_pl_add_item_byid(int plid, int fileid); void db_pl_clear_items(int id); int db_pl_update(struct playlist_info *pli); void db_pl_delete(int id); void db_pl_delete_bypath(char *path); void db_pl_disable_bypath(char *path, char *strip, uint32_t cookie); void db_pl_disable_bymatch(char *path, char *strip, uint32_t cookie); int db_pl_enable_bycookie(uint32_t cookie, char *path); /* Groups */ int db_groups_cleanup(); int db_group_persistentid_byid(int id, int64_t *persistentid); /* Directories */ int db_directory_id_byvirtualpath(char *virtual_path); int db_directory_enum_start(struct directory_enum *de); int db_directory_enum_fetch(struct directory_enum *de, struct directory_info *di); void db_directory_enum_end(struct directory_enum *de); int db_directory_addorupdate(char *virtual_path, int disabled, int parent_id); void db_directory_ping_bymatch(char *virtual_path); void db_directory_disable_bymatch(char *path, char *strip, uint32_t cookie); int db_directory_enable_bycookie(uint32_t cookie, char *path); int db_directory_enable_bypath(char *path); /* Remotes */ int db_pairing_add(struct pairing_info *pi); int db_pairing_fetch_byguid(struct pairing_info *pi); #ifdef HAVE_SPOTIFY_H /* Spotify */ void db_spotify_purge(void); void db_spotify_pl_delete(int id); void db_spotify_files_delete(void); #endif /* Admin */ int db_admin_set(const char *key, const char *value); char * db_admin_get(const char *key); int db_admin_delete(const char *key); /* Speakers/outputs */ int db_speaker_save(struct output_device *device); int db_speaker_get(struct output_device *device, uint64_t id); void db_speaker_clear_all(void); /* Queue */ int db_queue_get_version(); int db_queue_update_item(struct db_queue_item *queue_item); int db_queue_add_by_queryafteritemid(struct query_params *qp, uint32_t item_id); int db_queue_add_by_query(struct query_params *qp, char reshuffle, uint32_t item_id); int db_queue_add_by_playlistid(int plid, char reshuffle, uint32_t item_id); int db_queue_add_by_fileid(int id, char reshuffle, uint32_t item_id); int db_queue_add_item(struct db_queue_item *queue_item, char reshuffle, uint32_t item_id); int db_queue_enum_start(struct query_params *qp); void db_queue_enum_end(struct query_params *qp); int db_queue_enum_fetch(struct query_params *qp, struct db_queue_item *queue_item); struct db_queue_item * db_queue_fetch_byitemid(uint32_t item_id); struct db_queue_item * db_queue_fetch_byfileid(uint32_t file_id); struct db_queue_item * db_queue_fetch_bypos(uint32_t pos, char shuffle); struct db_queue_item * db_queue_fetch_byposrelativetoitem(int pos, uint32_t item_id, char shuffle); struct db_queue_item * db_queue_fetch_next(uint32_t item_id, char shuffle); struct db_queue_item * db_queue_fetch_prev(uint32_t item_id, char shuffle); int db_queue_cleanup(); int db_queue_clear(uint32_t keep_item_id); int db_queue_delete_byitemid(uint32_t item_id); int db_queue_delete_bypos(uint32_t pos, int count); int db_queue_delete_byposrelativetoitem(uint32_t pos, uint32_t item_id, char shuffle); int db_queue_move_byitemid(uint32_t item_id, int pos_to, char shuffle); int db_queue_move_bypos(int pos_from, int pos_to); int db_queue_move_byposrelativetoitem(uint32_t from_pos, uint32_t to_offset, uint32_t item_id, char shuffle); int db_queue_reshuffle(uint32_t item_id); int db_queue_get_count(); int db_queue_get_pos(uint32_t item_id, char shuffle); int db_queue_get_pos_byfileid(uint32_t file_id, char shuffle); /* Inotify */ int db_watch_clear(void); int db_watch_add(struct watch_info *wi); int db_watch_delete_bywd(uint32_t wd); int db_watch_delete_bypath(char *path); int db_watch_delete_bymatch(char *path); int db_watch_delete_bycookie(uint32_t cookie); int db_watch_get_bywd(struct watch_info *wi); int db_watch_get_bypath(struct watch_info *wi); void db_watch_mark_bypath(char *path, char *strip, uint32_t cookie); void db_watch_mark_bymatch(char *path, char *strip, uint32_t cookie); void db_watch_move_bycookie(uint32_t cookie, char *path); int db_watch_cookie_known(uint32_t cookie); int db_watch_enum_start(struct watch_enum *we); void db_watch_enum_end(struct watch_enum *we); int db_watch_enum_fetchwd(struct watch_enum *we, uint32_t *wd); int db_perthread_init(void); void db_perthread_deinit(void); int db_init(void); void db_deinit(void); #endif /* !__DB_H__ */ forked-daapd-25.0/src/db_init.c000066400000000000000000000440461313447753700163520ustar00rootroot00000000000000 /* * Copyright (C) 2009-2011 Julien BLACHE * Copyright (C) 2010 Kai Elwert * Copyright (C) 2016 Christian Meffert * * 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 */ #include #include #include "db_init.h" #include "logger.h" #define T_ADMIN \ "CREATE TABLE IF NOT EXISTS admin(" \ " key VARCHAR(32) PRIMARY KEY NOT NULL," \ " value VARCHAR(255) NOT NULL" \ ");" #define T_FILES \ "CREATE TABLE IF NOT EXISTS files (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " path VARCHAR(4096) NOT NULL," \ " fname VARCHAR(255) NOT NULL," \ " title VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album VARCHAR(1024) NOT NULL COLLATE DAAP," \ " genre VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " comment VARCHAR(4096) DEFAULT NULL COLLATE DAAP," \ " type VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " composer VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " orchestra VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " conductor VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " grouping VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " url VARCHAR(1024) DEFAULT NULL," \ " bitrate INTEGER DEFAULT 0," \ " samplerate INTEGER DEFAULT 0," \ " song_length INTEGER DEFAULT 0," \ " file_size INTEGER DEFAULT 0," \ " year INTEGER DEFAULT 0," \ " track INTEGER DEFAULT 0," \ " total_tracks INTEGER DEFAULT 0," \ " disc INTEGER DEFAULT 0," \ " total_discs INTEGER DEFAULT 0," \ " bpm INTEGER DEFAULT 0," \ " compilation INTEGER DEFAULT 0," \ " artwork INTEGER DEFAULT 0," \ " rating INTEGER DEFAULT 0," \ " play_count INTEGER DEFAULT 0," \ " seek INTEGER DEFAULT 0," \ " data_kind INTEGER DEFAULT 0," \ " item_kind INTEGER DEFAULT 0," \ " description INTEGER DEFAULT 0," \ " time_added INTEGER DEFAULT 0," \ " time_modified INTEGER DEFAULT 0," \ " time_played INTEGER DEFAULT 0," \ " db_timestamp INTEGER DEFAULT 0," \ " disabled INTEGER DEFAULT 0," \ " sample_count INTEGER DEFAULT 0," \ " codectype VARCHAR(5) DEFAULT NULL," \ " idx INTEGER NOT NULL," \ " has_video INTEGER DEFAULT 0," \ " contentrating INTEGER DEFAULT 0," \ " bits_per_sample INTEGER DEFAULT 0," \ " album_artist VARCHAR(1024) NOT NULL COLLATE DAAP," \ " media_kind INTEGER NOT NULL," \ " tv_series_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_num_str VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_network_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_sort INTEGER NOT NULL," \ " tv_season_num INTEGER NOT NULL," \ " songartistid INTEGER NOT NULL," \ " songalbumid INTEGER NOT NULL," \ " title_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " virtual_path VARCHAR(4096) DEFAULT NULL," \ " directory_id INTEGER DEFAULT 0," \ " date_released INTEGER DEFAULT 0" \ ");" #define T_PL \ "CREATE TABLE IF NOT EXISTS playlists (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " title VARCHAR(255) NOT NULL COLLATE DAAP," \ " type INTEGER NOT NULL," \ " query VARCHAR(1024)," \ " db_timestamp INTEGER NOT NULL," \ " disabled INTEGER DEFAULT 0," \ " path VARCHAR(4096)," \ " idx INTEGER NOT NULL," \ " special_id INTEGER DEFAULT 0," \ " virtual_path VARCHAR(4096)," \ " parent_id INTEGER DEFAULT 0," \ " directory_id INTEGER DEFAULT 0" \ ");" #define T_PLITEMS \ "CREATE TABLE IF NOT EXISTS playlistitems (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " playlistid INTEGER NOT NULL," \ " filepath VARCHAR(4096) NOT NULL" \ ");" #define T_GROUPS \ "CREATE TABLE IF NOT EXISTS groups (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " type INTEGER NOT NULL," \ " name VARCHAR(1024) NOT NULL COLLATE DAAP," \ " persistentid INTEGER NOT NULL," \ "CONSTRAINT groups_type_unique_persistentid UNIQUE (type, persistentid)" \ ");" #define T_PAIRINGS \ "CREATE TABLE IF NOT EXISTS pairings(" \ " remote VARCHAR(64) PRIMARY KEY NOT NULL," \ " name VARCHAR(255) NOT NULL," \ " guid VARCHAR(16) NOT NULL" \ ");" #define T_SPEAKERS \ "CREATE TABLE IF NOT EXISTS speakers(" \ " id INTEGER PRIMARY KEY NOT NULL," \ " selected INTEGER NOT NULL," \ " volume INTEGER NOT NULL," \ " name VARCHAR(255) DEFAULT NULL," \ " auth_key VARCHAR(2048) DEFAULT NULL" \ ");" #define T_INOTIFY \ "CREATE TABLE IF NOT EXISTS inotify (" \ " wd INTEGER PRIMARY KEY NOT NULL," \ " cookie INTEGER NOT NULL," \ " path VARCHAR(4096) NOT NULL" \ ");" #define T_DIRECTORIES \ "CREATE TABLE IF NOT EXISTS directories (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " virtual_path VARCHAR(4096) NOT NULL," \ " db_timestamp INTEGER DEFAULT 0," \ " disabled INTEGER DEFAULT 0," \ " parent_id INTEGER DEFAULT 0" \ ");" #define T_QUEUE \ "CREATE TABLE IF NOT EXISTS queue (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " file_id INTEGER NOT NULL," \ " pos INTEGER NOT NULL," \ " shuffle_pos INTEGER NOT NULL," \ " data_kind INTEGER NOT NULL," \ " media_kind INTEGER NOT NULL," \ " song_length INTEGER NOT NULL," \ " path VARCHAR(4096) NOT NULL," \ " virtual_path VARCHAR(4096) NOT NULL," \ " title VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist VARCHAR(1024) NOT NULL COLLATE DAAP," \ " album VARCHAR(1024) NOT NULL COLLATE DAAP," \ " genre VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " songalbumid INTEGER NOT NULL," \ " time_modified INTEGER DEFAULT 0," \ " artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " year INTEGER DEFAULT 0," \ " track INTEGER DEFAULT 0," \ " disc INTEGER DEFAULT 0," \ " artwork_url VARCHAR(4096) DEFAULT NULL" \ ");" #define TRG_GROUPS_INSERT_FILES \ "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (2, NEW.album_artist, NEW.songartistid);" \ " END;" #define TRG_GROUPS_UPDATE_FILES \ "CREATE TRIGGER update_groups_update_file AFTER UPDATE OF songalbumid ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (2, NEW.album_artist, NEW.songartistid);" \ " END;" #define Q_PL1 \ "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(1, 'Library', 0, '1 = 1', 0, '', 0, 0);" #define Q_PL2 \ "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(2, 'Music', 0, 'f.media_kind = 1', 0, '', 0, 6);" #define Q_PL3 \ "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(3, 'Movies', 0, 'f.media_kind = 2', 0, '', 0, 4);" #define Q_PL4 \ "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(4, 'TV Shows', 0, 'f.media_kind = 64', 0, '', 0, 5);" #define Q_PL5 \ "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(5, 'Podcasts', 0, 'f.media_kind = 4', 0, '', 0, 1);" #define Q_PL6 \ "INSERT INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(6, 'Audiobooks', 0, 'f.media_kind = 8', 0, '', 0, 7);" /* These are the remaining automatically-created iTunes playlists, but * their query is unknown " VALUES(6, 'iTunes U', 0, 'media_kind = 256', 0, '', 0, 13);" " VALUES(8, 'Purchased', 0, 'media_kind = 1024', 0, '', 0, 8);" */ #define Q_DIR1 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (1, '/', 0, 0, 0);" #define Q_DIR2 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (2, '/file:', 0, 0, 1);" #define Q_DIR3 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (3, '/http:', 0, 0, 1);" #define Q_DIR4 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (4, '/spotify:', 0, 4294967296, 1);" #define Q_QUEUE_VERSION \ "INSERT INTO admin (key, value) VALUES ('queue_version', '0');" #define Q_SCVER_MAJOR \ "INSERT INTO admin (key, value) VALUES ('schema_version_major', '%d');" #define Q_SCVER_MINOR \ "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '%02d');" struct db_init_query { char *query; char *desc; }; static const struct db_init_query db_init_table_queries[] = { { T_ADMIN, "create table admin" }, { T_FILES, "create table files" }, { T_PL, "create table playlists" }, { T_PLITEMS, "create table playlistitems" }, { T_GROUPS, "create table groups" }, { T_PAIRINGS, "create table pairings" }, { T_SPEAKERS, "create table speakers" }, { T_INOTIFY, "create table inotify" }, { T_DIRECTORIES, "create table directories" }, { T_QUEUE, "create table queue" }, { TRG_GROUPS_INSERT_FILES, "create trigger update_groups_new_file" }, { TRG_GROUPS_UPDATE_FILES, "create trigger update_groups_update_file" }, { Q_PL1, "create default playlist" }, { Q_PL2, "create default smart playlist 'Music'" }, { Q_PL3, "create default smart playlist 'Movies'" }, { Q_PL4, "create default smart playlist 'TV Shows'" }, { Q_PL5, "create default smart playlist 'Podcasts'" }, { Q_PL6, "create default smart playlist 'Audiobooks'" }, { Q_DIR1, "create default root directory '/'" }, { Q_DIR2, "create default base directory '/file:'" }, { Q_DIR3, "create default base directory '/http:'" }, { Q_DIR4, "create default base directory '/spotify:'" }, { Q_QUEUE_VERSION, "initialize queue version" }, }; /* Indices must be prefixed with idx_ for db_drop_indices() to id them */ #define I_RESCAN \ "CREATE INDEX IF NOT EXISTS idx_rescan ON files(path, db_timestamp);" #define I_SONGARTISTID \ "CREATE INDEX IF NOT EXISTS idx_sari ON files(songartistid);" /* Used by Q_GROUP_ALBUMS */ #define I_SONGALBUMID \ "CREATE INDEX IF NOT EXISTS idx_sali ON files(songalbumid, disabled, media_kind, album_sort, disc, track);" /* Used by Q_GROUP_ARTISTS */ #define I_STATEMKINDSARI \ "CREATE INDEX IF NOT EXISTS idx_state_mkind_sari ON files(disabled, media_kind, songartistid);" #define I_STATEMKINDSALI \ "CREATE INDEX IF NOT EXISTS idx_state_mkind_sali ON files(disabled, media_kind, songalbumid);" #define I_ARTIST \ "CREATE INDEX IF NOT EXISTS idx_artist ON files(artist, artist_sort);" #define I_ALBUMARTIST \ "CREATE INDEX IF NOT EXISTS idx_albumartist ON files(album_artist, album_artist_sort);" /* Used by Q_BROWSE_COMPOSERS */ #define I_COMPOSER \ "CREATE INDEX IF NOT EXISTS idx_composer ON files(disabled, media_kind, composer_sort);" /* Used by Q_BROWSE_GENRES */ #define I_GENRE \ "CREATE INDEX IF NOT EXISTS idx_genre ON files(disabled, media_kind, genre);" /* Used by Q_PLITEMS for smart playlists */ #define I_TITLE \ "CREATE INDEX IF NOT EXISTS idx_title ON files(disabled, media_kind, title_sort);" #define I_ALBUM \ "CREATE INDEX IF NOT EXISTS idx_album ON files(album, album_sort);" #define I_FILELIST \ "CREATE INDEX IF NOT EXISTS idx_filelist ON files(disabled, virtual_path, time_modified);" #define I_FILE_DIR \ "CREATE INDEX IF NOT EXISTS idx_file_dir ON files(disabled, directory_id);" #define I_PL_PATH \ "CREATE INDEX IF NOT EXISTS idx_pl_path ON playlists(path);" #define I_PL_DISABLED \ "CREATE INDEX IF NOT EXISTS idx_pl_disabled ON playlists(disabled, type, virtual_path, db_timestamp);" #define I_PL_DIR \ "CREATE INDEX IF NOT EXISTS idx_pl_dir ON files(disabled, directory_id);" #define I_FILEPATH \ "CREATE INDEX IF NOT EXISTS idx_filepath ON playlistitems(filepath ASC);" #define I_PLITEMID \ "CREATE INDEX IF NOT EXISTS idx_playlistid ON playlistitems(playlistid, filepath);" #define I_GRP_PERSIST \ "CREATE INDEX IF NOT EXISTS idx_grp_persist ON groups(persistentid);" #define I_PAIRING \ "CREATE INDEX IF NOT EXISTS idx_pairingguid ON pairings(guid);" #define I_DIR_VPATH \ "CREATE INDEX IF NOT EXISTS idx_dir_vpath ON directories(disabled, virtual_path);" #define I_DIR_PARENT \ "CREATE INDEX IF NOT EXISTS idx_dir_parentid ON directories(parent_id);" #define I_QUEUE_POS \ "CREATE INDEX IF NOT EXISTS idx_queue_pos ON queue(pos);" #define I_QUEUE_SHUFFLEPOS \ "CREATE INDEX IF NOT EXISTS idx_queue_shufflepos ON queue(shuffle_pos);" static const struct db_init_query db_init_index_queries[] = { { I_RESCAN, "create rescan index" }, { I_SONGARTISTID, "create songartistid index" }, { I_SONGALBUMID, "create songalbumid index" }, { I_STATEMKINDSARI, "create state/mkind/sari index" }, { I_STATEMKINDSALI, "create state/mkind/sali index" }, { I_ARTIST, "create artist index" }, { I_ALBUMARTIST, "create album_artist index" }, { I_COMPOSER, "create composer index" }, { I_GENRE, "create genre index" }, { I_TITLE, "create title index" }, { I_ALBUM, "create album index" }, { I_FILELIST, "create filelist index" }, { I_FILE_DIR, "create file dir index" }, { I_PL_PATH, "create playlist path index" }, { I_PL_DISABLED, "create playlist state index" }, { I_PL_DIR, "create playlist dir index" }, { I_FILEPATH, "create file path index" }, { I_PLITEMID, "create playlist id index" }, { I_GRP_PERSIST, "create groups persistentid index" }, { I_PAIRING, "create pairing guid index" }, { I_DIR_VPATH, "create directories disabled_virtualpath index" }, { I_DIR_PARENT, "create directories parentid index" }, { I_QUEUE_POS, "create queue pos index" }, { I_QUEUE_SHUFFLEPOS, "create queue shuffle pos index" }, }; int db_init_indices(sqlite3 *hdl) { char *errmsg; int i; int ret; for (i = 0; i < (sizeof(db_init_index_queries) / sizeof(db_init_index_queries[0])); i++) { DPRINTF(E_DBG, L_DB, "DB init index query: %s\n", db_init_index_queries[i].desc); ret = sqlite3_exec(hdl, db_init_index_queries[i].query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "DB init error: %s\n", errmsg); sqlite3_free(errmsg); return -1; } } return 0; } int db_init_tables(sqlite3 *hdl) { char *query; char *errmsg; int i; int ret; for (i = 0; i < (sizeof(db_init_table_queries) / sizeof(db_init_table_queries[0])); i++) { DPRINTF(E_DBG, L_DB, "DB init table query: %s\n", db_init_table_queries[i].desc); ret = sqlite3_exec(hdl, db_init_table_queries[i].query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "DB init error: %s\n", errmsg); sqlite3_free(errmsg); return -1; } } query = sqlite3_mprintf(Q_SCVER_MAJOR, SCHEMA_VERSION_MAJOR); DPRINTF(E_DBG, L_DB, "DB init table query: %s\n", query); ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "DB init error: %s\n", errmsg); sqlite3_free(errmsg); return -1; } query = sqlite3_mprintf(Q_SCVER_MINOR, SCHEMA_VERSION_MINOR); DPRINTF(E_DBG, L_DB, "DB init table query: %s\n", query); ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg); sqlite3_free(query); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "DB init error: %s\n", errmsg); sqlite3_free(errmsg); return -1; } ret = db_init_indices(hdl); return ret; } forked-daapd-25.0/src/db_init.h000066400000000000000000000024501313447753700163500ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ #ifndef SRC_DB_INIT_H_ #define SRC_DB_INIT_H_ #include /* Rule of thumb: Will the current version of forked-daapd work with the new * version of the database? If yes, then it is a minor upgrade, if no, then it * is a major upgrade. In other words minor version upgrades permit downgrading * forked-daapd after the database was upgraded. */ #define SCHEMA_VERSION_MAJOR 19 #define SCHEMA_VERSION_MINOR 04 int db_init_indices(sqlite3 *hdl); int db_init_tables(sqlite3 *hdl); #endif /* SRC_DB_INIT_H_ */ forked-daapd-25.0/src/db_upgrade.c000066400000000000000000001611621313447753700170350ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ #include #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "misc.h" struct db_upgrade_query { char *query; char *desc; }; static int db_drop_indices(sqlite3 *hdl) { #define Q_INDEX "SELECT name FROM sqlite_master WHERE type == 'index' AND name LIKE 'idx_%';" #define Q_TMPL "DROP INDEX %q;" sqlite3_stmt *stmt; char *errmsg; char *query; char *index[256]; int ret; int i; int n; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", Q_INDEX); ret = sqlite3_prepare_v2(hdl, Q_INDEX, strlen(Q_INDEX) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } n = 0; while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) { index[n] = strdup((char *)sqlite3_column_text(stmt, 0)); n++; } if (ret != SQLITE_DONE) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); return -1; } sqlite3_finalize(stmt); for (i = 0; i < n; i++) { query = sqlite3_mprintf(Q_TMPL, index[i]); free(index[i]); DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "DB error while running '%s': %s\n", query, errmsg); sqlite3_free(errmsg); return -1; } sqlite3_free(query); } return 0; #undef Q_TMPL #undef Q_INDEX } static int db_generic_upgrade(sqlite3 *hdl, const struct db_upgrade_query *queries, int nqueries) { char *errmsg; int i; int ret; for (i = 0; i < nqueries; i++, queries++) { DPRINTF(E_DBG, L_DB, "DB upgrade query: %s\n", queries->desc); ret = sqlite3_exec(hdl, queries->query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_FATAL, L_DB, "DB upgrade error: %s\n", errmsg); sqlite3_free(errmsg); return -1; } } return 0; } /* Upgrade the files table to the new schema by dumping and reloading the * table. A bit tedious. */ static int db_upgrade_files_table(sqlite3 *hdl, const char *dumpquery, const char *newtablequery) { struct stat sb; FILE *fp; sqlite3_stmt *stmt; const unsigned char *dumprow; char *dump; char *errmsg; int fd; int ret; DPRINTF(E_LOG, L_DB, "Upgrading files table...\n"); fp = tmpfile(); if (!fp) { DPRINTF(E_LOG, L_DB, "Could not create temporary file for files table dump: %s\n", strerror(errno)); return -1; } DPRINTF(E_LOG, L_DB, "Dumping old files table...\n"); /* dump */ ret = sqlite3_prepare_v2(hdl, dumpquery, strlen(dumpquery) + 1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); ret = -1; goto out_fclose; } while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) { dumprow = sqlite3_column_text(stmt, 0); ret = fprintf(fp, "%s\n", dumprow); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Could not write dump: %s\n", strerror(errno)); sqlite3_finalize(stmt); ret = -1; goto out_fclose; } } if (ret != SQLITE_DONE) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); ret = -1; goto out_fclose; } sqlite3_finalize(stmt); /* Seek back to start of dump file */ ret = fseek(fp, 0, SEEK_SET); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Could not seek back to start of dump: %s\n", strerror(errno)); ret = -1; goto out_fclose; } /* Map dump file */ fd = fileno(fp); if (fd < 0) { DPRINTF(E_LOG, L_DB, "Could not obtain file descriptor: %s\n", strerror(errno)); ret = -1; goto out_fclose; } ret = fstat(fd, &sb); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Could not stat dump file: %s\n", strerror(errno)); ret = -1; goto out_fclose; } if (sb.st_size == 0) dump = NULL; else { dump = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0); if (dump == MAP_FAILED) { DPRINTF(E_LOG, L_DB, "Could not map dump file: %s\n", strerror(errno)); ret = -1; goto out_fclose; } } /* Drop remnants from last upgrade if still present */ DPRINTF(E_LOG, L_DB, "Clearing old backups...\n"); ret = sqlite3_exec(hdl, "DROP TABLE IF EXISTS files_backup;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error clearing old backup - will continue anyway: %s\n", errmsg); sqlite3_free(errmsg); } /* Move old table out of the way */ DPRINTF(E_LOG, L_DB, "Moving old files table out of the way...\n"); ret = sqlite3_exec(hdl, "ALTER TABLE files RENAME TO files_backup;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error making backup of old files table: %s\n", errmsg); sqlite3_free(errmsg); ret = -1; goto out_munmap; } /* Create new table */ DPRINTF(E_LOG, L_DB, "Creating new files table...\n"); ret = sqlite3_exec(hdl, newtablequery, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error creating new files table: %s\n", errmsg); sqlite3_free(errmsg); ret = -1; goto out_munmap; } /* Reload dump */ DPRINTF(E_LOG, L_DB, "Reloading new files table...\n"); if (dump) { ret = sqlite3_exec(hdl, dump, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error reloading files table data: %s\n", errmsg); sqlite3_free(errmsg); ret = -1; goto out_munmap; } } /* Delete old files table */ DPRINTF(E_LOG, L_DB, "Deleting backup files table...\n"); ret = sqlite3_exec(hdl, "DROP TABLE files_backup;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error dropping backup files table: %s\n", errmsg); sqlite3_free(errmsg); /* Not an issue, but takes up space in the database */ } DPRINTF(E_LOG, L_DB, "Upgrade of files table complete!\n"); out_munmap: if (dump) { if (munmap(dump, sb.st_size) < 0) DPRINTF(E_LOG, L_DB, "Could not unmap dump file: %s\n", strerror(errno)); } out_fclose: fclose(fp); return ret; } /* Upgrade from schema v10 to v11 */ #define U_V11_SPEAKERS \ "CREATE TABLE speakers(" \ " id INTEGER PRIMARY KEY NOT NULL," \ " selected INTEGER NOT NULL," \ " volume INTEGER NOT NULL" \ ");" #define U_V11_SCVER \ "UPDATE admin SET value = '11' WHERE key = 'schema_version';" static const struct db_upgrade_query db_upgrade_v11_queries[] = { { U_V11_SPEAKERS, "create new table speakers" }, { U_V11_SCVER, "set schema_version to 11" }, }; static int db_upgrade_v11(sqlite3 *hdl) { #define Q_NEWSPK "INSERT INTO speakers (id, selected, volume) VALUES (%" PRIi64 ", 1, 75);" #define Q_SPKVOL "UPDATE speakers SET volume = %d;" sqlite3_stmt *stmt; char *query; char *errmsg; const char *strid; uint64_t *spkids; int volume; int count; int i; int qret; int ret; /* Get saved speakers */ query = "SELECT COUNT(*) FROM admin WHERE key = 'player:active-spk';"; ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); goto clear_vars; } qret = sqlite3_step(stmt); if (qret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); goto clear_vars; } count = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); if (count == 0) goto clear_vars; else if (count < 0) return -1; spkids = calloc(count, sizeof(uint64_t)); if (!spkids) { DPRINTF(E_LOG, L_DB, "Out of memory for speaker IDs\n"); return -1; } query = "SELECT value FROM admin WHERE key = 'player:active-spk';"; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); goto out_free_ids; } i = 0; ret = 0; while ((qret = sqlite3_step(stmt)) == SQLITE_ROW) { strid = (const char *)sqlite3_column_text(stmt, 0); ret = safe_hextou64(strid, spkids + i); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Could not convert speaker ID: %s\n", strid); break; } i++; } sqlite3_finalize(stmt); if ((ret == 0) && (qret != SQLITE_DONE)) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); goto out_free_ids; } else if (ret < 0) goto out_free_ids; /* Get saved volume */ query = "SELECT value FROM admin WHERE key = 'player:volume';"; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); goto out_free_ids; } ret = sqlite3_step(stmt); if (ret != SQLITE_ROW) { DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); sqlite3_finalize(stmt); goto out_free_ids; } volume = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); /* Add speakers to the table */ for (i = 0; i < count; i++) { query = sqlite3_mprintf(Q_NEWSPK, spkids[i]); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); goto out_free_ids; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) DPRINTF(E_LOG, L_DB, "Error adding speaker: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); } free(spkids); /* Update with volume */ query = sqlite3_mprintf(Q_SPKVOL, volume); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) DPRINTF(E_LOG, L_DB, "Error adding speaker: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); /* Clear old config keys */ clear_vars: query = "DELETE FROM admin WHERE key = 'player:volume' OR key = 'player:active-spk';"; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) DPRINTF(E_LOG, L_DB, "Error adding speaker: %s\n", errmsg); sqlite3_free(errmsg); return 0; out_free_ids: free(spkids); return -1; #undef Q_NEWSPK #undef Q_SPKVOL } /* Upgrade from schema v11 to v12 */ #define U_V12_NEW_FILES_TABLE \ "CREATE TABLE IF NOT EXISTS files (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " path VARCHAR(4096) NOT NULL," \ " fname VARCHAR(255) NOT NULL," \ " title VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album VARCHAR(1024) NOT NULL COLLATE DAAP," \ " genre VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " comment VARCHAR(4096) DEFAULT NULL COLLATE DAAP," \ " type VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " composer VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " orchestra VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " conductor VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " grouping VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " url VARCHAR(1024) DEFAULT NULL," \ " bitrate INTEGER DEFAULT 0," \ " samplerate INTEGER DEFAULT 0," \ " song_length INTEGER DEFAULT 0," \ " file_size INTEGER DEFAULT 0," \ " year INTEGER DEFAULT 0," \ " track INTEGER DEFAULT 0," \ " total_tracks INTEGER DEFAULT 0," \ " disc INTEGER DEFAULT 0," \ " total_discs INTEGER DEFAULT 0," \ " bpm INTEGER DEFAULT 0," \ " compilation INTEGER DEFAULT 0," \ " rating INTEGER DEFAULT 0," \ " play_count INTEGER DEFAULT 0," \ " data_kind INTEGER DEFAULT 0," \ " item_kind INTEGER DEFAULT 0," \ " description INTEGER DEFAULT 0," \ " time_added INTEGER DEFAULT 0," \ " time_modified INTEGER DEFAULT 0," \ " time_played INTEGER DEFAULT 0," \ " db_timestamp INTEGER DEFAULT 0," \ " disabled INTEGER DEFAULT 0," \ " sample_count INTEGER DEFAULT 0," \ " codectype VARCHAR(5) DEFAULT NULL," \ " idx INTEGER NOT NULL," \ " has_video INTEGER DEFAULT 0," \ " contentrating INTEGER DEFAULT 0," \ " bits_per_sample INTEGER DEFAULT 0," \ " album_artist VARCHAR(1024) NOT NULL COLLATE DAAP," \ " media_kind INTEGER NOT NULL," \ " tv_series_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_num_str VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_network_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_sort INTEGER NOT NULL," \ " tv_season_num INTEGER NOT NULL," \ " songalbumid INTEGER NOT NULL," \ " title_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP" \ ");" #define U_V12_TRG1 \ "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " END;" #define U_V12_TRG2 \ "CREATE TRIGGER update_groups_update_file AFTER UPDATE OF songalbumid ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " END;" #define U_V12_SCVER \ "UPDATE admin SET value = '12' WHERE key = 'schema_version';" static const struct db_upgrade_query db_upgrade_v12_queries[] = { { U_V12_TRG1, "create trigger update_groups_new_file" }, { U_V12_TRG2, "create trigger update_groups_update_file" }, { U_V12_SCVER, "set schema_version to 12" }, }; static int db_upgrade_v12(sqlite3 *hdl) { #define Q_DUMP "SELECT 'INSERT INTO files " \ "(id, path, fname, title, artist, album, genre, comment, type, composer," \ " orchestra, conductor, grouping, url, bitrate, samplerate, song_length, file_size, year, track," \ " total_tracks, disc, total_discs, bpm, compilation, rating, play_count, data_kind, item_kind," \ " description, time_added, time_modified, time_played, db_timestamp, disabled, sample_count," \ " codectype, idx, has_video, contentrating, bits_per_sample, album_artist," \ " media_kind, tv_series_name, tv_episode_num_str, tv_network_name, tv_episode_sort, tv_season_num, " \ " songalbumid, title_sort, artist_sort, album_sort, composer_sort, album_artist_sort)" \ " VALUES (' || id || ', ' || QUOTE(path) || ', ' || QUOTE(fname) || ', ' || QUOTE(title) || ', '" \ " || QUOTE(artist) || ', ' || QUOTE(album) || ', ' || QUOTE(genre) || ', ' || QUOTE(comment) || ', '" \ " || QUOTE(type) || ', ' || QUOTE(composer) || ', ' || QUOTE(orchestra) || ', ' || QUOTE(conductor) || ', '" \ " || QUOTE(grouping) || ', ' || QUOTE(url) || ', ' || bitrate || ', ' || samplerate || ', '" \ " || song_length || ', ' || file_size || ', ' || year || ', ' || track || ', ' || total_tracks || ', '" \ " || disc || ', ' || total_discs || ', ' || bpm || ', ' || compilation || ', ' || rating || ', '" \ " || play_count || ', ' || data_kind || ', ' || item_kind || ', ' || QUOTE(description) || ', '" \ " || time_added || ', ' || time_modified || ', ' || time_played || ', 1, '" \ " || disabled || ', ' || sample_count || ', ' || QUOTE(codectype) || ', ' || idx || ', '" \ " || has_video || ', ' || contentrating || ', ' || bits_per_sample || ', ' || QUOTE(album_artist) || ', '" \ " || media_kind || ', ' || QUOTE(tv_series_name) || ', ' || QUOTE(tv_episode_num_str) || ', '" \ " || QUOTE(tv_network_name) || ', ' || tv_episode_sort || ', ' || tv_season_num || ', '" \ " || songalbumid || ', ' || QUOTE(title) || ', ' || QUOTE(artist) || ', ' || QUOTE(album) || ', '" \ " || QUOTE(composer) || ', ' || QUOTE(album_artist) || ');' FROM files;" return db_upgrade_files_table(hdl, Q_DUMP, U_V12_NEW_FILES_TABLE); #undef Q_DUMP } /* Upgrade from schema v12 to v13 */ #define U_V13_PL2 \ "UPDATE playlists SET query = 'f.media_kind = 1' where id = 2;" #define U_V13_PL3 \ "UPDATE playlists SET query = 'f.media_kind = 2' where id = 3;" #define U_V13_PL4 \ "UPDATE playlists SET query = 'f.media_kind = 64' where id = 4;" #define U_V13_SCVER \ "UPDATE admin SET value = '13' WHERE key = 'schema_version';" static const struct db_upgrade_query db_upgrade_v13_queries[] = { { U_V13_PL2, "update default smart playlist 'Music'" }, { U_V13_PL3, "update default smart playlist 'Movies'" }, { U_V13_PL4, "update default smart playlist 'TV Shows'" }, { U_V13_SCVER, "set schema_version to 13" }, }; /* Upgrade from schema v13 to v14 */ /* Adds seek, songartistid, and two new smart playlists */ #define U_V14_NEW_FILES_TABLE \ "CREATE TABLE IF NOT EXISTS files (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " path VARCHAR(4096) NOT NULL," \ " fname VARCHAR(255) NOT NULL," \ " title VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album VARCHAR(1024) NOT NULL COLLATE DAAP," \ " genre VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " comment VARCHAR(4096) DEFAULT NULL COLLATE DAAP," \ " type VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " composer VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " orchestra VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " conductor VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " grouping VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " url VARCHAR(1024) DEFAULT NULL," \ " bitrate INTEGER DEFAULT 0," \ " samplerate INTEGER DEFAULT 0," \ " song_length INTEGER DEFAULT 0," \ " file_size INTEGER DEFAULT 0," \ " year INTEGER DEFAULT 0," \ " track INTEGER DEFAULT 0," \ " total_tracks INTEGER DEFAULT 0," \ " disc INTEGER DEFAULT 0," \ " total_discs INTEGER DEFAULT 0," \ " bpm INTEGER DEFAULT 0," \ " compilation INTEGER DEFAULT 0," \ " rating INTEGER DEFAULT 0," \ " play_count INTEGER DEFAULT 0," \ " seek INTEGER DEFAULT 0," \ " data_kind INTEGER DEFAULT 0," \ " item_kind INTEGER DEFAULT 0," \ " description INTEGER DEFAULT 0," \ " time_added INTEGER DEFAULT 0," \ " time_modified INTEGER DEFAULT 0," \ " time_played INTEGER DEFAULT 0," \ " db_timestamp INTEGER DEFAULT 0," \ " disabled INTEGER DEFAULT 0," \ " sample_count INTEGER DEFAULT 0," \ " codectype VARCHAR(5) DEFAULT NULL," \ " idx INTEGER NOT NULL," \ " has_video INTEGER DEFAULT 0," \ " contentrating INTEGER DEFAULT 0," \ " bits_per_sample INTEGER DEFAULT 0," \ " album_artist VARCHAR(1024) NOT NULL COLLATE DAAP," \ " media_kind INTEGER NOT NULL," \ " tv_series_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_num_str VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_network_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_sort INTEGER NOT NULL," \ " tv_season_num INTEGER NOT NULL," \ " songartistid INTEGER NOT NULL," \ " songalbumid INTEGER NOT NULL," \ " title_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP" \ ");" #define U_V14_DELETE_PL5_1 \ "DELETE FROM playlists WHERE id=5;" #define U_V14_DELETE_PL5_2 \ "DELETE FROM playlistitems WHERE playlistid=5;" #define U_V14_DELETE_PL6_1 \ "DELETE FROM playlists WHERE id=6;" #define U_V14_DELETE_PL6_2 \ "DELETE FROM playlistitems WHERE playlistid=6;" #define U_V14_TRG1 \ "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (2, NEW.album_artist, NEW.songartistid);" \ " END;" #define U_V14_TRG2 \ "CREATE TRIGGER update_groups_update_file AFTER UPDATE OF songalbumid ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (2, NEW.album_artist, NEW.songartistid);" \ " END;" #define U_V14_PL5 \ "INSERT OR IGNORE INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(5, 'Podcasts', 1, 'f.media_kind = 4', 0, '', 0, 1);" #define U_V14_PL6 \ "INSERT OR IGNORE INTO playlists (id, title, type, query, db_timestamp, path, idx, special_id)" \ " VALUES(6, 'Audiobooks', 1, 'f.media_kind = 8', 0, '', 0, 7);" #define U_V14_SCVER \ "UPDATE admin SET value = '14' WHERE key = 'schema_version';" static const struct db_upgrade_query db_upgrade_v14_queries[] = { { U_V14_DELETE_PL5_1, "delete playlist id 5 table playlists" }, { U_V14_DELETE_PL5_2, "delete playlist id 5 table playlistitems" }, { U_V14_DELETE_PL6_1, "delete playlist id 6 table playlists" }, { U_V14_DELETE_PL6_2, "delete playlist id 6 table playlistitems" }, { U_V14_TRG1, "create trigger update_groups_new_file" }, { U_V14_TRG2, "create trigger update_groups_update_file" }, { U_V14_PL5, "create default smart playlist 'Podcasts' table playlists" }, { U_V14_PL6, "create default smart playlist 'Audiobooks' table playlists" }, { U_V14_SCVER, "set schema_version to 14" }, }; static int db_upgrade_v14(sqlite3 *hdl) { #define Q_DUMP "SELECT 'INSERT INTO files " \ "(id, path, fname, title, artist, album, genre, comment, type, composer," \ " orchestra, conductor, grouping, url, bitrate, samplerate, song_length, file_size, year, track," \ " total_tracks, disc, total_discs, bpm, compilation, rating, play_count, seek, data_kind, item_kind," \ " description, time_added, time_modified, time_played, db_timestamp, disabled, sample_count," \ " codectype, idx, has_video, contentrating, bits_per_sample, album_artist," \ " media_kind, tv_series_name, tv_episode_num_str, tv_network_name, tv_episode_sort, tv_season_num, " \ " songartistid, songalbumid, " \ " title_sort, artist_sort, album_sort, composer_sort, album_artist_sort)" \ " VALUES (' || id || ', ' || QUOTE(path) || ', ' || QUOTE(fname) || ', ' || QUOTE(title) || ', '" \ " || QUOTE(artist) || ', ' || QUOTE(album) || ', ' || QUOTE(genre) || ', ' || QUOTE(comment) || ', '" \ " || QUOTE(type) || ', ' || QUOTE(composer) || ', ' || QUOTE(orchestra) || ', ' || QUOTE(conductor) || ', '" \ " || QUOTE(grouping) || ', ' || QUOTE(url) || ', ' || bitrate || ', ' || samplerate || ', '" \ " || song_length || ', ' || file_size || ', ' || year || ', ' || track || ', ' || total_tracks || ', '" \ " || disc || ', ' || total_discs || ', ' || bpm || ', ' || compilation || ', ' || rating || ', '" \ " || play_count || ', 0, ' || data_kind || ', ' || item_kind || ', ' || QUOTE(description) || ', '" \ " || time_added || ', ' || time_modified || ', ' || time_played || ', ' || db_timestamp || ', '" \ " || disabled || ', ' || sample_count || ', ' || QUOTE(codectype) || ', ' || idx || ', '" \ " || has_video || ', ' || contentrating || ', ' || bits_per_sample || ', ' || QUOTE(album_artist) || ', '" \ " || media_kind || ', ' || QUOTE(tv_series_name) || ', ' || QUOTE(tv_episode_num_str) || ', '" \ " || QUOTE(tv_network_name) || ', ' || tv_episode_sort || ', ' || tv_season_num || ', " \ " daap_songalbumid(' || QUOTE(album_artist) || ', ''''), ' || songalbumid || ', '" \ " || QUOTE(title_sort) || ', ' || QUOTE(artist_sort) || ', ' || QUOTE(album_sort) || ', '" \ " || QUOTE(composer_sort) || ', ' || QUOTE(album_artist_sort) || ');' FROM files;" return db_upgrade_files_table(hdl, Q_DUMP, U_V14_NEW_FILES_TABLE); #undef Q_DUMP } /* Upgrade from schema v14 to v15 */ /* Adds artwork field - nothing else */ #define U_V15_NEW_FILES_TABLE \ "CREATE TABLE IF NOT EXISTS files (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " path VARCHAR(4096) NOT NULL," \ " fname VARCHAR(255) NOT NULL," \ " title VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album VARCHAR(1024) NOT NULL COLLATE DAAP," \ " genre VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " comment VARCHAR(4096) DEFAULT NULL COLLATE DAAP," \ " type VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " composer VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " orchestra VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " conductor VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " grouping VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " url VARCHAR(1024) DEFAULT NULL," \ " bitrate INTEGER DEFAULT 0," \ " samplerate INTEGER DEFAULT 0," \ " song_length INTEGER DEFAULT 0," \ " file_size INTEGER DEFAULT 0," \ " year INTEGER DEFAULT 0," \ " track INTEGER DEFAULT 0," \ " total_tracks INTEGER DEFAULT 0," \ " disc INTEGER DEFAULT 0," \ " total_discs INTEGER DEFAULT 0," \ " bpm INTEGER DEFAULT 0," \ " compilation INTEGER DEFAULT 0," \ " artwork INTEGER DEFAULT 0," \ " rating INTEGER DEFAULT 0," \ " play_count INTEGER DEFAULT 0," \ " seek INTEGER DEFAULT 0," \ " data_kind INTEGER DEFAULT 0," \ " item_kind INTEGER DEFAULT 0," \ " description INTEGER DEFAULT 0," \ " time_added INTEGER DEFAULT 0," \ " time_modified INTEGER DEFAULT 0," \ " time_played INTEGER DEFAULT 0," \ " db_timestamp INTEGER DEFAULT 0," \ " disabled INTEGER DEFAULT 0," \ " sample_count INTEGER DEFAULT 0," \ " codectype VARCHAR(5) DEFAULT NULL," \ " idx INTEGER NOT NULL," \ " has_video INTEGER DEFAULT 0," \ " contentrating INTEGER DEFAULT 0," \ " bits_per_sample INTEGER DEFAULT 0," \ " album_artist VARCHAR(1024) NOT NULL COLLATE DAAP," \ " media_kind INTEGER NOT NULL," \ " tv_series_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_num_str VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_network_name VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " tv_episode_sort INTEGER NOT NULL," \ " tv_season_num INTEGER NOT NULL," \ " songartistid INTEGER NOT NULL," \ " songalbumid INTEGER NOT NULL," \ " title_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP" \ ");" #define U_V15_TRG1 \ "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (2, NEW.album_artist, NEW.songartistid);" \ " END;" #define U_V15_TRG2 \ "CREATE TRIGGER update_groups_update_file AFTER UPDATE OF songalbumid ON files FOR EACH ROW" \ " BEGIN" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (1, NEW.album, NEW.songalbumid);" \ " INSERT OR IGNORE INTO groups (type, name, persistentid) VALUES (2, NEW.album_artist, NEW.songartistid);" \ " END;" #define U_V15_SCVER \ "UPDATE admin SET value = '15' WHERE key = 'schema_version';" static const struct db_upgrade_query db_upgrade_v15_queries[] = { { U_V15_TRG1, "create trigger update_groups_new_file" }, { U_V15_TRG2, "create trigger update_groups_update_file" }, { U_V15_SCVER, "set schema_version to 15" }, }; static int db_upgrade_v15(sqlite3 *hdl) { #define Q_DUMP "SELECT 'INSERT INTO files " \ "(id, path, fname, title, artist, album, genre, comment, type, composer," \ " orchestra, conductor, grouping, url, bitrate, samplerate, song_length, file_size, year, track," \ " total_tracks, disc, total_discs, bpm, compilation, artwork, rating, play_count, seek, data_kind, item_kind," \ " description, time_added, time_modified, time_played, db_timestamp, disabled, sample_count," \ " codectype, idx, has_video, contentrating, bits_per_sample, album_artist," \ " media_kind, tv_series_name, tv_episode_num_str, tv_network_name, tv_episode_sort, tv_season_num, " \ " songartistid, songalbumid, " \ " title_sort, artist_sort, album_sort, composer_sort, album_artist_sort)" \ " VALUES (' || id || ', ' || QUOTE(path) || ', ' || QUOTE(fname) || ', ' || QUOTE(title) || ', '" \ " || QUOTE(artist) || ', ' || QUOTE(album) || ', ' || QUOTE(genre) || ', ' || QUOTE(comment) || ', '" \ " || QUOTE(type) || ', ' || QUOTE(composer) || ', ' || QUOTE(orchestra) || ', ' || QUOTE(conductor) || ', '" \ " || QUOTE(grouping) || ', ' || QUOTE(url) || ', ' || bitrate || ', ' || samplerate || ', '" \ " || song_length || ', ' || file_size || ', ' || year || ', ' || track || ', ' || total_tracks || ', '" \ " || disc || ', ' || total_discs || ', ' || bpm || ', ' || compilation || ', 0, ' || rating || ', '" \ " || play_count || ', ' || seek || ', ' || data_kind || ', ' || item_kind || ', ' || QUOTE(description) || ', '" \ " || time_added || ', ' || time_modified || ', ' || time_played || ', ' || db_timestamp || ', '" \ " || disabled || ', ' || sample_count || ', ' || QUOTE(codectype) || ', ' || idx || ', '" \ " || has_video || ', ' || contentrating || ', ' || bits_per_sample || ', ' || QUOTE(album_artist) || ', '" \ " || media_kind || ', ' || QUOTE(tv_series_name) || ', ' || QUOTE(tv_episode_num_str) || ', '" \ " || QUOTE(tv_network_name) || ', ' || tv_episode_sort || ', ' || tv_season_num || ', '" \ " || songartistid ||', ' || songalbumid || ', '" \ " || QUOTE(title_sort) || ', ' || QUOTE(artist_sort) || ', ' || QUOTE(album_sort) || ', '" \ " || QUOTE(composer_sort) || ', ' || QUOTE(album_artist_sort) || ');' FROM files;" return db_upgrade_files_table(hdl, Q_DUMP, U_V15_NEW_FILES_TABLE); #undef Q_DUMP } /* Upgrade from schema v15 to v15.01 */ /* Improved indices (will be generated by generic schema update) */ #define U_V1501_SCVER_MAJOR \ "INSERT INTO admin (key, value) VALUES ('schema_version_major', '15');" #define U_V1501_SCVER_MINOR \ "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '01');" static const struct db_upgrade_query db_upgrade_v1501_queries[] = { { U_V1501_SCVER_MAJOR, "set schema_version_major to 15" }, { U_V1501_SCVER_MINOR, "set schema_version_minor to 01" }, }; /* Upgrade from schema v15.01 to v16 */ #define U_V16_CREATE_VIEW_FILELIST \ "CREATE VIEW IF NOT EXISTS filelist as" \ " SELECT " \ " virtual_path, time_modified, 3 as type " \ " FROM files WHERE disabled = 0" \ " UNION " \ " SELECT " \ " virtual_path, db_timestamp, 1 as type " \ " FROM playlists WHERE disabled = 0 AND type = 0" \ ";" #define U_V16_ALTER_TBL_FILES_ADD_COL \ "ALTER TABLE files ADD COLUMN virtual_path VARCHAR(4096) DEFAULT NULL;" #define U_V16_ALTER_TBL_PL_ADD_COL \ "ALTER TABLE playlists ADD COLUMN virtual_path VARCHAR(4096) DEFAULT NULL;" #define D_V1600_SCVER \ "DELETE FROM admin WHERE key = 'schema_version';" #define U_V1600_SCVER_MAJOR \ "UPDATE admin SET value = '16' WHERE key = 'schema_version_major';" #define U_V1600_SCVER_MINOR \ "UPDATE admin SET value = '00' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v16_queries[] = { { U_V16_ALTER_TBL_FILES_ADD_COL, "alter table files add column virtual_path" }, { U_V16_ALTER_TBL_PL_ADD_COL, "alter table playlists add column virtual_path" }, { U_V16_CREATE_VIEW_FILELIST, "create new view filelist" }, { D_V1600_SCVER, "delete schema_version" }, { U_V1600_SCVER_MAJOR, "set schema_version_major to 16" }, { U_V1600_SCVER_MINOR, "set schema_version_minor to 00" }, }; static int db_upgrade_v16(sqlite3 *hdl) { sqlite3_stmt *stmt; char *query; char *uquery; char *errmsg; char *artist; char *album; char *title; int id; char *path; int type; char virtual_path[PATH_MAX]; int ret; query = "SELECT id, album_artist, album, title, path FROM files;"; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) { id = sqlite3_column_int(stmt, 0); artist = (char *)sqlite3_column_text(stmt, 1); album = (char *)sqlite3_column_text(stmt, 2); title = (char *)sqlite3_column_text(stmt, 3); path = (char *)sqlite3_column_text(stmt, 4); if (strncmp(path, "http:", strlen("http:")) == 0) { snprintf(virtual_path, PATH_MAX, "/http:/%s", title); } else if (strncmp(path, "spotify:", strlen("spotify:")) == 0) { snprintf(virtual_path, PATH_MAX, "/spotify:/%s/%s/%s", artist, album, title); } else { snprintf(virtual_path, PATH_MAX, "/file:%s", path); } uquery = sqlite3_mprintf("UPDATE files SET virtual_path = '%q' WHERE id = %d;", virtual_path, id); ret = sqlite3_exec(hdl, uquery, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error updating files: %s\n", errmsg); } sqlite3_free(uquery); sqlite3_free(errmsg); } sqlite3_finalize(stmt); query = "SELECT id, title, path, type FROM playlists;"; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) { id = sqlite3_column_int(stmt, 0); title = (char *)sqlite3_column_text(stmt, 1); path = (char *)sqlite3_column_text(stmt, 2); type = sqlite3_column_int(stmt, 3); if (type == 0) /* Excludes default/Smart playlists and playlist folders */ { if (strncmp(path, "spotify:", strlen("spotify:")) == 0) snprintf(virtual_path, PATH_MAX, "/spotify:/%s", title); else snprintf(virtual_path, PATH_MAX, "/file:%s", path); uquery = sqlite3_mprintf("UPDATE playlists SET virtual_path = '%q' WHERE id = %d;", virtual_path, id); ret = sqlite3_exec(hdl, uquery, NULL, NULL, &errmsg); if (ret != SQLITE_OK) DPRINTF(E_LOG, L_DB, "Error updating playlists: %s\n", errmsg); sqlite3_free(uquery); sqlite3_free(errmsg); } } sqlite3_finalize(stmt); return 0; } /* Upgrade from schema v16.00 to v17.00 */ /* Expand data model to allow for nested playlists and change default playlist * enumeration */ #define U_V17_PL_PARENTID_ADD \ "ALTER TABLE playlists ADD COLUMN parent_id INTEGER DEFAULT 0;" #define U_V17_PL_TYPE_CHANGE \ "UPDATE playlists SET type = 2 WHERE type = 1;" #define U_V17_SCVER_MAJOR \ "UPDATE admin SET value = '17' WHERE key = 'schema_version_major';" #define U_V17_SCVER_MINOR \ "UPDATE admin SET value = '00' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v17_queries[] = { { U_V17_PL_PARENTID_ADD,"expanding table playlists with parent_id column" }, { U_V17_PL_TYPE_CHANGE, "changing numbering of default playlists 1 -> 2" }, { U_V17_SCVER_MAJOR, "set schema_version_major to 17" }, { U_V17_SCVER_MINOR, "set schema_version_minor to 00" }, }; /* Upgrade from schema v17.00 to v18.00 */ /* Change playlist type enumeration and recreate filelist view (include smart * playlists in view) */ #define U_V18_PL_TYPE_CHANGE_PLAIN \ "UPDATE playlists SET type = 3 WHERE type = 0;" #define U_V18_PL_TYPE_CHANGE_SPECIAL \ "UPDATE playlists SET type = 0 WHERE type = 2;" #define U_V18_DROP_VIEW_FILELIST \ "DROP VIEW IF EXISTS filelist;" #define U_V18_CREATE_VIEW_FILELIST \ "CREATE VIEW IF NOT EXISTS filelist as" \ " SELECT " \ " virtual_path, time_modified, 3 as type " \ " FROM files WHERE disabled = 0" \ " UNION " \ " SELECT " \ " virtual_path, db_timestamp, 1 as type " \ " FROM playlists where disabled = 0 AND type IN (2, 3)" \ ";" #define U_V18_SCVER_MAJOR \ "UPDATE admin SET value = '18' WHERE key = 'schema_version_major';" #define U_V18_SCVER_MINOR \ "UPDATE admin SET value = '00' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v18_queries[] = { { U_V18_PL_TYPE_CHANGE_PLAIN, "changing numbering of plain playlists 0 -> 3" }, { U_V18_PL_TYPE_CHANGE_SPECIAL, "changing numbering of default playlists 2 -> 0" }, { U_V18_DROP_VIEW_FILELIST, "dropping view filelist" }, { U_V18_CREATE_VIEW_FILELIST, "creating view filelist" }, { U_V18_SCVER_MAJOR, "set schema_version_major to 18" }, { U_V18_SCVER_MINOR, "set schema_version_minor to 00" }, }; /* Upgrade from schema v18.00 to v18.01 */ /* Change virtual_path for playlists: remove file extension */ #define U_V1801_UPDATE_PLAYLISTS_M3U \ "UPDATE playlists SET virtual_path = replace(virtual_path, '.m3u', '');" #define U_V1801_UPDATE_PLAYLISTS_PLS \ "UPDATE playlists SET virtual_path = replace(virtual_path, '.pls', '');" #define U_V1801_UPDATE_PLAYLISTS_SMARTPL \ "UPDATE playlists SET virtual_path = replace(virtual_path, '.smartpl', '');" #define U_V1801_SCVER_MAJOR \ "UPDATE admin SET value = '18' WHERE key = 'schema_version_major';" #define U_V1801_SCVER_MINOR \ "UPDATE admin SET value = '01' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v1801_queries[] = { { U_V1801_UPDATE_PLAYLISTS_M3U, "update table playlists" }, { U_V1801_UPDATE_PLAYLISTS_PLS, "update table playlists" }, { U_V1801_UPDATE_PLAYLISTS_SMARTPL, "update table playlists" }, { U_V1801_SCVER_MAJOR, "set schema_version_major to 18" }, { U_V1801_SCVER_MINOR, "set schema_version_minor to 01" }, }; /* Upgrade from schema v18.01 to v19.00 */ /* Replace 'filelist' view with new table 'directories' */ #define U_V1900_CREATE_TABLE_DIRECTORIES \ "CREATE TABLE IF NOT EXISTS directories (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " virtual_path VARCHAR(4096) NOT NULL," \ " db_timestamp INTEGER DEFAULT 0," \ " disabled INTEGER DEFAULT 0," \ " parent_id INTEGER DEFAULT 0" \ ");" #define U_V1900_DROP_VIEW_FILELIST \ "DROP VIEW IF EXISTS filelist;" #define U_V1900_ALTER_PL_ADD_DIRECTORYID \ "ALTER TABLE playlists ADD COLUMN directory_id INTEGER DEFAULT 0;" #define U_V1900_ALTER_FILES_ADD_DIRECTORYID \ "ALTER TABLE files ADD COLUMN directory_id INTEGER DEFAULT 0;" #define U_V1900_ALTER_FILES_ADD_DATERELEASED \ "ALTER TABLE files ADD COLUMN date_released INTEGER DEFAULT 0;" #define U_V1900_ALTER_SPEAKERS_ADD_NAME \ "ALTER TABLE speakers ADD COLUMN name VARCHAR(255) DEFAULT NULL;" #define U_V1900_INSERT_DIR1 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (1, '/', 0, 0, 0);" #define U_V1900_INSERT_DIR2 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (2, '/file:', 0, 0, 1);" #define U_V1900_INSERT_DIR3 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (3, '/http:', 0, 0, 1);" #define U_V1900_INSERT_DIR4 \ "INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \ " VALUES (4, '/spotify:', 0, 4294967296, 1);" #define U_V1900_SCVER_MAJOR \ "UPDATE admin SET value = '19' WHERE key = 'schema_version_major';" #define U_V1900_SCVER_MINOR \ "UPDATE admin SET value = '00' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v1900_queries[] = { { U_V1900_CREATE_TABLE_DIRECTORIES, "create table directories" }, { U_V1900_ALTER_PL_ADD_DIRECTORYID, "alter table pl add column directory_id" }, { U_V1900_ALTER_FILES_ADD_DIRECTORYID, "alter table files add column directory_id" }, { U_V1900_ALTER_FILES_ADD_DATERELEASED,"alter table files add column date_released" }, { U_V1900_ALTER_SPEAKERS_ADD_NAME, "alter table speakers add column name" }, { U_V1900_INSERT_DIR1, "insert root directory" }, { U_V1900_INSERT_DIR2, "insert /file: directory" }, { U_V1900_INSERT_DIR3, "insert /htttp: directory" }, { U_V1900_INSERT_DIR4, "insert /spotify: directory" }, { U_V1900_DROP_VIEW_FILELIST, "drop view directories" }, { U_V1900_SCVER_MAJOR, "set schema_version_major to 19" }, { U_V1900_SCVER_MINOR, "set schema_version_minor to 00" }, }; int db_upgrade_v19_directory_id(sqlite3 *hdl, char *virtual_path) { sqlite3_stmt *stmt; char *query; int id; int ret; query = sqlite3_mprintf("SELECT d.id FROM directories d WHERE d.disabled = 0 AND d.virtual_path = '%q';", virtual_path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret < 0) { DPRINTF(E_LOG, L_DB, "Error preparing query '%s'\n", query); sqlite3_free(query); return -1; } ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) id = sqlite3_column_int(stmt, 0); else if (ret == SQLITE_DONE) id = 0; // Not found else { DPRINTF(E_LOG, L_DB, "Error stepping query '%s'\n", query); sqlite3_free(query); sqlite3_finalize(stmt); return -1; } sqlite3_free(query); sqlite3_finalize(stmt); return id; } int db_upgrade_v19_insert_directory(sqlite3 *hdl, char *virtual_path, int parent_id) { char *query; char *errmsg; int id; int ret; query = sqlite3_mprintf( "INSERT INTO directories (virtual_path, db_timestamp, disabled, parent_id) VALUES (TRIM(%Q), %d, %d, %d);", virtual_path, (uint64_t)time(NULL), 0, parent_id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); return -1; } ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg); sqlite3_free(errmsg); sqlite3_free(query); return -1; } sqlite3_free(query); id = (int)sqlite3_last_insert_rowid(hdl); DPRINTF(E_DBG, L_DB, "Added directory %s with id %d\n", virtual_path, id); return id; } static int db_upgrade_v19_insert_parent_directories(sqlite3 *hdl, char *virtual_path) { char *ptr; int dir_id; int parent_id; char buf[PATH_MAX]; // The root directoy ID parent_id = 1; ptr = virtual_path + 1; // Skip first '/' while (ptr && (ptr = strchr(ptr, '/'))) { strncpy(buf, virtual_path, (ptr - virtual_path)); buf[(ptr - virtual_path)] = '\0'; dir_id = db_upgrade_v19_directory_id(hdl, buf); if (dir_id < 0) { DPRINTF(E_LOG, L_SCAN, "Select of directory failed '%s'\n", buf); return -1; } else if (dir_id == 0) { dir_id = db_upgrade_v19_insert_directory(hdl, buf, parent_id); if (dir_id < 0) { DPRINTF(E_LOG, L_SCAN, "Insert of directory failed '%s'\n", buf); return -1; } } parent_id = dir_id; ptr++; } return parent_id; } static int db_upgrade_v19(sqlite3 *hdl) { sqlite3_stmt *stmt; char *query; char *uquery; char *errmsg; int id; char *virtual_path; int dir_id; int ret; query = "SELECT id, virtual_path FROM files;"; DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) { id = sqlite3_column_int(stmt, 0); virtual_path = (char *)sqlite3_column_text(stmt, 1); dir_id = db_upgrade_v19_insert_parent_directories(hdl, virtual_path); if (dir_id < 0) { DPRINTF(E_LOG, L_DB, "Error processing parent directories for file: %s\n", virtual_path); } else { uquery = sqlite3_mprintf("UPDATE files SET directory_id = %d WHERE id = %d;", dir_id, id); ret = sqlite3_exec(hdl, uquery, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error updating files: %s\n", errmsg); } sqlite3_free(uquery); sqlite3_free(errmsg); } } sqlite3_finalize(stmt); query = "SELECT id, virtual_path FROM playlists WHERE type = 2 OR type = 3;"; //Only update normal and smart playlists DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); return -1; } while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) { id = sqlite3_column_int(stmt, 0); virtual_path = (char *)sqlite3_column_text(stmt, 1); dir_id = db_upgrade_v19_insert_parent_directories(hdl, virtual_path); if (dir_id < 0) { DPRINTF(E_LOG, L_DB, "Error processing parent directories for file: %s\n", virtual_path); } else { uquery = sqlite3_mprintf("UPDATE files SET directory_id = %d WHERE id = %d;", dir_id, id); ret = sqlite3_exec(hdl, uquery, NULL, NULL, &errmsg); if (ret != SQLITE_OK) { DPRINTF(E_LOG, L_DB, "Error updating files: %s\n", errmsg); } sqlite3_free(uquery); sqlite3_free(errmsg); } } sqlite3_finalize(stmt); return 0; } /* Upgrade from schema v19.00 to v19.01 */ /* Create new table queue for persistent playqueue */ #define U_V1901_CREATE_TABLE_QUEUE \ "CREATE TABLE IF NOT EXISTS queue (" \ " id INTEGER PRIMARY KEY NOT NULL," \ " file_id INTEGER NOT NULL," \ " pos INTEGER NOT NULL," \ " shuffle_pos INTEGER NOT NULL," \ " data_kind INTEGER NOT NULL," \ " media_kind INTEGER NOT NULL," \ " song_length INTEGER NOT NULL," \ " path VARCHAR(4096) NOT NULL," \ " virtual_path VARCHAR(4096) NOT NULL," \ " title VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " artist VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist VARCHAR(1024) NOT NULL COLLATE DAAP," \ " album VARCHAR(1024) NOT NULL COLLATE DAAP," \ " genre VARCHAR(255) DEFAULT NULL COLLATE DAAP," \ " songalbumid INTEGER NOT NULL," \ " time_modified INTEGER DEFAULT 0," \ " artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " year INTEGER DEFAULT 0," \ " track INTEGER DEFAULT 0," \ " disc INTEGER DEFAULT 0" \ ");" #define U_V1901_QUEUE_VERSION \ "INSERT INTO admin (key, value) VALUES ('queue_version', '0');" #define U_V1901_SCVER_MAJOR \ "UPDATE admin SET value = '19' WHERE key = 'schema_version_major';" #define U_V1901_SCVER_MINOR \ "UPDATE admin SET value = '01' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v1901_queries[] = { { U_V1901_CREATE_TABLE_QUEUE, "create table directories" }, { U_V1901_QUEUE_VERSION, "insert queue version" }, { U_V1901_SCVER_MAJOR, "set schema_version_major to 19" }, { U_V1901_SCVER_MINOR, "set schema_version_minor to 01" }, }; /* Upgrade from schema v19.01 to v19.02 */ /* Set key column as primary key in the admin table */ #define U_V1902_CREATE_TABLE_ADMINTMP \ "CREATE TEMPORARY TABLE IF NOT EXISTS admin_tmp(" \ " key VARCHAR(32) NOT NULL," \ " value VARCHAR(32) NOT NULL" \ ");" #define U_V1902_INSERT_ADMINTMP \ "INSERT INTO admin_tmp SELECT * FROM admin;" #define U_V1902_DROP_TABLE_ADMIN \ "DROP TABLE admin;" #define U_V1902_CREATE_TABLE_ADMIN \ "CREATE TABLE IF NOT EXISTS admin(" \ " key VARCHAR(32) PRIMARY KEY NOT NULL," \ " value VARCHAR(32) NOT NULL" \ ");" #define U_V1902_INSERT_ADMIN \ "INSERT OR IGNORE INTO admin SELECT * FROM admin_tmp;" #define U_V1902_DROP_TABLE_ADMINTMP \ "DROP TABLE admin_tmp;" #define U_V1902_SCVER_MAJOR \ "UPDATE admin SET value = '19' WHERE key = 'schema_version_major';" #define U_V1902_SCVER_MINOR \ "UPDATE admin SET value = '02' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v1902_queries[] = { { U_V1902_CREATE_TABLE_ADMINTMP, "create temporary table admin_tmp" }, { U_V1902_INSERT_ADMINTMP, "insert admin_tmp" }, { U_V1902_DROP_TABLE_ADMIN, "drop table admin" }, { U_V1902_CREATE_TABLE_ADMIN, "create table admin" }, { U_V1902_INSERT_ADMIN, "insert admin" }, { U_V1902_DROP_TABLE_ADMINTMP, "drop table admin_tmp" }, { U_V1902_SCVER_MAJOR, "set schema_version_major to 19" }, { U_V1902_SCVER_MINOR, "set schema_version_minor to 02" }, }; #define U_V1903_ALTER_QUEUE_ADD_ARTWORKURL \ "ALTER TABLE queue ADD COLUMN artwork_url VARCHAR(4096) DEFAULT NULL;" #define U_V1903_SCVER_MAJOR \ "UPDATE admin SET value = '19' WHERE key = 'schema_version_major';" #define U_V1903_SCVER_MINOR \ "UPDATE admin SET value = '03' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v1903_queries[] = { { U_V1903_ALTER_QUEUE_ADD_ARTWORKURL, "alter table queue add column artwork_url" }, { U_V1903_SCVER_MAJOR, "set schema_version_major to 19" }, { U_V1903_SCVER_MINOR, "set schema_version_minor to 03" }, }; #define U_V1904_ALTER_SPEAKERS_ADD_AUTHKEY \ "ALTER TABLE speakers ADD COLUMN auth_key VARCHAR(2048) DEFAULT NULL;" #define U_V1904_SCVER_MAJOR \ "UPDATE admin SET value = '19' WHERE key = 'schema_version_major';" #define U_V1904_SCVER_MINOR \ "UPDATE admin SET value = '04' WHERE key = 'schema_version_minor';" static const struct db_upgrade_query db_upgrade_v1904_queries[] = { { U_V1904_ALTER_SPEAKERS_ADD_AUTHKEY, "alter table speakers add column auth_key" }, { U_V1904_SCVER_MAJOR, "set schema_version_major to 19" }, { U_V1904_SCVER_MINOR, "set schema_version_minor to 04" }, }; int db_upgrade(sqlite3 *hdl, int db_ver) { int ret; ret = db_drop_indices(hdl); if (ret < 0) return -1; switch (db_ver) { case 1000: ret = db_generic_upgrade(hdl, db_upgrade_v11_queries, sizeof(db_upgrade_v11_queries) / sizeof(db_upgrade_v11_queries[0])); if (ret < 0) return -1; ret = db_upgrade_v11(hdl); if (ret < 0) return -1; /* FALLTHROUGH */ case 1100: ret = db_upgrade_v12(hdl); if (ret < 0) return -1; ret = db_generic_upgrade(hdl, db_upgrade_v12_queries, sizeof(db_upgrade_v12_queries) / sizeof(db_upgrade_v12_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1200: ret = db_generic_upgrade(hdl, db_upgrade_v13_queries, sizeof(db_upgrade_v13_queries) / sizeof(db_upgrade_v13_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1300: ret = db_upgrade_v14(hdl); if (ret < 0) return -1; ret = db_generic_upgrade(hdl, db_upgrade_v14_queries, sizeof(db_upgrade_v14_queries) / sizeof(db_upgrade_v14_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1400: ret = db_upgrade_v15(hdl); if (ret < 0) return -1; ret = db_generic_upgrade(hdl, db_upgrade_v15_queries, sizeof(db_upgrade_v15_queries) / sizeof(db_upgrade_v15_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1500: ret = db_generic_upgrade(hdl, db_upgrade_v1501_queries, sizeof(db_upgrade_v1501_queries) / sizeof(db_upgrade_v1501_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1501: ret = db_generic_upgrade(hdl, db_upgrade_v16_queries, sizeof(db_upgrade_v16_queries) / sizeof(db_upgrade_v16_queries[0])); if (ret < 0) return -1; ret = db_upgrade_v16(hdl); if (ret < 0) return -1; /* FALLTHROUGH */ case 1600: ret = db_generic_upgrade(hdl, db_upgrade_v17_queries, sizeof(db_upgrade_v17_queries) / sizeof(db_upgrade_v17_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1700: ret = db_generic_upgrade(hdl, db_upgrade_v18_queries, sizeof(db_upgrade_v18_queries) / sizeof(db_upgrade_v18_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1800: ret = db_generic_upgrade(hdl, db_upgrade_v1801_queries, sizeof(db_upgrade_v1801_queries) / sizeof(db_upgrade_v1801_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1801: ret = db_generic_upgrade(hdl, db_upgrade_v1900_queries, sizeof(db_upgrade_v1900_queries) / sizeof(db_upgrade_v1900_queries[0])); if (ret < 0) return -1; ret = db_upgrade_v19(hdl); if (ret < 0) return -1; /* FALLTHROUGH */ case 1900: ret = db_generic_upgrade(hdl, db_upgrade_v1901_queries, sizeof(db_upgrade_v1901_queries) / sizeof(db_upgrade_v1901_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1901: ret = db_generic_upgrade(hdl, db_upgrade_v1902_queries, sizeof(db_upgrade_v1902_queries) / sizeof(db_upgrade_v1902_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1902: ret = db_generic_upgrade(hdl, db_upgrade_v1903_queries, sizeof(db_upgrade_v1903_queries) / sizeof(db_upgrade_v1903_queries[0])); if (ret < 0) return -1; /* FALLTHROUGH */ case 1903: ret = db_generic_upgrade(hdl, db_upgrade_v1904_queries, sizeof(db_upgrade_v1904_queries) / sizeof(db_upgrade_v1904_queries[0])); if (ret < 0) return -1; break; default: DPRINTF(E_FATAL, L_DB, "No upgrade path from the current DB schema\n"); return -1; } return 0; } forked-daapd-25.0/src/db_upgrade.h000066400000000000000000000016641313447753700170420ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ #ifndef SRC_DB_UPGRADE_H_ #define SRC_DB_UPGRADE_H_ #include int db_upgrade(sqlite3 *hdl, int db_ver); #endif /* SRC_DB_UPGRADE_H_ */ forked-daapd-25.0/src/dmap_common.c000066400000000000000000000322761313447753700172350ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include "db.h" #include "misc.h" #include "httpd.h" #include "logger.h" #include "dmap_common.h" /* gperf static hash, dmap_fields.gperf */ #include "dmap_fields_hash.h" const struct dmap_field * dmap_get_fields_table(int *nfields) { *nfields = sizeof(dmap_fields) / sizeof(dmap_fields[0]); return dmap_fields; } void dmap_add_container(struct evbuffer *evbuf, const char *tag, int len) { unsigned char buf[4]; evbuffer_add(evbuf, tag, 4); /* Container length */ buf[0] = (len >> 24) & 0xff; buf[1] = (len >> 16) & 0xff; buf[2] = (len >> 8) & 0xff; buf[3] = len & 0xff; evbuffer_add(evbuf, buf, sizeof(buf)); } void dmap_add_long(struct evbuffer *evbuf, const char *tag, int64_t val) { unsigned char buf[12]; evbuffer_add(evbuf, tag, 4); /* Length */ buf[0] = 0; buf[1] = 0; buf[2] = 0; buf[3] = 8; /* Value */ buf[4] = (val >> 56) & 0xff; buf[5] = (val >> 48) & 0xff; buf[6] = (val >> 40) & 0xff; buf[7] = (val >> 32) & 0xff; buf[8] = (val >> 24) & 0xff; buf[9] = (val >> 16) & 0xff; buf[10] = (val >> 8) & 0xff; buf[11] = val & 0xff; evbuffer_add(evbuf, buf, sizeof(buf)); } void dmap_add_int(struct evbuffer *evbuf, const char *tag, int val) { unsigned char buf[8]; evbuffer_add(evbuf, tag, 4); /* Length */ buf[0] = 0; buf[1] = 0; buf[2] = 0; buf[3] = 4; /* Value */ buf[4] = (val >> 24) & 0xff; buf[5] = (val >> 16) & 0xff; buf[6] = (val >> 8) & 0xff; buf[7] = val & 0xff; evbuffer_add(evbuf, buf, sizeof(buf)); } void dmap_add_short(struct evbuffer *evbuf, const char *tag, short val) { unsigned char buf[6]; evbuffer_add(evbuf, tag, 4); /* Length */ buf[0] = 0; buf[1] = 0; buf[2] = 0; buf[3] = 2; /* Value */ buf[4] = (val >> 8) & 0xff; buf[5] = val & 0xff; evbuffer_add(evbuf, buf, sizeof(buf)); } void dmap_add_char(struct evbuffer *evbuf, const char *tag, char val) { unsigned char buf[5]; evbuffer_add(evbuf, tag, 4); /* Length */ buf[0] = 0; buf[1] = 0; buf[2] = 0; buf[3] = 1; /* Value */ buf[4] = val; evbuffer_add(evbuf, buf, sizeof(buf)); } void dmap_add_literal(struct evbuffer *evbuf, const char *tag, char *str, int len) { char buf[4]; evbuffer_add(evbuf, tag, 4); /* Length */ buf[0] = (len >> 24) & 0xff; buf[1] = (len >> 16) & 0xff; buf[2] = (len >> 8) & 0xff; buf[3] = len & 0xff; evbuffer_add(evbuf, buf, sizeof(buf)); if (str && (len > 0)) evbuffer_add(evbuf, str, len); } void dmap_add_raw_uint32(struct evbuffer *evbuf, uint32_t val) { unsigned char buf[4]; /* Value */ buf[0] = (val >> 24) & 0xff; buf[1] = (val >> 16) & 0xff; buf[2] = (val >> 8) & 0xff; buf[3] = val & 0xff; evbuffer_add(evbuf, buf, sizeof(buf)); } void dmap_add_string(struct evbuffer *evbuf, const char *tag, const char *str) { unsigned char buf[4]; int len; if (str) len = strlen(str); else len = 0; evbuffer_add(evbuf, tag, 4); /* String length */ buf[0] = (len >> 24) & 0xff; buf[1] = (len >> 16) & 0xff; buf[2] = (len >> 8) & 0xff; buf[3] = len & 0xff; evbuffer_add(evbuf, buf, sizeof(buf)); if (len) evbuffer_add(evbuf, str, len); } void dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval, int32_t intval) { union { int32_t v_i32; uint32_t v_u32; int64_t v_i64; uint64_t v_u64; } val; int ret; if (strval && (df->type != DMAP_TYPE_STRING)) { switch (df->type) { case DMAP_TYPE_DATE: case DMAP_TYPE_UBYTE: case DMAP_TYPE_USHORT: case DMAP_TYPE_UINT: ret = safe_atou32(strval, &val.v_u32); if (ret < 0) val.v_u32 = 0; break; case DMAP_TYPE_BYTE: case DMAP_TYPE_SHORT: case DMAP_TYPE_INT: ret = safe_atoi32(strval, &val.v_i32); if (ret < 0) val.v_i32 = 0; break; case DMAP_TYPE_ULONG: ret = safe_atou64(strval, &val.v_u64); if (ret < 0) val.v_u64 = 0; break; case DMAP_TYPE_LONG: ret = safe_atoi64(strval, &val.v_i64); if (ret < 0) val.v_i64 = 0; break; /* DMAP_TYPE_VERSION & DMAP_TYPE_LIST not handled here */ default: DPRINTF(E_LOG, L_DAAP, "Unsupported DMAP type %d for DMAP field %s\n", df->type, df->desc); return; } } else if (!strval && (df->type != DMAP_TYPE_STRING)) { switch (df->type) { case DMAP_TYPE_DATE: case DMAP_TYPE_UBYTE: case DMAP_TYPE_USHORT: case DMAP_TYPE_UINT: val.v_u32 = intval; break; case DMAP_TYPE_BYTE: case DMAP_TYPE_SHORT: case DMAP_TYPE_INT: val.v_i32 = intval; break; case DMAP_TYPE_ULONG: val.v_u64 = intval; break; case DMAP_TYPE_LONG: val.v_i64 = intval; break; /* DMAP_TYPE_VERSION & DMAP_TYPE_LIST not handled here */ default: DPRINTF(E_LOG, L_DAAP, "Unsupported DMAP type %d for DMAP field %s\n", df->type, df->desc); return; } } switch (df->type) { case DMAP_TYPE_UBYTE: if (val.v_u32) dmap_add_char(evbuf, df->tag, val.v_u32); break; case DMAP_TYPE_BYTE: if (val.v_i32) dmap_add_char(evbuf, df->tag, val.v_i32); break; case DMAP_TYPE_USHORT: if (val.v_u32) dmap_add_short(evbuf, df->tag, val.v_u32); break; case DMAP_TYPE_SHORT: if (val.v_i32) dmap_add_short(evbuf, df->tag, val.v_i32); break; case DMAP_TYPE_DATE: case DMAP_TYPE_UINT: if (val.v_u32) dmap_add_int(evbuf, df->tag, val.v_u32); break; case DMAP_TYPE_INT: if (val.v_i32) dmap_add_int(evbuf, df->tag, val.v_i32); break; case DMAP_TYPE_ULONG: if (val.v_u64) dmap_add_long(evbuf, df->tag, val.v_u64); break; case DMAP_TYPE_LONG: if (val.v_i64) dmap_add_long(evbuf, df->tag, val.v_i64); break; case DMAP_TYPE_STRING: if (strval) dmap_add_string(evbuf, df->tag, strval); break; case DMAP_TYPE_VERSION: case DMAP_TYPE_LIST: return; } } void dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg) { struct evbuffer *evbuf; int len; int ret; if (!req) return; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_DMAP, "Could not allocate evbuffer for DMAP error\n"); httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return; } len = 12 + 8 + 8 + strlen(errmsg); ret = evbuffer_expand(evbuf, len); if (ret < 0) { DPRINTF(E_LOG, L_DMAP, "Could not expand evbuffer for DMAP error\n"); httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); evbuffer_free(evbuf); return; } dmap_add_container(evbuf, container, len - 8); dmap_add_int(evbuf, "mstt", 500); dmap_add_string(evbuf, "msts", errmsg); httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); evbuffer_free(evbuf); } int dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav) { const struct dmap_field_map *dfm; const struct dmap_field *df; char **strval; char *ptr; int32_t val; int want_mikd; int want_asdk; int want_ased; int i; int ret; want_mikd = 0; want_asdk = 0; want_ased = 0; i = -1; while (1) { i++; /* Specific meta tags requested (or default list) */ if (nmeta > 0) { if (i == nmeta) break; df = meta[i]; if (df->dfm) dfm = df->dfm; else break; } /* No specific meta tags requested, send out everything */ else { /* End of list */ if (i == (sizeof(dmap_fields) / sizeof(dmap_fields[0]))) break; df = &dmap_fields[i]; dfm = dmap_fields[i].dfm; } /* Extradata not in media_file_info but flag for reply */ if (dfm == &dfm_dmap_ased) { want_ased = 1; continue; } /* Not in struct media_file_info */ if (dfm->mfi_offset < 0) continue; /* Will be prepended to the list */ if (dfm == &dfm_dmap_mikd) { /* item kind */ want_mikd = 1; continue; } else if (dfm == &dfm_dmap_asdk) { /* data kind */ want_asdk = 1; continue; } DPRINTF(E_SPAM, L_DAAP, "Investigating %s\n", df->desc); strval = (char **) ((char *)dbmfi + dfm->mfi_offset); if (!(*strval) || (**strval == '\0')) continue; /* Here's one exception ... codectype (ascd) is actually an integer */ if (dfm == &dfm_dmap_ascd) { dmap_add_literal(song, df->tag, *strval, 4); continue; } val = 0; if (force_wav) { switch (dfm->mfi_offset) { case dbmfi_offsetof(type): ptr = "wav"; strval = &ptr; break; case dbmfi_offsetof(bitrate): val = 0; ret = safe_atoi32(dbmfi->samplerate, &val); if ((ret < 0) || (val == 0)) val = 1411; else val = (val * 8) / 250; ptr = NULL; strval = &ptr; break; case dbmfi_offsetof(description): ptr = "wav audio file"; strval = &ptr; break; default: break; } } dmap_add_field(song, df, *strval, val); DPRINTF(E_SPAM, L_DAAP, "Done with meta tag %s (%s)\n", df->desc, *strval); } /* Required for artwork in iTunes, set songartworkcount (asac) = 1 */ if (want_ased) { dmap_add_short(song, "ased", 1); dmap_add_short(song, "asac", 1); } if (sort_tags) { dmap_add_string(song, "assn", dbmfi->title_sort); dmap_add_string(song, "assa", dbmfi->artist_sort); dmap_add_string(song, "assu", dbmfi->album_sort); dmap_add_string(song, "assl", dbmfi->album_artist_sort); if (dbmfi->composer_sort) dmap_add_string(song, "assc", dbmfi->composer_sort); } val = 0; if (want_mikd) val += 9; if (want_asdk) val += 9; dmap_add_container(songlist, "mlit", evbuffer_get_length(song) + val); /* Prepend mikd & asdk if needed */ if (want_mikd) { /* dmap.itemkind must come first */ ret = safe_atoi32(dbmfi->item_kind, &val); if (ret < 0) val = 2; /* music by default */ dmap_add_char(songlist, "mikd", val); } if (want_asdk) { ret = safe_atoi32(dbmfi->data_kind, &val); if (ret < 0) val = 0; dmap_add_char(songlist, "asdk", val); } ret = evbuffer_add_buffer(songlist, song); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add song to song list\n"); return -1; } return 0; } int dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_queue_item *queue_item) { int32_t val; int want_mikd; int want_asdk; int want_ased; int ret; dmap_add_int(song, "miid", queue_item->file_id); dmap_add_string(song, "minm", queue_item->title); dmap_add_long(song, "mper", queue_item->file_id); dmap_add_int(song, "mcti", queue_item->file_id); dmap_add_string(song, "asal", queue_item->album); dmap_add_long(song, "asai", queue_item->songalbumid); dmap_add_string(song, "asaa", queue_item->album_artist); dmap_add_string(song, "asar", queue_item->artist); dmap_add_int(song, "asdm", queue_item->time_modified); dmap_add_short(song, "asdn", queue_item->disc); dmap_add_string(song, "asgn", queue_item->genre); dmap_add_int(song, "astm", queue_item->song_length); dmap_add_short(song, "astn", queue_item->track); dmap_add_short(song, "asyr", queue_item->year); dmap_add_int(song, "aeMK", queue_item->media_kind); dmap_add_char(song, "aeMk", queue_item->media_kind); dmap_add_string(song, "asfm", "wav"); dmap_add_short(song, "asbr", 1411); dmap_add_string(song, "asdt", "wav audio file"); want_mikd = 1;/* Will be prepended to the list *//* item kind */ want_asdk = 1;/* Will be prepended to the list *//* data kind */ want_ased = 1;/* Extradata not in media_file_info but flag for reply */ /* Required for artwork in iTunes, set songartworkcount (asac) = 1 */ if (want_ased) { dmap_add_short(song, "ased", 1); dmap_add_short(song, "asac", 1); } val = 0; if (want_mikd) val += 9; if (want_asdk) val += 9; dmap_add_container(songlist, "mlit", evbuffer_get_length(song) + val); /* Prepend mikd & asdk if needed */ if (want_mikd) { /* dmap.itemkind must come first */ val = 2; /* music by default */ dmap_add_char(songlist, "mikd", val); } if (want_asdk) { ret = queue_item->data_kind; if (ret < 0) val = 0; dmap_add_char(songlist, "asdk", val); } ret = evbuffer_add_buffer(songlist, song); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add song to song list\n"); return -1; } return 0; } forked-daapd-25.0/src/dmap_common.h000066400000000000000000000042261313447753700172340ustar00rootroot00000000000000 #ifndef __DMAP_HELPERS_H__ #define __DMAP_HELPERS_H__ #include "config.h" #include #include #include "db.h" enum dmap_type { DMAP_TYPE_UBYTE = 0x01, DMAP_TYPE_BYTE = 0x02, DMAP_TYPE_USHORT = 0x03, DMAP_TYPE_SHORT = 0x04, DMAP_TYPE_UINT = 0x05, DMAP_TYPE_INT = 0x06, DMAP_TYPE_ULONG = 0x07, DMAP_TYPE_LONG = 0x08, DMAP_TYPE_STRING = 0x09, DMAP_TYPE_DATE = 0x0a, DMAP_TYPE_VERSION = 0x0b, DMAP_TYPE_LIST = 0x0c, }; struct dmap_field_map { ssize_t mfi_offset; ssize_t pli_offset; ssize_t gri_offset; }; struct dmap_field { char *desc; char *tag; const struct dmap_field_map *dfm; enum dmap_type type; }; extern const struct dmap_field_map dfm_dmap_mimc; extern const struct dmap_field_map dfm_dmap_aeSP; const struct dmap_field * dmap_get_fields_table(int *nfields); /* From dmap_fields.gperf - keep in sync, don't alter */ const struct dmap_field * dmap_find_field (register const char *str, register GPERF_LEN_TYPE len); void dmap_add_container(struct evbuffer *evbuf, const char *tag, int len); void dmap_add_long(struct evbuffer *evbuf, const char *tag, int64_t val); void dmap_add_int(struct evbuffer *evbuf, const char *tag, int val); void dmap_add_short(struct evbuffer *evbuf, const char *tag, short val); void dmap_add_char(struct evbuffer *evbuf, const char *tag, char val); void dmap_add_literal(struct evbuffer *evbuf, const char *tag, char *str, int len); void dmap_add_raw_uint32(struct evbuffer *evbuf, uint32_t val); void dmap_add_string(struct evbuffer *evbuf, const char *tag, const char *str); void dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval, int32_t intval); void dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg); int dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav); int dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_queue_item *queue_item); #endif /* !__DMAP_HELPERS_H__ */ forked-daapd-25.0/src/dmap_fields.gperf000066400000000000000000000762431313447753700200760ustar00rootroot00000000000000%language=ANSI-C %readonly-tables %enum %switch=1 %compare-lengths %define hash-function-name dmap_hash_field %define lookup-function-name dmap_find_field %define slot-name desc %global-table %define word-array-name dmap_fields %struct-type %omit-struct-type %{ /* Non-static fields are exported by dmap_common.h */ static const struct dmap_field_map dfm_dmap_miid = { dbmfi_offsetof(id), dbpli_offsetof(id), -1 }; static const struct dmap_field_map dfm_dmap_minm = { dbmfi_offsetof(title), dbpli_offsetof(title), dbgri_offsetof(itemname) }; static const struct dmap_field_map dfm_dmap_mikd = { dbmfi_offsetof(item_kind), -1, -1 }; static const struct dmap_field_map dfm_dmap_mper = { dbmfi_offsetof(id), dbpli_offsetof(id), dbgri_offsetof(persistentid) }; static const struct dmap_field_map dfm_dmap_mcon = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mcti = { dbmfi_offsetof(id), -1, -1 }; static const struct dmap_field_map dfm_dmap_mpco = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mstt = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msts = { -1, -1, -1 }; const struct dmap_field_map dfm_dmap_mimc = { dbmfi_offsetof(total_tracks), dbpli_offsetof(items), dbgri_offsetof(itemcount) }; static const struct dmap_field_map dfm_dmap_mctc = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mrco = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mtco = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mlcl = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mlit = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mbcl = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mdcl = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msrv = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msau = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mslr = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mpro = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msal = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msup = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mspi = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msex = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msbr = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msqy = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msix = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msrs = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mstm = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_msdc = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mlog = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mlid = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mupd = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_musr = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_muty = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mudl = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mccr = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mcnm = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mcna = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_mcty = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_apro = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_avdb = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_abro = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_abal = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_abar = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_abcp = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_abgn = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_adbs = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_asal = { dbmfi_offsetof(album), -1, -1 }; static const struct dmap_field_map dfm_dmap_asai = { dbmfi_offsetof(songalbumid), -1, -1 }; static const struct dmap_field_map dfm_dmap_asaa = { dbmfi_offsetof(album_artist), -1, dbgri_offsetof(songalbumartist) }; static const struct dmap_field_map dfm_dmap_asac = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_asar = { dbmfi_offsetof(artist), -1, -1 }; static const struct dmap_field_map dfm_dmap_asri = { dbmfi_offsetof(songartistid), -1, dbgri_offsetof(songartistid) }; static const struct dmap_field_map dfm_dmap_asbr = { dbmfi_offsetof(bitrate), -1, -1 }; static const struct dmap_field_map dfm_dmap_asbt = { dbmfi_offsetof(bpm), -1, -1 }; static const struct dmap_field_map dfm_dmap_ascm = { dbmfi_offsetof(comment), -1, -1 }; static const struct dmap_field_map dfm_dmap_asco = { dbmfi_offsetof(compilation), -1, -1 }; static const struct dmap_field_map dfm_dmap_ascp = { dbmfi_offsetof(composer), -1, -1 }; static const struct dmap_field_map dfm_dmap_asda = { dbmfi_offsetof(time_added), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdb = { dbmfi_offsetof(disabled), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdc = { dbmfi_offsetof(total_discs), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdm = { dbmfi_offsetof(time_modified), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdn = { dbmfi_offsetof(disc), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdk = { dbmfi_offsetof(data_kind), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdr = { dbmfi_offsetof(date_released), -1, -1 }; static const struct dmap_field_map dfm_dmap_asdt = { dbmfi_offsetof(description), -1, -1 }; static const struct dmap_field_map dfm_dmap_ased = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aseq = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_asfm = { dbmfi_offsetof(type), -1, -1 }; static const struct dmap_field_map dfm_dmap_asgn = { dbmfi_offsetof(genre), -1, -1 }; static const struct dmap_field_map dfm_dmap_ashp = { dbmfi_offsetof(play_count), -1, -1 }; static const struct dmap_field_map dfm_dmap_aspc = { dbmfi_offsetof(play_count), -1, -1 }; static const struct dmap_field_map dfm_dmap_assp = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_assr = { dbmfi_offsetof(samplerate), -1, -1 }; static const struct dmap_field_map dfm_dmap_asst = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_assz = { dbmfi_offsetof(file_size), -1, -1 }; static const struct dmap_field_map dfm_dmap_asrv = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_astc = { dbmfi_offsetof(total_tracks), -1, -1 }; static const struct dmap_field_map dfm_dmap_astm = { dbmfi_offsetof(song_length), -1, dbgri_offsetof(song_length) }; static const struct dmap_field_map dfm_dmap_astn = { dbmfi_offsetof(track), -1, -1 }; static const struct dmap_field_map dfm_dmap_asul = { dbmfi_offsetof(url), -1, -1 }; static const struct dmap_field_map dfm_dmap_asur = { dbmfi_offsetof(rating), -1, -1 }; static const struct dmap_field_map dfm_dmap_asyr = { dbmfi_offsetof(year), -1, -1 }; static const struct dmap_field_map dfm_dmap_aply = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_abpl = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_apso = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_arsv = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_arif = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aeNV = { -1, -1, -1 }; const struct dmap_field_map dfm_dmap_aeSP = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aePS = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_ascd = { dbmfi_offsetof(codectype), -1, -1 }; static const struct dmap_field_map dfm_dmap_ascs = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_agac = { -1, -1, dbgri_offsetof(groupalbumcount) }; static const struct dmap_field_map dfm_dmap_agrp = { dbmfi_offsetof(grouping), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeSV = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aePI = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aeCI = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aeGI = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aeAI = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aeSI = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aeSF = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_ascr = { dbmfi_offsetof(contentrating), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeHV = { dbmfi_offsetof(has_video), -1, -1 }; static const struct dmap_field_map dfm_dmap_msas = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_asct = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_ascn = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aslc = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_asky = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_apsm = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aprm = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aePC = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aePP = { -1, -1, -1 }; static const struct dmap_field_map dfm_dmap_aeMK = { dbmfi_offsetof(media_kind), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeMk = { dbmfi_offsetof(media_kind), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeSN = { dbmfi_offsetof(tv_series_name), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeNN = { dbmfi_offsetof(tv_network_name), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeEN = { dbmfi_offsetof(tv_episode_num_str), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeES = { dbmfi_offsetof(tv_episode_sort), -1, -1 }; static const struct dmap_field_map dfm_dmap_aeSU = { dbmfi_offsetof(tv_season_num), -1, -1 }; static const struct dmap_field_map dfm_dmap_assn = { dbmfi_offsetof(title_sort), -1, -1 }; static const struct dmap_field_map dfm_dmap_assa = { dbmfi_offsetof(artist_sort), -1, -1 }; static const struct dmap_field_map dfm_dmap_assu = { dbmfi_offsetof(album_sort), -1, -1 }; static const struct dmap_field_map dfm_dmap_assc = { dbmfi_offsetof(composer_sort), -1, -1 }; static const struct dmap_field_map dfm_dmap_assl = { dbmfi_offsetof(album_artist_sort), -1, -1 }; %} struct dmap_field; %% "daap.browsealbumlisting", "abal", &dfm_dmap_abal, DMAP_TYPE_LIST "daap.browseartistlisting", "abar", &dfm_dmap_abar, DMAP_TYPE_LIST "daap.browsecomposerlisting", "abcp", &dfm_dmap_abcp, DMAP_TYPE_LIST "daap.browsegenrelisting", "abgn", &dfm_dmap_abgn, DMAP_TYPE_LIST "daap.baseplaylist", "abpl", &dfm_dmap_abpl, DMAP_TYPE_UBYTE "daap.databasebrowse", "abro", &dfm_dmap_abro, DMAP_TYPE_LIST "daap.databasesongs", "adbs", &dfm_dmap_adbs, DMAP_TYPE_LIST "com.apple.itunes.itms-artistid", "aeAI", &dfm_dmap_aeAI, DMAP_TYPE_UINT "com.apple.itunes.itms-composerid", "aeCI", &dfm_dmap_aeCI, DMAP_TYPE_UINT "com.apple.itunes.episode-num-str", "aeEN", &dfm_dmap_aeEN, DMAP_TYPE_STRING "com.apple.itunes.episode-sort", "aeES", &dfm_dmap_aeES, DMAP_TYPE_UINT "com.apple.itunes.itms-genreid", "aeGI", &dfm_dmap_aeGI, DMAP_TYPE_UINT "com.apple.itunes.has-video", "aeHV", &dfm_dmap_aeHV, DMAP_TYPE_UBYTE "com.apple.itunes.extended-media-kind", "aeMk", &dfm_dmap_aeMk, DMAP_TYPE_UINT "com.apple.itunes.mediakind", "aeMK", &dfm_dmap_aeMK, DMAP_TYPE_UBYTE "com.apple.itunes.network-name", "aeNN", &dfm_dmap_aeNN, DMAP_TYPE_STRING "com.apple.itunes.norm-volume", "aeNV", &dfm_dmap_aeNV, DMAP_TYPE_UINT "com.apple.itunes.is-podcast", "aePC", &dfm_dmap_aePC, DMAP_TYPE_UBYTE "com.apple.itunes.itms-playlistid", "aePI", &dfm_dmap_aePI, DMAP_TYPE_UINT "com.apple.itunes.is-podcast-playlist", "aePP", &dfm_dmap_aePP, DMAP_TYPE_UBYTE "com.apple.itunes.special-playlist", "aePS", &dfm_dmap_aePS, DMAP_TYPE_UBYTE "com.apple.itunes.itms-storefrontid", "aeSF", &dfm_dmap_aeSF, DMAP_TYPE_UINT "com.apple.itunes.itms-songid", "aeSI", &dfm_dmap_aeSI, DMAP_TYPE_UINT "com.apple.itunes.series-name", "aeSN", &dfm_dmap_aeSN, DMAP_TYPE_STRING "com.apple.itunes.smart-playlist", "aeSP", &dfm_dmap_aeSP, DMAP_TYPE_UBYTE "com.apple.itunes.season-num", "aeSU", &dfm_dmap_aeSU, DMAP_TYPE_UINT "com.apple.itunes.music-sharing-version", "aeSV", &dfm_dmap_aeSV, DMAP_TYPE_UINT "daap.groupalbumcount", "agac", &dfm_dmap_agac, DMAP_TYPE_UINT "daap.songgrouping", "agrp", &dfm_dmap_agrp, DMAP_TYPE_STRING "daap.databaseplaylists", "aply", &dfm_dmap_aply, DMAP_TYPE_LIST "daap.playlistrepeatmode", "aprm", &dfm_dmap_aprm, DMAP_TYPE_UBYTE "daap.protocolversion", "apro", &dfm_dmap_apro, DMAP_TYPE_VERSION "daap.playlistshufflemode", "apsm", &dfm_dmap_apsm, DMAP_TYPE_UBYTE "daap.playlistsongs", "apso", &dfm_dmap_apso, DMAP_TYPE_LIST "daap.resolveinfo", "arif", &dfm_dmap_arif, DMAP_TYPE_LIST "daap.resolve", "arsv", &dfm_dmap_arsv, DMAP_TYPE_LIST "daap.songalbumartist", "asaa", &dfm_dmap_asaa, DMAP_TYPE_STRING "daap.songartworkcount", "asac", &dfm_dmap_asac, DMAP_TYPE_USHORT "daap.songalbumid", "asai", &dfm_dmap_asai, DMAP_TYPE_ULONG "daap.songalbum", "asal", &dfm_dmap_asal, DMAP_TYPE_STRING "daap.songartist", "asar", &dfm_dmap_asar, DMAP_TYPE_STRING "daap.songartistid", "asri", &dfm_dmap_asri, DMAP_TYPE_ULONG "daap.songbitrate", "asbr", &dfm_dmap_asbr, DMAP_TYPE_USHORT "daap.songbeatsperminute", "asbt", &dfm_dmap_asbt, DMAP_TYPE_USHORT "daap.songcodectype", "ascd", &dfm_dmap_ascd, DMAP_TYPE_UINT "daap.songcomment", "ascm", &dfm_dmap_ascm, DMAP_TYPE_STRING "daap.songcontentdescription", "ascn", &dfm_dmap_ascn, DMAP_TYPE_STRING "daap.songcompilation", "asco", &dfm_dmap_asco, DMAP_TYPE_UBYTE "daap.songcomposer", "ascp", &dfm_dmap_ascp, DMAP_TYPE_STRING "daap.songcontentrating", "ascr", &dfm_dmap_ascr, DMAP_TYPE_UBYTE "daap.songcodecsubtype", "ascs", &dfm_dmap_ascs, DMAP_TYPE_UINT "daap.songcategory", "asct", &dfm_dmap_asct, DMAP_TYPE_STRING "daap.songdateadded", "asda", &dfm_dmap_asda, DMAP_TYPE_DATE "daap.songdisabled", "asdb", &dfm_dmap_asdb, DMAP_TYPE_UBYTE "daap.songdisccount", "asdc", &dfm_dmap_asdc, DMAP_TYPE_USHORT "daap.songdatakind", "asdk", &dfm_dmap_asdk, DMAP_TYPE_UBYTE "daap.songdatemodified", "asdm", &dfm_dmap_asdm, DMAP_TYPE_DATE "daap.songdiscnumber", "asdn", &dfm_dmap_asdn, DMAP_TYPE_USHORT "daap.songdatereleased", "asdr", &dfm_dmap_asdr, DMAP_TYPE_DATE "daap.songdescription", "asdt", &dfm_dmap_asdt, DMAP_TYPE_STRING "daap.songextradata", "ased", &dfm_dmap_ased, DMAP_TYPE_USHORT "daap.songeqpreset", "aseq", &dfm_dmap_aseq, DMAP_TYPE_STRING "daap.songformat", "asfm", &dfm_dmap_asfm, DMAP_TYPE_STRING "daap.songgenre", "asgn", &dfm_dmap_asgn, DMAP_TYPE_STRING "daap.songhasbeenplayed", "ashp", &dfm_dmap_ashp, DMAP_TYPE_UBYTE "daap.songkeywords", "asky", &dfm_dmap_asky, DMAP_TYPE_STRING "daap.songlongcontentdescription", "aslc", &dfm_dmap_aslc, DMAP_TYPE_STRING "daap.songuserplaycount", "aspc", &dfm_dmap_aspc, DMAP_TYPE_UINT "daap.songrelativevolume", "asrv", &dfm_dmap_asrv, DMAP_TYPE_BYTE "daap.sortartist", "assa", &dfm_dmap_assa, DMAP_TYPE_STRING "daap.sortcomposer", "assc", &dfm_dmap_assc, DMAP_TYPE_STRING "daap.sortalbumartist", "assl", &dfm_dmap_assl, DMAP_TYPE_STRING "daap.sortname", "assn", &dfm_dmap_assn, DMAP_TYPE_STRING "daap.songstoptime", "assp", &dfm_dmap_assp, DMAP_TYPE_UINT "daap.songsamplerate", "assr", &dfm_dmap_assr, DMAP_TYPE_UINT "daap.songstarttime", "asst", &dfm_dmap_asst, DMAP_TYPE_UINT "daap.sortalbum", "assu", &dfm_dmap_assu, DMAP_TYPE_STRING "daap.songsize", "assz", &dfm_dmap_assz, DMAP_TYPE_UINT "daap.songtrackcount", "astc", &dfm_dmap_astc, DMAP_TYPE_USHORT "daap.songtime", "astm", &dfm_dmap_astm, DMAP_TYPE_UINT "daap.songtracknumber", "astn", &dfm_dmap_astn, DMAP_TYPE_USHORT "daap.songdataurl", "asul", &dfm_dmap_asul, DMAP_TYPE_STRING "daap.songuserrating", "asur", &dfm_dmap_asur, DMAP_TYPE_UBYTE "daap.songyear", "asyr", &dfm_dmap_asyr, DMAP_TYPE_USHORT "daap.serverdatabases", "avdb", &dfm_dmap_avdb, DMAP_TYPE_LIST "dmap.bag", "mbcl", &dfm_dmap_mbcl, DMAP_TYPE_LIST "dmap.contentcodesresponse", "mccr", &dfm_dmap_mccr, DMAP_TYPE_LIST "dmap.contentcodesname", "mcna", &dfm_dmap_mcna, DMAP_TYPE_STRING "dmap.contentcodesnumber", "mcnm", &dfm_dmap_mcnm, DMAP_TYPE_UINT "dmap.container", "mcon", &dfm_dmap_mcon, DMAP_TYPE_LIST "dmap.containercount", "mctc", &dfm_dmap_mctc, DMAP_TYPE_UINT "dmap.containeritemid", "mcti", &dfm_dmap_mcti, DMAP_TYPE_UINT "dmap.contentcodestype", "mcty", &dfm_dmap_mcty, DMAP_TYPE_USHORT "dmap.dictionary", "mdcl", &dfm_dmap_mdcl, DMAP_TYPE_LIST "dmap.itemid", "miid", &dfm_dmap_miid, DMAP_TYPE_UINT "dmap.itemkind", "mikd", &dfm_dmap_mikd, DMAP_TYPE_UBYTE "dmap.itemcount", "mimc", &dfm_dmap_mimc, DMAP_TYPE_UINT "dmap.itemname", "minm", &dfm_dmap_minm, DMAP_TYPE_STRING "dmap.listing", "mlcl", &dfm_dmap_mlcl, DMAP_TYPE_LIST "dmap.sessionid", "mlid", &dfm_dmap_mlid, DMAP_TYPE_UINT "dmap.listingitem", "mlit", &dfm_dmap_mlit, DMAP_TYPE_LIST "dmap.loginresponse", "mlog", &dfm_dmap_mlog, DMAP_TYPE_LIST "dmap.parentcontainerid", "mpco", &dfm_dmap_mpco, DMAP_TYPE_UINT "dmap.persistentid", "mper", &dfm_dmap_mper, DMAP_TYPE_ULONG "dmap.protocolversion", "mpro", &dfm_dmap_mpro, DMAP_TYPE_VERSION "dmap.returnedcount", "mrco", &dfm_dmap_mrco, DMAP_TYPE_UINT "dmap.supportsautologout", "msal", &dfm_dmap_msal, DMAP_TYPE_UBYTE "dmap.authenticationschemes", "msas", &dfm_dmap_msas, DMAP_TYPE_UINT "dmap.authenticationmethod", "msau", &dfm_dmap_msau, DMAP_TYPE_UBYTE "dmap.supportsbrowse", "msbr", &dfm_dmap_msbr, DMAP_TYPE_UBYTE "dmap.databasescount", "msdc", &dfm_dmap_msdc, DMAP_TYPE_UINT "dmap.supportsextensions", "msex", &dfm_dmap_msex, DMAP_TYPE_UBYTE "dmap.supportsindex", "msix", &dfm_dmap_msix, DMAP_TYPE_UBYTE "dmap.loginrequired", "mslr", &dfm_dmap_mslr, DMAP_TYPE_UBYTE "dmap.supportspersistentids", "mspi", &dfm_dmap_mspi, DMAP_TYPE_UBYTE "dmap.supportsquery", "msqy", &dfm_dmap_msqy, DMAP_TYPE_UBYTE "dmap.supportsresolve", "msrs", &dfm_dmap_msrs, DMAP_TYPE_UBYTE "dmap.serverinforesponse", "msrv", &dfm_dmap_msrv, DMAP_TYPE_LIST "dmap.timeoutinterval", "mstm", &dfm_dmap_mstm, DMAP_TYPE_UINT "dmap.statusstring", "msts", &dfm_dmap_msts, DMAP_TYPE_STRING "dmap.status", "mstt", &dfm_dmap_mstt, DMAP_TYPE_UINT "dmap.supportsupdate", "msup", &dfm_dmap_msup, DMAP_TYPE_UBYTE "dmap.specifiedtotalcount", "mtco", &dfm_dmap_mtco, DMAP_TYPE_UINT "dmap.deletedidlisting", "mudl", &dfm_dmap_mudl, DMAP_TYPE_LIST "dmap.updateresponse", "mupd", &dfm_dmap_mupd, DMAP_TYPE_LIST "dmap.serverrevision", "musr", &dfm_dmap_musr, DMAP_TYPE_UINT "dmap.updatetype", "muty", &dfm_dmap_muty, DMAP_TYPE_UBYTE %% /* Sort fields */ /* "dmap.sortingheaderlisting", "mshl", &dfm_dmap_mshl, DMAP_TYPE_UINT "dmap.sortingheaderchar", "mshc", &dfm_dmap_mshc, DMAP_TYPE_SHORT "dmap.sortingheaderindex", "mshi", &dfm_dmap_mshi, DMAP_TYPE_UINT "dmap.sortingheadernumber", "mshn", &dfm_dmap_mshn, DMAP_TYPE_UINT */ /* Unsupported DMAP fields */ /* "com.apple.itunes.adam-ids-array", "aeAD", &dfm_dmap_aeAD, DMAP_TYPE_LIST "com.apple.itunes.content-rating", "aeCR", &dfm_dmap_aeCR, DMAP_TYPE_STRING "com.apple.itunes.drm-platform-id", "aeDP", &dfm_dmap_aeDP, DMAP_TYPE_UINT "com.apple.itunes.drm-user-id", "aeDR", &dfm_dmap_aeDR, DMAP_TYPE_ULONG "com.apple.itunes.drm-versions", "aeDV", &dfm_dmap_aeDV, DMAP_TYPE_UINT "com.apple.itunes.gapless-enc-dr", "aeGD", &dfm_dmap_aeGD, DMAP_TYPE_UINT "com.apple.itunes.gapless-enc-del", "aeGE", &dfm_dmap_aeGE, DMAP_TYPE_UINT "com.apple.itunes.gapless-heur", "aeGH", &dfm_dmap_aeGH, DMAP_TYPE_UINT "com.apple.itunes.gapless-resy", "aeGR", &dfm_dmap_aeGR, DMAP_TYPE_ULONG "com.apple.itunes.gapless-dur", "aeGU", &dfm_dmap_aeGU, DMAP_TYPE_ULONG "com.apple.itunes.is-hd-video", "aeHD", &dfm_dmap_aeHD, DMAP_TYPE_UBYTE "com.apple.itunes.drm-key1-id", "aeK1", &dfm_dmap_aeK1, DMAP_TYPE_ULONG "com.apple.itunes.drm-key2-id", "aeK2", &dfm_dmap_aeK2, DMAP_TYPE_ULONG "com.apple.itunes.non-drm-user-id", "aeND", &dfm_dmap_aeND, DMAP_TYPE_ULONG "com.apple.itunes.store-pers-id", "aeSE", &dfm_dmap_aeSE, DMAP_TYPE_ULONG "com.apple.itunes.saved-genius", "aeSG", &dfm_dmap_aeSG, DMAP_TYPE_UBYTE "com.apple.itunes.xid", "aeXD", &dfm_dmap_aeXD, DMAP_TYPE_STRING "daap.bookmarkable", "asbk", &dfm_dmap_asbk, DMAP_TYPE_UBYTE "daap.songbookmark", "asbo", &dfm_dmap_asbo, DMAP_TYPE_UINT "daap.songdatepurchased", "asdp", &dfm_dmap_asdp, DMAP_TYPE_DATE "daap.songgapless", "asgp", &dfm_dmap_asgp, DMAP_TYPE_UBYTE "daap.songlongsize", "asls", &dfm_dmap_asls, DMAP_TYPE_ULONG "daap.songpodcasturl", "aspu", &dfm_dmap_aspu, DMAP_TYPE_STRING "daap.sortseriesname", "asss", &dfm_dmap_asss, DMAP_TYPE_STRING "daap.supportsextradata", "ated", &dfm_dmap_ated, DMAP_TYPE_USHORT "com.apple.itunes.jukebox-client-vote", "ceJC", &dfm_dmap_ceJC, DMAP_TYPE_BYTE "com.apple.itunes.jukebox-current", "ceJI", &dfm_dmap_ceJI, DMAP_TYPE_UINT "com.apple.itunes.jukebox-score", "ceJS", &dfm_dmap_ceJS, DMAP_TYPE_SHORT "com.apple.itunes.jukebox-vote", "ceJV", &dfm_dmap_ceJV, DMAP_TYPE_UINT "dmap.editcommandssupported", "meds", &dfm_dmap_meds, DMAP_TYPE_UINT "dmap.utctime", "mstc", &dfm_dmap_mstc, DMAP_TYPE_DATE "dmap.utcoffset", "msto", &dfm_dmap_msto, DMAP_TYPE_INT */ /* DMAP test fields */ /* "test.container", "TEST", &dfm_dmap_TEST, DMAP_TYPE_LIST "test.ubyte", "TST1", &dfm_dmap_TST1, DMAP_TYPE_UBYTE "test.byte", "TST2", &dfm_dmap_TST2, DMAP_TYPE_BYTE "test.ushort", "TST3", &dfm_dmap_TST3, DMAP_TYPE_USHORT "test.short", "TST4", &dfm_dmap_TST4, DMAP_TYPE_SHORT "test.uint", "TST5", &dfm_dmap_TST5, DMAP_TYPE_UINT "test.int", "TST6", &dfm_dmap_TST6, DMAP_TYPE_INT "test.ulong", "TST7", &dfm_dmap_TST7, DMAP_TYPE_ULONG "test.long", "TST8", &dfm_dmap_TST8, DMAP_TYPE_LONG "test.string", "TST9", &dfm_dmap_TST9, DMAP_TYPE_STRING */ forked-daapd-25.0/src/evrtsp/000077500000000000000000000000001313447753700161115ustar00rootroot00000000000000forked-daapd-25.0/src/evrtsp/evrtsp.h000066400000000000000000000125011313447753700176040ustar00rootroot00000000000000/* * Copyright (C) 2010 Julien BLACHE * Based on evhttp from libevent 1.4.x * * Copyright (c) 2000-2004 Niels Provos * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef _EVRTSP_H_ #define _EVRTSP_H_ #include #ifdef __cplusplus extern "C" { #endif #ifdef WIN32 #define WIN32_LEAN_AND_MEAN #include #include #undef WIN32_LEAN_AND_MEAN #endif /* Response codes */ #define RTSP_OK 200 #define RTSP_UNAUTHORIZED 401 #define RTSP_FORBIDDEN 403 struct evrtsp_connection; /* * Interfaces for making requests */ enum evrtsp_cmd_type { EVRTSP_REQ_ANNOUNCE, EVRTSP_REQ_OPTIONS, EVRTSP_REQ_SETUP, EVRTSP_REQ_RECORD, EVRTSP_REQ_PAUSE, EVRTSP_REQ_GET_PARAMETER, EVRTSP_REQ_SET_PARAMETER, EVRTSP_REQ_FLUSH, EVRTSP_REQ_TEARDOWN, EVRTSP_REQ_POST, }; enum evrtsp_request_kind { EVRTSP_REQUEST, EVRTSP_RESPONSE }; struct evrtsp_request { #if defined(TAILQ_ENTRY) TAILQ_ENTRY(evrtsp_request) next; #else struct { struct evrtsp_request *tqe_next; struct evrtsp_request **tqe_prev; } next; #endif /* the connection object that this request belongs to */ struct evrtsp_connection *evcon; int flags; #define EVRTSP_REQ_OWN_CONNECTION 0x0001 struct evkeyvalq *input_headers; struct evkeyvalq *output_headers; enum evrtsp_request_kind kind; enum evrtsp_cmd_type type; char *uri; /* uri after RTSP request was parsed */ char major; /* RTSP Major number */ char minor; /* RTSP Minor number */ int response_code; /* RTSP Response code */ char *response_code_line; /* Readable response */ struct evbuffer *input_buffer; /* read data */ ev_int64_t ntoread; struct evbuffer *output_buffer; /* outgoing post or data */ /* Callback */ void (*cb)(struct evrtsp_request *, void *); void *cb_arg; }; /** * Creates a new request object that needs to be filled in with the request * parameters. The callback is executed when the request completed or an * error occurred. */ struct evrtsp_request *evrtsp_request_new( void (*cb)(struct evrtsp_request *, void *), void *arg); /** Frees the request object and removes associated events. */ void evrtsp_request_free(struct evrtsp_request *req); /** * A connection object that can be used to for making RTSP requests. The * connection object tries to establish the connection when it is given an * rtsp request object. */ struct evrtsp_connection *evrtsp_connection_new( const char *address, unsigned short port); /** Frees an rtsp connection */ void evrtsp_connection_free(struct evrtsp_connection *evcon); /** Set a callback for connection close. */ void evrtsp_connection_set_closecb(struct evrtsp_connection *evcon, void (*)(struct evrtsp_connection *, void *), void *); /** * Associates an event base with the connection - can only be called * on a freshly created connection object that has not been used yet. */ void evrtsp_connection_set_base(struct evrtsp_connection *evcon, struct event_base *base); /** Get the remote address and port associated with this connection. */ void evrtsp_connection_get_peer(struct evrtsp_connection *evcon, char **address, u_short *port); /** Get the local address, port and family associated with this connection. */ void evrtsp_connection_get_local_address(struct evrtsp_connection *evcon, char **address, u_short *port, int *family); /** The connection gets ownership of the request */ int evrtsp_make_request(struct evrtsp_connection *evcon, struct evrtsp_request *req, enum evrtsp_cmd_type type, const char *uri); const char *evrtsp_request_uri(struct evrtsp_request *req); /* Interfaces for dealing with headers */ const char *evrtsp_find_header(const struct evkeyvalq *, const char *); int evrtsp_remove_header(struct evkeyvalq *, const char *); int evrtsp_add_header(struct evkeyvalq *, const char *, const char *); void evrtsp_clear_headers(struct evkeyvalq *); const char *evrtsp_method(enum evrtsp_cmd_type type); #ifdef __cplusplus } #endif #endif /* !_EVRTSP_H_ */ forked-daapd-25.0/src/evrtsp/log.h000066400000000000000000000041521313447753700170450ustar00rootroot00000000000000/* * Copyright (c) 2000-2004 Niels Provos * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef _LOG_H_ #define _LOG_H_ #ifdef __GNUC__ #define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b))) #else #define EV_CHECK_FMT(a,b) #endif void event_err(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3); void event_warn(const char *fmt, ...) EV_CHECK_FMT(1,2); void event_errx(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3); void event_warnx(const char *fmt, ...) EV_CHECK_FMT(1,2); void event_msgx(const char *fmt, ...) EV_CHECK_FMT(1,2); void _event_debugx(const char *fmt, ...) EV_CHECK_FMT(1,2); #ifdef USE_DEBUG #define event_debug(x) _event_debugx x #else #define event_debug(x) do {;} while (0) #endif #undef EV_CHECK_FMT #endif forked-daapd-25.0/src/evrtsp/rtsp-internal.h000066400000000000000000000060741313447753700210730ustar00rootroot00000000000000/* * Copyright (C) 2010 Julien BLACHE * Based on evhttp from libevent 1.4.x * * Copyright 2001 Niels Provos * All rights reserved. * * This header file contains definitions for dealing with RTSP requests * that are internal to libevent. As user of the library, you should not * need to know about these. */ #ifndef _RTSP_H_ #define _RTSP_H_ #include #include #define RTSP_CONNECT_TIMEOUT 45 #define RTSP_WRITE_TIMEOUT 50 #define RTSP_READ_TIMEOUT 50 #define RTSP_PREFIX "rtsp://" enum message_read_status { ALL_DATA_READ = 1, MORE_DATA_EXPECTED = 0, DATA_CORRUPTED = -1, REQUEST_CANCELED = -2 }; enum evrtsp_connection_error { EVCON_RTSP_TIMEOUT, EVCON_RTSP_EOF, EVCON_RTSP_INVALID_HEADER }; struct evbuffer; struct addrinfo; struct evrtsp_request; /* A stupid connection object - maybe make this a bufferevent later */ enum evrtsp_connection_state { EVCON_DISCONNECTED, /**< not currently connected not trying either*/ EVCON_CONNECTING, /**< tries to currently connect */ EVCON_IDLE, /**< connection is established */ EVCON_READING_FIRSTLINE,/**< reading Request-Line (incoming conn) or **< Status-Line (outgoing conn) */ EVCON_READING_HEADERS, /**< reading request/response headers */ EVCON_READING_BODY, /**< reading request/response body */ EVCON_READING_TRAILER, /**< reading request/response chunked trailer */ EVCON_WRITING /**< writing request/response headers/body */ }; struct event_base; struct evrtsp_connection { int fd; struct event ev; struct event close_ev; struct evbuffer *input_buffer; struct evbuffer *output_buffer; char *bind_address; /* address to use for binding the src */ u_short bind_port; /* local port for binding the src */ char *address; /* address to connect to */ int family; u_short port; int flags; #define EVRTSP_CON_CLOSEDETECT 0x0004 /* detecting if persistent close */ int timeout; /* timeout in seconds for events */ enum evrtsp_connection_state state; int cseq; TAILQ_HEAD(evcon_requestq, evrtsp_request) requests; void (*cb)(struct evrtsp_connection *, void *); void *cb_arg; void (*closecb)(struct evrtsp_connection *, void *); void *closecb_arg; struct event_base *base; }; /* resets the connection; can be reused for more requests */ void evrtsp_connection_reset(struct evrtsp_connection *); /* connects if necessary */ int evrtsp_connection_connect(struct evrtsp_connection *); /* notifies the current request that it failed; resets connection */ void evrtsp_connection_fail(struct evrtsp_connection *, enum evrtsp_connection_error error); int evrtsp_hostportfile(char *, char **, u_short *, char **); int evrtsp_parse_firstline(struct evrtsp_request *, struct evbuffer*); int evrtsp_parse_headers(struct evrtsp_request *, struct evbuffer*); void evrtsp_start_read(struct evrtsp_connection *); void evrtsp_make_header(struct evrtsp_connection *, struct evrtsp_request *); void evrtsp_write_buffer(struct evrtsp_connection *, void (*)(struct evrtsp_connection *, void *), void *); #endif /* _RTSP_H */ forked-daapd-25.0/src/evrtsp/rtsp.c000066400000000000000000001232771313447753700172610ustar00rootroot00000000000000/* * Copyright (C) 2010 Julien BLACHE * Based on evhttp from libevent 1.4.x * * Copyright (c) 2002-2006 Niels Provos * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #undef timeout_pending #undef timeout_initialized #include "evrtsp.h" /* #define USE_DEBUG */ #include "log.h" #include "rtsp-internal.h" // For compability with libevent 2.0 (HAVE_LIBEVENT2_OLD) #if defined(_EVENT_HAVE_GETNAMEINFO) # define EVENT__HAVE_GETNAMEINFO 1 #endif #if defined(_EVENT_HAVE_GETADDRINFO) # define EVENT__HAVE_GETADDRINFO 1 #endif #if defined(_EVENT_HAVE_STRSEP) # define EVENT__HAVE_STRSEP 1 #endif #ifndef EVENT__HAVE_GETNAMEINFO #define NI_MAXSERV 32 #define NI_MAXHOST 1025 #define NI_NUMERICHOST 1 #define NI_NUMERICSERV 2 static int fake_getnameinfo(const struct sockaddr *sa, size_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags) { struct sockaddr_in *sin = (struct sockaddr_in *)sa; int ret; if (serv != NULL) { char tmpserv[16]; evutil_snprintf(tmpserv, sizeof(tmpserv), "%d", ntohs(sin->sin_port)); ret = evutil_snprintf(serv, servlen, "%s", tmpserv); if ((ret < 0) || (ret >= servlen)) return (-1); } if (host != NULL) { if (flags & NI_NUMERICHOST) { ret = evutil_snprintf(host, hostlen, "%s", inet_ntoa(sin->sin_addr)); if ((ret < 0) || (ret >= hostlen)) return (-1); else return (0); } else { struct hostent *hp; hp = gethostbyaddr((char *)&sin->sin_addr, sizeof(struct in_addr), AF_INET); if (hp == NULL) return (-2); ret = evutil_snprintf(host, hostlen, "%s", hp->h_name); if ((ret < 0) || (ret >= hostlen)) return (-1); else return (0); } } return (0); } #endif #ifndef EVENT__HAVE_GETADDRINFO struct addrinfo { int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; struct sockaddr *ai_addr; struct addrinfo *ai_next; }; static int fake_getaddrinfo(const char *hostname, struct addrinfo *ai) { struct hostent *he = NULL; struct sockaddr_in *sa; if (hostname) { he = gethostbyname(hostname); if (!he) return (-1); } ai->ai_family = he ? he->h_addrtype : AF_INET; ai->ai_socktype = SOCK_STREAM; ai->ai_protocol = 0; ai->ai_addrlen = sizeof(struct sockaddr_in); if (NULL == (ai->ai_addr = malloc(ai->ai_addrlen))) return (-1); sa = (struct sockaddr_in*)ai->ai_addr; memset(sa, 0, ai->ai_addrlen); if (he) { sa->sin_family = he->h_addrtype; memcpy(&sa->sin_addr, he->h_addr_list[0], he->h_length); } else { sa->sin_family = AF_INET; sa->sin_addr.s_addr = INADDR_ANY; } ai->ai_next = NULL; return (0); } static void fake_freeaddrinfo(struct addrinfo *ai) { free(ai->ai_addr); } #endif #ifndef MIN #define MIN(a,b) (((a)<(b))?(a):(b)) #endif /* wrapper for setting the base from the rtsp server */ #define EVRTSP_BASE_SET(x, y) do { \ if ((x)->base != NULL) event_base_set((x)->base, y); \ } while (0) extern int debug; static int socket_connect(int fd, const char *address, unsigned short port); static int bind_socket_ai(int family, struct addrinfo *, int reuse); static int bind_socket(int family, const char *, u_short, int reuse); static void name_from_addr(struct sockaddr *, socklen_t, char **, char **); static void evrtsp_connection_start_detectclose( struct evrtsp_connection *evcon); static void evrtsp_connection_stop_detectclose( struct evrtsp_connection *evcon); static void evrtsp_request_dispatch(struct evrtsp_connection* evcon); static void evrtsp_read_firstline(struct evrtsp_connection *evcon, struct evrtsp_request *req); static void evrtsp_read_header(struct evrtsp_connection *evcon, struct evrtsp_request *req); static int evrtsp_add_header_internal(struct evkeyvalq *headers, const char *key, const char *value); void evrtsp_read(int, short, void *); void evrtsp_write(int, short, void *); #ifndef EVENT__HAVE_STRSEP /* strsep replacement for platforms that lack it. Only works if * del is one character long. */ static char * strsep(char **s, const char *del) { char *d, *tok; assert(strlen(del) == 1); if (!s || !*s) return NULL; tok = *s; d = strstr(tok, del); if (d) { *d = '\0'; *s = d + 1; } else *s = NULL; return tok; } #endif const char * evrtsp_method(enum evrtsp_cmd_type type) { const char *method; switch (type) { case EVRTSP_REQ_ANNOUNCE: method = "ANNOUNCE"; break; case EVRTSP_REQ_OPTIONS: method = "OPTIONS"; break; case EVRTSP_REQ_SETUP: method = "SETUP"; break; case EVRTSP_REQ_RECORD: method = "RECORD"; break; case EVRTSP_REQ_PAUSE: method = "PAUSE"; break; case EVRTSP_REQ_GET_PARAMETER: method = "GET_PARAMETER"; break; case EVRTSP_REQ_SET_PARAMETER: method = "SET_PARAMETER"; break; case EVRTSP_REQ_FLUSH: method = "FLUSH"; break; case EVRTSP_REQ_TEARDOWN: method = "TEARDOWN"; break; case EVRTSP_REQ_POST: method = "POST"; break; default: method = NULL; break; } return (method); } static void evrtsp_add_event(struct event *ev, int timeout, int default_timeout) { if (timeout != 0) { struct timeval tv; evutil_timerclear(&tv); tv.tv_sec = timeout != -1 ? timeout : default_timeout; event_add(ev, &tv); } else { event_add(ev, NULL); } } void evrtsp_write_buffer(struct evrtsp_connection *evcon, void (*cb)(struct evrtsp_connection *, void *), void *arg) { event_debug(("%s: preparing to write buffer", __func__)); /* Set call back */ evcon->cb = cb; evcon->cb_arg = arg; /* check if the event is already pending */ if (event_pending(&evcon->ev, EV_WRITE|EV_TIMEOUT, NULL)) event_del(&evcon->ev); event_assign(&evcon->ev, evcon->base, evcon->fd, EV_WRITE, evrtsp_write, evcon); evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_WRITE_TIMEOUT); } static int evrtsp_connected(struct evrtsp_connection *evcon) { switch (evcon->state) { case EVCON_DISCONNECTED: case EVCON_CONNECTING: return (0); case EVCON_IDLE: case EVCON_READING_FIRSTLINE: case EVCON_READING_HEADERS: case EVCON_READING_BODY: case EVCON_READING_TRAILER: case EVCON_WRITING: default: return (1); } } /* * Create the headers needed for an RTSP request */ static void evrtsp_make_header_request(struct evrtsp_connection *evcon, struct evrtsp_request *req) { const char *method; /* Generate request line */ method = evrtsp_method(req->type); evbuffer_add_printf(evcon->output_buffer, "%s %s RTSP/%d.%d\r\n", method, req->uri, req->major, req->minor); /* Content-Length is mandatory, absent means 0 */ if ((evbuffer_get_length(req->output_buffer) > 0) && (evrtsp_find_header(req->output_headers, "Content-Length") == NULL)) { char size[12]; evutil_snprintf(size, sizeof(size), "%ld", (long)evbuffer_get_length(req->output_buffer)); evrtsp_add_header(req->output_headers, "Content-Length", size); } } void evrtsp_make_header(struct evrtsp_connection *evcon, struct evrtsp_request *req) { struct evkeyval *header; evrtsp_make_header_request(evcon, req); TAILQ_FOREACH(header, req->output_headers, next) { evbuffer_add_printf(evcon->output_buffer, "%s: %s\r\n", header->key, header->value); } evbuffer_add(evcon->output_buffer, "\r\n", 2); if (evbuffer_get_length(req->output_buffer) > 0) { evbuffer_add_buffer(evcon->output_buffer, req->output_buffer); } } /* Separated host, port and file from URI */ int /* FIXME: needed? */ evrtsp_hostportfile(char *url, char **phost, u_short *pport, char **pfile) { /* XXX not threadsafe. */ static char host[1024]; static char file[1024]; char *p; const char *p2; int len; int ret; u_short port; len = strlen(RTSP_PREFIX); if (strncasecmp(url, RTSP_PREFIX, len)) return (-1); url += len; /* We might overrun */ ret = evutil_snprintf(host, sizeof(host), "%s", url); if ((ret < 0) || (ret >= sizeof(host))) return (-1); p = strchr(host, '/'); if (p != NULL) { *p = '\0'; p2 = p + 1; } else p2 = NULL; if (pfile != NULL) { /* Generate request file */ if (p2 == NULL) p2 = ""; evutil_snprintf(file, sizeof(file), "/%s", p2); } p = strchr(host, ':'); if (p != NULL) { *p = '\0'; port = atoi(p + 1); if (port == 0) return (-1); } else return -1; if (phost != NULL) *phost = host; if (pport != NULL) *pport = port; if (pfile != NULL) *pfile = file; return (0); } void evrtsp_connection_fail(struct evrtsp_connection *evcon, enum evrtsp_connection_error error) { struct evrtsp_request* req = TAILQ_FIRST(&evcon->requests); void (*cb)(struct evrtsp_request *, void *); void *cb_arg; assert(req != NULL); /* save the callback for later; the cb might free our object */ cb = req->cb; cb_arg = req->cb_arg; TAILQ_REMOVE(&evcon->requests, req, next); evrtsp_request_free(req); /* xxx: maybe we should fail all requests??? */ /* reset the connection */ evrtsp_connection_reset(evcon); /* We are trying the next request that was queued on us */ if (TAILQ_FIRST(&evcon->requests) != NULL) evrtsp_connection_connect(evcon); /* inform the user */ if (cb != NULL) (*cb)(NULL, cb_arg); } void evrtsp_write(int fd, short what, void *arg) { struct evrtsp_connection *evcon = arg; int n; if (what == EV_TIMEOUT) { event_warn("%s: write timeout", __func__); evrtsp_connection_fail(evcon, EVCON_RTSP_TIMEOUT); return; } n = evbuffer_write(evcon->output_buffer, fd); if (n == -1) { event_warn("%s: evbuffer_write", __func__); evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); return; } if (n == 0) { event_warn("%s: write nothing", __func__); evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); return; } if (evbuffer_get_length(evcon->output_buffer) != 0) { evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_WRITE_TIMEOUT); return; } /* Activate our call back */ if (evcon->cb != NULL) (*evcon->cb)(evcon, evcon->cb_arg); } /** * Advance the connection state. * - If this is an outgoing connection, we've just processed the response; * idle or close the connection. */ static void evrtsp_connection_done(struct evrtsp_connection *evcon) { struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); /* idle or close the connection */ TAILQ_REMOVE(&evcon->requests, req, next); req->evcon = NULL; evcon->state = EVCON_IDLE; if (TAILQ_FIRST(&evcon->requests) != NULL) { /* * We have more requests; reset the connection * and deal with the next request. */ if (!evrtsp_connected(evcon)) evrtsp_connection_connect(evcon); else evrtsp_request_dispatch(evcon); } else { /* * The connection is going to be persistent, but we * need to detect if the other side closes it. */ evrtsp_connection_start_detectclose(evcon); } /* notify the user of the request */ (*req->cb)(req, req->cb_arg); evrtsp_request_free(req); } static void /* FIXME: needed? */ evrtsp_read_trailer(struct evrtsp_connection *evcon, struct evrtsp_request *req) { struct evbuffer *buf = evcon->input_buffer; switch (evrtsp_parse_headers(req, buf)) { case DATA_CORRUPTED: event_warn("%s: invalid header", __func__); evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); break; case ALL_DATA_READ: event_del(&evcon->ev); evrtsp_connection_done(evcon); break; case MORE_DATA_EXPECTED: default: evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); break; } } static void evrtsp_read_body(struct evrtsp_connection *evcon, struct evrtsp_request *req) { struct evbuffer *buf = evcon->input_buffer; if (req->ntoread < 0) { /* Read until connection close. */ evbuffer_add_buffer(req->input_buffer, buf); } else if (evbuffer_get_length(buf) >= req->ntoread) { /* Completed content length */ evbuffer_add(req->input_buffer, evbuffer_pullup(buf,-1), (size_t)req->ntoread); evbuffer_drain(buf, (size_t)req->ntoread); req->ntoread = 0; evrtsp_connection_done(evcon); return; } /* Read more! */ event_assign(&evcon->ev, evcon->base, evcon->fd, EV_READ, evrtsp_read, evcon); evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); } /* * Reads data into a buffer structure until no more data * can be read on the file descriptor or we have read all * the data that we wanted to read. * Execute callback when done. */ void evrtsp_read(int fd, short what, void *arg) { struct evrtsp_connection *evcon = arg; struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); struct evbuffer *buf = evcon->input_buffer; int n; if (what == EV_TIMEOUT) { event_warn("%s: read timeout", __func__); evrtsp_connection_fail(evcon, EVCON_RTSP_TIMEOUT); return; } n = evbuffer_read(buf, fd, -1); event_debug(("%s: got %d on %d", __func__, n, fd)); if (n == -1) { if (errno != EINTR && errno != EAGAIN) { event_warn("%s: evbuffer_read", __func__); evrtsp_connection_fail(evcon, EVCON_RTSP_EOF); } else { evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); } return; } else if (n == 0) { /* Connection closed */ evcon->state = EVCON_DISCONNECTED; evrtsp_connection_done(evcon); return; } switch (evcon->state) { case EVCON_READING_FIRSTLINE: evrtsp_read_firstline(evcon, req); break; case EVCON_READING_HEADERS: evrtsp_read_header(evcon, req); break; case EVCON_READING_BODY: evrtsp_read_body(evcon, req); break; case EVCON_READING_TRAILER: evrtsp_read_trailer(evcon, req); break; case EVCON_DISCONNECTED: case EVCON_CONNECTING: case EVCON_IDLE: case EVCON_WRITING: default: event_errx(1, "%s: illegal connection state %d", __func__, evcon->state); } } static void evrtsp_write_connectioncb(struct evrtsp_connection *evcon, void *arg) { /* This is after writing the request to the server */ struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); assert(req != NULL); assert(evcon->state == EVCON_WRITING); /* We are done writing our header and are now expecting the response */ req->kind = EVRTSP_RESPONSE; evrtsp_start_read(evcon); } /* * Clean up a connection object */ void evrtsp_connection_free(struct evrtsp_connection *evcon) { struct evrtsp_request *req; /* notify interested parties that this connection is going down */ if (evcon->fd != -1) { if (evrtsp_connected(evcon) && evcon->closecb != NULL) (*evcon->closecb)(evcon, evcon->closecb_arg); } /* remove all requests that might be queued on this connection */ while ((req = TAILQ_FIRST(&evcon->requests)) != NULL) { TAILQ_REMOVE(&evcon->requests, req, next); evrtsp_request_free(req); } if (event_initialized(&evcon->close_ev)) event_del(&evcon->close_ev); if (event_initialized(&evcon->ev)) event_del(&evcon->ev); if (evcon->fd != -1) EVUTIL_CLOSESOCKET(evcon->fd); if (evcon->bind_address != NULL) free(evcon->bind_address); if (evcon->address != NULL) free(evcon->address); if (evcon->input_buffer != NULL) evbuffer_free(evcon->input_buffer); if (evcon->output_buffer != NULL) evbuffer_free(evcon->output_buffer); free(evcon); } static void evrtsp_request_dispatch(struct evrtsp_connection* evcon) { struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); /* this should not usually happy but it's possible */ if (req == NULL) return; /* delete possible close detection events */ evrtsp_connection_stop_detectclose(evcon); /* we assume that the connection is connected already */ assert(evcon->state == EVCON_IDLE); evcon->state = EVCON_WRITING; /* Create the header from the store arguments */ evrtsp_make_header(evcon, req); evrtsp_write_buffer(evcon, evrtsp_write_connectioncb, NULL); } /* Reset our connection state */ void evrtsp_connection_reset(struct evrtsp_connection *evcon) { if (event_initialized(&evcon->ev)) event_del(&evcon->ev); if (evcon->fd != -1) { /* inform interested parties about connection close */ if (evrtsp_connected(evcon) && evcon->closecb != NULL) (*evcon->closecb)(evcon, evcon->closecb_arg); EVUTIL_CLOSESOCKET(evcon->fd); evcon->fd = -1; } evcon->state = EVCON_DISCONNECTED; evbuffer_drain(evcon->input_buffer, evbuffer_get_length(evcon->input_buffer)); evbuffer_drain(evcon->output_buffer, evbuffer_get_length(evcon->output_buffer)); } static void evrtsp_detect_close_cb(int fd, short what, void *arg) { struct evrtsp_connection *evcon = arg; evrtsp_connection_reset(evcon); } static void evrtsp_connection_start_detectclose(struct evrtsp_connection *evcon) { evcon->flags |= EVRTSP_CON_CLOSEDETECT; if (event_initialized(&evcon->close_ev)) event_del(&evcon->close_ev); event_assign(&evcon->close_ev, evcon->base, evcon->fd, EV_READ, evrtsp_detect_close_cb, evcon); event_add(&evcon->close_ev, NULL); } static void evrtsp_connection_stop_detectclose(struct evrtsp_connection *evcon) { evcon->flags &= ~EVRTSP_CON_CLOSEDETECT; if (event_initialized(&evcon->close_ev)) event_del(&evcon->close_ev); } /* * Call back for asynchronous connection attempt. */ static void evrtsp_connectioncb(int fd, short what, void *arg) { struct evrtsp_connection *evcon = arg; int error; socklen_t errsz = sizeof(error); if (what == EV_TIMEOUT) { event_warnx("%s: connection timeout for \"%s:%d\" on %d", __func__, evcon->address, evcon->port, evcon->fd); goto cleanup; } /* Check if the connection completed */ if (getsockopt(evcon->fd, SOL_SOCKET, SO_ERROR, (void*)&error, &errsz) == -1) { event_warnx("%s: getsockopt for \"%s:%d\" on %d", __func__, evcon->address, evcon->port, evcon->fd); goto cleanup; } if (error) { event_warnx("%s: connect failed for \"%s:%d\" on %d: %s", __func__, evcon->address, evcon->port, evcon->fd, strerror(error)); goto cleanup; } /* We are connected to the server now */ event_debug(("%s: connected to \"%s:%d\" on %d", __func__, evcon->address, evcon->port, evcon->fd)); evcon->state = EVCON_IDLE; /* try to start requests that have queued up on this connection */ evrtsp_request_dispatch(evcon); return; cleanup: evrtsp_connection_reset(evcon); /* for now, we just signal all requests by executing their callbacks */ while (TAILQ_FIRST(&evcon->requests) != NULL) { struct evrtsp_request *request = TAILQ_FIRST(&evcon->requests); TAILQ_REMOVE(&evcon->requests, request, next); request->evcon = NULL; /* we might want to set an error here */ request->cb(request, request->cb_arg); evrtsp_request_free(request); } } /* * Check if we got a valid response code. */ static int evrtsp_valid_response_code(int code) { if (code == 0) return (0); return (1); } /* Parses the status line of an RTSP server */ static int evrtsp_parse_response_line(struct evrtsp_request *req, char *line) { char *protocol; char *number; const char *readable = ""; protocol = strsep(&line, " "); if (line == NULL) return (-1); number = strsep(&line, " "); if (line != NULL) readable = line; if (strcmp(protocol, "RTSP/1.0") == 0) { req->major = 1; req->minor = 0; } else if (strcmp(protocol, "RTSP/1.1") == 0) { req->major = 1; req->minor = 1; } else { event_warnx("%s: bad protocol \"%s\"", __func__, protocol); return (-1); } req->response_code = atoi(number); if (!evrtsp_valid_response_code(req->response_code)) { event_warnx("%s: bad response code \"%s\"", __func__, number); return (-1); } if ((req->response_code_line = strdup(readable)) == NULL) event_err(1, "%s: strdup", __func__); return (0); } const char * evrtsp_find_header(const struct evkeyvalq *headers, const char *key) { struct evkeyval *header; TAILQ_FOREACH(header, headers, next) { if (strcasecmp(header->key, key) == 0) return (header->value); } return (NULL); } void evrtsp_clear_headers(struct evkeyvalq *headers) { struct evkeyval *header; for (header = TAILQ_FIRST(headers); header != NULL; header = TAILQ_FIRST(headers)) { TAILQ_REMOVE(headers, header, next); free(header->key); free(header->value); free(header); } } /* * Returns 0, if the header was successfully removed. * Returns -1, if the header could not be found. */ int evrtsp_remove_header(struct evkeyvalq *headers, const char *key) { struct evkeyval *header; TAILQ_FOREACH(header, headers, next) { if (strcasecmp(header->key, key) == 0) break; } if (header == NULL) return (-1); /* Free and remove the header that we found */ TAILQ_REMOVE(headers, header, next); free(header->key); free(header->value); free(header); return (0); } static int evrtsp_header_is_valid_value(const char *value) { const char *p = value; while ((p = strpbrk(p, "\r\n")) != NULL) { /* we really expect only one new line */ p += strspn(p, "\r\n"); /* we expect a space or tab for continuation */ if (*p != ' ' && *p != '\t') return (0); } return (1); } int evrtsp_add_header(struct evkeyvalq *headers, const char *key, const char *value) { event_debug(("%s: key: %s val: %s", __func__, key, value)); if (strchr(key, '\r') != NULL || strchr(key, '\n') != NULL) { /* drop illegal headers */ event_warn("%s: dropping illegal header key", __func__); return (-1); } if (!evrtsp_header_is_valid_value(value)) { event_warn("%s: dropping illegal header value", __func__); return (-1); } return (evrtsp_add_header_internal(headers, key, value)); } static int evrtsp_add_header_internal(struct evkeyvalq *headers, const char *key, const char *value) { struct evkeyval *header = calloc(1, sizeof(struct evkeyval)); if (header == NULL) { event_warn("%s: calloc", __func__); return (-1); } if ((header->key = strdup(key)) == NULL) { free(header); event_warn("%s: strdup", __func__); return (-1); } if ((header->value = strdup(value)) == NULL) { free(header->key); free(header); event_warn("%s: strdup", __func__); return (-1); } TAILQ_INSERT_TAIL(headers, header, next); return (0); } /* * Parses header lines from a request or a response into the specified * request object given an event buffer. * * Returns * DATA_CORRUPTED on error * MORE_DATA_EXPECTED when we need to read more headers * ALL_DATA_READ when all headers have been read. */ enum message_read_status evrtsp_parse_firstline(struct evrtsp_request *req, struct evbuffer *buffer) { char *line; enum message_read_status status = ALL_DATA_READ; line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_ANY); if (line == NULL) return (MORE_DATA_EXPECTED); switch (req->kind) { case EVRTSP_RESPONSE: if (evrtsp_parse_response_line(req, line) == -1) status = DATA_CORRUPTED; break; default: status = DATA_CORRUPTED; } free(line); return (status); } static int evrtsp_append_to_last_header(struct evkeyvalq *headers, const char *line) { struct evkeyval *header = TAILQ_LAST(headers, evkeyvalq); char *newval; size_t old_len, line_len; if (header == NULL) return (-1); old_len = strlen(header->value); line_len = strlen(line); newval = realloc(header->value, old_len + line_len + 1); if (newval == NULL) return (-1); memcpy(newval + old_len, line, line_len + 1); header->value = newval; return (0); } enum message_read_status evrtsp_parse_headers(struct evrtsp_request *req, struct evbuffer *buffer) { char *line; enum message_read_status status = MORE_DATA_EXPECTED; struct evkeyvalq *headers = req->input_headers; while ((line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_CRLF)) != NULL) { char *skey, *svalue; if (*line == '\0') { /* Last header - Done */ status = ALL_DATA_READ; free(line); break; } /* Check if this is a continuation line */ if (*line == ' ' || *line == '\t') { if (evrtsp_append_to_last_header(headers, line) == -1) goto error; free(line); continue; } /* Processing of header lines */ svalue = line; skey = strsep(&svalue, ":"); if (svalue == NULL) goto error; svalue += strspn(svalue, " "); if (evrtsp_add_header(headers, skey, svalue) == -1) goto error; free(line); } return (status); error: free(line); return (DATA_CORRUPTED); } static int evrtsp_get_body_length(struct evrtsp_request *req) { struct evkeyvalq *headers = req->input_headers; const char *content_length; content_length = evrtsp_find_header(headers, "Content-Length"); if (content_length == NULL) { /* If there is no Content-Length: header, a value of 0 is assumed, per spec. */ req->ntoread = 0; } else { ev_int64_t ntoread = evutil_strtoll(content_length, NULL, 10); if (*content_length == '\0' || ntoread < 0) { event_warnx("%s: illegal content length: %s", __func__, content_length); return (-1); } req->ntoread = ntoread; } event_debug(("%s: bytes to read: %lld (in buffer %zu)", __func__, req->ntoread, evbuffer_get_length(req->evcon->input_buffer))); return (0); } static void evrtsp_get_body(struct evrtsp_connection *evcon, struct evrtsp_request *req) { evcon->state = EVCON_READING_BODY; if (evrtsp_get_body_length(req) == -1) { event_warn("%s: invalid body", __func__); evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); return; } evrtsp_read_body(evcon, req); } static void evrtsp_read_firstline(struct evrtsp_connection *evcon, struct evrtsp_request *req) { enum message_read_status res; res = evrtsp_parse_firstline(req, evcon->input_buffer); if (res == DATA_CORRUPTED) { /* Error while reading, terminate */ event_warnx("%s: bad header lines on %d", __func__, evcon->fd); evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); return; } else if (res == MORE_DATA_EXPECTED) { /* Need more header lines */ evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); return; } evcon->state = EVCON_READING_HEADERS; evrtsp_read_header(evcon, req); } static void evrtsp_read_header(struct evrtsp_connection *evcon, struct evrtsp_request *req) { enum message_read_status res; int fd = evcon->fd; res = evrtsp_parse_headers(req, evcon->input_buffer); if (res == DATA_CORRUPTED) { /* Error while reading, terminate */ event_warnx("%s: bad header lines on %d", __func__, fd); evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); return; } else if (res == MORE_DATA_EXPECTED) { /* Need more header lines */ evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); return; } /* Done reading headers, do the real work */ switch (req->kind) { case EVRTSP_RESPONSE: event_debug(("%s: start of read body on %d", __func__, fd)); evrtsp_get_body(evcon, req); break; default: event_warnx("%s: bad header on %d", __func__, fd); evrtsp_connection_fail(evcon, EVCON_RTSP_INVALID_HEADER); break; } } /* * Creates a TCP connection to the specified port and executes a callback * when finished. Failure or sucess is indicate by the passed connection * object. * * Although this interface accepts a hostname, it is intended to take * only numeric hostnames so that non-blocking DNS resolution can * happen elsewhere. */ struct evrtsp_connection * evrtsp_connection_new(const char *address, unsigned short port) { struct evrtsp_connection *evcon = NULL; char *intf; char *addr; unsigned char scratch[16]; int family; if ((addr = strdup(address)) == NULL) { event_warn("%s: strdup failed", __func__); goto error; } intf = strchr(addr, '%'); if (intf) *intf = '\0'; if (inet_pton(AF_INET6, addr, scratch) == 1) family = AF_INET6; else if (inet_pton(AF_INET, addr, scratch) == 1) family = AF_INET; else { free(addr); event_warn("%s: address is neither IPv6 nor IPv4", __func__); return NULL; } if (intf) *intf = '%'; event_debug(("Attempting connection to %s:%d", address, port)); if ((evcon = calloc(1, sizeof(struct evrtsp_connection))) == NULL) { free(addr); event_warn("%s: calloc failed", __func__); goto error; } evcon->fd = -1; evcon->port = port; evcon->timeout = -1; evcon->cseq = 1; evcon->family = family; evcon->address = addr; if ((evcon->input_buffer = evbuffer_new()) == NULL) { event_warn("%s: evbuffer_new failed", __func__); goto error; } if ((evcon->output_buffer = evbuffer_new()) == NULL) { event_warn("%s: evbuffer_new failed", __func__); goto error; } evcon->state = EVCON_DISCONNECTED; TAILQ_INIT(&evcon->requests); return (evcon); error: if (evcon != NULL) evrtsp_connection_free(evcon); return (NULL); } void evrtsp_connection_set_base(struct evrtsp_connection *evcon, struct event_base *base) { assert(evcon->base == NULL); assert(evcon->state == EVCON_DISCONNECTED); evcon->base = base; } void evrtsp_connection_set_timeout(struct evrtsp_connection *evcon, int timeout_in_secs) { evcon->timeout = timeout_in_secs; } void evrtsp_connection_set_closecb(struct evrtsp_connection *evcon, void (*cb)(struct evrtsp_connection *, void *), void *cbarg) { evcon->closecb = cb; evcon->closecb_arg = cbarg; } void evrtsp_connection_get_local_address(struct evrtsp_connection *evcon, char **address, u_short *port, int *family) { union { struct sockaddr_storage ss; struct sockaddr sa; struct sockaddr_in sin; struct sockaddr_in6 sin6; } addr; socklen_t slen; int ret; *address = NULL; *port = 0; if (!evrtsp_connected(evcon)) return; slen = sizeof(struct sockaddr_storage); ret = getsockname(evcon->fd, &addr.sa, &slen); if (ret < 0) return; name_from_addr(&addr.sa, slen, address, NULL); if (!*address) return; *family = addr.ss.ss_family; switch (*family) { case AF_INET: *port = ntohs(addr.sin.sin_port); break; #ifdef AF_INET6 case AF_INET6: *port = ntohs(addr.sin6.sin6_port); break; #endif default: free(*address); address = NULL; event_err(1, "%s: unhandled address family\n", __func__); return; } } void evrtsp_connection_get_peer(struct evrtsp_connection *evcon, char **address, u_short *port) { *address = evcon->address; *port = evcon->port; } int evrtsp_connection_connect(struct evrtsp_connection *evcon) { if (evcon->state == EVCON_CONNECTING) return (0); evrtsp_connection_reset(evcon); evcon->fd = bind_socket(evcon->family, evcon->bind_address, evcon->bind_port, 0 /*reuse*/); if (evcon->fd == -1) { event_warnx("%s: failed to bind to \"%s\"", __func__, evcon->bind_address); return (-1); } if (socket_connect(evcon->fd, evcon->address, evcon->port) == -1) { EVUTIL_CLOSESOCKET(evcon->fd); evcon->fd = -1; return (-1); } /* Set up a callback for successful connection setup */ event_assign(&evcon->ev, evcon->base, evcon->fd, EV_WRITE, evrtsp_connectioncb, evcon); evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_CONNECT_TIMEOUT); evcon->state = EVCON_CONNECTING; return (0); } /* * Starts an RTSP request on the provided evrtsp_connection object. * If the connection object is not connected to the server already, * this will start the connection. */ int evrtsp_make_request(struct evrtsp_connection *evcon, struct evrtsp_request *req, enum evrtsp_cmd_type type, const char *uri) { event_debug(("%s: TEST", __func__)); /* We are making a request */ req->kind = EVRTSP_REQUEST; req->type = type; if (req->uri != NULL) free(req->uri); if ((req->uri = strdup(uri)) == NULL) event_err(1, "%s: strdup", __func__); /* Set the protocol version if it is not supplied */ if (!req->major && !req->minor) { req->major = 1; req->minor = 0; } assert(req->evcon == NULL); req->evcon = evcon; assert(!(req->flags & EVRTSP_REQ_OWN_CONNECTION)); TAILQ_INSERT_TAIL(&evcon->requests, req, next); /* If the connection object is not connected; make it so */ if (!evrtsp_connected(evcon)) return (evrtsp_connection_connect(evcon)); /* * If it's connected already and we are the first in the queue, * then we can dispatch this request immediately. Otherwise, it * will be dispatched once the pending requests are completed. */ if (TAILQ_FIRST(&evcon->requests) == req) evrtsp_request_dispatch(evcon); return (0); } /* * Reads data from file descriptor into request structure * Request structure needs to be set up correctly. */ void evrtsp_start_read(struct evrtsp_connection *evcon) { /* Set up an event to read the headers */ if (event_initialized(&evcon->ev)) event_del(&evcon->ev); event_assign(&evcon->ev, evcon->base, evcon->fd, EV_READ, evrtsp_read, evcon); evrtsp_add_event(&evcon->ev, evcon->timeout, RTSP_READ_TIMEOUT); evcon->state = EVCON_READING_FIRSTLINE; } static void evrtsp_send_done(struct evrtsp_connection *evcon, void *arg) { struct evrtsp_request *req = TAILQ_FIRST(&evcon->requests); TAILQ_REMOVE(&evcon->requests, req, next); /* delete possible close detection events */ evrtsp_connection_stop_detectclose(evcon); assert(req->flags & EVRTSP_REQ_OWN_CONNECTION); evrtsp_request_free(req); } /* Requires that headers and response code are already set up */ static inline void evrtsp_send(struct evrtsp_request *req, struct evbuffer *databuf) { struct evrtsp_connection *evcon = req->evcon; if (evcon == NULL) { evrtsp_request_free(req); return; } assert(TAILQ_FIRST(&evcon->requests) == req); /* xxx: not sure if we really should expose the data buffer this way */ if (databuf != NULL) evbuffer_add_buffer(req->output_buffer, databuf); /* Adds headers to the response */ evrtsp_make_header(evcon, req); evrtsp_write_buffer(evcon, evrtsp_send_done, NULL); } /* * Request related functions */ struct evrtsp_request * evrtsp_request_new(void (*cb)(struct evrtsp_request *, void *), void *arg) { struct evrtsp_request *req = NULL; /* Allocate request structure */ if ((req = calloc(1, sizeof(struct evrtsp_request))) == NULL) { event_warn("%s: calloc", __func__); goto error; } req->kind = EVRTSP_RESPONSE; req->input_headers = calloc(1, sizeof(struct evkeyvalq)); if (req->input_headers == NULL) { event_warn("%s: calloc", __func__); goto error; } TAILQ_INIT(req->input_headers); req->output_headers = calloc(1, sizeof(struct evkeyvalq)); if (req->output_headers == NULL) { event_warn("%s: calloc", __func__); goto error; } TAILQ_INIT(req->output_headers); if ((req->input_buffer = evbuffer_new()) == NULL) { event_warn("%s: evbuffer_new", __func__); goto error; } if ((req->output_buffer = evbuffer_new()) == NULL) { event_warn("%s: evbuffer_new", __func__); goto error; } req->cb = cb; req->cb_arg = arg; return (req); error: if (req != NULL) evrtsp_request_free(req); return (NULL); } void evrtsp_request_free(struct evrtsp_request *req) { if (req->uri != NULL) free(req->uri); if (req->response_code_line != NULL) free(req->response_code_line); evrtsp_clear_headers(req->input_headers); free(req->input_headers); evrtsp_clear_headers(req->output_headers); free(req->output_headers); if (req->input_buffer != NULL) evbuffer_free(req->input_buffer); if (req->output_buffer != NULL) evbuffer_free(req->output_buffer); free(req); } /* * Allows for inspection of the request URI */ const char * evrtsp_request_uri(struct evrtsp_request *req) { if (req->uri == NULL) event_warn("%s: request has no uri", __func__); return (req->uri); } /* * Network helper functions that we do not want to export to the rest of * the world. */ #if 0 /* Unused */ static struct addrinfo * addr_from_name(char *address) { #ifdef EVENT__HAVE_GETADDRINFO struct addrinfo ai, *aitop; int ai_result; memset(&ai, 0, sizeof(ai)); ai.ai_family = AF_INET; ai.ai_socktype = SOCK_RAW; ai.ai_flags = 0; if ((ai_result = getaddrinfo(address, NULL, &ai, &aitop)) != 0) { if ( ai_result == EAI_SYSTEM ) event_warn("getaddrinfo"); else event_warnx("getaddrinfo: %s", gai_strerror(ai_result)); } return (aitop); #else assert(0); return NULL; /* XXXXX Use gethostbyname, if this function is ever used. */ #endif } #endif static void name_from_addr(struct sockaddr *sa, socklen_t salen, char **phost, char **pport) { char ntop[NI_MAXHOST]; char strport[NI_MAXSERV]; int ni_result; #ifdef EVENT__HAVE_GETNAMEINFO ni_result = getnameinfo(sa, salen, ntop, sizeof(ntop), strport, sizeof(strport), NI_NUMERICHOST|NI_NUMERICSERV); if (ni_result != 0) { if (ni_result == EAI_SYSTEM) event_err(1, "getnameinfo failed"); else event_errx(1, "getnameinfo failed: %s", gai_strerror(ni_result)); return; } #else ni_result = fake_getnameinfo(sa, salen, ntop, sizeof(ntop), strport, sizeof(strport), NI_NUMERICHOST|NI_NUMERICSERV); if (ni_result != 0) return; #endif if (phost) *phost = strdup(ntop); if (pport) *pport = strdup(strport); } /* Create a non-blocking socket and bind it */ /* todo: rename this function */ static int bind_socket_ai(int family, struct addrinfo *ai, int reuse) { int fd, on = 1, r; int serrno; if (ai) family = ai->ai_family; /* Create listen socket */ fd = socket(family, SOCK_STREAM, 0); if (fd == -1) { event_warn("socket"); return (-1); } if (evutil_make_socket_nonblocking(fd) < 0) goto out; #ifndef WIN32 if (fcntl(fd, F_SETFD, 1) == -1) { event_warn("fcntl(F_SETFD)"); goto out; } #endif if (family == AF_INET6) setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)); setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, sizeof(on)); if (reuse) { setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on)); } if (ai != NULL) { r = bind(fd, ai->ai_addr, ai->ai_addrlen); if (r == -1) goto out; } return (fd); out: serrno = EVUTIL_SOCKET_ERROR(); EVUTIL_CLOSESOCKET(fd); EVUTIL_SET_SOCKET_ERROR(serrno); return (-1); } static struct addrinfo * make_addrinfo(const char *address, u_short port) { struct addrinfo *aitop = NULL; #ifdef EVENT__HAVE_GETADDRINFO struct addrinfo ai; char strport[NI_MAXSERV]; int ai_result; memset(&ai, 0, sizeof(ai)); ai.ai_family = AF_UNSPEC; ai.ai_socktype = SOCK_STREAM; ai.ai_flags = AI_PASSIVE; /* turn NULL host name into INADDR_ANY */ evutil_snprintf(strport, sizeof(strport), "%d", port); if ((ai_result = getaddrinfo(address, strport, &ai, &aitop)) != 0) { if ( ai_result == EAI_SYSTEM ) event_warn("getaddrinfo"); else event_warnx("getaddrinfo: %s", gai_strerror(ai_result)); return (NULL); } #else static int cur; static struct addrinfo ai[2]; /* We will be returning the address of some of this memory so it has to last even after this call. */ if (++cur == 2) cur = 0; /* allow calling this function twice */ if (fake_getaddrinfo(address, &ai[cur]) < 0) { event_warn("fake_getaddrinfo"); return (NULL); } aitop = &ai[cur]; ((struct sockaddr_in *) aitop->ai_addr)->sin_port = htons(port); #endif return (aitop); } static int bind_socket(int family, const char *address, u_short port, int reuse) { int fd; struct addrinfo *aitop = NULL; /* just create an unbound socket */ if (address == NULL && port == 0) return bind_socket_ai(family, NULL, 0); aitop = make_addrinfo(address, port); if (aitop == NULL) return (-1); fd = bind_socket_ai(family, aitop, reuse); #ifdef EVENT__HAVE_GETADDRINFO freeaddrinfo(aitop); #else fake_freeaddrinfo(aitop); #endif return (fd); } static int socket_connect(int fd, const char *address, unsigned short port) { struct addrinfo *ai = make_addrinfo(address, port); int res = -1; if (ai == NULL) { event_warnx("%s: make_addrinfo: \"%s:%d\"", __func__, address, port); return (-1); } if (connect(fd, ai->ai_addr, ai->ai_addrlen) == -1) { #ifdef WIN32 int tmp_error = WSAGetLastError(); if (tmp_error != WSAEWOULDBLOCK && tmp_error != WSAEINVAL && tmp_error != WSAEINPROGRESS) { goto out; } #else if (errno != EINPROGRESS) { goto out; } #endif } /* everything is fine */ res = 0; out: #ifdef EVENT__HAVE_GETADDRINFO freeaddrinfo(ai); #else fake_freeaddrinfo(ai); #endif return (res); } forked-daapd-25.0/src/ffmpeg-compat.h000066400000000000000000000060461313447753700174720ustar00rootroot00000000000000#ifndef __FFMPEG_COMPAT_H__ #define __FFMPEG_COMPAT_H__ #ifdef HAVE_LIBAVUTIL_CHANNEL_LAYOUT_H # include #endif #ifdef HAVE_LIBAVUTIL_MATHEMATICS_H # include #endif #ifndef HAVE_FFMPEG # define avcodec_find_best_pix_fmt_of_list(a, b, c, d) avcodec_find_best_pix_fmt2((enum AVPixelFormat *)(a), (b), (c), (d)) #endif #if !HAVE_DECL_AV_FRAME_ALLOC # define av_frame_alloc() avcodec_alloc_frame() # define av_frame_free(x) avcodec_free_frame((x)) #endif #if !HAVE_DECL_AV_FRAME_GET_BEST_EFFORT_TIMESTAMP # define av_frame_get_best_effort_timestamp(x) (x)->pts #endif #if !HAVE_DECL_AV_IMAGE_GET_BUFFER_SIZE # define av_image_get_buffer_size(a, b, c, d) avpicture_get_size((a), (b), (c)) #endif #if !HAVE_DECL_AV_PACKET_UNREF # define av_packet_unref(a) av_free_packet((a)) #endif #if !HAVE_DECL_AV_PACKET_RESCALE_TS __attribute__((unused)) static void av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb) { if (pkt->pts != AV_NOPTS_VALUE) pkt->pts = av_rescale_q(pkt->pts, src_tb, dst_tb); if (pkt->dts != AV_NOPTS_VALUE) pkt->dts = av_rescale_q(pkt->dts, src_tb, dst_tb); if (pkt->duration > 0) pkt->duration = av_rescale_q(pkt->duration, src_tb, dst_tb); if (pkt->convergence_duration > 0) pkt->convergence_duration = av_rescale_q(pkt->convergence_duration, src_tb, dst_tb); } #endif #if !HAVE_DECL_AVFORMAT_ALLOC_OUTPUT_CONTEXT2 # include __attribute__((unused)) static int avformat_alloc_output_context2(AVFormatContext **avctx, AVOutputFormat *oformat, const char *format, const char *filename) { AVFormatContext *s = avformat_alloc_context(); int ret = 0; *avctx = NULL; if (!s) goto nomem; if (!oformat) { if (format) { oformat = av_guess_format(format, NULL, NULL); if (!oformat) { av_log(s, AV_LOG_ERROR, "Requested output format '%s' is not a suitable output format\n", format); ret = AVERROR(EINVAL); goto error; } } else { oformat = av_guess_format(NULL, filename, NULL); if (!oformat) { ret = AVERROR(EINVAL); av_log(s, AV_LOG_ERROR, "Unable to find a suitable output format for '%s'\n", filename); goto error; } } } s->oformat = oformat; if (s->oformat->priv_data_size > 0) { s->priv_data = av_mallocz(s->oformat->priv_data_size); if (!s->priv_data) goto nomem; if (s->oformat->priv_class) { *(const AVClass**)s->priv_data= s->oformat->priv_class; av_opt_set_defaults(s->priv_data); } } else s->priv_data = NULL; if (filename) snprintf(s->filename, sizeof(s->filename), "%s", filename); *avctx = s; return 0; nomem: av_log(s, AV_LOG_ERROR, "Out of memory\n"); ret = AVERROR(ENOMEM); error: avformat_free_context(s); return ret; } #endif #endif /* !__FFMPEG_COMPAT_H__ */ forked-daapd-25.0/src/http.c000066400000000000000000000437221313447753700157210ustar00rootroot00000000000000/* * Copyright (C) 2016 Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_LIBCURL #include #endif #include "http.h" #include "logger.h" #include "misc.h" /* ======================= libevent HTTP client =============================*/ // Number of seconds the client will wait for a response before aborting #define HTTP_CLIENT_TIMEOUT 8 /* The strict libevent api does not permit walking through an evkeyvalq and saving * all the http headers, so we predefine what we are looking for. You can add * extra headers here that you would like to save. */ static char *header_list[] = { "icy-name", "icy-description", "icy-metaint", "icy-genre", "Content-Type", }; /* Copies headers we are searching for from one keyval struct to another * */ static void headers_save(struct keyval *kv, struct evkeyvalq *headers) { const char *value; uint8_t *utf; int i; if (!kv || !headers) return; for (i = 0; i < (sizeof(header_list) / sizeof(header_list[0])); i++) if ( (value = evhttp_find_header(headers, header_list[i])) ) { utf = u8_strconv_from_encoding(value, "ISO−8859−1", iconveh_question_mark); if (!utf) continue; keyval_add(kv, header_list[i], (char *)utf); free(utf); } } static void request_cb(struct evhttp_request *req, void *arg) { struct http_client_ctx *ctx; const char *response_code_line; int response_code; ctx = (struct http_client_ctx *)arg; if (ctx->headers_only) { ctx->ret = 0; event_base_loopbreak(ctx->evbase); return; } if (!req) { DPRINTF(E_WARN, L_HTTP, "Connection to %s failed: Connection timed out\n", ctx->url); goto connection_error; } response_code = evhttp_request_get_response_code(req); #ifndef HAVE_LIBEVENT2_OLD response_code_line = evhttp_request_get_response_code_line(req); #else response_code_line = "no error text"; #endif if (response_code == 0) { DPRINTF(E_WARN, L_HTTP, "Connection to %s failed: Connection refused\n", ctx->url); goto connection_error; } else if (response_code != 200) { DPRINTF(E_WARN, L_HTTP, "Connection to %s failed: %s (error %d)\n", ctx->url, response_code_line, response_code); goto connection_error; } ctx->ret = 0; if (ctx->input_headers) headers_save(ctx->input_headers, evhttp_request_get_input_headers(req)); if (ctx->input_body) evbuffer_add_buffer(ctx->input_body, evhttp_request_get_input_buffer(req)); event_base_loopbreak(ctx->evbase); return; connection_error: ctx->ret = -1; event_base_loopbreak(ctx->evbase); return; } /* This callback is only invoked if ctx->headers_only is set. Since that means * we only want headers, it will always return -1 to make evhttp close the * connection. The headers will be saved in a keyval struct in ctx, since we * cannot address the *evkeyvalq after the connection is free'd. */ #ifndef HAVE_LIBEVENT2_OLD static int request_header_cb(struct evhttp_request *req, void *arg) { struct http_client_ctx *ctx; ctx = (struct http_client_ctx *)arg; if (!ctx->input_headers) { DPRINTF(E_LOG, L_HTTP, "BUG: Header callback invoked but caller did not say where to save the headers\n"); return -1; } headers_save(ctx->input_headers, evhttp_request_get_input_headers(req)); return -1; } #endif static int http_client_request_impl(struct http_client_ctx *ctx) { struct evhttp_connection *evcon; struct evhttp_request *req; struct evkeyvalq *headers; struct evbuffer *output_buffer; struct onekeyval *okv; enum evhttp_cmd_type method; char host[512]; char host_port[1024]; char path[2048]; char tmp[128]; int port; int ret; ctx->ret = -1; av_url_split(NULL, 0, NULL, 0, host, sizeof(host), &port, path, sizeof(path), ctx->url); if (strlen(host) == 0) { DPRINTF(E_LOG, L_HTTP, "Error extracting hostname from URL: %s\n", ctx->url); return ctx->ret; } if (port <= 0) snprintf(host_port, sizeof(host_port), "%s", host); else snprintf(host_port, sizeof(host_port), "%s:%d", host, port); if (port <= 0) port = 80; if (strlen(path) == 0) { path[0] = '/'; path[1] = '\0'; } ctx->evbase = event_base_new(); if (!ctx->evbase) { DPRINTF(E_LOG, L_HTTP, "Could not create or find http client event base\n"); return ctx->ret; } evcon = evhttp_connection_base_new(ctx->evbase, NULL, host, (unsigned short)port); if (!evcon) { DPRINTF(E_LOG, L_HTTP, "Could not create connection to %s\n", host_port); event_base_free(ctx->evbase); return ctx->ret; } evhttp_connection_set_timeout(evcon, HTTP_CLIENT_TIMEOUT); /* Set up request */ req = evhttp_request_new(request_cb, ctx); if (!req) { DPRINTF(E_LOG, L_HTTP, "Could not create request to %s\n", host_port); evhttp_connection_free(evcon); event_base_free(ctx->evbase); return ctx->ret; } #ifndef HAVE_LIBEVENT2_OLD if (ctx->headers_only) evhttp_request_set_header_cb(req, request_header_cb); #endif headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Host", host_port); evhttp_add_header(headers, "User-Agent", "forked-daapd/" VERSION); evhttp_add_header(headers, "Icy-MetaData", "1"); if (ctx->output_headers) { for (okv = ctx->output_headers->head; okv; okv = okv->next) evhttp_add_header(headers, okv->name, okv->value); } if (ctx->output_body) { output_buffer = evhttp_request_get_output_buffer(req); evbuffer_add(output_buffer, ctx->output_body, strlen(ctx->output_body)); evbuffer_add_printf(output_buffer, "\n"); snprintf(tmp, sizeof(tmp), "%zu", evbuffer_get_length(output_buffer)); evhttp_add_header(headers, "Content-Length", tmp); method = EVHTTP_REQ_POST; } else { evhttp_add_header(headers, "Content-Length", "0"); method = EVHTTP_REQ_GET; } /* Make request */ DPRINTF(E_INFO, L_HTTP, "Making %s request for http://%s%s\n", ((method==EVHTTP_REQ_GET) ? "GET" : "POST"), host_port, path); ret = evhttp_make_request(evcon, req, method, path); if (ret < 0) { DPRINTF(E_LOG, L_HTTP, "Error making request for http://%s%s\n", host_port, path); evhttp_connection_free(evcon); event_base_free(ctx->evbase); return ctx->ret; } event_base_dispatch(ctx->evbase); evhttp_connection_free(evcon); event_base_free(ctx->evbase); return ctx->ret; } #ifdef HAVE_LIBCURL static size_t curl_request_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { size_t realsize; struct http_client_ctx *ctx; int ret; realsize = size * nmemb; ctx = (struct http_client_ctx *)userdata; if (!ctx->input_body) return realsize; ret = evbuffer_add(ctx->input_body, ptr, realsize); if (ret < 0) { DPRINTF(E_LOG, L_HTTP, "Error adding reply from %s to input buffer\n", ctx->url); return 0; } return realsize; } static int https_client_request_impl(struct http_client_ctx *ctx) { CURL *curl; CURLcode res; struct curl_slist *headers; struct onekeyval *okv; char header[1024]; curl = curl_easy_init(); if (!curl) { DPRINTF(E_LOG, L_HTTP, "Error: Could not get curl handle\n"); return -1; } curl_easy_setopt(curl, CURLOPT_URL, ctx->url); curl_easy_setopt(curl, CURLOPT_USERAGENT, "forked-daapd/" VERSION); if (ctx->output_headers) { headers = NULL; for (okv = ctx->output_headers->head; okv; okv = okv->next) { snprintf(header, sizeof(header), "%s: %s", okv->name, okv->value); headers = curl_slist_append(headers, header); } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); } if (ctx->output_body) curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ctx->output_body); curl_easy_setopt(curl, CURLOPT_TIMEOUT, HTTP_CLIENT_TIMEOUT); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_request_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx); /* Make request */ DPRINTF(E_INFO, L_HTTP, "Making request for %s\n", ctx->url); res = curl_easy_perform(curl); if (res != CURLE_OK) { DPRINTF(E_LOG, L_HTTP, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res)); curl_easy_cleanup(curl); return -1; } curl_easy_cleanup(curl); return 0; } #endif /* HAVE_LIBCURL */ int http_client_request(struct http_client_ctx *ctx) { if (strncmp(ctx->url, "http:", strlen("http:")) == 0) return http_client_request_impl(ctx); #ifdef HAVE_LIBCURL if (strncmp(ctx->url, "https:", strlen("https:")) == 0) return https_client_request_impl(ctx); #endif DPRINTF(E_LOG, L_HTTP, "Request for %s is not supported (not built with libcurl?)\n", ctx->url); return -1; } char * http_form_urlencode(struct keyval *kv) { struct evbuffer *evbuf; struct onekeyval *okv; char *body; char *k; char *v; evbuf = evbuffer_new(); for (okv = kv->head; okv; okv = okv->next) { k = evhttp_encode_uri(okv->name); if (!k) continue; v = evhttp_encode_uri(okv->value); if (!v) { free(k); continue; } evbuffer_add_printf(evbuf, "%s=%s", k, v); if (okv->next) evbuffer_add_printf(evbuf, "&"); free(k); free(v); } evbuffer_add(evbuf, "\n", 1); body = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_ANY); evbuffer_free(evbuf); DPRINTF(E_DBG, L_HTTP, "Parameters in request are: %s\n", body); return body; } int http_stream_setup(char **stream, const char *url) { struct http_client_ctx ctx; struct evbuffer *evbuf; const char *ext; char *line; int ret; int n; *stream = NULL; ext = strrchr(url, '.'); if (!ext || (strcasecmp(ext, ".m3u") != 0)) { *stream = strdup(url); return 0; } // It was a m3u playlist, so now retrieve it memset(&ctx, 0, sizeof(struct http_client_ctx)); evbuf = evbuffer_new(); if (!evbuf) return -1; ctx.url = url; ctx.input_body = evbuf; ret = http_client_request(&ctx); if (ret < 0) { DPRINTF(E_LOG, L_HTTP, "Couldn't fetch internet playlist: %s\n", url); evbuffer_free(evbuf); return -1; } // Pad with CRLF because evbuffer_readln() might not read the last line otherwise evbuffer_add(ctx.input_body, "\r\n", 2); /* Read the playlist until the first stream link is found, but give up if * nothing is found in the first 10 lines */ n = 0; while ((line = evbuffer_readln(ctx.input_body, NULL, EVBUFFER_EOL_ANY)) && (n < 10)) { n++; if (strncasecmp(line, "http://", strlen("http://")) == 0) { DPRINTF(E_DBG, L_HTTP, "Found internet playlist stream (line %d): %s\n", n, line); n = -1; break; } free(line); } evbuffer_free(ctx.input_body); if (n != -1) { DPRINTF(E_LOG, L_HTTP, "Couldn't find stream in internet playlist: %s\n", url); return -1; } *stream = line; return 0; } /* ======================= ICY metadata handling =============================*/ #if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) static int metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) { uint8_t *buffer; char *icy_token; char *ptr; char *end; av_opt_get(fmtctx, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN, &buffer); if (!buffer) return -1; icy_token = strtok((char *)buffer, ";"); while (icy_token != NULL) { ptr = strchr(icy_token, '='); if (!ptr || (ptr[1] == '\0')) { icy_token = strtok(NULL, ";"); continue; } ptr++; if (ptr[0] == '\'') ptr++; end = strrchr(ptr, '\''); if (end) *end = '\0'; if ((strncmp(icy_token, "StreamTitle", strlen("StreamTitle")) == 0) && !metadata->title) { metadata->title = ptr; /* Dash separates artist from title, if no dash assume all is title */ ptr = strstr(ptr, " - "); if (ptr) { *ptr = '\0'; metadata->artist = strdup(metadata->title); *ptr = ' '; metadata->title = strdup(ptr + 3); } else metadata->title = strdup(metadata->title); } else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->artwork_url) { metadata->artwork_url = strdup(ptr); } if (end) *end = '\''; icy_token = strtok(NULL, ";"); } av_free(buffer); if (metadata->title) metadata->hash = djb_hash(metadata->title, strlen(metadata->title)); return 0; } static int metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) { uint8_t *buffer; uint8_t *utf; char *icy_token; char *ptr; av_opt_get(fmtctx, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN, &buffer); if (!buffer) return -1; /* Headers are ascii or iso-8859-1 according to: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 */ utf = u8_strconv_from_encoding((char *)buffer, "ISO−8859−1", iconveh_question_mark); av_free(buffer); if (!utf) return -1; icy_token = strtok((char *)utf, "\r\n"); while (icy_token != NULL) { ptr = strchr(icy_token, ':'); if (!ptr || (ptr[1] == '\0')) { icy_token = strtok(NULL, "\r\n"); continue; } ptr++; if (ptr[0] == ' ') ptr++; if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && !metadata->name) metadata->name = strdup(ptr); else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && !metadata->description) metadata->description = strdup(ptr); else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && !metadata->genre) metadata->genre = strdup(ptr); icy_token = strtok(NULL, "\r\n"); } free(utf); return 0; } struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) { struct http_icy_metadata *metadata; int got_packet; int got_header; metadata = malloc(sizeof(struct http_icy_metadata)); if (!metadata) return NULL; memset(metadata, 0, sizeof(struct http_icy_metadata)); got_packet = (metadata_packet_get(metadata, fmtctx) == 0); got_header = (!packet_only) && (metadata_header_get(metadata, fmtctx) == 0); if (!got_packet && !got_header) { free(metadata); return NULL; } /* DPRINTF(E_DBG, L_HTTP, "Found ICY: N %s, D %s, G %s, T %s, A %s, U %s, I %" PRIu32 "\n", metadata->name, metadata->description, metadata->genre, metadata->title, metadata->artist, metadata->artwork_url, metadata->hash ); */ return metadata; } #elif defined(HAVE_LIBEVENT2_OLD) struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) { DPRINTF(E_INFO, L_HTTP, "Skipping Shoutcast metadata request for %s (requires libevent>=2.1.4 or libav 10)\n", fmtctx->filename); return NULL; } #else /* Earlier versions of ffmpeg/libav do not seem to allow access to the http * headers, so we must instead open the stream ourselves to get the metadata. * Sorry about the extra connections, you radio streaming people! * * It is not possible to get the packet metadata with these versions of ffmpeg */ struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only) { struct http_icy_metadata *metadata; struct http_client_ctx ctx; struct keyval *kv; const char *value; int got_header; int ret; /* Can only get header metadata */ if (packet_only) return NULL; kv = keyval_alloc(); if (!kv) return NULL; memset(&ctx, 0, sizeof(struct http_client_ctx)); ctx.url = fmtctx->filename; ctx.input_headers = kv; ctx.headers_only = 1; ctx.input_body = NULL; ret = http_client_request(&ctx); if (ret < 0) { DPRINTF(E_LOG, L_HTTP, "Error fetching %s\n", fmtctx->filename); free(kv); return NULL; } metadata = malloc(sizeof(struct http_icy_metadata)); if (!metadata) return NULL; memset(metadata, 0, sizeof(struct http_icy_metadata)); got_header = 0; if ( (value = keyval_get(ctx.input_headers, "icy-name")) ) { metadata->name = strdup(value); got_header = 1; } if ( (value = keyval_get(ctx.input_headers, "icy-description")) ) { metadata->description = strdup(value); got_header = 1; } if ( (value = keyval_get(ctx.input_headers, "icy-genre")) ) { metadata->genre = strdup(value); got_header = 1; } keyval_clear(kv); free(kv); if (!got_header) { free(metadata); return NULL; } /* DPRINTF(E_DBG, L_HTTP, "Found ICY: N %s, D %s, G %s, T %s, A %s, U %s, I %" PRIu32 "\n", metadata->name, metadata->description, metadata->genre, metadata->title, metadata->artist, metadata->artwork_url, metadata->hash );*/ return metadata; } #endif void http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only) { if (metadata->name) free(metadata->name); if (metadata->description) free(metadata->description); if (metadata->genre) free(metadata->genre); if (metadata->title) free(metadata->title); if (metadata->artist) free(metadata->artist); if (metadata->artwork_url) free(metadata->artwork_url); if (!content_only) free(metadata); } forked-daapd-25.0/src/http.h000066400000000000000000000061721313447753700157240ustar00rootroot00000000000000 #ifndef __HTTP_H__ #define __HTTP_H__ #include #include #include "misc.h" #include struct http_client_ctx { /* Destination URL, header and body of outgoing request body. If output_body * is set, the request will be POST, otherwise it will be GET */ const char *url; struct keyval *output_headers; char *output_body; /* A keyval/evbuf to store response headers and body. * Can be set to NULL to ignore that part of the response. */ struct keyval *input_headers; struct evbuffer *input_body; /* Cut the connection after the headers have been received * Used for getting Shoutcast/ICY headers for old versions of libav/ffmpeg * (requires libevent 1 or 2.1.4+) */ int headers_only; /* Private */ int ret; void *evbase; }; struct http_icy_metadata { uint32_t id; /* Static stream metadata from icy_metadata_headers */ char *name; char *description; char *genre; /* Track specific, comes from icy_metadata_packet */ char *title; char *artist; char *artwork_url; uint32_t hash; }; /* Make a http(s) request. We use libcurl to make https requests. We could use * libevent and avoid the dependency, but for SSL, libevent needs to be v2.1 * or better, which is still a bit too new to be in the major distros. * * @param ctx HTTP request params, see above * @return 0 if successful, -1 if an error occurred (e.g. no libcurl) */ int http_client_request(struct http_client_ctx *ctx); /* Converts the keyval dictionary to a application/x-www-form-urlencoded string. * The values will be uri_encoded. Example output: "key1=foo%20bar&key2=123". * * @param kv is the struct containing the parameters * @return encoded string if succesful, NULL if an error occurred */ char * http_form_urlencode(struct keyval *kv); /* Returns a newly allocated string with the first stream in the m3u given in * url. If url is not a m3u, the string will be a copy of url. * * @param stream the newly allocated string with link to stream (NULL on error) * @param url link to either stream or m3u * @return 0 if successful, -1 if an error occurred */ int http_stream_setup(char **stream, const char *url); /* Extracts ICY header and packet metadata (requires libav 10) * * example header metadata (standard http header format): * icy-name: Rock On Radio * example packet metadata (track currently being played): * StreamTitle='Robert Miles - Black Rubber';StreamUrl=''; * * The extraction is straight from the stream and done in the player thread, so * it must not produce significant delay. * * @param fmtctx the libav/ffmpeg AVFormatContext containing the stream * @param packet_only only get currently playing info (see struct above) * @return metadata struct if successful, NULL on error or nothing found */ struct http_icy_metadata * http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only); /* Frees an ICY metadata struct * * @param metadata struct to free * @param content_only just free content, not the struct */ void http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only); #endif /* !__HTTP_H__ */ forked-daapd-25.0/src/httpd.c000066400000000000000000001107411313447753700160610ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include #include #include #include #include #include #include #ifdef HAVE_EVENTFD # include #endif #include #include #ifdef HAVE_LIBEVENT2_OLD # include # include #endif #include #include "logger.h" #include "db.h" #include "conffile.h" #include "misc.h" #include "worker.h" #include "httpd.h" #include "httpd_rsp.h" #include "httpd_daap.h" #include "httpd_dacp.h" #include "httpd_streaming.h" #include "transcode.h" #ifdef LASTFM # include "lastfm.h" #endif #ifdef HAVE_SPOTIFY_H # include "spotify.h" #endif /* * HTTP client quirks by User-Agent, from mt-daapd * * - iTunes: * + Connection: Keep-Alive on HTTP error 401 * - Hifidelio: * + Connection: Keep-Alive for streaming (Connection: close not honoured) * * These quirks are not implemented. Implement as needed. * * Implemented quirks: * * - Roku: * + Does not encode space as + in query string * - iTunes: * + Does not encode space as + in query string */ #define STREAM_CHUNK_SIZE (64 * 1024) #define WEBFACE_ROOT DATADIR "/webface/" #define ERR_PAGE "\n\n" \ "%d %s\n" \ "\n\n" \ "

%s

\n" \ "\n\n" struct content_type_map { char *ext; char *ctype; }; struct stream_ctx { struct evhttp_request *req; uint8_t *buf; struct evbuffer *evbuf; struct event *ev; int id; int fd; off_t size; off_t stream_size; off_t offset; off_t start_offset; off_t end_offset; int marked; struct transcode_ctx *xcode; }; static const struct content_type_map ext2ctype[] = { { ".html", "text/html; charset=utf-8" }, { ".xml", "text/xml; charset=utf-8" }, { ".css", "text/css; charset=utf-8" }, { ".txt", "text/plain; charset=utf-8" }, { ".js", "application/javascript; charset=utf-8" }, { ".gif", "image/gif" }, { ".ico", "image/x-ico" }, { ".png", "image/png" }, { NULL, NULL } }; struct event_base *evbase_httpd; #ifdef HAVE_EVENTFD static int exit_efd; #else static int exit_pipe[2]; #endif static int httpd_exit; static struct event *exitev; static struct evhttp *evhttpd; static pthread_t tid_httpd; static const char *allow_origin; static int httpd_port; #ifdef HAVE_LIBEVENT2_OLD struct stream_ctx *g_st; #endif static void stream_end(struct stream_ctx *st, int failed) { struct evhttp_connection *evcon; evcon = evhttp_request_get_connection(st->req); if (evcon) evhttp_connection_set_closecb(evcon, NULL, NULL); if (!failed) evhttp_send_reply_end(st->req); evbuffer_free(st->evbuf); event_free(st->ev); if (st->xcode) transcode_cleanup(st->xcode); else { free(st->buf); close(st->fd); } #ifdef HAVE_LIBEVENT2_OLD if (g_st == st) g_st = NULL; #endif free(st); } /* Callback from the worker thread (async operation as it may block) */ static void playcount_inc_cb(void *arg) { int *id = arg; db_file_inc_playcount(*id); } #ifdef LASTFM /* Callback from the worker thread (async operation as it may block) */ static void scrobble_cb(void *arg) { int *id = arg; lastfm_scrobble(*id); } #endif static void oauth_interface(struct evhttp_request *req, const char *uri) { struct evbuffer *evbuf; struct evkeyvalq query; const char *req_uri; const char *ptr; char __attribute__((unused)) redirect_uri[256]; int ret; req_uri = evhttp_request_get_uri(req); evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_HTTPD, "Could not alloc evbuf for oauth\n"); return; } evbuffer_add_printf(evbuf, "

forked-daapd oauth

\n\n"); memset(&query, 0, sizeof(struct evkeyvalq)); ptr = strchr(req_uri, '?'); if (ptr) { ret = evhttp_parse_query_str(ptr + 1, &query); if (ret < 0) { evbuffer_add_printf(evbuf, "OAuth error: Could not parse parameters in callback (%s)\n", req_uri); httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); evbuffer_free(evbuf); return; } } #ifdef HAVE_SPOTIFY_H snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port); if (strncmp(uri, "/oauth/spotify", strlen("/oauth/spotify")) == 0) spotify_oauth_callback(evbuf, &query, redirect_uri); else spotify_oauth_interface(evbuf, redirect_uri); #else evbuffer_add_printf(evbuf, "

This version was built without modules requiring OAuth support

\n"); #endif evbuffer_add_printf(evbuf, "

(sorry about this ugly interface)

\n"); evhttp_clear_headers(&query); httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); evbuffer_free(evbuf); } static void stream_end_register(struct stream_ctx *st) { if (!st->marked && (st->stream_size > ((st->size * 50) / 100)) && (st->offset > ((st->size * 80) / 100))) { st->marked = 1; worker_execute(playcount_inc_cb, &st->id, sizeof(int), 0); #ifdef LASTFM worker_execute(scrobble_cb, &st->id, sizeof(int), 1); #endif } } static void stream_chunk_resched_cb(struct evhttp_connection *evcon, void *arg) { struct stream_ctx *st; struct timeval tv; int ret; st = (struct stream_ctx *)arg; evutil_timerclear(&tv); ret = event_add(st->ev, &tv); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not re-add one-shot event for streaming\n"); stream_end(st, 0); } } #ifdef HAVE_LIBEVENT2_OLD static void stream_chunk_resched_cb_wrapper(struct bufferevent *bufev, void *arg) { if (g_st) stream_chunk_resched_cb(NULL, g_st); } #endif static void stream_chunk_xcode_cb(int fd, short event, void *arg) { struct stream_ctx *st; struct timeval tv; int xcoded; int ret; int dummy; st = (struct stream_ctx *)arg; xcoded = transcode(st->evbuf, STREAM_CHUNK_SIZE, st->xcode, &dummy); if (xcoded <= 0) { if (xcoded == 0) DPRINTF(E_LOG, L_HTTPD, "Done streaming transcoded file id %d\n", st->id); else DPRINTF(E_LOG, L_HTTPD, "Transcoding error, file id %d\n", st->id); stream_end(st, 0); return; } DPRINTF(E_DBG, L_HTTPD, "Got %d bytes from transcode; streaming file id %d\n", xcoded, st->id); /* Consume transcoded data until we meet start_offset */ if (st->start_offset > st->offset) { ret = st->start_offset - st->offset; if (ret < xcoded) { evbuffer_drain(st->evbuf, ret); st->offset += ret; ret = xcoded - ret; } else { evbuffer_drain(st->evbuf, xcoded); st->offset += xcoded; goto consume; } } else ret = xcoded; #ifdef HAVE_LIBEVENT2_OLD evhttp_send_reply_chunk(st->req, st->evbuf); struct evhttp_connection *evcon = evhttp_request_get_connection(st->req); struct bufferevent *bufev = evhttp_connection_get_bufferevent(evcon); g_st = st; // Can't pass st to callback so use global - limits libevent 2.0 to a single stream bufev->writecb = stream_chunk_resched_cb_wrapper; #else evhttp_send_reply_chunk_with_cb(st->req, st->evbuf, stream_chunk_resched_cb, st); #endif st->offset += ret; stream_end_register(st); return; consume: /* reschedule immediately - consume up to start_offset */ evutil_timerclear(&tv); ret = event_add(st->ev, &tv); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not re-add one-shot event for streaming (xcode)\n"); stream_end(st, 0); return; } } static void stream_chunk_raw_cb(int fd, short event, void *arg) { struct stream_ctx *st; size_t chunk_size; int ret; st = (struct stream_ctx *)arg; if (st->end_offset && (st->offset > st->end_offset)) { stream_end(st, 0); return; } if (st->end_offset && ((st->offset + STREAM_CHUNK_SIZE) > (st->end_offset + 1))) chunk_size = st->end_offset + 1 - st->offset; else chunk_size = STREAM_CHUNK_SIZE; ret = read(st->fd, st->buf, chunk_size); if (ret <= 0) { if (ret == 0) DPRINTF(E_LOG, L_HTTPD, "Done streaming file id %d\n", st->id); else DPRINTF(E_LOG, L_HTTPD, "Streaming error, file id %d\n", st->id); stream_end(st, 0); return; } DPRINTF(E_DBG, L_HTTPD, "Read %d bytes; streaming file id %d\n", ret, st->id); evbuffer_add(st->evbuf, st->buf, ret); #ifdef HAVE_LIBEVENT2_OLD evhttp_send_reply_chunk(st->req, st->evbuf); struct evhttp_connection *evcon = evhttp_request_get_connection(st->req); struct bufferevent *bufev = evhttp_connection_get_bufferevent(evcon); g_st = st; // Can't pass st to callback so use global - limits libevent 2.0 to a single stream bufev->writecb = stream_chunk_resched_cb_wrapper; #else evhttp_send_reply_chunk_with_cb(st->req, st->evbuf, stream_chunk_resched_cb, st); #endif st->offset += ret; stream_end_register(st); } static void stream_fail_cb(struct evhttp_connection *evcon, void *arg) { struct stream_ctx *st; st = (struct stream_ctx *)arg; DPRINTF(E_WARN, L_HTTPD, "Connection failed; stopping streaming of file ID %d\n", st->id); /* Stop streaming */ event_del(st->ev); stream_end(st, 1); } /* Thread: httpd */ void httpd_stream_file(struct evhttp_request *req, int id) { struct media_file_info *mfi; struct stream_ctx *st; void (*stream_cb)(int fd, short event, void *arg); struct stat sb; struct timeval tv; struct evhttp_connection *evcon; struct evkeyvalq *input_headers; struct evkeyvalq *output_headers; const char *param; const char *param_end; const char *ua; const char *client_codecs; char buf[64]; int64_t offset; int64_t end_offset; off_t pos; int transcode; int ret; offset = 0; end_offset = 0; input_headers = evhttp_request_get_input_headers(req); param = evhttp_find_header(input_headers, "Range"); if (param) { DPRINTF(E_DBG, L_HTTPD, "Found Range header: %s\n", param); /* Start offset */ ret = safe_atoi64(param + strlen("bytes="), &offset); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Invalid start offset, will stream whole file (%s)\n", param); offset = 0; } /* End offset, if any */ else { param_end = strchr(param, '-'); if (param_end && (strlen(param_end) > 1)) { ret = safe_atoi64(param_end + 1, &end_offset); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Invalid end offset, will stream to end of file (%s)\n", param); end_offset = 0; } if (end_offset < offset) { DPRINTF(E_LOG, L_HTTPD, "End offset < start offset, will stream to end of file (%" PRIi64 " < %" PRIi64 ")\n", end_offset, offset); end_offset = 0; } } } } mfi = db_file_fetch_byid(id); if (!mfi) { DPRINTF(E_LOG, L_HTTPD, "Item %d not found\n", id); evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); return; } if (mfi->data_kind != DATA_KIND_FILE) { evhttp_send_error(req, 500, "Cannot stream radio station"); goto out_free_mfi; } st = (struct stream_ctx *)malloc(sizeof(struct stream_ctx)); if (!st) { DPRINTF(E_LOG, L_HTTPD, "Out of memory for struct stream_ctx\n"); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); goto out_free_mfi; } memset(st, 0, sizeof(struct stream_ctx)); st->fd = -1; ua = evhttp_find_header(input_headers, "User-Agent"); client_codecs = evhttp_find_header(input_headers, "Accept-Codecs"); transcode = transcode_needed(ua, client_codecs, mfi->codectype); output_headers = evhttp_request_get_output_headers(req); if (transcode) { DPRINTF(E_INFO, L_HTTPD, "Preparing to transcode %s\n", mfi->path); stream_cb = stream_chunk_xcode_cb; st->xcode = transcode_setup(mfi->data_kind, mfi->path, mfi->song_length, XCODE_PCM16_HEADER, &st->size); if (!st->xcode) { DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n"); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); goto out_free_st; } if (!evhttp_find_header(output_headers, "Content-Type")) evhttp_add_header(output_headers, "Content-Type", "audio/wav"); } else { /* Stream the raw file */ DPRINTF(E_INFO, L_HTTPD, "Preparing to stream %s\n", mfi->path); st->buf = (uint8_t *)malloc(STREAM_CHUNK_SIZE); if (!st->buf) { DPRINTF(E_LOG, L_HTTPD, "Out of memory for raw streaming buffer\n"); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); goto out_free_st; } stream_cb = stream_chunk_raw_cb; st->fd = open(mfi->path, O_RDONLY); if (st->fd < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", mfi->path, strerror(errno)); evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); goto out_cleanup; } ret = stat(mfi->path, &sb); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", mfi->path, strerror(errno)); evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); goto out_cleanup; } st->size = sb.st_size; pos = lseek(st->fd, offset, SEEK_SET); if (pos == (off_t) -1) { DPRINTF(E_LOG, L_HTTPD, "Could not seek into %s: %s\n", mfi->path, strerror(errno)); evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); goto out_cleanup; } st->offset = offset; st->end_offset = end_offset; /* Content-Type for video files is different than for audio files * and overrides whatever may have been set previously, like * application/x-dmap-tagged when we're speaking DAAP. */ if (mfi->has_video) { /* Front Row and others expect video/ */ ret = snprintf(buf, sizeof(buf), "video/%s", mfi->type); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Type too large for buffer, dropping\n"); else { evhttp_remove_header(output_headers, "Content-Type"); evhttp_add_header(output_headers, "Content-Type", buf); } } /* If no Content-Type has been set and we're streaming audio, add a proper * Content-Type for the file we're streaming. Remember DAAP streams audio * with application/x-dmap-tagged as the Content-Type (ugh!). */ else if (!evhttp_find_header(output_headers, "Content-Type") && mfi->type) { ret = snprintf(buf, sizeof(buf), "audio/%s", mfi->type); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Type too large for buffer, dropping\n"); else evhttp_add_header(output_headers, "Content-Type", buf); } } st->evbuf = evbuffer_new(); if (!st->evbuf) { DPRINTF(E_LOG, L_HTTPD, "Could not allocate an evbuffer for streaming\n"); evhttp_clear_headers(output_headers); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); goto out_cleanup; } ret = evbuffer_expand(st->evbuf, STREAM_CHUNK_SIZE); if (ret != 0) { DPRINTF(E_LOG, L_HTTPD, "Could not expand evbuffer for streaming\n"); evhttp_clear_headers(output_headers); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); goto out_cleanup; } st->ev = event_new(evbase_httpd, -1, EV_TIMEOUT, stream_cb, st); evutil_timerclear(&tv); if (!st->ev || (event_add(st->ev, &tv) < 0)) { DPRINTF(E_LOG, L_HTTPD, "Could not add one-shot event for streaming\n"); evhttp_clear_headers(output_headers); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); goto out_cleanup; } st->id = mfi->id; st->start_offset = offset; st->stream_size = st->size; st->req = req; if ((offset == 0) && (end_offset == 0)) { /* If we are not decoding, send the Content-Length. We don't do * that if we are decoding because we can only guesstimate the * size in this case and the error margin is unknown and variable. */ if (!transcode) { ret = snprintf(buf, sizeof(buf), "%" PRIi64, (int64_t)st->size); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Length too large for buffer, dropping\n"); else evhttp_add_header(output_headers, "Content-Length", buf); } evhttp_send_reply_start(req, HTTP_OK, "OK"); } else { if (offset > 0) st->stream_size -= offset; if (end_offset > 0) st->stream_size -= (st->size - end_offset); DPRINTF(E_DBG, L_HTTPD, "Stream request with range %" PRIi64 "-%" PRIi64 "\n", offset, end_offset); ret = snprintf(buf, sizeof(buf), "bytes %" PRIi64 "-%" PRIi64 "/%" PRIi64, offset, (end_offset) ? end_offset : (int64_t)st->size, (int64_t)st->size); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Range too large for buffer, dropping\n"); else evhttp_add_header(output_headers, "Content-Range", buf); ret = snprintf(buf, sizeof(buf), "%" PRIi64, ((end_offset) ? end_offset + 1 : (int64_t)st->size) - offset); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Length too large for buffer, dropping\n"); else evhttp_add_header(output_headers, "Content-Length", buf); evhttp_send_reply_start(req, 206, "Partial Content"); } #ifdef HAVE_POSIX_FADVISE if (!transcode) { /* Hint the OS */ posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_WILLNEED); posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_SEQUENTIAL); posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_NOREUSE); } #endif evcon = evhttp_request_get_connection(req); evhttp_connection_set_closecb(evcon, stream_fail_cb, st); DPRINTF(E_INFO, L_HTTPD, "Kicking off streaming for %s\n", mfi->path); free_mfi(mfi, 0); return; out_cleanup: if (st->evbuf) evbuffer_free(st->evbuf); if (st->xcode) transcode_cleanup(st->xcode); if (st->buf) free(st->buf); if (st->fd > 0) close(st->fd); out_free_st: free(st); out_free_mfi: free_mfi(mfi, 0); } struct evbuffer * httpd_gzip_deflate(struct evbuffer *in) { struct evbuffer *out; struct evbuffer_iovec iovec[1]; z_stream strm; int ret; strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL; // Set up a gzip stream (the "+ 16" in 15 + 16), instead of a zlib stream (default) ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); if (ret != Z_OK) { DPRINTF(E_LOG, L_HTTPD, "zlib setup failed: %s\n", zError(ret)); return NULL; } strm.next_in = evbuffer_pullup(in, -1); strm.avail_in = evbuffer_get_length(in); out = evbuffer_new(); if (!out) { DPRINTF(E_LOG, L_HTTPD, "Could not allocate evbuffer for gzipped reply\n"); goto out_deflate_end; } // We use this to avoid a memcpy. The 512 is an arbitrary padding to make sure // there is enough space, even if the compressed output should be slightly // larger than input (could happen with small inputs). ret = evbuffer_reserve_space(out, strm.avail_in + 512, iovec, 1); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not reserve memory for gzipped reply\n"); goto out_evbuf_free; } strm.next_out = iovec[0].iov_base; strm.avail_out = iovec[0].iov_len; ret = deflate(&strm, Z_FINISH); if (ret != Z_STREAM_END) goto out_evbuf_free; iovec[0].iov_len -= strm.avail_out; evbuffer_commit_space(out, iovec, 1); deflateEnd(&strm); return out; out_evbuf_free: evbuffer_free(out); out_deflate_end: deflateEnd(&strm); return NULL; } void httpd_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *evbuf, enum httpd_send_flags flags) { struct evbuffer *gzbuf; struct evkeyvalq *input_headers; struct evkeyvalq *output_headers; const char *param; int do_gzip; if (!req) return; input_headers = evhttp_request_get_input_headers(req); output_headers = evhttp_request_get_output_headers(req); do_gzip = ( (!(flags & HTTPD_SEND_NO_GZIP)) && evbuf && (evbuffer_get_length(evbuf) > 512) && (param = evhttp_find_header(input_headers, "Accept-Encoding")) && (strstr(param, "gzip") || strstr(param, "*")) ); if (allow_origin) evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin); if (do_gzip && (gzbuf = httpd_gzip_deflate(evbuf))) { DPRINTF(E_DBG, L_HTTPD, "Gzipping response\n"); evhttp_add_header(output_headers, "Content-Encoding", "gzip"); evhttp_send_reply(req, code, reason, gzbuf); evbuffer_free(gzbuf); // Drain original buffer, as would be after evhttp_send_reply() evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); } else { evhttp_send_reply(req, code, reason, evbuf); } } // This is a modified version of evhttp_send_error (credit libevent) void httpd_send_error(struct evhttp_request* req, int error, const char* reason) { struct evkeyvalq *output_headers; struct evbuffer *evbuf; if (!allow_origin) { evhttp_send_error(req, error, reason); return; } output_headers = evhttp_request_get_output_headers(req); evhttp_clear_headers(output_headers); evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin); evhttp_add_header(output_headers, "Content-Type", "text/html"); evhttp_add_header(output_headers, "Connection", "close"); evbuf = evbuffer_new(); if (!evbuf) DPRINTF(E_LOG, L_HTTPD, "Could not allocate evbuffer for error page\n"); else evbuffer_add_printf(evbuf, ERR_PAGE, error, reason, reason); evhttp_send_reply(req, error, reason, evbuf); if (evbuf) evbuffer_free(evbuf); } /* Thread: httpd */ static int path_is_legal(char *path) { return strncmp(WEBFACE_ROOT, path, strlen(WEBFACE_ROOT)); } /* Thread: httpd */ static void redirect_to_index(struct evhttp_request *req, char *uri) { struct evkeyvalq *headers; char buf[256]; int slashed; int ret; slashed = (uri[strlen(uri) - 1] == '/'); ret = snprintf(buf, sizeof(buf), "%s%sindex.html", uri, (slashed) ? "" : "/"); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_HTTPD, "Redirection URL exceeds buffer length\n"); httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); return; } headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Location", buf); httpd_send_reply(req, HTTP_MOVETEMP, "Moved", NULL, HTTPD_SEND_NO_GZIP); } /* Thread: httpd */ static void serve_file(struct evhttp_request *req, char *uri) { const char *host; const char *passwd; char *ext; char path[PATH_MAX]; char *deref; char *ctype; struct evbuffer *evbuf; struct evkeyvalq *headers; struct stat sb; int fd; int i; int ret; /* Check authentication */ passwd = cfg_getstr(cfg_getsec(cfg, "general"), "admin_password"); if (passwd) { DPRINTF(E_DBG, L_HTTPD, "Checking web interface authentication\n"); ret = httpd_basic_auth(req, "admin", passwd, PACKAGE " web interface"); if (ret != 0) return; DPRINTF(E_DBG, L_HTTPD, "Authentication successful\n"); } else { host = evhttp_request_get_host(req); if ((strcmp(host, "::1") != 0) && (strcmp(host, "127.0.0.1") != 0)) { DPRINTF(E_LOG, L_HTTPD, "Remote web interface request denied; no password set\n"); httpd_send_error(req, 403, "Forbidden"); return; } } if (strncmp(uri, "/oauth", strlen("/oauth")) == 0) { oauth_interface(req, uri); return; } ret = snprintf(path, sizeof(path), "%s%s", WEBFACE_ROOT, uri + 1); /* skip starting '/' */ if ((ret < 0) || (ret >= sizeof(path))) { DPRINTF(E_LOG, L_HTTPD, "Request exceeds PATH_MAX: %s\n", uri); httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); return; } ret = lstat(path, &sb); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not lstat() %s: %s\n", path, strerror(errno)); httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); return; } if (S_ISDIR(sb.st_mode)) { redirect_to_index(req, uri); return; } else if (S_ISLNK(sb.st_mode)) { deref = m_realpath(path); if (!deref) { DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno)); httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); return; } if (strlen(deref) + 1 > PATH_MAX) { DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path); httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); free(deref); return; } strcpy(path, deref); free(deref); ret = stat(path, &sb); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", path, strerror(errno)); httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); return; } if (S_ISDIR(sb.st_mode)) { redirect_to_index(req, uri); return; } } if (path_is_legal(path) != 0) { httpd_send_error(req, 403, "Forbidden"); return; } evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_HTTPD, "Could not create evbuffer\n"); httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error"); return; } fd = open(path, O_RDONLY); if (fd < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", path, strerror(errno)); httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); return; } /* FIXME: this is broken, if we ever need to serve files here, * this must be fixed. */ ret = evbuffer_read(evbuf, fd, sb.st_size); close(fd); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not read file into evbuffer\n"); httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error"); return; } ctype = "application/octet-stream"; ext = strrchr(path, '.'); if (ext) { for (i = 0; ext2ctype[i].ext; i++) { if (strcmp(ext, ext2ctype[i].ext) == 0) { ctype = ext2ctype[i].ctype; break; } } } headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Content-Type", ctype); httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); evbuffer_free(evbuf); } /* Thread: httpd */ static void httpd_gen_cb(struct evhttp_request *req, void *arg) { struct evkeyvalq *input_headers; struct evkeyvalq *output_headers; const char *req_uri; char *uri; char *ptr; // Did we get a CORS preflight request? input_headers = evhttp_request_get_input_headers(req); if ( input_headers && allow_origin && (evhttp_request_get_command(req) == EVHTTP_REQ_OPTIONS) && evhttp_find_header(input_headers, "Origin") && evhttp_find_header(input_headers, "Access-Control-Request-Method") ) { output_headers = evhttp_request_get_output_headers(req); evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin); // Allow only GET method and authorization header in cross origin requests evhttp_add_header(output_headers, "Access-Control-Allow-Method", "GET"); evhttp_add_header(output_headers, "Access-Control-Allow-Headers", "authorization"); // In this case there is no reason to go through httpd_send_reply evhttp_send_reply(req, HTTP_OK, "OK", NULL); return; } req_uri = evhttp_request_get_uri(req); if (!req_uri) { redirect_to_index(req, "/"); return; } uri = strdup(req_uri); ptr = strchr(uri, '?'); if (ptr) { DPRINTF(E_SPAM, L_HTTPD, "Found query string\n"); *ptr = '\0'; } ptr = uri; uri = evhttp_decode_uri(uri); free(ptr); /* Dispatch protocol-specific URIs */ if (rsp_is_request(req, uri)) { rsp_request(req); goto out; } else if (daap_is_request(req, uri)) { daap_request(req); goto out; } else if (dacp_is_request(req, uri)) { dacp_request(req); goto out; } else if (streaming_is_request(req, uri)) { streaming_request(req); goto out; } DPRINTF(E_DBG, L_HTTPD, "HTTP request: %s\n", uri); /* Serve web interface files */ serve_file(req, uri); out: free(uri); } /* Thread: httpd */ static void * httpd(void *arg) { int ret; ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Error: DB init failed\n"); pthread_exit(NULL); } event_base_dispatch(evbase_httpd); if (!httpd_exit) DPRINTF(E_FATAL, L_HTTPD, "HTTPd event loop terminated ahead of time!\n"); db_perthread_deinit(); pthread_exit(NULL); } /* Thread: httpd */ static void exit_cb(int fd, short event, void *arg) { event_base_loopbreak(evbase_httpd); httpd_exit = 1; } char * httpd_fixup_uri(struct evhttp_request *req) { struct evkeyvalq *headers; const char *ua; const char *uri; const char *u; const char *q; char *fixed; char *f; int len; uri = evhttp_request_get_uri(req); if (!uri) return NULL; /* No query string, nothing to do */ q = strchr(uri, '?'); if (!q) return strdup(uri); headers = evhttp_request_get_input_headers(req); ua = evhttp_find_header(headers, "User-Agent"); if (!ua) return strdup(uri); if ((strncmp(ua, "iTunes", strlen("iTunes")) != 0) && (strncmp(ua, "Remote", strlen("Remote")) != 0) && (strncmp(ua, "Roku", strlen("Roku")) != 0)) return strdup(uri); /* Reencode + as %2B and space as + in the query, which iTunes and Roku devices don't do */ len = strlen(uri); u = q; while (*u) { if (*u == '+') len += 2; u++; } fixed = (char *)malloc(len + 1); if (!fixed) return NULL; strncpy(fixed, uri, q - uri); f = fixed + (q - uri); while (*q) { switch (*q) { case '+': *f = '%'; f++; *f = '2'; f++; *f = 'B'; break; case ' ': *f = '+'; break; default: *f = *q; break; } q++; f++; } *f = '\0'; return fixed; } static const char *http_reply_401 = "401 UnauthorizedAuthorization required"; int httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm) { struct evbuffer *evbuf; struct evkeyvalq *headers; char header[256]; const char *auth; char *authuser; char *authpwd; int ret; headers = evhttp_request_get_input_headers(req); auth = evhttp_find_header(headers, "Authorization"); if (!auth) { DPRINTF(E_DBG, L_HTTPD, "No Authorization header\n"); goto need_auth; } if (strncmp(auth, "Basic ", strlen("Basic ")) != 0) { DPRINTF(E_LOG, L_HTTPD, "Bad Authentication header\n"); goto need_auth; } auth += strlen("Basic "); authuser = b64_decode(auth); if (!authuser) { DPRINTF(E_LOG, L_HTTPD, "Could not decode Authentication header\n"); goto need_auth; } authpwd = strchr(authuser, ':'); if (!authpwd) { DPRINTF(E_LOG, L_HTTPD, "Malformed Authentication header\n"); free(authuser); goto need_auth; } *authpwd = '\0'; authpwd++; if (user) { if (strcmp(user, authuser) != 0) { DPRINTF(E_LOG, L_HTTPD, "Username mismatch\n"); free(authuser); goto need_auth; } } if (strcmp(passwd, authpwd) != 0) { DPRINTF(E_LOG, L_HTTPD, "Bad password\n"); free(authuser); goto need_auth; } free(authuser); return 0; need_auth: ret = snprintf(header, sizeof(header), "Basic realm=\"%s\"", realm); if ((ret < 0) || (ret >= sizeof(header))) { httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return -1; } evbuf = evbuffer_new(); if (!evbuf) { httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return -1; } headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "WWW-Authenticate", header); evbuffer_add(evbuf, http_reply_401, strlen(http_reply_401)); httpd_send_reply(req, 401, "Unauthorized", evbuf, HTTPD_SEND_NO_GZIP); evbuffer_free(evbuf); return -1; } /* Thread: main */ int httpd_init(void) { int v6enabled; int ret; httpd_exit = 0; evbase_httpd = event_base_new(); if (!evbase_httpd) { DPRINTF(E_FATAL, L_HTTPD, "Could not create an event base\n"); return -1; } ret = rsp_init(); if (ret < 0) { DPRINTF(E_FATAL, L_HTTPD, "RSP protocol init failed\n"); goto rsp_fail; } ret = daap_init(); if (ret < 0) { DPRINTF(E_FATAL, L_HTTPD, "DAAP protocol init failed\n"); goto daap_fail; } ret = dacp_init(); if (ret < 0) { DPRINTF(E_FATAL, L_HTTPD, "DACP protocol init failed\n"); goto dacp_fail; } streaming_init(); #ifdef HAVE_EVENTFD exit_efd = eventfd(0, EFD_CLOEXEC); if (exit_efd < 0) { DPRINTF(E_FATAL, L_HTTPD, "Could not create eventfd: %s\n", strerror(errno)); goto pipe_fail; } exitev = event_new(evbase_httpd, exit_efd, EV_READ, exit_cb, NULL); #else # ifdef HAVE_PIPE2 ret = pipe2(exit_pipe, O_CLOEXEC); # else ret = pipe(exit_pipe); # endif if (ret < 0) { DPRINTF(E_FATAL, L_HTTPD, "Could not create pipe: %s\n", strerror(errno)); goto pipe_fail; } exitev = event_new(evbase_httpd, exit_pipe[0], EV_READ, exit_cb, NULL); #endif /* HAVE_EVENTFD */ if (!exitev) { DPRINTF(E_FATAL, L_HTTPD, "Could not create exit event\n"); goto event_fail; } event_add(exitev, NULL); evhttpd = evhttp_new(evbase_httpd); if (!evhttpd) { DPRINTF(E_FATAL, L_HTTPD, "Could not create HTTP server\n"); goto event_fail; } v6enabled = cfg_getbool(cfg_getsec(cfg, "general"), "ipv6"); httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port"); // For CORS headers allow_origin = cfg_getstr(cfg_getsec(cfg, "general"), "allow_origin"); if (allow_origin) { if (strlen(allow_origin) != 0) evhttp_set_allowed_methods(evhttpd, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_HEAD | EVHTTP_REQ_OPTIONS); else allow_origin = NULL; } if (v6enabled) { ret = evhttp_bind_socket(evhttpd, "::", httpd_port); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not bind to port %d with IPv6, falling back to IPv4\n", httpd_port); v6enabled = 0; } } if (!v6enabled) { ret = evhttp_bind_socket(evhttpd, "0.0.0.0", httpd_port); if (ret < 0) { DPRINTF(E_FATAL, L_HTTPD, "Could not bind to port %d (forked-daapd already running?)\n", httpd_port); goto bind_fail; } } evhttp_set_gencb(evhttpd, httpd_gen_cb, NULL); ret = pthread_create(&tid_httpd, NULL, httpd, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_HTTPD, "Could not spawn HTTPd thread: %s\n", strerror(errno)); goto thread_fail; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_httpd, "httpd"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_httpd, "httpd"); #endif return 0; thread_fail: bind_fail: evhttp_free(evhttpd); event_fail: #ifdef HAVE_EVENTFD close(exit_efd); #else close(exit_pipe[0]); close(exit_pipe[1]); #endif pipe_fail: streaming_deinit(); dacp_deinit(); dacp_fail: daap_deinit(); daap_fail: rsp_deinit(); rsp_fail: event_base_free(evbase_httpd); return -1; } /* Thread: main */ void httpd_deinit(void) { int ret; #ifdef HAVE_EVENTFD ret = eventfd_write(exit_efd, 1); if (ret < 0) { DPRINTF(E_FATAL, L_HTTPD, "Could not send exit event: %s\n", strerror(errno)); return; } #else int dummy = 42; ret = write(exit_pipe[1], &dummy, sizeof(dummy)); if (ret != sizeof(dummy)) { DPRINTF(E_FATAL, L_HTTPD, "Could not write to exit fd: %s\n", strerror(errno)); return; } #endif ret = pthread_join(tid_httpd, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_HTTPD, "Could not join HTTPd thread: %s\n", strerror(errno)); return; } streaming_deinit(); rsp_deinit(); dacp_deinit(); daap_deinit(); #ifdef HAVE_EVENTFD close(exit_efd); #else close(exit_pipe[0]); close(exit_pipe[1]); #endif evhttp_free(evhttpd); event_base_free(evbase_httpd); } forked-daapd-25.0/src/httpd.h000066400000000000000000000035431313447753700160670ustar00rootroot00000000000000 #ifndef __HTTPD_H__ #define __HTTPD_H__ #include #include enum httpd_send_flags { HTTPD_SEND_NO_GZIP = (1 << 0), }; void httpd_stream_file(struct evhttp_request *req, int id); /* * Gzips an evbuffer * * @in in Data to be compressed * @return Compressed data - must be freed by caller */ struct evbuffer * httpd_gzip_deflate(struct evbuffer *in); /* * This wrapper around evhttp_send_reply should be used whenever a request may * come from a browser. It will automatically gzip if feasible, but the caller * may direct it not to. It will set CORS headers as appropriate. Should be * thread safe. * * @in req The evhttp request struct * @in code HTTP code, e.g. 200 * @in reason A brief explanation of the error - if NULL the standard meaning of the error code will be used * @in evbuf Data for the response body * @in flags See flags above */ void httpd_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *evbuf, enum httpd_send_flags flags); /* * This is a substitute for evhttp_send_error that should be used whenever an * error may be returned to a browser. It will set CORS headers as appropriate, * which is not possible with evhttp_send_error, because it clears the headers. * Should be thread safe. * * @in req The evhttp request struct * @in error HTTP code, e.g. 200 * @in reason A brief explanation of the error - if NULL the standard meaning of the error code will be used */ void httpd_send_error(struct evhttp_request *req, int error, const char *reason); char * httpd_fixup_uri(struct evhttp_request *req); int httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm); int httpd_init(void); void httpd_deinit(void); #endif /* !__HTTPD_H__ */ forked-daapd-25.0/src/httpd_daap.c000066400000000000000000002220311313447753700170420ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * Copyright (C) 2010 Kai Elwert * * Adapted from mt-daapd: * Copyright (C) 2003-2007 Ron Pedde * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "db.h" #include "conffile.h" #include "misc.h" #include "httpd.h" #include "transcode.h" #include "artwork.h" #include "httpd_daap.h" #include "daap_query.h" #include "dmap_common.h" #include "cache.h" #include #include #include #include /* httpd event base, from httpd.c */ extern struct event_base *evbase_httpd; /* Max number of sessions and session timeout * Many clients (including iTunes) don't seem to respect the timeout capability * that we announce, and just keep using the same session. Therefore we take a * lenient approach to actually timing out: We wait an entire week, and to * avoid running a timer for that long, we only check for expiration when adding * new sessions - see daap_session_cleanup(). */ #define DAAP_SESSION_MAX 200 #define DAAP_SESSION_TIMEOUT 604800 // One week in seconds /* We announce this timeout to the client when returning server capabilities */ #define DAAP_SESSION_TIMEOUT_CAPABILITY 1800 // 30 minutes /* Update requests refresh interval in seconds */ #define DAAP_UPDATE_REFRESH 0 /* Database number for the Radio item */ #define DAAP_DB_RADIO 2 struct uri_map { regex_t preg; char *regexp; int (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua); }; struct daap_session { int id; char *user_agent; time_t mtime; struct daap_session *next; }; struct daap_update_request { struct evhttp_request *req; /* Refresh tiemout */ struct event *timeout; struct daap_update_request *next; }; struct sort_ctx { struct evbuffer *headerlist; int16_t mshc; uint32_t mshi; uint32_t mshn; uint32_t misc_mshn; }; /* Default meta tags if not provided in the query */ static char *default_meta_plsongs = "dmap.itemkind,dmap.itemid,dmap.itemname,dmap.containeritemid,dmap.parentcontainerid"; static char *default_meta_pl = "dmap.itemid,dmap.itemname,dmap.persistentid,com.apple.itunes.smart-playlist"; static char *default_meta_group = "dmap.itemname,dmap.persistentid,daap.songalbumartist"; /* DAAP session tracking */ static struct daap_session *daap_sessions; /* Update requests */ static int current_rev; static struct daap_update_request *update_requests; static struct timeval daap_update_refresh_tv = { DAAP_UPDATE_REFRESH, 0 }; /* Session handling */ static void daap_session_free(struct daap_session *s) { if (s->user_agent) free(s->user_agent); free(s); } static void daap_session_remove(struct daap_session *s) { struct daap_session *ptr; struct daap_session *prev; prev = NULL; for (ptr = daap_sessions; ptr; ptr = ptr->next) { if (ptr == s) break; prev = ptr; } if (!ptr) { DPRINTF(E_LOG, L_DAAP, "Error: Request to remove non-existent session. BUG!\n"); return; } if (!prev) daap_sessions = s->next; else prev->next = s->next; daap_session_free(s); } static struct daap_session * daap_session_get(int id) { struct daap_session *s; for (s = daap_sessions; s; s = s->next) { if (id == s->id) return s; } return NULL; } /* Removes stale sessions and also drops the oldest sessions if DAAP_SESSION_MAX * will otherwise be exceeded */ static void daap_session_cleanup(void) { struct daap_session *s; struct daap_session *next; time_t now; int count; count = 0; now = time(NULL); for (s = daap_sessions; s; s = next) { count++; next = s->next; if ((difftime(now, s->mtime) > DAAP_SESSION_TIMEOUT) || (count > DAAP_SESSION_MAX)) { DPRINTF(E_LOG, L_DAAP, "Cleaning up DAAP session (id %d)\n", s->id); daap_session_remove(s); } } } static struct daap_session * daap_session_add(const char *user_agent, int request_session_id) { struct daap_session *s; daap_session_cleanup(); s = (struct daap_session *)malloc(sizeof(struct daap_session)); if (!s) { DPRINTF(E_LOG, L_DAAP, "Out of memory for DAAP session\n"); return NULL; } memset(s, 0, sizeof(struct daap_session)); if (request_session_id) { if (daap_session_get(request_session_id)) { DPRINTF(E_LOG, L_DAAP, "Session id requested in login (%d) is not available\n", request_session_id); return NULL; } s->id = request_session_id; } else { while ( (s->id = rand() + 100) && daap_session_get(s->id) ); } s->mtime = time(NULL); if (user_agent) s->user_agent = strdup(user_agent); if (daap_sessions) s->next = daap_sessions; daap_sessions = s; return s; } struct daap_session * daap_session_find(struct evhttp_request *req, struct evkeyvalq *query, struct evbuffer *evbuf) { struct daap_session *s; const char *param; int id; int ret; if (!req) return NULL; param = evhttp_find_header(query, "session-id"); if (!param) { DPRINTF(E_WARN, L_DAAP, "No session-id specified in request\n"); goto invalid; } ret = safe_atoi32(param, &id); if (ret < 0) goto invalid; s = daap_session_get(id); if (!s) { DPRINTF(E_LOG, L_DAAP, "DAAP session id %d not found\n", id); goto invalid; } s->mtime = time(NULL); return s; invalid: httpd_send_error(req, 403, "Forbidden"); return NULL; } /* Update requests helpers */ static void update_free(struct daap_update_request *ur) { if (ur->timeout) event_free(ur->timeout); free(ur); } static void update_remove(struct daap_update_request *ur) { struct daap_update_request *p; if (ur == update_requests) update_requests = ur->next; else { for (p = update_requests; p && (p->next != ur); p = p->next) ; if (!p) { DPRINTF(E_LOG, L_DAAP, "WARNING: struct daap_update_request not found in list; BUG!\n"); return; } p->next = ur->next; } update_free(ur); } static void update_refresh_cb(int fd, short event, void *arg) { struct daap_update_request *ur; struct evhttp_connection *evcon; struct evbuffer *evbuf; int ret; ur = (struct daap_update_request *)arg; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_DAAP, "Could not allocate evbuffer for DAAP update data\n"); return; } ret = evbuffer_expand(evbuf, 32); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP update data\n"); return; } /* Send back current revision */ dmap_add_container(evbuf, "mupd", 24); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "musr", current_rev); /* 12 */ evcon = evhttp_request_get_connection(ur->req); evhttp_connection_set_closecb(evcon, NULL, NULL); httpd_send_reply(ur->req, HTTP_OK, "OK", evbuf, 0); update_remove(ur); } static void update_fail_cb(struct evhttp_connection *evcon, void *arg) { struct evhttp_connection *evc; struct daap_update_request *ur; ur = (struct daap_update_request *)arg; DPRINTF(E_DBG, L_DAAP, "Update request: client closed connection\n"); evc = evhttp_request_get_connection(ur->req); if (evc) evhttp_connection_set_closecb(evc, NULL, NULL); update_remove(ur); } /* DAAP sort headers helpers */ static struct sort_ctx * daap_sort_context_new(void) { struct sort_ctx *ctx; int ret; ctx = (struct sort_ctx *)malloc(sizeof(struct sort_ctx)); if (!ctx) { DPRINTF(E_LOG, L_DAAP, "Out of memory for sorting context\n"); return NULL; } memset(ctx, 0, sizeof(struct sort_ctx)); ctx->headerlist = evbuffer_new(); if (!ctx->headerlist) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP sort headers list\n"); free(ctx); return NULL; } ret = evbuffer_expand(ctx->headerlist, 512); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP sort headers list\n"); evbuffer_free(ctx->headerlist); free(ctx); return NULL; } ctx->mshc = -1; return ctx; } static void daap_sort_context_free(struct sort_ctx *ctx) { evbuffer_free(ctx->headerlist); free(ctx); } static int daap_sort_build(struct sort_ctx *ctx, char *str) { uint8_t *ret; size_t len; char fl; len = strlen(str); if (len > 0) { ret = u8_normalize(UNINORM_NFD, (uint8_t *)str, len + 1, NULL, &len); if (!ret) { DPRINTF(E_LOG, L_DAAP, "Could not normalize string for sort header\n"); return -1; } fl = ret[0]; free(ret); } else fl = 0; if (isascii(fl) && isalpha(fl)) { fl = toupper(fl); /* Init */ if (ctx->mshc == -1) ctx->mshc = fl; if (fl == ctx->mshc) ctx->mshn++; else { dmap_add_container(ctx->headerlist, "mlit", 34); dmap_add_short(ctx->headerlist, "mshc", ctx->mshc); /* 10 */ dmap_add_int(ctx->headerlist, "mshi", ctx->mshi); /* 12 */ dmap_add_int(ctx->headerlist, "mshn", ctx->mshn); /* 12 */ DPRINTF(E_DBG, L_DAAP, "Added sort header: mshc = %c, mshi = %u, mshn = %u fl %c\n", ctx->mshc, ctx->mshi, ctx->mshn, fl); ctx->mshi = ctx->mshi + ctx->mshn; ctx->mshn = 1; ctx->mshc = fl; } } else { /* Non-ASCII, goes to misc category */ ctx->misc_mshn++; } return 0; } static void daap_sort_finalize(struct sort_ctx *ctx) { /* Add current entry, if any */ if (ctx->mshc != -1) { dmap_add_container(ctx->headerlist, "mlit", 34); dmap_add_short(ctx->headerlist, "mshc", ctx->mshc); /* 10 */ dmap_add_int(ctx->headerlist, "mshi", ctx->mshi); /* 12 */ dmap_add_int(ctx->headerlist, "mshn", ctx->mshn); /* 12 */ ctx->mshi = ctx->mshi + ctx->mshn; DPRINTF(E_DBG, L_DAAP, "Added sort header: mshc = %c, mshi = %u, mshn = %u (final)\n", ctx->mshc, ctx->mshi, ctx->mshn); } /* Add misc category */ dmap_add_container(ctx->headerlist, "mlit", 34); dmap_add_short(ctx->headerlist, "mshc", '0'); /* 10 */ dmap_add_int(ctx->headerlist, "mshi", ctx->mshi); /* 12 */ dmap_add_int(ctx->headerlist, "mshn", ctx->misc_mshn); /* 12 */ } /* Remotes are clients that will issue DACP commands. For these clients we will * do the playback, and we will not stream to them. This is a crude function to * identify them, so we can give them appropriate treatment. */ static int is_remote(const char *user_agent) { if (!user_agent) return 0; if (strcasestr(user_agent, "remote")) return 1; if (strstr(user_agent, "Retune")) return 1; return 0; } /* We try not to return items that the client cannot play (like Spotify and * internet streams in iTunes), or which are inappropriate (like internet streams * in the album tab of remotes) */ static void user_agent_filter(const char *user_agent, struct query_params *qp) { const char *filter; char *buffer; int len; if (!user_agent) return; // Valgrind doesn't like strlen(filter) below, so instead we allocate 128 bytes // to hold the string and the leading " AND ". Remember to adjust the 128 if // you define strings here that will be too large for the buffer. if (is_remote(user_agent)) filter = "(f.data_kind <> 1)"; // No internet radio else filter = "(f.data_kind = 0)"; // Only real files if (qp->filter) { len = strlen(qp->filter) + 128; buffer = (char *)malloc(len); snprintf(buffer, len, "%s AND %s", qp->filter, filter); free(qp->filter); qp->filter = strdup(buffer); free(buffer); } else qp->filter = strdup(filter); DPRINTF(E_DBG, L_DAAP, "SQL filter w/client mod: %s\n", qp->filter); } /* Returns eg /databases/1/containers from /databases/1/containers?meta=dmap.item... */ static char * extract_uri(char *full_uri) { char *uri; char *ptr; ptr = strchr(full_uri, '?'); if (ptr) *ptr = '\0'; uri = strdup(full_uri); if (ptr) *ptr = '?'; if (!uri) return NULL; ptr = uri; uri = evhttp_decode_uri(uri); free(ptr); return uri; } static void get_query_params(struct evkeyvalq *query, int *sort_headers, struct query_params *qp) { const char *param; char *ptr; int low; int high; int ret; low = 0; high = -1; /* No limit */ param = evhttp_find_header(query, "index"); if (param) { if (param[0] == '-') /* -n, last n entries */ DPRINTF(E_LOG, L_DAAP, "Unsupported index range: %s\n", param); else { ret = safe_atoi32(param, &low); if (ret < 0) DPRINTF(E_LOG, L_DAAP, "Could not parse index range: %s\n", param); else { ptr = strchr(param, '-'); if (!ptr) /* single item */ high = low; else { ptr++; if (*ptr != '\0') /* low-high */ { ret = safe_atoi32(ptr, &high); if (ret < 0) DPRINTF(E_LOG, L_DAAP, "Could not parse high index in range: %s\n", param); } } } } DPRINTF(E_DBG, L_DAAP, "Index range %s: low %d, high %d (offset %d, limit %d)\n", param, low, high, qp->offset, qp->limit); } if (high < low) high = -1; /* No limit */ qp->offset = low; if (high < 0) qp->limit = -1; /* No limit */ else qp->limit = (high - low) + 1; if (qp->limit == -1 && qp->offset == 0) qp->idx_type = I_NONE; else qp->idx_type = I_SUB; qp->sort = S_NONE; param = evhttp_find_header(query, "sort"); if (param) { if (strcmp(param, "name") == 0) qp->sort = S_NAME; else if (strcmp(param, "album") == 0) qp->sort = S_ALBUM; else if (strcmp(param, "artist") == 0) qp->sort = S_ARTIST; else if (strcmp(param, "releasedate") == 0) qp->sort = S_NAME; else DPRINTF(E_DBG, L_DAAP, "Unknown sort param: %s\n", param); if (qp->sort != S_NONE) DPRINTF(E_DBG, L_DAAP, "Sorting songlist by %s\n", param); } if (sort_headers) { *sort_headers = 0; param = evhttp_find_header(query, "include-sort-headers"); if (param && (strcmp(param, "1") == 0)) { *sort_headers = 1; DPRINTF(E_SPAM, L_DAAP, "Sort headers requested\n"); } } param = evhttp_find_header(query, "query"); if (!param) param = evhttp_find_header(query, "filter"); if (param) { DPRINTF(E_DBG, L_DAAP, "DAAP browse query filter: %s\n", param); qp->filter = daap_query_parse_sql(param); if (!qp->filter) DPRINTF(E_LOG, L_DAAP, "Ignoring improper DAAP query: %s\n", param); /* iTunes seems to default to this when there is a query (which there is for audiobooks, but not for normal playlists) */ if (qp->sort == S_NONE) qp->sort = S_ALBUM; } } static int parse_meta(struct evhttp_request *req, char *tag, const char *param, const struct dmap_field ***out_meta) { const struct dmap_field **meta; char *ptr; char *field; char *metastr; int nmeta; int i; int n; metastr = strdup(param); if (!metastr) { DPRINTF(E_LOG, L_DAAP, "Could not duplicate meta parameter; out of memory\n"); dmap_send_error(req, tag, "Out of memory"); return -1; } nmeta = 1; ptr = metastr; while ((ptr = strchr(ptr + 1, ',')) && (strlen(ptr) > 1)) nmeta++; DPRINTF(E_DBG, L_DAAP, "Asking for %d meta tags\n", nmeta); meta = (const struct dmap_field **)malloc(nmeta * sizeof(const struct dmap_field *)); if (!meta) { DPRINTF(E_LOG, L_DAAP, "Could not allocate meta array; out of memory\n"); dmap_send_error(req, tag, "Out of memory"); nmeta = -1; goto out; } memset(meta, 0, nmeta * sizeof(struct dmap_field *)); field = strtok_r(metastr, ",", &ptr); for (i = 0; i < nmeta; i++) { for (n = 0; (n < i) && (strcmp(field, meta[n]->desc) != 0); n++); if (n == i) { meta[i] = dmap_find_field(field, strlen(field)); if (!meta[i]) { DPRINTF(E_WARN, L_DAAP, "Could not find requested meta field '%s'\n", field); i--; nmeta--; } } else { DPRINTF(E_WARN, L_DAAP, "Parser will ignore duplicate occurrence of meta field '%s'\n", field); i--; nmeta--; } field = strtok_r(NULL, ",", &ptr); if (!field) break; } DPRINTF(E_DBG, L_DAAP, "Found %d meta tags\n", nmeta); *out_meta = meta; out: free(metastr); return nmeta; } static int daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct evbuffer *content; struct evkeyvalq *headers; cfg_t *lib; char *name; char *passwd; const char *clientver; size_t len; int mpro; int apro; lib = cfg_getsec(cfg, "library"); passwd = cfg_getstr(lib, "password"); name = cfg_getstr(lib, "name"); content = evbuffer_new(); if (!content) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP server-info reply\n"); dmap_send_error(req, "msrv", "Out of memory"); return -1; } mpro = 2 << 16 | 10; apro = 3 << 16 | 12; headers = evhttp_request_get_input_headers(req); clientver = evhttp_find_header(headers, "Client-DAAP-Version"); if (clientver) { if (strcmp(clientver, "1.0") == 0) { mpro = 1 << 16; apro = 1 << 16; } else if (strcmp(clientver, "2.0") == 0) { mpro = 1 << 16; apro = 2 << 16; } } dmap_add_int(content, "mstt", 200); dmap_add_int(content, "mpro", mpro); // dmap.protocolversion dmap_add_string(content, "minm", name); // dmap.itemname (server name) dmap_add_int(content, "apro", apro); // daap.protocolversion dmap_add_int(content, "aeSV", apro); // com.apple.itunes.music-sharing-version (determines if itunes shows share types) dmap_add_short(content, "ated", 7); // daap.supportsextradata /* Sub-optimal user-agent sniffing to solve the problem that iTunes 12.1 * does not work if we announce support for groups. */ ua = evhttp_find_header(headers, "User-Agent"); if (ua && (strncmp(ua, "iTunes", strlen("iTunes")) == 0)) dmap_add_short(content, "asgr", 0); // daap.supportsgroups (1=artists, 2=albums, 3=both) else dmap_add_short(content, "asgr", 3); // daap.supportsgroups (1=artists, 2=albums, 3=both) // dmap_add_long(content, "asse", 0x80000); // unknown - used by iTunes dmap_add_char(content, "aeMQ", 1); // unknown - used by iTunes // dmap_add_long(content, "mscu", ); // unknown - used by iTunes // dmap_add_char(content, "aeFR", ); // unknown - used by iTunes dmap_add_char(content, "aeTr", 1); // unknown - used by iTunes dmap_add_char(content, "aeSL", 1); // unknown - used by iTunes dmap_add_char(content, "aeSR", 1); // unknown - used by iTunes // dmap_add_char(content, "aeFP", 2); // triggers FairPlay request // dmap_add_long(content, "aeSX", ); // unknown - used by iTunes // dmap_add_int(content, "ppro", ); // dpap.protocolversion dmap_add_char(content, "msed", 0); // dmap.supportsedit? - we don't support playlist editing dmap_add_char(content, "mslr", 1); // dmap.loginrequired dmap_add_int(content, "mstm", DAAP_SESSION_TIMEOUT_CAPABILITY); // dmap.timeoutinterval dmap_add_char(content, "msal", 1); // dmap.supportsautologout // dmap_add_char(content, "msas", 3); // dmap.authenticationschemes dmap_add_char(content, "msau", (passwd) ? 2 : 0); // dmap.authenticationmethod dmap_add_char(content, "msup", 1); // dmap.supportsupdate dmap_add_char(content, "mspi", 1); // dmap.supportspersistentids dmap_add_char(content, "msex", 1); // dmap.supportsextensions dmap_add_char(content, "msbr", 1); // dmap.supportsbrowse dmap_add_char(content, "msqy", 1); // dmap.supportsquery dmap_add_char(content, "msix", 1); // dmap.supportsindex // dmap_add_char(content, "msrs", 1); // dmap.supportsresolve dmap_add_int(content, "msdc", 2); // dmap.databasescount // dmap_add_int(content, "mstc", ); // dmap.utctime // dmap_add_int(content, "msto", ); // dmap.utcoffset // Create container len = evbuffer_get_length(content); dmap_add_container(evbuf, "msrv", len); evbuffer_add_buffer(evbuf, content); evbuffer_free(content); httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; } static int daap_reply_content_codes(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { const struct dmap_field *dmap_fields; size_t len; int nfields; int i; int ret; dmap_fields = dmap_get_fields_table(&nfields); len = 12; for (i = 0; i < nfields; i++) len += 8 + 12 + 10 + 8 + strlen(dmap_fields[i].desc); ret = evbuffer_expand(evbuf, len + 8); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP content-codes reply\n"); dmap_send_error(req, "mccr", "Out of memory"); return -1; } dmap_add_container(evbuf, "mccr", len); dmap_add_int(evbuf, "mstt", 200); for (i = 0; i < nfields; i++) { len = 12 + 10 + 8 + strlen(dmap_fields[i].desc); dmap_add_container(evbuf, "mdcl", len); dmap_add_string(evbuf, "mcnm", dmap_fields[i].tag); /* 12 */ dmap_add_string(evbuf, "mcna", dmap_fields[i].desc); /* 8 + strlen(desc) */ dmap_add_short(evbuf, "mcty", dmap_fields[i].type); /* 10 */ } httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; } static int daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct pairing_info pi; struct daap_session *s; const char *param; int request_session_id; int ret; ret = evbuffer_expand(evbuf, 32); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP login reply\n"); dmap_send_error(req, "mlog", "Out of memory"); return -1; } if (ua && (strncmp(ua, "Remote", strlen("Remote")) == 0)) { param = evhttp_find_header(query, "pairing-guid"); if (!param) { DPRINTF(E_LOG, L_DAAP, "Login attempt with U-A: Remote and no pairing-guid\n"); httpd_send_error(req, 403, "Forbidden"); return -1; } memset(&pi, 0, sizeof(struct pairing_info)); pi.guid = strdup(param + 2); /* Skip leading 0X */ ret = db_pairing_fetch_byguid(&pi); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Login attempt with invalid pairing-guid\n"); free_pi(&pi, 1); httpd_send_error(req, 403, "Forbidden"); return -1; } DPRINTF(E_INFO, L_DAAP, "Remote '%s' logging in with GUID %s\n", pi.name, pi.guid); free_pi(&pi, 1); } param = evhttp_find_header(query, "request-session-id"); if (param) { ret = safe_atoi32(param, &request_session_id); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Login request where request-session-id is not an integer\n"); request_session_id = 0; } } else request_session_id = 0; s = daap_session_add(ua, request_session_id); if (!s) { dmap_send_error(req, "mlog", "Could not start session"); return -1; } dmap_add_container(evbuf, "mlog", 24); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "mlid", s->id); /* 12 */ httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; } static int daap_reply_logout(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return -1; daap_session_remove(s); httpd_send_reply(req, 204, "Logout Successful", evbuf, 0); return 0; } static int daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct daap_session *s; struct daap_update_request *ur; struct evhttp_connection *evcon; const char *param; int reqd_rev; int ret; s = daap_session_find(req, query, evbuf); if (!s) return -1; param = evhttp_find_header(query, "revision-number"); if (!param) { DPRINTF(E_DBG, L_DAAP, "Missing revision-number in client update request\n"); /* Some players (Amarok, Banshee) don't supply a revision number. They get a standard update of everything. */ param = "1"; /* Default to "1" will insure update */ } ret = safe_atoi32(param, &reqd_rev); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Parameter revision-number not an integer\n"); dmap_send_error(req, "mupd", "Invalid request"); return -1; } if (reqd_rev == 1) /* Or revision is not valid */ { ret = evbuffer_expand(evbuf, 32); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP update reply\n"); dmap_send_error(req, "mupd", "Out of memory"); return -1; } /* Send back current revision */ dmap_add_container(evbuf, "mupd", 24); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "musr", current_rev); /* 12 */ httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; } /* Else, just let the request hang until we have changes to push back */ ur = (struct daap_update_request *)malloc(sizeof(struct daap_update_request)); if (!ur) { DPRINTF(E_LOG, L_DAAP, "Out of memory for update request\n"); dmap_send_error(req, "mupd", "Out of memory"); return -1; } memset(ur, 0, sizeof(struct daap_update_request)); if (DAAP_UPDATE_REFRESH > 0) { ur->timeout = evtimer_new(evbase_httpd, update_refresh_cb, ur); if (ur->timeout) ret = evtimer_add(ur->timeout, &daap_update_refresh_tv); else ret = -1; if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Out of memory for update request event\n"); dmap_send_error(req, "mupd", "Could not register timer"); update_free(ur); return -1; } } /* NOTE: we may need to keep reqd_rev in there too */ ur->req = req; ur->next = update_requests; update_requests = ur; /* If the connection fails before we have an update to push out * to the client, we need to know. */ evcon = evhttp_request_get_connection(req); if (evcon) evhttp_connection_set_closecb(evcon, update_fail_cb, ur); return 0; } static int daap_reply_activity(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { /* That's so nice, thanks for letting us know */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); return 0; } static int daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct evbuffer *content; struct evbuffer *item; struct daap_session *s; cfg_t *lib; char *name; char *name_radio; size_t len; int count; s = daap_session_find(req, query, evbuf); if (!s) return -1; lib = cfg_getsec(cfg, "library"); name = cfg_getstr(lib, "name"); name_radio = cfg_getstr(lib, "name_radio"); content = evbuffer_new(); if (!content) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist reply\n"); dmap_send_error(req, "avdb", "Out of memory"); return -1; } // Add db entry for library with dbid = 1 item = evbuffer_new(); if (!item) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist library item\n"); dmap_send_error(req, "avdb", "Out of memory"); return -1; } dmap_add_int(item, "miid", 1); dmap_add_long(item, "mper", 1); dmap_add_int(item, "mdbk", 1); dmap_add_int(item, "aeCs", 1); dmap_add_string(item, "minm", name); count = db_files_get_count(); dmap_add_int(item, "mimc", count); count = db_pl_get_count(); // TODO Don't count empty smart playlists, because they get excluded in aply dmap_add_int(item, "mctc", count); // dmap_add_int(content, "aeMk", 0x405); // com.apple.itunes.extended-media-kind (OR of all in library) dmap_add_int(item, "meds", 3); // Create container for library db len = evbuffer_get_length(item); dmap_add_container(content, "mlit", len); evbuffer_add_buffer(content, item); evbuffer_free(item); // Add second db entry for radio with dbid = DAAP_DB_RADIO item = evbuffer_new(); if (!item) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist radio item\n"); dmap_send_error(req, "avdb", "Out of memory"); return -1; } dmap_add_int(item, "miid", DAAP_DB_RADIO); dmap_add_long(item, "mper", DAAP_DB_RADIO); dmap_add_int(item, "mdbk", 0x64); dmap_add_int(item, "aeCs", 0); dmap_add_string(item, "minm", name_radio); count = db_pl_get_count(); // TODO This counts too much, should only include stream playlists dmap_add_int(item, "mimc", count); dmap_add_int(item, "mctc", 0); dmap_add_int(item, "aeMk", 1); // com.apple.itunes.extended-media-kind (OR of all in library) dmap_add_int(item, "meds", 3); // Create container for radio db len = evbuffer_get_length(item); dmap_add_container(content, "mlit", len); evbuffer_add_buffer(content, item); evbuffer_free(item); // Create container len = evbuffer_get_length(content); dmap_add_container(evbuf, "avdb", len + 53); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_char(evbuf, "muty", 0); /* 9 */ dmap_add_int(evbuf, "mtco", 2); /* 12 */ dmap_add_int(evbuf, "mrco", 2); /* 12 */ dmap_add_container(evbuf, "mlcl", len); /* 8 */ evbuffer_add_buffer(evbuf, content); evbuffer_free(content); httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; } static int daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf, int playlist, struct evkeyvalq *query, const char *ua) { struct daap_session *s; struct query_params qp; struct db_media_file_info dbmfi; struct evbuffer *song; struct evbuffer *songlist; struct evkeyvalq *headers; const struct dmap_field **meta; struct sort_ctx *sctx; const char *param; const char *client_codecs; char *last_codectype; char *tag; size_t len; int nmeta; int sort_headers; int nsongs; int remote; int transcode; int ret; s = daap_session_find(req, query, evbuf); if (!s && req) return -1; DPRINTF(E_DBG, L_DAAP, "Fetching song list for playlist %d\n", playlist); if (playlist != -1) tag = "apso"; /* Songs in playlist */ else tag = "adbs"; /* Songs in database */ ret = evbuffer_expand(evbuf, 61); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP song list reply\n"); dmap_send_error(req, tag, "Out of memory"); return -1; } songlist = evbuffer_new(); if (!songlist) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP song list\n"); dmap_send_error(req, tag, "Out of memory"); return -1; } /* Start with a big enough evbuffer - it'll expand as needed */ ret = evbuffer_expand(songlist, 4096); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DMAP song list\n"); dmap_send_error(req, tag, "Out of memory"); goto out_list_free; } song = evbuffer_new(); if (!song) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP song block\n"); dmap_send_error(req, tag, "Out of memory"); goto out_list_free; } /* The buffer will expand if needed */ ret = evbuffer_expand(song, 512); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DMAP song block\n"); dmap_send_error(req, tag, "Out of memory"); goto out_song_free; } param = evhttp_find_header(query, "meta"); if (!param) { DPRINTF(E_DBG, L_DAAP, "No meta parameter in query, using default\n"); if (playlist != -1) param = default_meta_plsongs; } if (param) { nmeta = parse_meta(req, tag, param, &meta); if (nmeta < 0) { DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n"); goto out_song_free; } } else { meta = NULL; nmeta = 0; } memset(&qp, 0, sizeof(struct query_params)); get_query_params(query, &sort_headers, &qp); if (playlist == -1) user_agent_filter(ua, &qp); sctx = NULL; if (sort_headers) { sctx = daap_sort_context_new(); if (!sctx) { DPRINTF(E_LOG, L_DAAP, "Could not create sort context\n"); dmap_send_error(req, tag, "Out of memory"); goto out_query_free; } } if (playlist != -1) { qp.type = Q_PLITEMS; qp.id = playlist; } else qp.type = Q_ITEMS; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); dmap_send_error(req, tag, "Could not start query"); if (sort_headers) daap_sort_context_free(sctx); goto out_query_free; } remote = is_remote(ua); client_codecs = NULL; if (!remote && req) { headers = evhttp_request_get_input_headers(req); client_codecs = evhttp_find_header(headers, "Accept-Codecs"); } nsongs = 0; last_codectype = NULL; while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { nsongs++; if (!dbmfi.codectype) { DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname); transcode = 0; } else if (remote) { transcode = 1; } else if (!last_codectype || (strcmp(last_codectype, dbmfi.codectype) != 0)) { transcode = transcode_needed(ua, client_codecs, dbmfi.codectype); if (last_codectype) free(last_codectype); last_codectype = strdup(dbmfi.codectype); } ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers, transcode); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Failed to encode song metadata\n"); ret = -100; break; } if (sort_headers) { ret = daap_sort_build(sctx, dbmfi.title_sort); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add sort header to DAAP song list reply\n"); ret = -100; break; } } DPRINTF(E_SPAM, L_DAAP, "Done with song\n"); } DPRINTF(E_DBG, L_DAAP, "Done with song list, %d songs\n", nsongs); if (last_codectype) free(last_codectype); if (nmeta > 0) free(meta); evbuffer_free(song); if (qp.filter) free(qp.filter); if (ret < 0) { if (ret == -100) dmap_send_error(req, tag, "Out of memory"); else { DPRINTF(E_LOG, L_DAAP, "Error fetching results\n"); dmap_send_error(req, tag, "Error fetching query results"); } db_query_end(&qp); if (sort_headers) daap_sort_context_free(sctx); goto out_list_free; } /* Add header to evbuf, add songlist to evbuf */ len = evbuffer_get_length(songlist); if (sort_headers) { daap_sort_finalize(sctx); dmap_add_container(evbuf, tag, len + evbuffer_get_length(sctx->headerlist) + 61); } else dmap_add_container(evbuf, tag, len + 53); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_char(evbuf, "muty", 0); /* 9 */ dmap_add_int(evbuf, "mtco", qp.results); /* 12 */ dmap_add_int(evbuf, "mrco", nsongs); /* 12 */ dmap_add_container(evbuf, "mlcl", len); /* 8 */ db_query_end(&qp); ret = evbuffer_add_buffer(evbuf, songlist); evbuffer_free(songlist); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add song list to DAAP song list reply\n"); dmap_send_error(req, tag, "Out of memory"); if (sort_headers) daap_sort_context_free(sctx); return -1; } if (sort_headers) { len = evbuffer_get_length(sctx->headerlist); dmap_add_container(evbuf, "mshl", len); /* 8 */ ret = evbuffer_add_buffer(evbuf, sctx->headerlist); daap_sort_context_free(sctx); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add sort headers to DAAP song list reply\n"); dmap_send_error(req, tag, "Out of memory"); return -1; } } httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; out_query_free: if (nmeta > 0) free(meta); if (qp.filter) free(qp.filter); out_song_free: evbuffer_free(song); out_list_free: evbuffer_free(songlist); return -1; } static int daap_reply_dbsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { return daap_reply_songlist_generic(req, evbuf, -1, query, ua); } static int daap_reply_plsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { int playlist; int ret; ret = safe_atoi32(uri[3], &playlist); if (ret < 0) { dmap_send_error(req, "apso", "Invalid playlist ID"); return -1; } return daap_reply_songlist_generic(req, evbuf, playlist, query, ua); } static int daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct query_params qp; struct db_playlist_info dbpli; struct daap_session *s; struct evbuffer *playlistlist; struct evbuffer *playlist; cfg_t *lib; const struct dmap_field_map *dfm; const struct dmap_field *df; const struct dmap_field **meta; const char *param; char **strval; size_t len; int database; int cfg_radiopl; int nmeta; int npls; int32_t plid; int32_t pltype; int32_t plitems; int32_t plstreams; int32_t plparent; int i; int ret; s = daap_session_find(req, query, evbuf); if (!s) return -1; ret = safe_atoi32(uri[1], &database); if (ret < 0) { dmap_send_error(req, "aply", "Invalid database ID"); return -1; } lib = cfg_getsec(cfg, "library"); cfg_radiopl = cfg_getbool(lib, "radio_playlists"); ret = evbuffer_expand(evbuf, 61); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP playlists reply\n"); dmap_send_error(req, "aply", "Out of memory"); return -1; } playlistlist = evbuffer_new(); if (!playlistlist) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP playlist list\n"); dmap_send_error(req, "aply", "Out of memory"); return -1; } /* Start with a big enough evbuffer - it'll expand as needed */ ret = evbuffer_expand(playlistlist, 1024); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DMAP playlist list\n"); dmap_send_error(req, "aply", "Out of memory"); goto out_list_free; } playlist = evbuffer_new(); if (!playlist) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP playlist block\n"); dmap_send_error(req, "aply", "Out of memory"); goto out_list_free; } /* The buffer will expand if needed */ ret = evbuffer_expand(playlist, 128); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DMAP playlist block\n"); dmap_send_error(req, "aply", "Out of memory"); goto out_pl_free; } param = evhttp_find_header(query, "meta"); if (!param) { DPRINTF(E_LOG, L_DAAP, "No meta parameter in query, using default\n"); param = default_meta_pl; } nmeta = parse_meta(req, "aply", param, &meta); if (nmeta < 0) { DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n"); goto out_pl_free; } memset(&qp, 0, sizeof(struct query_params)); get_query_params(query, NULL, &qp); qp.type = Q_PL; if (qp.sort == S_NONE) qp.sort = S_PLAYLIST; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); dmap_send_error(req, "aply", "Could not start query"); goto out_query_free; } npls = 0; while (((ret = db_query_fetch_pl(&qp, &dbpli, 1)) == 0) && (dbpli.id)) { plid = 1; if (safe_atoi32(dbpli.id, &plid) != 0) continue; pltype = 0; if (safe_atoi32(dbpli.type, &pltype) != 0) continue; plitems = 0; if (safe_atoi32(dbpli.items, &plitems) != 0) continue; plstreams = 0; if (safe_atoi32(dbpli.streams, &plstreams) != 0) continue; /* Database DAAP_DB_RADIO is radio, so for that db skip playlists without * streams and for other databases skip playlists which are just streams */ if ((database == DAAP_DB_RADIO) && (plstreams == 0)) continue; if (!cfg_radiopl && (database != DAAP_DB_RADIO) && (plstreams > 0) && (plstreams == plitems)) continue; /* Don't add empty Special playlists */ if ((plid > 1) && (plitems == 0) && (pltype == PL_SPECIAL)) continue; npls++; for (i = 0; i < nmeta; i++) { df = meta[i]; dfm = df->dfm; /* dmap.itemcount - always added */ if (dfm == &dfm_dmap_mimc) continue; /* Add field "com.apple.itunes.smart-playlist" for special and smart playlists (excluding the special playlist for "library" with id = 1) */ if (dfm == &dfm_dmap_aeSP) { if ((pltype == PL_SMART) || ((pltype == PL_SPECIAL) && (plid != 1))) { dmap_add_char(playlist, "aeSP", 1); } /* Add field "com.apple.itunes.special-playlist" for special playlists (excluding the special playlist for "library" with id = 1) */ if ((pltype == PL_SPECIAL) && (plid != 1)) { int32_t aePS = 0; ret = safe_atoi32(dbpli.special_id, &aePS); if ((ret == 0) && (aePS > 0)) dmap_add_char(playlist, "aePS", aePS); } continue; } /* Not in struct playlist_info */ if (dfm->pli_offset < 0) continue; strval = (char **) ((char *)&dbpli + dfm->pli_offset); if (!(*strval) || (**strval == '\0')) continue; dmap_add_field(playlist, df, *strval, 0); DPRINTF(E_SPAM, L_DAAP, "Done with meta tag %s (%s)\n", df->desc, *strval); } /* Item count (mimc) */ dmap_add_int(playlist, "mimc", plitems); /* Container ID (mpco) */ ret = safe_atoi32(dbpli.parent_id, &plparent); if (ret == 0) dmap_add_int(playlist, "mpco", plparent); else dmap_add_int(playlist, "mpco", 0); /* Base playlist (abpl), id = 1 */ if (plid == 1) dmap_add_char(playlist, "abpl", 1); DPRINTF(E_DBG, L_DAAP, "Done with playlist\n"); len = evbuffer_get_length(playlist); dmap_add_container(playlistlist, "mlit", len); ret = evbuffer_add_buffer(playlistlist, playlist); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add playlist to playlist list for DAAP playlists reply\n"); ret = -100; break; } } DPRINTF(E_DBG, L_DAAP, "Done with playlist list, %d playlists\n", npls); free(meta); evbuffer_free(playlist); if (qp.filter) free(qp.filter); if (ret < 0) { if (ret == -100) dmap_send_error(req, "aply", "Out of memory"); else { DPRINTF(E_LOG, L_DAAP, "Error fetching results\n"); dmap_send_error(req, "aply", "Error fetching query results"); } db_query_end(&qp); goto out_list_free; } /* Add header to evbuf, add playlistlist to evbuf */ len = evbuffer_get_length(playlistlist); dmap_add_container(evbuf, "aply", len + 53); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_char(evbuf, "muty", 0); /* 9 */ dmap_add_int(evbuf, "mtco", qp.results); /* 12 */ dmap_add_int(evbuf,"mrco", npls); /* 12 */ dmap_add_container(evbuf, "mlcl", len); db_query_end(&qp); ret = evbuffer_add_buffer(evbuf, playlistlist); evbuffer_free(playlistlist); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add playlist list to DAAP playlists reply\n"); dmap_send_error(req, "aply", "Out of memory"); return -1; } httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; out_query_free: free(meta); if (qp.filter) free(qp.filter); out_pl_free: evbuffer_free(playlist); out_list_free: evbuffer_free(playlistlist); return -1; } static int daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct query_params qp; struct db_group_info dbgri; struct daap_session *s; struct evbuffer *group; struct evbuffer *grouplist; const struct dmap_field_map *dfm; const struct dmap_field *df; const struct dmap_field **meta; struct sort_ctx *sctx; cfg_t *lib; const char *param; char **strval; char *tag; size_t len; int nmeta; int sort_headers; int ngrp; int32_t val; int i; int ret; s = daap_session_find(req, query, evbuf); if (!s && req) return -1; memset(&qp, 0, sizeof(struct query_params)); get_query_params(query, &sort_headers, &qp); user_agent_filter(ua, &qp); param = evhttp_find_header(query, "group-type"); if (strcmp(param, "artists") == 0) { // Request from Remote may have the form: // groups?meta=dmap.xxx,dma...&type=music&group-type=artists&sort=album&include-sort-headers=1&query=('...')&session-id=... // Note: Since grouping by artist and sorting by album is crazy we override tag = "agar"; qp.type = Q_GROUP_ARTISTS; qp.sort = S_ARTIST; } else { // Request from Remote may have the form: // groups?meta=dmap.xxx,dma...&type=music&group-type=albums&sort=artist&include-sort-headers=0&query=('...'))&session-id=... // Sort may also be 'album' tag = "agal"; qp.type = Q_GROUP_ALBUMS; if (qp.sort == S_NONE) qp.sort = S_ALBUM; } ret = evbuffer_expand(evbuf, 61); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP groups reply\n"); dmap_send_error(req, tag, "Out of memory"); goto out_qfilter_free; } grouplist = evbuffer_new(); if (!grouplist) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP group list\n"); dmap_send_error(req, tag, "Out of memory"); goto out_qfilter_free; } /* Start with a big enough evbuffer - it'll expand as needed */ ret = evbuffer_expand(grouplist, 1024); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DMAP group list\n"); dmap_send_error(req, tag, "Out of memory"); goto out_list_free; } group = evbuffer_new(); if (!group) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP group block\n"); dmap_send_error(req, tag, "Out of memory"); goto out_list_free; } /* The buffer will expand if needed */ ret = evbuffer_expand(group, 128); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DMAP group block\n"); dmap_send_error(req, tag, "Out of memory"); goto out_group_free; } param = evhttp_find_header(query, "meta"); if (!param) { DPRINTF(E_LOG, L_DAAP, "No meta parameter in query, using default\n"); param = default_meta_group; } nmeta = parse_meta(req, tag, param, &meta); if (nmeta < 0) { DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n"); goto out_group_free; } sctx = NULL; if (sort_headers) { sctx = daap_sort_context_new(); if (!sctx) { DPRINTF(E_LOG, L_DAAP, "Could not create sort context\n"); dmap_send_error(req, tag, "Out of memory"); goto out_query_free; } } ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); dmap_send_error(req, tag, "Could not start query"); if (sort_headers) daap_sort_context_free(sctx); goto out_query_free; } ngrp = 0; while ((ret = db_query_fetch_group(&qp, &dbgri)) == 0) { /* Don't add item if no name (eg blank album name) */ if (strlen(dbgri.itemname) == 0) continue; /* Don't add single item albums/artists if configured to hide */ lib = cfg_getsec(cfg, "library"); if (cfg_getbool(lib, "hide_singles") && (strcmp(dbgri.itemcount, "1") == 0)) continue; ngrp++; for (i = 0; i < nmeta; i++) { df = meta[i]; dfm = df->dfm; /* dmap.itemcount - always added */ if (dfm == &dfm_dmap_mimc) continue; /* Not in struct group_info */ if (dfm->gri_offset < 0) continue; strval = (char **) ((char *)&dbgri + dfm->gri_offset); if (!(*strval) || (**strval == '\0')) continue; dmap_add_field(group, df, *strval, 0); DPRINTF(E_SPAM, L_DAAP, "Done with meta tag %s (%s)\n", df->desc, *strval); } if (sort_headers) { ret = daap_sort_build(sctx, dbgri.itemname_sort); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add sort header to DAAP groups reply\n"); ret = -100; break; } } /* Item count, always added (mimc) */ val = 0; ret = safe_atoi32(dbgri.itemcount, &val); if ((ret == 0) && (val > 0)) dmap_add_int(group, "mimc", val); /* Song album artist (asaa), always added if group-type is albums */ if (qp.type == Q_GROUP_ALBUMS) dmap_add_string(group, "asaa", dbgri.songalbumartist); /* Item id (miid) */ val = 0; ret = safe_atoi32(dbgri.id, &val); if ((ret == 0) && (val > 0)) dmap_add_int(group, "miid", val); DPRINTF(E_SPAM, L_DAAP, "Done with group\n"); len = evbuffer_get_length(group); dmap_add_container(grouplist, "mlit", len); ret = evbuffer_add_buffer(grouplist, group); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add group to group list for DAAP groups reply\n"); ret = -100; break; } } DPRINTF(E_DBG, L_DAAP, "Done with group list, %d groups\n", ngrp); free(meta); evbuffer_free(group); if (qp.filter) { free(qp.filter); qp.filter = NULL; } if (ret < 0) { if (ret == -100) dmap_send_error(req, tag, "Out of memory"); else { DPRINTF(E_LOG, L_DAAP, "Error fetching results\n"); dmap_send_error(req, tag, "Error fetching query results"); } db_query_end(&qp); if (sort_headers) daap_sort_context_free(sctx); goto out_list_free; } /* Add header to evbuf, add grouplist to evbuf */ len = evbuffer_get_length(grouplist); if (sort_headers) { daap_sort_finalize(sctx); dmap_add_container(evbuf, tag, len + evbuffer_get_length(sctx->headerlist) + 61); } else dmap_add_container(evbuf, tag, len + 53); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_char(evbuf, "muty", 0); /* 9 */ dmap_add_int(evbuf, "mtco", qp.results); /* 12 */ dmap_add_int(evbuf,"mrco", ngrp); /* 12 */ dmap_add_container(evbuf, "mlcl", len); /* 8 */ db_query_end(&qp); ret = evbuffer_add_buffer(evbuf, grouplist); evbuffer_free(grouplist); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add group list to DAAP groups reply\n"); dmap_send_error(req, tag, "Out of memory"); if (sort_headers) daap_sort_context_free(sctx); return -1; } if (sort_headers) { len = evbuffer_get_length(sctx->headerlist); dmap_add_container(evbuf, "mshl", len); /* 8 */ ret = evbuffer_add_buffer(evbuf, sctx->headerlist); daap_sort_context_free(sctx); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add sort headers to DAAP groups reply\n"); dmap_send_error(req, tag, "Out of memory"); return -1; } } httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return 0; out_query_free: free(meta); out_group_free: evbuffer_free(group); out_list_free: evbuffer_free(grouplist); out_qfilter_free: free(qp.filter); return -1; } static int daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { struct query_params qp; struct daap_session *s; struct evbuffer *itemlist; struct sort_ctx *sctx; char *browse_item; char *sort_item; char *tag; size_t len; int sort_headers; int nitems; int ret; s = daap_session_find(req, query, evbuf); if (!s && req) return -1; memset(&qp, 0, sizeof(struct query_params)); get_query_params(query, &sort_headers, &qp); user_agent_filter(ua, &qp); if (strcmp(uri[3], "artists") == 0) { tag = "abar"; qp.type = Q_BROWSE_ARTISTS; qp.sort = S_ARTIST; } else if (strcmp(uri[3], "albums") == 0) { tag = "abal"; qp.type = Q_BROWSE_ALBUMS; qp.sort = S_ALBUM; } else if (strcmp(uri[3], "genres") == 0) { tag = "abgn"; qp.type = Q_BROWSE_GENRES; qp.sort = S_GENRE; } else if (strcmp(uri[3], "composers") == 0) { tag = "abcp"; qp.type = Q_BROWSE_COMPOSERS; qp.sort = S_COMPOSER; } else { DPRINTF(E_LOG, L_DAAP, "Invalid DAAP browse request type '%s'\n", uri[3]); dmap_send_error(req, "abro", "Invalid browse type"); ret = -1; goto out_qfilter_free; } ret = evbuffer_expand(evbuf, 52); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP browse reply\n"); dmap_send_error(req, "abro", "Out of memory"); goto out_qfilter_free; } itemlist = evbuffer_new(); if (!itemlist) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP browse item list\n"); dmap_send_error(req, "abro", "Out of memory"); ret = -1; goto out_qfilter_free; } /* Start with a big enough evbuffer - it'll expand as needed */ ret = evbuffer_expand(itemlist, 512); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DMAP browse item list\n"); dmap_send_error(req, "abro", "Out of memory"); goto out_itemlist_free; } sctx = NULL; if (sort_headers) { sctx = daap_sort_context_new(); if (!sctx) { DPRINTF(E_LOG, L_DAAP, "Could not create sort context\n"); dmap_send_error(req, "abro", "Out of memory"); ret = -1; goto out_itemlist_free; } } ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); dmap_send_error(req, "abro", "Could not start query"); goto out_sort_headers_free; } nitems = 0; while (((ret = db_query_fetch_string_sort(&qp, &browse_item, &sort_item)) == 0) && (browse_item)) { nitems++; if (sort_headers) { ret = daap_sort_build(sctx, sort_item); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add sort header to DAAP browse reply\n"); break; } } dmap_add_string(itemlist, "mlit", browse_item); } if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Error fetching/building results\n"); dmap_send_error(req, "abro", "Error fetching/building query results"); db_query_end(&qp); goto out_sort_headers_free; } len = evbuffer_get_length(itemlist); if (sort_headers) { daap_sort_finalize(sctx); dmap_add_container(evbuf, "abro", len + evbuffer_get_length(sctx->headerlist) + 52); } else dmap_add_container(evbuf, "abro", len + 44); dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "mtco", qp.results); /* 12 */ dmap_add_int(evbuf, "mrco", nitems); /* 12 */ dmap_add_container(evbuf, tag, len); /* 8 */ db_query_end(&qp); ret = evbuffer_add_buffer(evbuf, itemlist); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add item list to DAAP browse reply\n"); dmap_send_error(req, tag, "Out of memory"); goto out_sort_headers_free; } if (sort_headers) { len = evbuffer_get_length(sctx->headerlist); dmap_add_container(evbuf, "mshl", len); /* 8 */ ret = evbuffer_add_buffer(evbuf, sctx->headerlist); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add sort headers to DAAP browse reply\n"); dmap_send_error(req, tag, "Out of memory"); goto out_sort_headers_free; } } httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); ret = 0; out_sort_headers_free: if (sort_headers) daap_sort_context_free(sctx); out_itemlist_free: evbuffer_free(itemlist); out_qfilter_free: if (qp.filter) free(qp.filter); return ret; } /* NOTE: We only handle artwork at the moment */ static int daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { char clen[32]; struct daap_session *s; struct evkeyvalq *headers; const char *param; char *ctype; size_t len; int id; int max_w; int max_h; int ret; s = daap_session_find(req, query, evbuf); if (!s) return -1; ret = safe_atoi32(uri[3], &id); if (ret < 0) { httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return -1; } if (evhttp_find_header(query, "mw") && evhttp_find_header(query, "mh")) { param = evhttp_find_header(query, "mw"); ret = safe_atoi32(param, &max_w); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not convert mw parameter to integer\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return -1; } param = evhttp_find_header(query, "mh"); ret = safe_atoi32(param, &max_h); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not convert mh parameter to integer\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return -1; } } else { DPRINTF(E_DBG, L_DAAP, "Request for artwork without mw/mh parameter\n"); max_w = 0; max_h = 0; } if (strcmp(uri[2], "groups") == 0) ret = artwork_get_group(evbuf, id, max_w, max_h); else if (strcmp(uri[2], "items") == 0) ret = artwork_get_item(evbuf, id, max_w, max_h); len = evbuffer_get_length(evbuf); switch (ret) { case ART_FMT_PNG: ctype = "image/png"; break; case ART_FMT_JPEG: ctype = "image/jpeg"; break; default: if (len > 0) evbuffer_drain(evbuf, len); goto no_artwork; } headers = evhttp_request_get_output_headers(req); evhttp_remove_header(headers, "Content-Type"); evhttp_add_header(headers, "Content-Type", ctype); snprintf(clen, sizeof(clen), "%ld", (long)len); evhttp_add_header(headers, "Content-Length", clen); httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); return 0; no_artwork: httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); return -1; } static int daap_stream(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { int id; int ret; ret = safe_atoi32(uri[3], &id); if (ret < 0) httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); else httpd_stream_file(req, id); return ret; } static char * uri_relative(char *uri, const char *protocol) { char *ret; if (strncmp(uri, protocol, strlen(protocol)) != 0) return NULL; ret = strchr(uri + strlen(protocol), '/'); if (!ret) { DPRINTF(E_LOG, L_DAAP, "Malformed DAAP Request URI '%s'\n", uri); return NULL; } return ret; } static char * daap_fix_request_uri(struct evhttp_request *req, char *uri) { char *ret; /* iTunes 9 gives us an absolute request-uri like * daap://10.1.1.20:3689/server-info * iTunes 12.1 gives us an absolute request-uri for streaming like * http://10.1.1.20:3689/databases/1/items/1.mp3 */ if ( (ret = uri_relative(uri, "daap://")) || (ret = uri_relative(uri, "http://")) ) { /* Clear the proxy request flag set by evhttp * due to the request URI being absolute. * It has side-effects on Connection: keep-alive */ req->flags &= ~EVHTTP_PROXY_REQUEST; return ret; } return uri; } #ifdef DMAP_TEST static const struct dmap_field dmap_TEST = { "test.container", "TEST", NULL, DMAP_TYPE_LIST }; static const struct dmap_field dmap_TST1 = { "test.ubyte", "TST1", NULL, DMAP_TYPE_UBYTE }; static const struct dmap_field dmap_TST2 = { "test.byte", "TST2", NULL, DMAP_TYPE_BYTE }; static const struct dmap_field dmap_TST3 = { "test.ushort", "TST3", NULL, DMAP_TYPE_USHORT }; static const struct dmap_field dmap_TST4 = { "test.short", "TST4", NULL, DMAP_TYPE_SHORT }; static const struct dmap_field dmap_TST5 = { "test.uint", "TST5", NULL, DMAP_TYPE_UINT }; static const struct dmap_field dmap_TST6 = { "test.int", "TST6", NULL, DMAP_TYPE_INT }; static const struct dmap_field dmap_TST7 = { "test.ulong", "TST7", NULL, DMAP_TYPE_ULONG }; static const struct dmap_field dmap_TST8 = { "test.long", "TST8", NULL, DMAP_TYPE_LONG }; static const struct dmap_field dmap_TST9 = { "test.string", "TST9", NULL, DMAP_TYPE_STRING }; static int daap_reply_dmap_test(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua) { char buf[64]; struct evbuffer *test; int ret; test = evbuffer_new(); if (!test) { DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP test\n"); dmap_send_error(req, dmap_TEST.tag, "Out of memory"); return -1; } /* UBYTE */ snprintf(buf, sizeof(buf), "%" PRIu8, UINT8_MAX); dmap_add_field(test, &dmap_TST1, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); /* BYTE */ snprintf(buf, sizeof(buf), "%" PRIi8, INT8_MIN); dmap_add_field(test, &dmap_TST2, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); snprintf(buf, sizeof(buf), "%" PRIi8, INT8_MAX); dmap_add_field(test, &dmap_TST2, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); /* USHORT */ snprintf(buf, sizeof(buf), "%" PRIu16, UINT16_MAX); dmap_add_field(test, &dmap_TST3, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); /* SHORT */ snprintf(buf, sizeof(buf), "%" PRIi16, INT16_MIN); dmap_add_field(test, &dmap_TST4, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); snprintf(buf, sizeof(buf), "%" PRIi16, INT16_MAX); dmap_add_field(test, &dmap_TST4, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); /* UINT */ snprintf(buf, sizeof(buf), "%" PRIu32, UINT32_MAX); dmap_add_field(test, &dmap_TST5, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); /* INT */ snprintf(buf, sizeof(buf), "%" PRIi32, INT32_MIN); dmap_add_field(test, &dmap_TST6, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); snprintf(buf, sizeof(buf), "%" PRIi32, INT32_MAX); dmap_add_field(test, &dmap_TST6, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); /* ULONG */ snprintf(buf, sizeof(buf), "%" PRIu64, UINT64_MAX); dmap_add_field(test, &dmap_TST7, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); /* LONG */ snprintf(buf, sizeof(buf), "%" PRIi64, INT64_MIN); dmap_add_field(test, &dmap_TST8, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); snprintf(buf, sizeof(buf), "%" PRIi64, INT64_MAX); dmap_add_field(test, &dmap_TST8, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); dmap_add_container(evbuf, dmap_TEST.tag, evbuffer_get_length(test)); ret = evbuffer_add_buffer(evbuf, test); evbuffer_free(test); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add test results to DMAP test reply\n"); dmap_send_error(req, dmap_TEST.tag, "Out of memory"); return -1; } httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); return 0; } #endif /* DMAP_TEST */ static struct uri_map daap_handlers[] = { { .regexp = "^/server-info$", .handler = daap_reply_server_info }, { .regexp = "^/content-codes$", .handler = daap_reply_content_codes }, { .regexp = "^/login$", .handler = daap_reply_login }, { .regexp = "^/logout$", .handler = daap_reply_logout }, { .regexp = "^/update$", .handler = daap_reply_update }, { .regexp = "^/activity$", .handler = daap_reply_activity }, { .regexp = "^/databases$", .handler = daap_reply_dblist }, { .regexp = "^/databases/[[:digit:]]+/browse/[^/]+$", .handler = daap_reply_browse }, { .regexp = "^/databases/[[:digit:]]+/items$", .handler = daap_reply_dbsonglist }, { .regexp = "^/databases/[[:digit:]]+/items/[[:digit:]]+[.][^/]+$", .handler = daap_stream }, { .regexp = "^/databases/[[:digit:]]+/items/[[:digit:]]+/extra_data/artwork$", .handler = daap_reply_extra_data }, { .regexp = "^/databases/[[:digit:]]+/containers$", .handler = daap_reply_playlists }, { .regexp = "^/databases/[[:digit:]]+/containers/[[:digit:]]+/items$", .handler = daap_reply_plsonglist }, { .regexp = "^/databases/[[:digit:]]+/groups$", .handler = daap_reply_groups }, { .regexp = "^/databases/[[:digit:]]+/groups/[[:digit:]]+/extra_data/artwork$", .handler = daap_reply_extra_data }, #ifdef DMAP_TEST { .regexp = "^/dmap-test$", .handler = daap_reply_dmap_test }, #endif /* DMAP_TEST */ { .regexp = NULL, .handler = NULL } }; void daap_request(struct evhttp_request *req) { char *full_uri; char *uri; char *ptr; char *uri_parts[7]; struct evbuffer *evbuf; struct evkeyvalq query; struct evkeyvalq *headers; struct timespec start; struct timespec end; const char *ua; cfg_t *lib; char *libname; char *passwd; int msec; int handler; int ret; int i; memset(&query, 0, sizeof(struct evkeyvalq)); full_uri = httpd_fixup_uri(req); if (!full_uri) { httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } ptr = daap_fix_request_uri(req, full_uri); if (!ptr) { free(full_uri); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } if (ptr != full_uri) { uri = strdup(ptr); free(full_uri); if (!uri) { httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } full_uri = uri; } uri = extract_uri(full_uri); if (!uri) { free(full_uri); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } DPRINTF(E_DBG, L_DAAP, "DAAP request: %s\n", full_uri); handler = -1; for (i = 0; daap_handlers[i].handler; i++) { ret = regexec(&daap_handlers[i].preg, uri, 0, NULL, 0); if (ret == 0) { handler = i; break; } } if (handler < 0) { DPRINTF(E_LOG, L_DAAP, "Unrecognized DAAP request\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); free(uri); free(full_uri); return; } /* Check authentication */ lib = cfg_getsec(cfg, "library"); passwd = cfg_getstr(lib, "password"); /* No authentication for these URIs */ if ((strcmp(uri, "/server-info") == 0) || (strcmp(uri, "/logout") == 0) || (strcmp(uri, "/content-codes") == 0) || (strncmp(uri, "/databases/1/items/", strlen("/databases/1/items/")) == 0)) passwd = NULL; /* Waive HTTP authentication for Remote * Remotes are authentified by their pairing-guid; DAAP queries require a * valid session-id that Remote can only obtain if its pairing-guid is in * our database. So HTTP authentication is waived for Remote. */ headers = evhttp_request_get_input_headers(req); ua = evhttp_find_header(headers, "User-Agent"); if ((ua) && (strncmp(ua, "Remote", strlen("Remote")) == 0)) passwd = NULL; if (passwd) { libname = cfg_getstr(lib, "name"); DPRINTF(E_DBG, L_HTTPD, "Checking authentication for library '%s'\n", libname); /* We don't care about the username */ ret = httpd_basic_auth(req, NULL, passwd, libname); if (ret != 0) { free(uri); free(full_uri); return; } DPRINTF(E_DBG, L_HTTPD, "Library authentication successful\n"); } memset(uri_parts, 0, sizeof(uri_parts)); uri_parts[0] = strtok_r(uri, "/", &ptr); for (i = 1; (i < sizeof(uri_parts) / sizeof(uri_parts[0])) && uri_parts[i - 1]; i++) { uri_parts[i] = strtok_r(NULL, "/", &ptr); } if (!uri_parts[0] || uri_parts[i - 1] || (i < 2)) { DPRINTF(E_LOG, L_DAAP, "DAAP URI has too many/few components (%d)\n", (uri_parts[0]) ? i : 0); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); free(uri); free(full_uri); return; } // Set reply headers headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Accept-Ranges", "bytes"); evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" VERSION); /* Content-Type for all replies, even the actual audio streaming. Note that * video streaming will override this Content-Type with a more appropriate * video/ Content-Type as expected by clients like Front Row. */ evhttp_add_header(headers, "Content-Type", "application/x-dmap-tagged"); evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_DAAP, "Could not allocate evbuffer for DAAP reply\n"); httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); free(uri); free(full_uri); return; } // Try the cache ret = cache_daap_get(full_uri, evbuf); if (ret == 0) { // The cache will return the data gzipped, so httpd_send_reply won't need to do it evhttp_add_header(headers, "Content-Encoding", "gzip"); httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); // TODO not all want this reply evbuffer_free(evbuf); free(uri); free(full_uri); return; } // No cache, so prepare handler arguments and send to the handler evhttp_parse_query(full_uri, &query); clock_gettime(CLOCK_MONOTONIC, &start); daap_handlers[handler].handler(req, evbuf, uri_parts, &query, ua); clock_gettime(CLOCK_MONOTONIC, &end); msec = (end.tv_sec * 1000 + end.tv_nsec / 1000000) - (start.tv_sec * 1000 + start.tv_nsec / 1000000); DPRINTF(E_DBG, L_DAAP, "DAAP request handled in %d milliseconds\n", msec); if (msec > cache_daap_threshold()) cache_daap_add(full_uri, ua, msec); evhttp_clear_headers(&query); evbuffer_free(evbuf); free(uri); free(full_uri); } int daap_is_request(struct evhttp_request *req, char *uri) { uri = daap_fix_request_uri(req, uri); if (!uri) return 0; if (strncmp(uri, "/databases/", strlen("/databases/")) == 0) return 1; if (strcmp(uri, "/databases") == 0) return 1; if (strcmp(uri, "/server-info") == 0) return 1; if (strcmp(uri, "/content-codes") == 0) return 1; if (strcmp(uri, "/login") == 0) return 1; if (strcmp(uri, "/update") == 0) return 1; if (strcmp(uri, "/activity") == 0) return 1; if (strcmp(uri, "/logout") == 0) return 1; #ifdef DMAP_TEST if (strcmp(uri, "/dmap-test") == 0) return 1; #endif return 0; } struct evbuffer * daap_reply_build(char *full_uri, const char *ua) { char *uri; char *ptr; char *uri_parts[7]; struct evbuffer *evbuf; struct evkeyvalq query; int handler; int ret; int i; DPRINTF(E_DBG, L_DAAP, "Building reply for DAAP request: %s\n", full_uri); uri = extract_uri(full_uri); if (!uri) { DPRINTF(E_LOG, L_DAAP, "Error extracting DAAP request: %s\n", full_uri); return NULL; } handler = -1; for (i = 0; daap_handlers[i].handler; i++) { ret = regexec(&daap_handlers[i].preg, uri, 0, NULL, 0); if (ret == 0) { handler = i; break; } } if (handler < 0) { DPRINTF(E_LOG, L_DAAP, "Unrecognized DAAP request: %s\n", full_uri); free(uri); return NULL; } memset(uri_parts, 0, sizeof(uri_parts)); uri_parts[0] = strtok_r(uri, "/", &ptr); for (i = 1; (i < sizeof(uri_parts) / sizeof(uri_parts[0])) && uri_parts[i - 1]; i++) { uri_parts[i] = strtok_r(NULL, "/", &ptr); } if (!uri_parts[0] || uri_parts[i - 1] || (i < 2)) { DPRINTF(E_LOG, L_DAAP, "DAAP URI has too many/few components (%d)\n", (uri_parts[0]) ? i : 0); free(uri); return NULL; } evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_DAAP, "Could not allocate evbuffer for building DAAP reply\n"); free(uri); return NULL; } evhttp_parse_query(full_uri, &query); ret = daap_handlers[handler].handler(NULL, evbuf, uri_parts, &query, ua); if (ret < 0) { evbuffer_free(evbuf); evbuf = NULL; } evhttp_clear_headers(&query); free(uri); return evbuf; } int daap_init(void) { char buf[64]; int i; int ret; srand((unsigned)time(NULL)); current_rev = 2; update_requests = NULL; for (i = 0; daap_handlers[i].handler; i++) { ret = regcomp(&daap_handlers[i].preg, daap_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); if (ret != 0) { regerror(ret, &daap_handlers[i].preg, buf, sizeof(buf)); DPRINTF(E_FATAL, L_DAAP, "DAAP init failed; regexp error: %s\n", buf); return -1; } } return 0; } void daap_deinit(void) { struct daap_session *s; struct daap_update_request *ur; struct evhttp_connection *evcon; int i; for (i = 0; daap_handlers[i].handler; i++) regfree(&daap_handlers[i].preg); for (s = daap_sessions; daap_sessions; s = daap_sessions) { daap_sessions = s->next; daap_session_free(s); } for (ur = update_requests; update_requests; ur = update_requests) { update_requests = ur->next; evcon = evhttp_request_get_connection(ur->req); if (evcon) { evhttp_connection_set_closecb(evcon, NULL, NULL); evhttp_connection_free(evcon); } update_free(ur); } } forked-daapd-25.0/src/httpd_daap.h000066400000000000000000000005161313447753700170510ustar00rootroot00000000000000 #ifndef __HTTPD_DAAP_H__ #define __HTTPD_DAAP_H__ #include int daap_init(void); void daap_deinit(void); void daap_request(struct evhttp_request *req); int daap_is_request(struct evhttp_request *req, char *uri); struct evbuffer * daap_reply_build(char *full_uri, const char *ua); #endif /* !__HTTPD_DAAP_H__ */ forked-daapd-25.0/src/httpd_dacp.c000066400000000000000000002236441313447753700170570ustar00rootroot00000000000000/* * Copyright (C) 2010-2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_EVENTFD # include #endif #include #include #include #include "logger.h" #include "misc.h" #include "conffile.h" #include "artwork.h" #include "httpd.h" #include "httpd_dacp.h" #include "dmap_common.h" #include "db.h" #include "daap_query.h" #include "player.h" #include "listener.h" /* httpd event base, from httpd.c */ extern struct event_base *evbase_httpd; /* From httpd_daap.c */ struct daap_session; struct daap_session * daap_session_find(struct evhttp_request *req, struct evkeyvalq *query, struct evbuffer *evbuf); struct uri_map { regex_t preg; char *regexp; void (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query); }; struct dacp_update_request { struct evhttp_request *req; struct dacp_update_request *next; }; typedef void (*dacp_propget)(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); typedef void (*dacp_propset)(const char *value, struct evkeyvalq *query); struct dacp_prop_map { char *desc; dacp_propget propget; dacp_propset propset; }; /* Forward - properties getters */ static void dacp_propget_volume(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_volumecontrollable(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_playerstate(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_shufflestate(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_availableshufflestates(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_repeatstate(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_availablerepeatstates(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_nowplaying(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_playingtime(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_fullscreenenabled(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_fullscreen(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_visualizerenabled(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_visualizer(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_itms_songid(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_haschapterdata(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_mediakind(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); static void dacp_propget_extendedmediakind(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item); /* Forward - properties setters */ static void dacp_propset_volume(const char *value, struct evkeyvalq *query); static void dacp_propset_playingtime(const char *value, struct evkeyvalq *query); static void dacp_propset_shufflestate(const char *value, struct evkeyvalq *query); static void dacp_propset_repeatstate(const char *value, struct evkeyvalq *query); static void dacp_propset_userrating(const char *value, struct evkeyvalq *query); /* gperf static hash, dacp_prop.gperf */ #include "dacp_prop_hash.h" /* Play status update */ #ifdef HAVE_EVENTFD static int update_efd; #else static int update_pipe[2]; #endif static struct event *updateev; static int current_rev; /* Play status update requests */ static struct dacp_update_request *update_requests; /* Seek timer */ static struct event *seek_timer; static int seek_target; /* If an item is removed from the library while in the queue, we replace it with this */ static struct media_file_info dummy_mfi = { .id = 9999999, .title = "(unknown title)", .artist = "(unknown artist)", .album = "(unknown album)", .genre = "(unknown genre)", }; static struct db_queue_item dummy_queue_item = { .file_id = 9999999, .title = "(unknown title)", .artist = "(unknown artist)", .album = "(unknown album)", .genre = "(unknown genre)", }; /* DACP helpers */ static void dacp_nowplaying(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { uint32_t id; int64_t songalbumid; int pos_pl; if ((status->status == PLAY_STOPPED) || !queue_item) return; /* Send bogus id's if playing internet radio, because clients like * Remote and Retune will only update metadata (like artwork) if the id's * change (which they wouldn't do if we sent the real ones) * FIXME: Giving the client invalid ids on purpose is hardly ideal, but the * clients don't seem to use these ids for anything other than rating. */ if (queue_item->data_kind == DATA_KIND_HTTP) { id = djb_hash(queue_item->album, strlen(queue_item->album)); songalbumid = (int64_t)id; } else { id = status->id; songalbumid = queue_item->songalbumid; } pos_pl = db_queue_get_pos(status->item_id, 0); dmap_add_container(evbuf, "canp", 16); dmap_add_raw_uint32(evbuf, 1); /* Database */ dmap_add_raw_uint32(evbuf, status->plid); dmap_add_raw_uint32(evbuf, pos_pl); dmap_add_raw_uint32(evbuf, id); dmap_add_string(evbuf, "cann", queue_item->title); dmap_add_string(evbuf, "cana", queue_item->artist); dmap_add_string(evbuf, "canl", queue_item->album); dmap_add_string(evbuf, "cang", queue_item->genre); dmap_add_long(evbuf, "asai", songalbumid); dmap_add_int(evbuf, "cmmk", 1); } static void dacp_playingtime(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { if ((status->status == PLAY_STOPPED) || !queue_item) return; if (queue_item->song_length) dmap_add_int(evbuf, "cant", queue_item->song_length - status->pos_ms); /* Remaining time in ms */ else dmap_add_int(evbuf, "cant", 0); /* Unknown remaining time */ dmap_add_int(evbuf, "cast", queue_item->song_length); /* Song length in ms */ } /* Update requests helpers */ static int make_playstatusupdate(struct evbuffer *evbuf) { struct player_status status; struct db_queue_item *queue_item = NULL; struct evbuffer *psu; int ret; psu = evbuffer_new(); if (!psu) { DPRINTF(E_LOG, L_DACP, "Could not allocate evbuffer for playstatusupdate\n"); return -1; } player_get_status(&status); if (status.status != PLAY_STOPPED) { queue_item = db_queue_fetch_byitemid(status.item_id); if (!queue_item) { DPRINTF(E_LOG, L_DACP, "Could not fetch item id %d (file id %d)\n", status.item_id, status.id); queue_item = &dummy_queue_item; } } dmap_add_int(psu, "mstt", 200); /* 12 */ dmap_add_int(psu, "cmsr", current_rev); /* 12 */ dmap_add_char(psu, "caps", status.status); /* 9 */ /* play status, 2 = stopped, 3 = paused, 4 = playing */ dmap_add_char(psu, "cash", status.shuffle); /* 9 */ /* shuffle, true/false */ dmap_add_char(psu, "carp", status.repeat); /* 9 */ /* repeat, 0 = off, 1 = repeat song, 2 = repeat (playlist) */ dmap_add_char(psu, "cafs", 0); /* 9 */ /* dacp.fullscreen */ dmap_add_char(psu, "cavs", 0); /* 9 */ /* dacp.visualizer */ dmap_add_char(psu, "cavc", 1); /* 9 */ /* volume controllable */ dmap_add_int(psu, "caas", 2); /* 12 */ /* available shuffle states */ dmap_add_int(psu, "caar", 6); /* 12 */ /* available repeat states */ dmap_add_char(psu, "cafe", 0); /* 9 */ /* dacp.fullscreenenabled */ dmap_add_char(psu, "cave", 0); /* 9 */ /* dacp.visualizerenabled */ if (queue_item) { dacp_nowplaying(psu, &status, queue_item); dmap_add_int(psu, "casa", 1); /* 12 */ /* unknown */ dmap_add_int(psu, "astm", queue_item->song_length); dmap_add_char(psu, "casc", 1); /* Maybe an indication of extra data? */ dmap_add_char(psu, "caks", 6); /* Unknown */ dacp_playingtime(psu, &status, queue_item); if (queue_item != &dummy_queue_item) free_queue_item(queue_item, 0); } dmap_add_char(psu, "casu", 1); /* 9 */ /* unknown */ dmap_add_char(psu, "ceQu", 0); /* 9 */ /* unknown */ dmap_add_container(evbuf, "cmst", evbuffer_get_length(psu)); /* 8 + len */ ret = evbuffer_add_buffer(evbuf, psu); evbuffer_free(psu); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add status data to playstatusupdate reply\n"); return -1; } return 0; } static void playstatusupdate_cb(int fd, short what, void *arg) { struct dacp_update_request *ur; struct evbuffer *evbuf; struct evbuffer *update; struct evhttp_connection *evcon; uint8_t *buf; size_t len; int ret; #ifdef HAVE_EVENTFD eventfd_t count; ret = eventfd_read(update_efd, &count); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not read playstatusupdate event counter: %s\n", strerror(errno)); goto readd; } #else int dummy; read(update_pipe[0], &dummy, sizeof(dummy)); #endif if (!update_requests) goto readd; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_DACP, "Could not allocate evbuffer for playstatusupdate reply\n"); goto readd; } update = evbuffer_new(); if (!update) { DPRINTF(E_LOG, L_DACP, "Could not allocate evbuffer for playstatusupdate data\n"); goto out_free_evbuf; } ret = make_playstatusupdate(update); if (ret < 0) goto out_free_update; len = evbuffer_get_length(update); for (ur = update_requests; update_requests; ur = update_requests) { update_requests = ur->next; evcon = evhttp_request_get_connection(ur->req); if (evcon) evhttp_connection_set_closecb(evcon, NULL, NULL); // Only copy buffer if we actually need to reuse it if (ur->next) { buf = evbuffer_pullup(update, -1); evbuffer_add(evbuf, buf, len); httpd_send_reply(ur->req, HTTP_OK, "OK", evbuf, 0); } else httpd_send_reply(ur->req, HTTP_OK, "OK", update, 0); free(ur); } current_rev++; out_free_update: evbuffer_free(update); out_free_evbuf: evbuffer_free(evbuf); readd: ret = event_add(updateev, NULL); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Couldn't re-add event for playstatusupdate\n"); } /* Thread: player */ static void dacp_playstatus_update_handler(enum listener_event_type type) { int ret; // Only send status update on player change events if (type != LISTENER_PLAYER) return; #ifdef HAVE_EVENTFD ret = eventfd_write(update_efd, 1); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Could not send status update event: %s\n", strerror(errno)); #else int dummy = 42; ret = write(update_pipe[1], &dummy, sizeof(dummy)); if (ret != sizeof(dummy)) DPRINTF(E_LOG, L_DACP, "Could not write to status update fd: %s\n", strerror(errno)); #endif } static void update_fail_cb(struct evhttp_connection *evcon, void *arg) { struct dacp_update_request *ur; struct dacp_update_request *p; struct evhttp_connection *evc; ur = (struct dacp_update_request *)arg; DPRINTF(E_DBG, L_DACP, "Update request: client closed connection\n"); evc = evhttp_request_get_connection(ur->req); if (evc) evhttp_connection_set_closecb(evc, NULL, NULL); if (ur == update_requests) update_requests = ur->next; else { for (p = update_requests; p && (p->next != ur); p = p->next) ; if (!p) { DPRINTF(E_LOG, L_DACP, "WARNING: struct dacp_update_request not found in list; BUG!\n"); return; } p->next = ur->next; } free(ur); } /* Properties getters */ static void dacp_propget_volume(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dmap_add_int(evbuf, "cmvo", status->volume); } static void dacp_propget_volumecontrollable(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dmap_add_char(evbuf, "cavc", 1); } static void dacp_propget_playerstate(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dmap_add_char(evbuf, "caps", status->status); } static void dacp_propget_shufflestate(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dmap_add_char(evbuf, "cash", status->shuffle); } static void dacp_propget_availableshufflestates(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dmap_add_int(evbuf, "caas", 2); } static void dacp_propget_repeatstate(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dmap_add_char(evbuf, "carp", status->repeat); } static void dacp_propget_availablerepeatstates(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dmap_add_int(evbuf, "caar", 6); } static void dacp_propget_nowplaying(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dacp_nowplaying(evbuf, status, queue_item); } static void dacp_propget_playingtime(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { dacp_playingtime(evbuf, status, queue_item); } static void dacp_propget_fullscreenenabled(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } static void dacp_propget_fullscreen(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } static void dacp_propget_visualizerenabled(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } static void dacp_propget_visualizer(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } static void dacp_propget_itms_songid(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } static void dacp_propget_haschapterdata(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } static void dacp_propget_mediakind(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } static void dacp_propget_extendedmediakind(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { // TODO } /* Properties setters */ static void dacp_propset_volume(const char *value, struct evkeyvalq *query) { const char *param; uint64_t id; int volume; int ret; ret = safe_atoi32(value, &volume); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "dmcp.volume argument doesn't convert to integer: %s\n", value); return; } param = evhttp_find_header(query, "speaker-id"); if (param) { ret = safe_atou64(param, &id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid speaker ID in dmcp.volume request\n"); return; } player_volume_setrel_speaker(id, volume); return; } param = evhttp_find_header(query, "include-speaker-id"); if (param) { ret = safe_atou64(param, &id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid speaker ID in dmcp.volume request\n"); return; } player_volume_setabs_speaker(id, volume); return; } player_volume_set(volume); } static void seek_timer_cb(int fd, short what, void *arg) { int ret; DPRINTF(E_DBG, L_DACP, "Seek timer expired, target %d ms\n", seek_target); ret = player_playback_seek(seek_target); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Player failed to seek to %d ms\n", seek_target); return; } ret = player_playback_start(); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Player returned an error for start after seek\n"); } static void dacp_propset_playingtime(const char *value, struct evkeyvalq *query) { struct timeval tv; int ret; ret = safe_atoi32(value, &seek_target); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "dacp.playingtime argument doesn't convert to integer: %s\n", value); return; } evutil_timerclear(&tv); tv.tv_usec = 200 * 1000; evtimer_add(seek_timer, &tv); } static void dacp_propset_shufflestate(const char *value, struct evkeyvalq *query) { int enable; int ret; ret = safe_atoi32(value, &enable); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "dacp.shufflestate argument doesn't convert to integer: %s\n", value); return; } player_shuffle_set(enable); } static void dacp_propset_repeatstate(const char *value, struct evkeyvalq *query) { int mode; int ret; ret = safe_atoi32(value, &mode); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "dacp.repeatstate argument doesn't convert to integer: %s\n", value); return; } player_repeat_set(mode); } static void dacp_propset_userrating(const char *value, struct evkeyvalq *query) { struct media_file_info *mfi; const char *param; uint32_t itemid; uint32_t rating; int ret; ret = safe_atou32(value, &rating); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "dacp.userrating argument doesn't convert to integer: %s\n", value); return; } param = evhttp_find_header(query, "item-spec"); // Remote if (!param) param = evhttp_find_header(query, "song-spec"); // Retune if (!param) { DPRINTF(E_LOG, L_DACP, "Missing item-spec/song-spec parameter in dacp.userrating query\n"); return; } param = strchr(param, ':'); if (!param) { DPRINTF(E_LOG, L_DACP, "Malformed item-spec/song-spec parameter in dacp.userrating query\n"); return; } param++; ret = safe_hextou32(param, &itemid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Couldn't convert item-spec/song-spec to an integer in dacp.userrating (%s)\n", param); return; } mfi = db_file_fetch_byid(itemid); /* If no mfi, it may be because we sent an invalid nowplaying itemid. In this * case request the real one from the player and default to that. */ if (!mfi) { DPRINTF(E_WARN, L_DACP, "Invalid id %d for rating, defaulting to player id\n", itemid); ret = player_now_playing(&itemid); if ((ret < 0) || !(mfi = db_file_fetch_byid(itemid))) { DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n"); return; } } mfi->rating = rating; /* We're not touching any string field in mfi, so it's safe to * skip unicode_fixup_mfi() before the update */ db_file_update(mfi); free_mfi(mfi, 0); } static void dacp_reply_ctrlint(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { /* /ctrl-int */ /* If tags are added or removed container sizes should be adjusted too */ dmap_add_container(evbuf, "caci", 194); /* 8, unknown dacp container - size of content */ dmap_add_int(evbuf, "mstt", 200); /* 12, dmap.status */ dmap_add_char(evbuf, "muty", 0); /* 9, dmap.updatetype */ dmap_add_int(evbuf, "mtco", 1); /* 12, dmap.specifiedtotalcount */ dmap_add_int(evbuf, "mrco", 1); /* 12, dmap.returnedcount */ dmap_add_container(evbuf, "mlcl", 141); /* 8, dmap.listing - size of content */ dmap_add_container(evbuf, "mlit", 133); /* 8, dmap.listingitem - size of content */ dmap_add_int(evbuf, "miid", 1); /* 12, dmap.itemid - database ID */ dmap_add_char(evbuf, "cmik", 1); /* 9, unknown */ dmap_add_int(evbuf, "cmpr", (2 << 16 | 2)); /* 12, dmcp.protocolversion */ dmap_add_int(evbuf, "capr", (2 << 16 | 5)); /* 12, dacp.protocolversion */ dmap_add_char(evbuf, "cmsp", 1); /* 9, unknown */ dmap_add_char(evbuf, "aeFR", 0x64); /* 9, unknown */ dmap_add_char(evbuf, "cmsv", 1); /* 9, unknown */ dmap_add_char(evbuf, "cass", 1); /* 9, unknown */ dmap_add_char(evbuf, "caov", 1); /* 9, unknown */ dmap_add_char(evbuf, "casu", 1); /* 9, unknown */ dmap_add_char(evbuf, "ceSG", 1); /* 9, unknown */ dmap_add_char(evbuf, "cmrl", 1); /* 9, unknown */ dmap_add_long(evbuf, "ceSX", (1 << 1 | 1)); /* 16, unknown dacp - lowest bit announces support for playqueue-contents/-edit */ httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); } static int find_first_song_id(const char *query) { struct db_media_file_info dbmfi; struct query_params qp; int id; int ret; id = 0; memset(&qp, 0, sizeof(struct query_params)); /* We only want the id of the first song */ qp.type = Q_ITEMS; qp.idx_type = I_FIRST; qp.sort = S_NONE; qp.offset = 0; qp.limit = 1; qp.filter = daap_query_parse_sql(query); if (!qp.filter) { DPRINTF(E_LOG, L_DACP, "Improper DAAP query!\n"); return -1; } ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start query\n"); goto no_query_start; } if (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { ret = safe_atoi32(dbmfi.id, &id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid song id in query result!\n"); goto no_result; } DPRINTF(E_DBG, L_DACP, "Found index song (id %d)\n", id); ret = 1; } else { DPRINTF(E_LOG, L_DACP, "No song matches query (results %d): %s\n", qp.results, qp.filter); goto no_result; } no_result: db_query_end(&qp); no_query_start: if (qp.filter) free(qp.filter); if (ret == 1) return id; else return -1; } static int dacp_queueitem_add(const char *query, const char *queuefilter, const char *sort, int quirk, int mode) { struct media_file_info *mfi; struct query_params qp; int64_t albumid; int64_t artistid; int plid; int id; int ret; int len; char *s; char buf[1024]; struct player_status status; if (query) { id = find_first_song_id(query); if (id < 0) return -1; } else id = 0; memset(&qp, 0, sizeof(struct query_params)); qp.offset = 0; qp.limit = 0; qp.sort = S_NONE; qp.idx_type = I_NONE; if (quirk) { qp.sort = S_ALBUM; qp.type = Q_ITEMS; mfi = db_file_fetch_byid(id); if (!mfi) return -1; snprintf(buf, sizeof(buf), "f.songalbumid = %" PRIi64, mfi->songalbumid); free_mfi(mfi, 0); qp.filter = strdup(buf); } else if (queuefilter) { len = strlen(queuefilter); if ((len > 6) && (strncmp(queuefilter, "album:", 6) == 0)) { qp.type = Q_ITEMS; ret = safe_atoi64(strchr(queuefilter, ':') + 1, &albumid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid album id in queuefilter: %s\n", queuefilter); return -1; } snprintf(buf, sizeof(buf), "f.songalbumid = %" PRIi64, albumid); qp.filter = strdup(buf); } else if ((len > 7) && (strncmp(queuefilter, "artist:", 7) == 0)) { qp.type = Q_ITEMS; ret = safe_atoi64(strchr(queuefilter, ':') + 1, &artistid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid artist id in queuefilter: %s\n", queuefilter); return -1; } snprintf(buf, sizeof(buf), "f.songartistid = %" PRIi64, artistid); qp.filter = strdup(buf); } else if ((len > 9) && (strncmp(queuefilter, "playlist:", 9) == 0)) { qp.type = Q_PLITEMS; ret = safe_atoi32(strchr(queuefilter, ':') + 1, &plid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid playlist id in queuefilter: %s\n", queuefilter); return -1; } qp.id = plid; qp.filter = strdup("1 = 1"); } else if ((len > 6) && (strncmp(queuefilter, "genre:", 6) == 0)) { qp.type = Q_ITEMS; s = db_escape_string(queuefilter + 6); if (!s) return -1; snprintf(buf, sizeof(buf), "f.genre = '%s'", s); qp.filter = strdup(buf); } else { DPRINTF(E_LOG, L_DACP, "Unknown queuefilter %s\n", queuefilter); // If the queuefilter is unkown, ignore it and use the query parameter instead to build the sql query id = 0; qp.type = Q_ITEMS; qp.filter = daap_query_parse_sql(query); } } else { id = 0; qp.type = Q_ITEMS; qp.filter = daap_query_parse_sql(query); } if (sort) { if (strcmp(sort, "name") == 0) qp.sort = S_NAME; else if (strcmp(sort, "album") == 0) qp.sort = S_ALBUM; else if (strcmp(sort, "artist") == 0) qp.sort = S_ARTIST; } player_get_status(&status); if (mode == 3) ret = db_queue_add_by_queryafteritemid(&qp, status.item_id); else ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id); if (qp.filter) free(qp.filter); if (ret < 0) return -1; if (status.shuffle && mode != 1) return 0; return id; } static void dacp_reply_cue_play(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct player_status status; const char *sort; const char *cuequery; const char *param; uint32_t item_id; uint32_t pos; int clear; struct db_queue_item *queue_item = NULL; struct player_history *history; int ret; /* /cue?command=play&query=...&sort=...&index=N */ param = evhttp_find_header(query, "clear-first"); if (param) { ret = safe_atoi32(param, &clear); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Invalid clear-first value in cue request\n"); else if (clear) { player_playback_stop(); db_queue_clear(0); } } player_get_status(&status); cuequery = evhttp_find_header(query, "query"); if (cuequery) { sort = evhttp_find_header(query, "sort"); ret = dacp_queueitem_add(cuequery, NULL, sort, 0, 0); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not build song queue\n"); dmap_send_error(req, "cacr", "Could not build song queue"); return; } } else if (status.status != PLAY_STOPPED) { player_playback_stop(); } param = evhttp_find_header(query, "dacp.shufflestate"); if (param) dacp_propset_shufflestate(param, NULL); item_id = 0; pos = 0; param = evhttp_find_header(query, "index"); if (param) { ret = safe_atou32(param, &pos); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Invalid index (%s) in cue request\n", param); } /* If selection was from Up Next queue or history queue (command will be playnow), then index is relative */ if ((param = evhttp_find_header(query, "command")) && (strcmp(param, "playnow") == 0)) { /* If mode parameter is -1 or 1, the index is relative to the history queue, otherwise to the Up Next queue */ param = evhttp_find_header(query, "mode"); if (param && ((strcmp(param, "-1") == 0) || (strcmp(param, "1") == 0))) { /* Play from history queue */ history = player_history_get(); if (history->count > pos) { pos = (history->start_index + history->count - pos - 1) % MAX_HISTORY_COUNT; item_id = history->item_id[pos]; queue_item = db_queue_fetch_byitemid(item_id); if (!queue_item) { DPRINTF(E_LOG, L_DACP, "Could not start playback from history\n"); dmap_send_error(req, "cacr", "Playback failed to start"); return; } } else { DPRINTF(E_LOG, L_DACP, "Could not start playback from history\n"); dmap_send_error(req, "cacr", "Playback failed to start"); return; } } else { /* Play from Up Next queue */ if (status.status == PLAY_STOPPED && pos > 0) pos--; queue_item = db_queue_fetch_byposrelativetoitem(pos, status.item_id, status.shuffle); if (!queue_item) { DPRINTF(E_LOG, L_DACP, "Could not fetch item from queue: pos=%d, now playing=%d\n", pos, status.item_id); dmap_send_error(req, "cacr", "Playback failed to start"); return; } } } else { queue_item = db_queue_fetch_bypos(pos, status.shuffle); if (!queue_item) { DPRINTF(E_LOG, L_DACP, "Could not fetch item from queue: pos=%d\n", pos); dmap_send_error(req, "cacr", "Playback failed to start"); return; } } ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); dmap_send_error(req, "cacr", "Playback failed to start"); return; } player_get_status(&status); dmap_add_container(evbuf, "cacr", 24); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "miid", status.id);/* 12 */ httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); } static void dacp_reply_cue_clear(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { /* /cue?command=clear */ player_playback_stop(); db_queue_clear(0); dmap_add_container(evbuf, "cacr", 24); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "miid", 0); /* 12 */ httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); } static void dacp_reply_cue(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; const char *param; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "command"); if (!param) { DPRINTF(E_DBG, L_DACP, "No command in cue request\n"); dmap_send_error(req, "cacr", "No command in cue request"); return; } if (strcmp(param, "clear") == 0) dacp_reply_cue_clear(req, evbuf, uri, query); else if (strcmp(param, "play") == 0) dacp_reply_cue_play(req, evbuf, uri, query); else { DPRINTF(E_LOG, L_DACP, "Unknown cue command %s\n", param); dmap_send_error(req, "cacr", "Unknown command in cue request"); return; } } static void dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct player_status status; struct daap_session *s; const char *param; const char *shuffle; uint32_t plid; uint32_t id; struct db_queue_item *queue_item = NULL; int ret; /* /ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x5'&container-item-spec='dmap.containeritemid:0x9' * or (Apple Remote when playing a Podcast) * /ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x5'&item-spec='dmap.itemid:0x9' * With our DAAP implementation, container-spec is the playlist ID and container-item-spec/item-spec is the song ID */ s = daap_session_find(req, query, evbuf); if (!s) return; /* Check for shuffle */ shuffle = evhttp_find_header(query, "dacp.shufflestate"); /* Playlist ID */ param = evhttp_find_header(query, "container-spec"); if (!param) { DPRINTF(E_LOG, L_DACP, "No container-spec in playspec request\n"); goto out_fail; } param = strchr(param, ':'); if (!param) { DPRINTF(E_LOG, L_DACP, "Malformed container-spec parameter in playspec request\n"); goto out_fail; } param++; ret = safe_hextou32(param, &plid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Couldn't convert container-spec to an integer in playspec (%s)\n", param); goto out_fail; } if (!shuffle) { /* Start song ID */ if ((param = evhttp_find_header(query, "item-spec"))) plid = 0; // This is a podcast/audiobook - just play a single item, not a playlist else if (!(param = evhttp_find_header(query, "container-item-spec"))) { DPRINTF(E_LOG, L_DACP, "No container-item-spec/item-spec in playspec request\n"); goto out_fail; } param = strchr(param, ':'); if (!param) { DPRINTF(E_LOG, L_DACP, "Malformed container-item-spec/item-spec parameter in playspec request\n"); goto out_fail; } param++; ret = safe_hextou32(param, &id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Couldn't convert container-item-spec/item-spec to an integer in playspec (%s)\n", param); goto out_fail; } } else id = 0; DPRINTF(E_DBG, L_DACP, "Playspec request for playlist %d, start song id %d%s\n", plid, id, (shuffle) ? ", shuffle" : ""); player_get_status(&status); if (status.status != PLAY_STOPPED) player_playback_stop(); db_queue_clear(0); if (plid > 0) ret = db_queue_add_by_playlistid(plid, status.shuffle, status.item_id); else if (id > 0) ret = db_queue_add_by_fileid(id, status.shuffle, status.item_id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not build song queue from playlist %d\n", plid); goto out_fail; } player_queue_plid(plid); if (shuffle) dacp_propset_shufflestate(shuffle, NULL); if (id > 0) queue_item = db_queue_fetch_byfileid(id); if (queue_item) { ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); } else ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); goto out_fail; } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); return; out_fail: httpd_send_error(req, 500, "Internal Server Error"); } static void dacp_reply_pause(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; player_playback_pause(); /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_playpause(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; struct player_status status; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; player_get_status(&status); if (status.status == PLAY_PLAYING) { player_playback_pause(); } else { ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Player returned an error for start after pause\n"); httpd_send_error(req, 500, "Internal Server Error"); return; } } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_nextitem(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; ret = player_playback_next(); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Player returned an error for nextitem\n"); httpd_send_error(req, 500, "Internal Server Error"); return; } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Player returned an error for start after nextitem\n"); httpd_send_error(req, 500, "Internal Server Error"); return; } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_previtem(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; ret = player_playback_prev(); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Player returned an error for previtem\n"); httpd_send_error(req, 500, "Internal Server Error"); return; } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Player returned an error for start after previtem\n"); httpd_send_error(req, 500, "Internal Server Error"); return; } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_beginff(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_beginrew(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_playresume(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static int playqueuecontents_add_source(struct evbuffer *songlist, uint32_t source_id, int pos_in_queue, uint32_t plid) { struct evbuffer *song; struct media_file_info *mfi; int ret; song = evbuffer_new(); if (!song) { DPRINTF(E_LOG, L_DACP, "Could not allocate song evbuffer for playqueue-contents\n"); return -1; } mfi = db_file_fetch_byid(source_id); if (!mfi) { DPRINTF(E_LOG, L_DACP, "Could not fetch file id %d\n", source_id); mfi = &dummy_mfi; } dmap_add_container(song, "ceQs", 16); dmap_add_raw_uint32(song, 1); /* Database */ dmap_add_raw_uint32(song, plid); dmap_add_raw_uint32(song, 0); /* Should perhaps be playlist index? */ dmap_add_raw_uint32(song, mfi->id); dmap_add_string(song, "ceQn", mfi->title); dmap_add_string(song, "ceQr", mfi->artist); dmap_add_string(song, "ceQa", mfi->album); dmap_add_string(song, "ceQg", mfi->genre); dmap_add_long(song, "asai", mfi->songalbumid); dmap_add_int(song, "cmmk", mfi->media_kind); dmap_add_int(song, "casa", 1); /* Unknown */ dmap_add_int(song, "astm", mfi->song_length); dmap_add_char(song, "casc", 1); /* Maybe an indication of extra data? */ dmap_add_char(song, "caks", 6); /* Unknown */ dmap_add_int(song, "ceQI", pos_in_queue); dmap_add_container(songlist, "mlit", evbuffer_get_length(song)); ret = evbuffer_add_buffer(songlist, song); evbuffer_free(song); if (mfi != &dummy_mfi) free_mfi(mfi, 0); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add song to songlist for playqueue-contents\n"); return ret; } return 0; } static int playqueuecontents_add_queue_item(struct evbuffer *songlist, struct db_queue_item *queue_item, int pos_in_queue, uint32_t plid) { struct evbuffer *song; int ret; song = evbuffer_new(); if (!song) { DPRINTF(E_LOG, L_DACP, "Could not allocate song evbuffer for playqueue-contents\n"); return -1; } dmap_add_container(song, "ceQs", 16); dmap_add_raw_uint32(song, 1); /* Database */ dmap_add_raw_uint32(song, plid); dmap_add_raw_uint32(song, 0); /* Should perhaps be playlist index? */ dmap_add_raw_uint32(song, queue_item->file_id); dmap_add_string(song, "ceQn", queue_item->title); dmap_add_string(song, "ceQr", queue_item->artist); dmap_add_string(song, "ceQa", queue_item->album); dmap_add_string(song, "ceQg", queue_item->genre); dmap_add_long(song, "asai", queue_item->songalbumid); dmap_add_int(song, "cmmk", queue_item->media_kind); dmap_add_int(song, "casa", 1); /* Unknown */ dmap_add_int(song, "astm", queue_item->song_length); dmap_add_char(song, "casc", 1); /* Maybe an indication of extra data? */ dmap_add_char(song, "caks", 6); /* Unknown */ dmap_add_int(song, "ceQI", pos_in_queue); dmap_add_container(songlist, "mlit", evbuffer_get_length(song)); ret = evbuffer_add_buffer(songlist, song); evbuffer_free(song); return ret; } static void dacp_reply_playqueuecontents(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; struct evbuffer *songlist; struct evbuffer *playlists; struct player_status status; struct player_history *history; const char *param; size_t songlist_length; size_t playlist_length; int span; int count; int ret; int start_index; struct query_params query_params; struct db_queue_item queue_item; /* /ctrl-int/1/playqueue-contents?span=50&session-id=... */ s = daap_session_find(req, query, evbuf); if (!s) return; DPRINTF(E_DBG, L_DACP, "Fetching playqueue contents\n"); span = 50; /* Default */ param = evhttp_find_header(query, "span"); if (param) { ret = safe_atoi32(param, &span); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Invalid span value in playqueue-contents request\n"); } count = 0; // count of songs in songlist songlist = evbuffer_new(); if (!songlist) { DPRINTF(E_LOG, L_DACP, "Could not allocate songlist evbuffer for playqueue-contents\n"); dmap_send_error(req, "ceQR", "Out of memory"); return; } player_get_status(&status); /* * If the span parameter is negativ make song list for Previously Played, * otherwise make song list for Up Next and begin with first song after playlist position. */ if (span < 0) { history = player_history_get(); if (abs(span) > history->count) { start_index = history->start_index; } else { start_index = (history->start_index + history->count - abs(span)) % MAX_HISTORY_COUNT; } for (count = 0; count < history->count && count < abs(span); count++) { ret = playqueuecontents_add_source(songlist, history->id[(start_index + count) % MAX_HISTORY_COUNT], (count + 1), status.plid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add song to songlist for playqueue-contents\n"); dmap_send_error(req, "ceQR", "Out of memory"); return; } } } else { memset(&query_params, 0, sizeof(struct query_params)); if (status.shuffle) query_params.sort = S_SHUFFLE_POS; ret = db_queue_enum_start(&query_params); count = 0; //FIXME [queue] Check count value while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) { if (status.item_id == 0 || status.item_id == queue_item.id) count = 1; else if (count > 0) { ret = playqueuecontents_add_queue_item(songlist, &queue_item, count, status.plid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add song to songlist for playqueue-contents\n"); dmap_send_error(req, "ceQR", "Out of memory"); return; } count++; } } db_queue_enum_end(&query_params); sqlite3_free(query_params.filter); } /* Playlists are hist, curr and main. */ playlists = evbuffer_new(); if (!playlists) { DPRINTF(E_LOG, L_DACP, "Could not allocate playlists evbuffer for playqueue-contents\n"); if (songlist) evbuffer_free(songlist); dmap_send_error(req, "ceQR", "Out of memory"); return; } dmap_add_container(playlists, "mlit", 61); dmap_add_string(playlists, "ceQk", "hist"); /* 12 */ dmap_add_int(playlists, "ceQi", -200); /* 12 */ dmap_add_int(playlists, "ceQm", 200); /* 12 */ dmap_add_string(playlists, "ceQl", "Previously Played"); /* 25 = 8 + 17 */ if (songlist) { dmap_add_container(playlists, "mlit", 36); dmap_add_string(playlists, "ceQk", "curr"); /* 12 */ dmap_add_int(playlists, "ceQi", 0); /* 12 */ dmap_add_int(playlists, "ceQm", 1); /* 12 */ dmap_add_container(playlists, "mlit", 69); dmap_add_string(playlists, "ceQk", "main"); /* 12 */ dmap_add_int(playlists, "ceQi", 1); /* 12 */ dmap_add_int(playlists, "ceQm", count); /* 12 */ dmap_add_string(playlists, "ceQl", "Up Next"); /* 15 = 8 + 7 */ dmap_add_string(playlists, "ceQh", "from Music"); /* 18 = 8 + 10 */ songlist_length = evbuffer_get_length(songlist); } else songlist_length = 0; /* Final construction of reply */ playlist_length = evbuffer_get_length(playlists); dmap_add_container(evbuf, "ceQR", 79 + playlist_length + songlist_length); /* size of entire container */ dmap_add_int(evbuf, "mstt", 200); /* 12, dmap.status */ dmap_add_int(evbuf, "mtco", abs(span)); /* 12 */ dmap_add_int(evbuf, "mrco", count); /* 12 */ dmap_add_char(evbuf, "ceQu", 0); /* 9 */ dmap_add_container(evbuf, "mlcl", 8 + playlist_length + songlist_length); /* 8 */ dmap_add_container(evbuf, "ceQS", playlist_length); /* 8 */ ret = evbuffer_add_buffer(evbuf, playlists); evbuffer_free(playlists); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add playlists to evbuffer for playqueue-contents\n"); if (songlist) evbuffer_free(songlist); dmap_send_error(req, "ceQR", "Out of memory"); return; } if (songlist) { ret = evbuffer_add_buffer(evbuf, songlist); evbuffer_free(songlist); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add songlist to evbuffer for playqueue-contents\n"); dmap_send_error(req, "ceQR", "Out of memory"); return; } } dmap_add_char(evbuf, "apsm", status.shuffle); /* 9, daap.playlistshufflemode - not part of mlcl container */ dmap_add_char(evbuf, "aprm", status.repeat); /* 9, daap.playlistrepeatmode - not part of mlcl container */ httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); } static void dacp_reply_playqueueedit_clear(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { const char *param; struct player_status status; param = evhttp_find_header(query, "mode"); /* * The mode parameter contains the playlist to be cleared. * If mode=0x68697374 (hex representation of the ascii string "hist") clear the history, * otherwise the current playlist. */ if (strcmp(param,"0x68697374") == 0) player_queue_clear_history(); else { player_get_status(&status); db_queue_clear(status.item_id); } dmap_add_container(evbuf, "cacr", 24); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "miid", 0); /* 12 */ httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); } static void dacp_reply_playqueueedit_add(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { //?command=add&query='dmap.itemid:156'&sort=album&mode=3&session-id=100 // -> mode=3: add to playqueue position 0 (play next) //?command=add&query='dmap.itemid:158'&sort=album&mode=0&session-id=100 // -> mode=0: add to end of playqueue //?command=add&query='dmap.itemid:306'&queuefilter=album:6525753023700533274&sort=album&mode=1&session-id=100 // -> mode 1: stop playblack, clear playqueue, add songs to playqueue //?command=add&query='dmap.itemid:2'&query-modifier=containers&sort=name&mode=2&session-id=100 // -> mode 2: stop playblack, clear playqueue, add shuffled songs from playlist=itemid to playqueue const char *editquery; const char *queuefilter; const char *querymodifier; const char *sort; const char *param; char modifiedquery[32]; int mode; int plid; int ret; int quirkyquery; struct db_queue_item *queue_item; struct player_status status; mode = 1; param = evhttp_find_header(query, "mode"); if (param) { ret = safe_atoi32(param, &mode); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid mode value in playqueue-edit request\n"); dmap_send_error(req, "cacr", "Invalid request"); return; } } if ((mode == 1) || (mode == 2)) { player_playback_stop(); db_queue_clear(0); } if (mode == 2) player_shuffle_set(1); editquery = evhttp_find_header(query, "query"); if (!editquery) { DPRINTF(E_LOG, L_DACP, "Could not add song queue, DACP query missing\n"); dmap_send_error(req, "cacr", "Invalid request"); return; } sort = evhttp_find_header(query, "sort"); // if sort param is missing and an album or artist is added to the queue, set sort to "album" if (!sort && (strstr(editquery, "daap.songalbumid:") || strstr(editquery, "daap.songartistid:"))) { sort = "album"; } // only use queryfilter if mode is not equal 0 (add to up next), 3 (play next) or 5 (add to up next) queuefilter = (mode == 0 || mode == 3 || mode == 5) ? NULL : evhttp_find_header(query, "queuefilter"); querymodifier = evhttp_find_header(query, "query-modifier"); if (!querymodifier || (strcmp(querymodifier, "containers") != 0)) { quirkyquery = (mode == 1) && strstr(editquery, "dmap.itemid:") && ((!queuefilter) || strstr(queuefilter, "(null)")); ret = dacp_queueitem_add(editquery, queuefilter, sort, quirkyquery, mode); } else { // Modify the query: Take the id from the editquery and use it as a queuefilter playlist id ret = safe_atoi32(strchr(editquery, ':') + 1, &plid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid playlist id in request: %s\n", editquery); dmap_send_error(req, "cacr", "Invalid request"); return; } snprintf(modifiedquery, sizeof(modifiedquery), "playlist:%d", plid); ret = dacp_queueitem_add(NULL, modifiedquery, sort, 0, mode); } if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not build song queue\n"); dmap_send_error(req, "cacr", "Invalid request"); return; } if (ret > 0) queue_item = db_queue_fetch_byfileid(ret); else queue_item = NULL; if (queue_item) { player_get_status(&status); if (status.shuffle) { DPRINTF(E_DBG, L_DACP, "Start shuffle queue with item %d\n", queue_item->id); db_queue_move_byitemid(queue_item->id, 0, status.shuffle); } DPRINTF(E_DBG, L_DACP, "Song queue built, starting playback at index %d\n", queue_item->pos); ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); } else { DPRINTF(E_DBG, L_DACP, "Song queue built, starting playback\n"); ret = player_playback_start(); } if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); dmap_send_error(req, "cacr", "Playback failed to start"); return; } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_playqueueedit_move(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { /* * Handles the move command. * Exampe request: * playqueue-edit?command=move&edit-params='edit-param.move-pair:3,0'&session-id=100 * * The 'edit-param.move-pair' param contains the index of the song in the playqueue to be moved (index 3 in the example) * and the index of the song after which it should be inserted (index 0 in the exampe, the now playing song). */ struct player_status status; int ret; const char *param; int src; int dst; param = evhttp_find_header(query, "edit-params"); if (param) { ret = safe_atoi32(strchr(param, ':') + 1, &src); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid edit-params move-from value in playqueue-edit request\n"); dmap_send_error(req, "cacr", "Invalid request"); return; } ret = safe_atoi32(strchr(param, ',') + 1, &dst); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid edit-params move-to value in playqueue-edit request\n"); dmap_send_error(req, "cacr", "Invalid request"); return; } player_get_status(&status); db_queue_move_byposrelativetoitem(src, dst, status.item_id, status.shuffle); } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_playqueueedit_remove(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { /* * Handles the remove command. * Exampe request (removes song at position 1 in the playqueue): * ?command=remove&items=1&session-id=100 */ struct player_status status; int ret; const char *param; int item_index; param = evhttp_find_header(query, "items"); if (param) { ret = safe_atoi32(param, &item_index); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid edit-params remove item value in playqueue-edit request\n"); dmap_send_error(req, "cacr", "Invalid request"); return; } player_get_status(&status); db_queue_delete_byposrelativetoitem(item_index, status.item_id, status.shuffle); } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void dacp_reply_playqueueedit(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; const char *param; /* Variations of /ctrl-int/1/playqueue-edit and expected behaviour User selected play (album or artist tab): ?command=add&query='...'&sort=album&mode=1&session-id=... -> clear queue, play query results User selected play (playlist): ?command=add&query='dmap.itemid:...'&query-modifier=containers&mode=1&session-id=... -> clear queue, play playlist with the id specified by itemid User selected track (album tab): ?command=add&query='dmap.itemid:...'&queuefilter=album:...&sort=album&mode=1&session-id=... -> clear queue, play itemid and the rest of album User selected track (artist tab): ?command=add&query='dmap.itemid:...'&queuefilter=artist:...&sort=album&mode=1&session-id=... -> clear queue, play itemid and the rest of artist tracks User selected track (song tab): ?command=add&query='dmap.itemid:...'&queuefilter=playlist:...&sort=name&mode=1&session-id=... -> clear queue, play itemid and the rest of playlist User selected track (playlist tab): ?command=add&query='dmap.containeritemid:...'&queuefilter=playlist:...&sort=physical&mode=1&session-id=... -> clear queue, play containeritemid and the rest of playlist User selected shuffle (artist tab): ?command=add&query='...'&sort=album&mode=2&session-id=... -> clear queue, play shuffled query results User selected add item to queue: ?command=add&query='...'&sort=album&mode=0&session-id=... -> add query results to queue User selected play next song (album tab) ?command=add&query='daap.songalbumid:...'&sort=album&mode=3&session-id=... -> replace queue from after current song with query results User selected track in queue: ?command=playnow&index=...&session-id=... -> play index And the quirky query from Remote - no sort and no queuefilter User selected track (Audiobooks): ?command=add&query='dmap.itemid:...'&mode=1&session-id=... -> clear queue, play itemid and the rest of album tracks ?command=move&edit-params='edit-param.move-pair:3,0'&session-id=100 -> move song from playqueue position 3 to be played after song at position 0 ?command=remove&items=1&session-id=100 -> remove song on position 1 from the playqueue */ s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "command"); if (!param) { DPRINTF(E_LOG, L_DACP, "No command in playqueue-edit request\n"); dmap_send_error(req, "cmst", "Invalid request"); return; } if (strcmp(param, "clear") == 0) dacp_reply_playqueueedit_clear(req, evbuf, uri, query); else if (strcmp(param, "playnow") == 0) dacp_reply_cue_play(req, evbuf, uri, query); else if (strcmp(param, "add") == 0) dacp_reply_playqueueedit_add(req, evbuf, uri, query); else if (strcmp(param, "move") == 0) dacp_reply_playqueueedit_move(req, evbuf, uri, query); else if (strcmp(param, "remove") == 0) dacp_reply_playqueueedit_remove(req, evbuf, uri, query); else { DPRINTF(E_LOG, L_DACP, "Unknown playqueue-edit command %s\n", param); dmap_send_error(req, "cmst", "Invalid request"); return; } } static void dacp_reply_playstatusupdate(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; struct dacp_update_request *ur; struct evhttp_connection *evcon; const char *param; int reqd_rev; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "revision-number"); if (!param) { DPRINTF(E_LOG, L_DACP, "Missing revision-number in update request\n"); dmap_send_error(req, "cmst", "Invalid request"); return; } ret = safe_atoi32(param, &reqd_rev); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Parameter revision-number not an integer\n"); dmap_send_error(req, "cmst", "Invalid request"); return; } if ((reqd_rev == 0) || (reqd_rev == 1)) { ret = make_playstatusupdate(evbuf); if (ret < 0) httpd_send_error(req, 500, "Internal Server Error"); else httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return; } /* Else, just let the request hang until we have changes to push back */ ur = (struct dacp_update_request *)malloc(sizeof(struct dacp_update_request)); if (!ur) { DPRINTF(E_LOG, L_DACP, "Out of memory for update request\n"); dmap_send_error(req, "cmst", "Out of memory"); return; } ur->req = req; ur->next = update_requests; update_requests = ur; /* If the connection fails before we have an update to push out * to the client, we need to know. */ evcon = evhttp_request_get_connection(req); if (evcon) evhttp_connection_set_closecb(evcon, update_fail_cb, ur); } static void dacp_reply_nowplayingartwork(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { char clen[32]; struct daap_session *s; struct evkeyvalq *headers; const char *param; char *ctype; size_t len; uint32_t id; int max_w; int max_h; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "mw"); if (!param) { DPRINTF(E_LOG, L_DACP, "Request for artwork without mw parameter\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } ret = safe_atoi32(param, &max_w); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not convert mw parameter to integer\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } param = evhttp_find_header(query, "mh"); if (!param) { DPRINTF(E_LOG, L_DACP, "Request for artwork without mh parameter\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } ret = safe_atoi32(param, &max_h); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not convert mh parameter to integer\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } ret = player_now_playing(&id); if (ret < 0) goto no_artwork; ret = artwork_get_item(evbuf, id, max_w, max_h); len = evbuffer_get_length(evbuf); switch (ret) { case ART_FMT_PNG: ctype = "image/png"; break; case ART_FMT_JPEG: ctype = "image/jpeg"; break; default: if (len > 0) evbuffer_drain(evbuf, len); goto no_artwork; } headers = evhttp_request_get_output_headers(req); evhttp_remove_header(headers, "Content-Type"); evhttp_add_header(headers, "Content-Type", ctype); snprintf(clen, sizeof(clen), "%ld", (long)len); evhttp_add_header(headers, "Content-Length", clen); httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); return; no_artwork: httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); } static void dacp_reply_getproperty(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct player_status status; struct daap_session *s; const struct dacp_prop_map *dpm; struct db_queue_item *queue_item = NULL; struct evbuffer *proplist; const char *param; char *ptr; char *prop; char *propstr; size_t len; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "properties"); if (!param) { DPRINTF(E_WARN, L_DACP, "Invalid DACP getproperty request, no properties\n"); dmap_send_error(req, "cmgt", "Invalid request"); return; } propstr = strdup(param); if (!propstr) { DPRINTF(E_LOG, L_DACP, "Could not duplicate properties parameter; out of memory\n"); dmap_send_error(req, "cmgt", "Out of memory"); return; } proplist = evbuffer_new(); if (!proplist) { DPRINTF(E_LOG, L_DACP, "Could not allocate evbuffer for properties list\n"); dmap_send_error(req, "cmgt", "Out of memory"); goto out_free_propstr; } player_get_status(&status); if (status.status != PLAY_STOPPED) { queue_item = db_queue_fetch_byitemid(status.item_id); if (!queue_item) { DPRINTF(E_LOG, L_DACP, "Could not fetch queue_item for item-id %d\n", status.item_id); dmap_send_error(req, "cmgt", "Server error"); goto out_free_proplist; } } prop = strtok_r(propstr, ",", &ptr); while (prop) { dpm = dacp_find_prop(prop, strlen(prop)); if (dpm) { if (dpm->propget) dpm->propget(proplist, &status, queue_item); else DPRINTF(E_WARN, L_DACP, "No getter method for DACP property %s\n", prop); } else DPRINTF(E_LOG, L_DACP, "Could not find requested property '%s'\n", prop); prop = strtok_r(NULL, ",", &ptr); } free(propstr); if (queue_item) free_queue_item(queue_item, 0); len = evbuffer_get_length(proplist); dmap_add_container(evbuf, "cmgt", 12 + len); dmap_add_int(evbuf, "mstt", 200); /* 12 */ ret = evbuffer_add_buffer(evbuf, proplist); evbuffer_free(proplist); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add properties to getproperty reply\n"); dmap_send_error(req, "cmgt", "Out of memory"); return; } httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); return; out_free_proplist: evbuffer_free(proplist); out_free_propstr: free(propstr); } static void dacp_reply_setproperty(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; const struct dacp_prop_map *dpm; struct evkeyval *param; s = daap_session_find(req, query, evbuf); if (!s) return; /* Known properties: * dacp.shufflestate 0/1 * dacp.repeatstate 0/1/2 * dacp.playingtime seek to time in ms * dmcp.volume 0-100, float */ /* /ctrl-int/1/setproperty?dacp.shufflestate=1&session-id=100 */ TAILQ_FOREACH(param, query, next) { dpm = dacp_find_prop(param->key, strlen(param->key)); if (!dpm) { DPRINTF(E_SPAM, L_DACP, "Unknown DACP property %s\n", param->key); continue; } if (dpm->propset) dpm->propset(param->value, query); else DPRINTF(E_WARN, L_DACP, "No setter method for DACP property %s\n", dpm->desc); } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static void speaker_enum_cb(uint64_t id, const char *name, int relvol, int absvol, struct spk_flags flags, void *arg) { struct evbuffer *evbuf; int len; evbuf = (struct evbuffer *)arg; len = 8 + strlen(name) + 28; if (flags.selected) len += 9; if (flags.has_password) len += 9; if (flags.has_video) len += 9; dmap_add_container(evbuf, "mdcl", len); /* 8 + len */ if (flags.selected) dmap_add_char(evbuf, "caia", 1); /* 9 */ if (flags.has_password) dmap_add_char(evbuf, "cahp", 1); /* 9 */ if (flags.has_video) dmap_add_char(evbuf, "caiv", 1); /* 9 */ dmap_add_string(evbuf, "minm", name); /* 8 + len */ dmap_add_long(evbuf, "msma", id); /* 16 */ dmap_add_int(evbuf, "cmvo", relvol); /* 12 */ } static void dacp_reply_getspeakers(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; struct evbuffer *spklist; size_t len; s = daap_session_find(req, query, evbuf); if (!s) return; spklist = evbuffer_new(); if (!spklist) { DPRINTF(E_LOG, L_DACP, "Could not create evbuffer for speaker list\n"); dmap_send_error(req, "casp", "Out of memory"); return; } player_speaker_enumerate(speaker_enum_cb, spklist); len = evbuffer_get_length(spklist); dmap_add_container(evbuf, "casp", 12 + len); dmap_add_int(evbuf, "mstt", 200); /* 12 */ evbuffer_add_buffer(evbuf, spklist); evbuffer_free(spklist); httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); } static void dacp_reply_setspeakers(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; const char *param; const char *ptr; uint64_t *ids; int nspk; int i; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "speaker-id"); if (!param) { DPRINTF(E_LOG, L_DACP, "Missing speaker-id parameter in DACP setspeakers request\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } if (strlen(param) == 0) { ids = NULL; goto fastpath; } nspk = 1; ptr = param; while ((ptr = strchr(ptr + 1, ','))) nspk++; ids = (uint64_t *)malloc((nspk + 1) * sizeof(uint64_t)); if (!ids) { DPRINTF(E_LOG, L_DACP, "Out of memory for speaker ids\n"); httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return; } param--; i = 1; do { param++; ret = safe_hextou64(param, &ids[i]); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid speaker id in request: %s\n", param); nspk--; continue; } else { DPRINTF(E_DBG, L_DACP, "Speaker id converted with ret %d, param %s, dec val %" PRIu64 ".\n", ret, param, ids[i]); } i++; } while ((param = strchr(param + 1, ','))); ids[0] = nspk; fastpath: ret = player_speaker_set(ids); if (ids) free(ids); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Speakers de/activation failed!\n"); /* Password problem */ if (ret == -2) httpd_send_error(req, 902, ""); else httpd_send_error(req, 500, "Internal Server Error"); return; } /* 204 No Content is the canonical reply */ httpd_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf, HTTPD_SEND_NO_GZIP); } static struct uri_map dacp_handlers[] = { { .regexp = "^/ctrl-int$", .handler = dacp_reply_ctrlint }, { .regexp = "^/ctrl-int/[[:digit:]]+/cue$", .handler = dacp_reply_cue }, { .regexp = "^/ctrl-int/[[:digit:]]+/playspec$", .handler = dacp_reply_playspec }, { .regexp = "^/ctrl-int/[[:digit:]]+/pause$", .handler = dacp_reply_pause }, { .regexp = "^/ctrl-int/[[:digit:]]+/playpause$", .handler = dacp_reply_playpause }, { .regexp = "^/ctrl-int/[[:digit:]]+/nextitem$", .handler = dacp_reply_nextitem }, { .regexp = "^/ctrl-int/[[:digit:]]+/previtem$", .handler = dacp_reply_previtem }, { .regexp = "^/ctrl-int/[[:digit:]]+/beginff$", .handler = dacp_reply_beginff }, { .regexp = "^/ctrl-int/[[:digit:]]+/beginrew$", .handler = dacp_reply_beginrew }, { .regexp = "^/ctrl-int/[[:digit:]]+/playresume$", .handler = dacp_reply_playresume }, { .regexp = "^/ctrl-int/[[:digit:]]+/playstatusupdate$", .handler = dacp_reply_playstatusupdate }, { .regexp = "^/ctrl-int/[[:digit:]]+/playqueue-contents$", .handler = dacp_reply_playqueuecontents }, { .regexp = "^/ctrl-int/[[:digit:]]+/playqueue-edit$", .handler = dacp_reply_playqueueedit }, { .regexp = "^/ctrl-int/[[:digit:]]+/nowplayingartwork$", .handler = dacp_reply_nowplayingartwork }, { .regexp = "^/ctrl-int/[[:digit:]]+/getproperty$", .handler = dacp_reply_getproperty }, { .regexp = "^/ctrl-int/[[:digit:]]+/setproperty$", .handler = dacp_reply_setproperty }, { .regexp = "^/ctrl-int/[[:digit:]]+/getspeakers$", .handler = dacp_reply_getspeakers }, { .regexp = "^/ctrl-int/[[:digit:]]+/setspeakers$", .handler = dacp_reply_setspeakers }, { .regexp = NULL, .handler = NULL } }; void dacp_request(struct evhttp_request *req) { char *full_uri; char *uri; char *ptr; char *uri_parts[7]; struct evbuffer *evbuf; struct evkeyvalq query; struct evkeyvalq *headers; int handler; int ret; int i; memset(&query, 0, sizeof(struct evkeyvalq)); full_uri = httpd_fixup_uri(req); if (!full_uri) { httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } ptr = strchr(full_uri, '?'); if (ptr) *ptr = '\0'; uri = strdup(full_uri); if (!uri) { free(full_uri); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } if (ptr) *ptr = '?'; ptr = uri; uri = evhttp_decode_uri(uri); free(ptr); DPRINTF(E_DBG, L_DACP, "DACP request: %s\n", full_uri); handler = -1; for (i = 0; dacp_handlers[i].handler; i++) { ret = regexec(&dacp_handlers[i].preg, uri, 0, NULL, 0); if (ret == 0) { handler = i; break; } } if (handler < 0) { DPRINTF(E_LOG, L_DACP, "Unrecognized DACP request\n"); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); free(uri); free(full_uri); return; } /* DACP has no HTTP authentication - Remote is identified by its pairing-guid */ memset(uri_parts, 0, sizeof(uri_parts)); uri_parts[0] = strtok_r(uri, "/", &ptr); for (i = 1; (i < sizeof(uri_parts) / sizeof(uri_parts[0])) && uri_parts[i - 1]; i++) { uri_parts[i] = strtok_r(NULL, "/", &ptr); } if (!uri_parts[0] || uri_parts[i - 1] || (i < 2)) { DPRINTF(E_LOG, L_DACP, "DACP URI has too many/few components (%d)\n", (uri_parts[0]) ? i : 0); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); free(uri); free(full_uri); return; } evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_DACP, "Could not allocate evbuffer for DACP reply\n"); httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); free(uri); free(full_uri); return; } evhttp_parse_query(full_uri, &query); headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" VERSION); /* Content-Type for all DACP replies; can be overriden as needed */ evhttp_add_header(headers, "Content-Type", "application/x-dmap-tagged"); dacp_handlers[handler].handler(req, evbuf, uri_parts, &query); evbuffer_free(evbuf); evhttp_clear_headers(&query); free(uri); free(full_uri); } int dacp_is_request(struct evhttp_request *req, char *uri) { if (strncmp(uri, "/ctrl-int/", strlen("/ctrl-int/")) == 0) return 1; if (strcmp(uri, "/ctrl-int") == 0) return 1; return 0; } int dacp_init(void) { char buf[64]; int i; int ret; current_rev = 2; update_requests = NULL; #ifdef HAVE_EVENTFD update_efd = eventfd(0, EFD_CLOEXEC); if (update_efd < 0) { DPRINTF(E_LOG, L_DACP, "Could not create update eventfd: %s\n", strerror(errno)); return -1; } #else # ifdef HAVE_PIPE2 ret = pipe2(update_pipe, O_CLOEXEC); # else ret = pipe(update_pipe); # endif if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not create update pipe: %s\n", strerror(errno)); return -1; } #endif /* HAVE_EVENTFD */ for (i = 0; dacp_handlers[i].handler; i++) { ret = regcomp(&dacp_handlers[i].preg, dacp_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); if (ret != 0) { regerror(ret, &dacp_handlers[i].preg, buf, sizeof(buf)); DPRINTF(E_FATAL, L_DACP, "DACP init failed; regexp error: %s\n", buf); goto regexp_fail; } } #ifdef HAVE_EVENTFD updateev = event_new(evbase_httpd, update_efd, EV_READ, playstatusupdate_cb, NULL); #else updateev = event_new(evbase_httpd, update_pipe[0], EV_READ, playstatusupdate_cb, NULL); #endif if (!updateev) { DPRINTF(E_LOG, L_DACP, "Could not create update event\n"); return -1; } event_add(updateev, NULL); seek_timer = evtimer_new(evbase_httpd, seek_timer_cb, NULL); if (!seek_timer) { DPRINTF(E_LOG, L_DACP, "Could not create seek_timer event\n"); return -1; } listener_add(dacp_playstatus_update_handler, LISTENER_PLAYER); return 0; regexp_fail: #ifdef HAVE_EVENTFD close(update_efd); #else close(update_pipe[0]); close(update_pipe[1]); #endif return -1; } void dacp_deinit(void) { struct dacp_update_request *ur; struct evhttp_connection *evcon; int i; listener_remove(dacp_playstatus_update_handler); event_free(seek_timer); for (i = 0; dacp_handlers[i].handler; i++) regfree(&dacp_handlers[i].preg); for (ur = update_requests; update_requests; ur = update_requests) { update_requests = ur->next; evcon = evhttp_request_get_connection(ur->req); if (evcon) { evhttp_connection_set_closecb(evcon, NULL, NULL); evhttp_connection_free(evcon); } free(ur); } event_free(updateev); #ifdef HAVE_EVENTFD close(update_efd); #else close(update_pipe[0]); close(update_pipe[1]); #endif } forked-daapd-25.0/src/httpd_dacp.h000066400000000000000000000004111313447753700170450ustar00rootroot00000000000000 #ifndef __HTTPD_DACP_H__ #define __HTTPD_DACP_H__ #include int dacp_init(void); void dacp_deinit(void); void dacp_request(struct evhttp_request *req); int dacp_is_request(struct evhttp_request *req, char *uri); #endif /* !__HTTPD_DACP_H__ */ forked-daapd-25.0/src/httpd_rsp.c000066400000000000000000000557061313447753700167560ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * Adapted from mt-daapd: * Copyright (C) 2006-2007 Ron Pedde * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "db.h" #include "conffile.h" #include "misc.h" #include "httpd.h" #include "transcode.h" #include "httpd_rsp.h" #include "rsp_query.h" #define RSP_VERSION "1.0" #define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?" #define F_FULL (1 << 0) #define F_BROWSE (1 << 1) #define F_ID (1 << 2) #define F_DETAILED (1 << 3) #define F_ALWAYS (F_FULL | F_BROWSE | F_ID | F_DETAILED) struct field_map { char *field; size_t offset; int flags; }; struct uri_map { regex_t preg; char *regexp; void (*handler)(struct evhttp_request *req, char **uri, struct evkeyvalq *query); }; static const struct field_map pl_fields[] = { { "id", dbpli_offsetof(id), F_ALWAYS }, { "title", dbpli_offsetof(title), F_FULL | F_BROWSE | F_DETAILED }, { "type", dbpli_offsetof(type), F_DETAILED }, { "items", dbpli_offsetof(items), F_FULL | F_BROWSE | F_DETAILED }, { "query", dbpli_offsetof(query), F_DETAILED }, { "db_timestamp", dbpli_offsetof(db_timestamp), F_DETAILED }, { "path", dbpli_offsetof(path), F_DETAILED }, { "index", dbpli_offsetof(index), F_DETAILED }, { NULL, 0, 0 } }; static const struct field_map rsp_fields[] = { { "id", dbmfi_offsetof(id), F_ALWAYS }, { "path", dbmfi_offsetof(path), F_DETAILED }, { "fname", dbmfi_offsetof(fname), F_DETAILED }, { "title", dbmfi_offsetof(title), F_ALWAYS }, { "artist", dbmfi_offsetof(artist), F_DETAILED | F_FULL | F_BROWSE }, { "album", dbmfi_offsetof(album), F_DETAILED | F_FULL | F_BROWSE }, { "genre", dbmfi_offsetof(genre), F_DETAILED | F_FULL }, { "comment", dbmfi_offsetof(comment), F_DETAILED | F_FULL }, { "type", dbmfi_offsetof(type), F_ALWAYS }, { "composer", dbmfi_offsetof(composer), F_DETAILED | F_FULL }, { "orchestra", dbmfi_offsetof(orchestra), F_DETAILED | F_FULL }, { "conductor", dbmfi_offsetof(conductor), F_DETAILED | F_FULL }, { "url", dbmfi_offsetof(url), F_DETAILED | F_FULL }, { "bitrate", dbmfi_offsetof(bitrate), F_DETAILED | F_FULL }, { "samplerate", dbmfi_offsetof(samplerate), F_DETAILED | F_FULL }, { "song_length", dbmfi_offsetof(song_length), F_DETAILED | F_FULL }, { "file_size", dbmfi_offsetof(file_size), F_DETAILED | F_FULL }, { "year", dbmfi_offsetof(year), F_DETAILED | F_FULL }, { "track", dbmfi_offsetof(track), F_DETAILED | F_FULL | F_BROWSE }, { "total_tracks", dbmfi_offsetof(total_tracks), F_DETAILED | F_FULL }, { "disc", dbmfi_offsetof(disc), F_DETAILED | F_FULL | F_BROWSE }, { "total_discs", dbmfi_offsetof(total_discs), F_DETAILED | F_FULL }, { "bpm", dbmfi_offsetof(bpm), F_DETAILED | F_FULL }, { "compilation", dbmfi_offsetof(compilation), F_DETAILED | F_FULL }, { "rating", dbmfi_offsetof(rating), F_DETAILED | F_FULL }, { "play_count", dbmfi_offsetof(play_count), F_DETAILED | F_FULL }, { "data_kind", dbmfi_offsetof(data_kind), F_DETAILED }, { "item_kind", dbmfi_offsetof(item_kind), F_DETAILED }, { "description", dbmfi_offsetof(description), F_DETAILED | F_FULL }, { "time_added", dbmfi_offsetof(time_added), F_DETAILED | F_FULL }, { "time_modified", dbmfi_offsetof(time_modified), F_DETAILED | F_FULL }, { "time_played", dbmfi_offsetof(time_played), F_DETAILED | F_FULL }, { "db_timestamp", dbmfi_offsetof(db_timestamp), F_DETAILED }, { "disabled", dbmfi_offsetof(disabled), F_ALWAYS }, { "sample_count", dbmfi_offsetof(sample_count), F_DETAILED }, { "codectype", dbmfi_offsetof(codectype), F_ALWAYS }, { "idx", dbmfi_offsetof(idx), F_DETAILED }, { "has_video", dbmfi_offsetof(has_video), F_DETAILED }, { "contentrating", dbmfi_offsetof(contentrating), F_DETAILED }, { NULL, 0, 0 } }; static struct evbuffer * mxml_to_evbuf(mxml_node_t *tree) { struct evbuffer *evbuf; char *xml; int ret; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_RSP, "Could not create evbuffer for RSP reply\n"); return NULL; } xml = mxmlSaveAllocString(tree, MXML_NO_CALLBACK); if (!xml) { DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n"); evbuffer_free(evbuf); return NULL; } ret = evbuffer_add(evbuf, xml, strlen(xml)); free(xml); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not load evbuffer for RSP reply\n"); evbuffer_free(evbuf); return NULL; } return evbuf; } /* Forward */ static void rsp_send_error(struct evhttp_request *req, char *errmsg); static int get_query_params(struct evhttp_request *req, struct evkeyvalq *query, struct query_params *qp) { const char *param; int ret; qp->offset = 0; param = evhttp_find_header(query, "offset"); if (param) { ret = safe_atoi32(param, &qp->offset); if (ret < 0) { rsp_send_error(req, "Invalid offset"); return -1; } } qp->limit = 0; param = evhttp_find_header(query, "limit"); if (param) { ret = safe_atoi32(param, &qp->limit); if (ret < 0) { rsp_send_error(req, "Invalid limit"); return -1; } } if (qp->offset || qp->limit) qp->idx_type = I_SUB; else qp->idx_type = I_NONE; qp->sort = S_NONE; param = evhttp_find_header(query, "query"); if (param) { DPRINTF(E_DBG, L_RSP, "RSP browse query filter: %s\n", param); qp->filter = rsp_query_parse_sql(param); if (!qp->filter) DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query\n"); } return 0; } static void rsp_send_error(struct evhttp_request *req, char *errmsg) { struct evbuffer *evbuf; struct evkeyvalq *headers; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *node; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "1"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, errmsg); node = mxmlNewElement(status, "records"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "totalrecords"); mxmlNewText(node, 0, "0"); evbuf = mxml_to_evbuf(reply); mxmlDelete(reply); if (!evbuf) { httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return; } headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8"); evhttp_add_header(headers, "Connection", "close"); httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); evbuffer_free(evbuf); } static void rsp_send_reply(struct evhttp_request *req, mxml_node_t *reply) { struct evbuffer *evbuf; struct evkeyvalq *headers; evbuf = mxml_to_evbuf(reply); mxmlDelete(reply); if (!evbuf) { rsp_send_error(req, "Could not finalize reply"); return; } headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8"); evhttp_add_header(headers, "Connection", "close"); httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); evbuffer_free(evbuf); } static void rsp_reply_info(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { mxml_node_t *reply; mxml_node_t *status; mxml_node_t *info; mxml_node_t *node; cfg_t *lib; char *library; int songcount; songcount = db_files_get_count(); lib = cfg_getsec(cfg, "library"); library = cfg_getstr(lib, "name"); /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); info = mxmlNewElement(node, "info"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "totalrecords"); mxmlNewText(node, 0, "0"); /* Info block */ node = mxmlNewElement(info, "count"); mxmlNewTextf(node, 0, "%d", songcount); node = mxmlNewElement(info, "rsp-version"); mxmlNewText(node, 0, RSP_VERSION); node = mxmlNewElement(info, "server-version"); mxmlNewText(node, 0, VERSION); node = mxmlNewElement(info, "name"); mxmlNewText(node, 0, library); rsp_send_reply(req, reply); } static void rsp_reply_db(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { struct query_params qp; struct db_playlist_info dbpli; char **strval; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *pls; mxml_node_t *pl; mxml_node_t *node; int i; int ret; memset(&qp, 0, sizeof(struct db_playlist_info)); qp.type = Q_PL; qp.idx_type = I_NONE; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(req, "Could not start query"); return; } /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); pls = mxmlNewElement(node, "playlists"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", qp.results); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Playlists block (all playlists) */ while (((ret = db_query_fetch_pl(&qp, &dbpli, 1)) == 0) && (dbpli.id)) { /* Playlist block (one playlist) */ pl = mxmlNewElement(pls, "playlist"); for (i = 0; pl_fields[i].field; i++) { if (pl_fields[i].flags & F_FULL) { strval = (char **) ((char *)&dbpli + pl_fields[i].offset); node = mxmlNewElement(pl, pl_fields[i].field); mxmlNewText(node, 0, *strval); } } } if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(req, "Error fetching query results"); return; } /* HACK * Add a dummy empty string to the playlists element if there is no data * to return - this prevents mxml from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(pls, 0, ""); db_query_end(&qp); rsp_send_reply(req, reply); } static void rsp_reply_playlist(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { struct query_params qp; struct db_media_file_info dbmfi; struct evkeyvalq *headers; const char *param; const char *ua; const char *client_codecs; char **strval; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *items; mxml_node_t *item; mxml_node_t *node; int mode; int records; int transcode; int32_t bitrate; int i; int ret; memset(&qp, 0, sizeof(struct query_params)); ret = safe_atoi32(uri[2], &qp.id); if (ret < 0) { rsp_send_error(req, "Invalid playlist ID"); return; } if (qp.id == 0) qp.type = Q_ITEMS; else qp.type = Q_PLITEMS; mode = F_FULL; param = evhttp_find_header(query, "type"); if (param) { if (strcasecmp(param, "full") == 0) mode = F_FULL; else if (strcasecmp(param, "browse") == 0) mode = F_BROWSE; else if (strcasecmp(param, "id") == 0) mode = F_ID; else if (strcasecmp(param, "detailed") == 0) mode = F_DETAILED; else DPRINTF(E_LOG, L_RSP, "Unknown browse mode %s\n", param); } ret = get_query_params(req, query, &qp); if (ret < 0) return; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(req, "Could not start query"); if (qp.filter) free(qp.filter); return; } if (qp.offset > qp.results) records = 0; else if (qp.limit > (qp.results - qp.offset)) records = qp.results - qp.offset; else records = qp.limit; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); items = mxmlNewElement(node, "items"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", records); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Items block (all items) */ while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { headers = evhttp_request_get_input_headers(req); ua = evhttp_find_header(headers, "User-Agent"); client_codecs = evhttp_find_header(headers, "Accept-Codecs"); transcode = transcode_needed(ua, client_codecs, dbmfi.codectype); /* Item block (one item) */ item = mxmlNewElement(items, "item"); for (i = 0; rsp_fields[i].field; i++) { if (!(rsp_fields[i].flags & mode)) continue; strval = (char **) ((char *)&dbmfi + rsp_fields[i].offset); if (!(*strval) || (strlen(*strval) == 0)) continue; node = mxmlNewElement(item, rsp_fields[i].field); if (!transcode) mxmlNewText(node, 0, *strval); else { switch (rsp_fields[i].offset) { case dbmfi_offsetof(type): mxmlNewText(node, 0, "wav"); break; case dbmfi_offsetof(bitrate): bitrate = 0; ret = safe_atoi32(dbmfi.samplerate, &bitrate); if ((ret < 0) || (bitrate == 0)) bitrate = 1411; else bitrate = (bitrate * 8) / 250; mxmlNewTextf(node, 0, "%d", bitrate); break; case dbmfi_offsetof(description): mxmlNewText(node, 0, "wav audio file"); break; case dbmfi_offsetof(codectype): mxmlNewText(node, 0, "wav"); node = mxmlNewElement(item, "original_codec"); mxmlNewText(node, 0, *strval); break; default: mxmlNewText(node, 0, *strval); break; } } } } if (qp.filter) free(qp.filter); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(req, "Error fetching query results"); return; } /* HACK * Add a dummy empty string to the items element if there is no data * to return - this prevents mxml from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(items, 0, ""); db_query_end(&qp); rsp_send_reply(req, reply); } static void rsp_reply_browse(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { struct query_params qp; char *browse_item; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *items; mxml_node_t *node; int records; int ret; memset(&qp, 0, sizeof(struct query_params)); if (strcmp(uri[3], "artist") == 0) qp.type = Q_BROWSE_ARTISTS; else if (strcmp(uri[3], "genre") == 0) qp.type = Q_BROWSE_GENRES; else if (strcmp(uri[3], "album") == 0) qp.type = Q_BROWSE_ALBUMS; else if (strcmp(uri[3], "composer") == 0) qp.type = Q_BROWSE_COMPOSERS; else { DPRINTF(E_LOG, L_RSP, "Unsupported browse type '%s'\n", uri[3]); rsp_send_error(req, "Unsupported browse type"); return; } ret = safe_atoi32(uri[2], &qp.id); if (ret < 0) { rsp_send_error(req, "Invalid playlist ID"); return; } ret = get_query_params(req, query, &qp); if (ret < 0) return; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(req, "Could not start query"); if (qp.filter) free(qp.filter); return; } if (qp.offset > qp.results) records = 0; else if (qp.limit > (qp.results - qp.offset)) records = qp.results - qp.offset; else records = qp.limit; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); items = mxmlNewElement(node, "items"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", records); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Items block (all items) */ while (((ret = db_query_fetch_string(&qp, &browse_item)) == 0) && (browse_item)) { node = mxmlNewElement(items, "item"); mxmlNewText(node, 0, browse_item); } if (qp.filter) free(qp.filter); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(req, "Error fetching query results"); return; } /* HACK * Add a dummy empty string to the items element if there is no data * to return - this prevents mxml from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(items, 0, ""); db_query_end(&qp); rsp_send_reply(req, reply); } static void rsp_stream(struct evhttp_request *req, char **uri, struct evkeyvalq *query) { int id; int ret; ret = safe_atoi32(uri[2], &id); if (ret < 0) httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); else httpd_stream_file(req, id); } static struct uri_map rsp_handlers[] = { { .regexp = "^/rsp/info$", .handler = rsp_reply_info }, { .regexp = "^/rsp/db$", .handler = rsp_reply_db }, { .regexp = "^/rsp/db/[[:digit:]]+$", .handler = rsp_reply_playlist }, { .regexp = "^/rsp/db/[[:digit:]]+/[^/]+$", .handler = rsp_reply_browse }, { .regexp = "^/rsp/stream/[[:digit:]]+$", .handler = rsp_stream }, { .regexp = NULL, .handler = NULL } }; void rsp_request(struct evhttp_request *req) { char *full_uri; char *uri; char *ptr; char *uri_parts[5]; struct evkeyvalq query; cfg_t *lib; char *libname; char *passwd; int handler; int i; int ret; memset(&query, 0, sizeof(struct evkeyvalq)); full_uri = httpd_fixup_uri(req); if (!full_uri) { rsp_send_error(req, "Server error"); return; } ptr = strchr(full_uri, '?'); if (ptr) *ptr = '\0'; uri = strdup(full_uri); if (!uri) { rsp_send_error(req, "Server error"); free(full_uri); return; } if (ptr) *ptr = '?'; ptr = uri; uri = evhttp_decode_uri(uri); free(ptr); DPRINTF(E_DBG, L_RSP, "RSP request: %s\n", full_uri); handler = -1; for (i = 0; rsp_handlers[i].handler; i++) { ret = regexec(&rsp_handlers[i].preg, uri, 0, NULL, 0); if (ret == 0) { handler = i; break; } } if (handler < 0) { DPRINTF(E_LOG, L_RSP, "Unrecognized RSP request\n"); rsp_send_error(req, "Bad path"); free(uri); free(full_uri); return; } /* Check authentication */ lib = cfg_getsec(cfg, "library"); passwd = cfg_getstr(lib, "password"); if (passwd) { libname = cfg_getstr(lib, "name"); DPRINTF(E_DBG, L_HTTPD, "Checking authentication for library '%s'\n", libname); /* We don't care about the username */ ret = httpd_basic_auth(req, NULL, passwd, libname); if (ret != 0) { free(uri); free(full_uri); return; } DPRINTF(E_DBG, L_HTTPD, "Library authentication successful\n"); } memset(uri_parts, 0, sizeof(uri_parts)); uri_parts[0] = strtok_r(uri, "/", &ptr); for (i = 1; (i < sizeof(uri_parts) / sizeof(uri_parts[0])) && uri_parts[i - 1]; i++) { uri_parts[i] = strtok_r(NULL, "/", &ptr); } if (!uri_parts[0] || uri_parts[i - 1] || (i < 2)) { DPRINTF(E_LOG, L_RSP, "RSP URI has too many/few components (%d)\n", (uri_parts[0]) ? i : 0); rsp_send_error(req, "Bad path"); free(uri); free(full_uri); return; } evhttp_parse_query(full_uri, &query); rsp_handlers[handler].handler(req, uri_parts, &query); evhttp_clear_headers(&query); free(uri); free(full_uri); } int rsp_is_request(struct evhttp_request *req, char *uri) { if (strncmp(uri, "/rsp/", strlen("/rsp/")) == 0) return 1; return 0; } int rsp_init(void) { char buf[64]; int i; int ret; for (i = 0; rsp_handlers[i].handler; i++) { ret = regcomp(&rsp_handlers[i].preg, rsp_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); if (ret != 0) { regerror(ret, &rsp_handlers[i].preg, buf, sizeof(buf)); DPRINTF(E_FATAL, L_RSP, "RSP init failed; regexp error: %s\n", buf); return -1; } } return 0; } void rsp_deinit(void) { int i; for (i = 0; rsp_handlers[i].handler; i++) regfree(&rsp_handlers[i].preg); } forked-daapd-25.0/src/httpd_rsp.h000066400000000000000000000004021313447753700167420ustar00rootroot00000000000000 #ifndef __HTTPD_RSP_H__ #define __HTTPD_RSP_H__ #include int rsp_init(void); void rsp_deinit(void); void rsp_request(struct evhttp_request *req); int rsp_is_request(struct evhttp_request *req, char *uri); #endif /* !__HTTPD_RSP_H__ */ forked-daapd-25.0/src/httpd_streaming.c000066400000000000000000000270711313447753700201350ustar00rootroot00000000000000/* * Copyright (C) 2015 Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "conffile.h" #include "transcode.h" #include "player.h" #include "listener.h" #include "httpd.h" #include "httpd_streaming.h" /* httpd event base, from httpd.c */ extern struct event_base *evbase_httpd; // Seconds between sending silence when player is idle // (to prevent client from hanging up) #define STREAMING_SILENCE_INTERVAL 1 // Buffer size for transmitting from player to httpd thread #define STREAMING_RAWBUF_SIZE (STOB(AIRTUNES_V2_PACKET_SAMPLES)) // Should prevent that we keep transcoding to dead connections #define STREAMING_CONNECTION_TIMEOUT 60 // Linked list of mp3 streaming requests struct streaming_session { struct evhttp_request *req; struct streaming_session *next; }; static struct streaming_session *streaming_sessions; static int streaming_initialized; // Buffers and interval for sending silence when playback is paused static uint8_t *streaming_silence_data; static size_t streaming_silence_size; static struct timeval streaming_silence_tv = { STREAMING_SILENCE_INTERVAL, 0 }; // Input buffer, output buffer and encoding ctx for transcode static uint8_t streaming_rawbuf[STREAMING_RAWBUF_SIZE]; static struct encode_ctx *streaming_encode_ctx; static struct evbuffer *streaming_encoded_data; // Used for pushing events and data from the player static struct event *streamingev; static struct player_status streaming_player_status; static int streaming_player_changed; static int streaming_pipe[2]; static void streaming_fail_cb(struct evhttp_connection *evcon, void *arg) { struct streaming_session *this; struct streaming_session *session; struct streaming_session *prev; this = (struct streaming_session *)arg; DPRINTF(E_WARN, L_STREAMING, "Connection failed; stopping mp3 streaming to client\n"); prev = NULL; for (session = streaming_sessions; session; session = session->next) { if (session->req == this->req) break; prev = session; } if (!session) { DPRINTF(E_LOG, L_STREAMING, "Bug! Got a failure callback for an unknown stream\n"); free(this); return; } if (!prev) streaming_sessions = session->next; else prev->next = session->next; free(session); if (!streaming_sessions) { DPRINTF(E_INFO, L_STREAMING, "No more clients, will stop streaming\n"); event_del(streamingev); } } static void streaming_send_cb(evutil_socket_t fd, short event, void *arg) { struct streaming_session *session; struct evbuffer *evbuf; struct decoded_frame *decoded; uint8_t *buf; int len; int ret; // Player wrote data to the pipe (EV_READ) if (event & EV_READ) { ret = read(streaming_pipe[0], &streaming_rawbuf, STREAMING_RAWBUF_SIZE); if (ret < 0) return; if (!streaming_sessions) return; decoded = transcode_raw2frame(streaming_rawbuf, STREAMING_RAWBUF_SIZE); if (!decoded) { DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); return; } ret = transcode_encode(streaming_encoded_data, decoded, streaming_encode_ctx); transcode_decoded_free(decoded); if (ret < 0) return; } // Event timed out, let's see what the player is doing and send silence if it is paused else { if (streaming_player_changed) { streaming_player_changed = 0; player_get_status(&streaming_player_status); } if (!streaming_sessions) return; if (streaming_player_status.status != PLAY_PAUSED) return; evbuffer_add(streaming_encoded_data, streaming_silence_data, streaming_silence_size); } len = evbuffer_get_length(streaming_encoded_data); // Send data evbuf = evbuffer_new(); for (session = streaming_sessions; session; session = session->next) { if (session->next) { buf = evbuffer_pullup(streaming_encoded_data, -1); evbuffer_add(evbuf, buf, len); evhttp_send_reply_chunk(session->req, evbuf); } else evhttp_send_reply_chunk(session->req, streaming_encoded_data); } evbuffer_free(evbuf); } // Thread: player (not fully thread safe, but hey...) static void player_change_cb(enum listener_event_type type) { streaming_player_changed = 1; } // Thread: player (also prone to race conditions, mostly during deinit) void streaming_write(uint8_t *buf, uint64_t rtptime) { int ret; if (!streaming_sessions) return; ret = write(streaming_pipe[1], buf, STREAMING_RAWBUF_SIZE); if (ret < 0) { if (errno == EAGAIN) DPRINTF(E_WARN, L_STREAMING, "Streaming pipe full, skipping write\n"); else DPRINTF(E_LOG, L_STREAMING, "Error writing to streaming pipe: %s\n", strerror(errno)); } } int streaming_is_request(struct evhttp_request *req, char *uri) { char *ptr; ptr = strrchr(uri, '/'); if (!ptr || (strcasecmp(ptr, "/stream.mp3") != 0)) return 0; return 1; } int streaming_request(struct evhttp_request *req) { struct streaming_session *session; struct evhttp_connection *evcon; struct evkeyvalq *output_headers; cfg_t *lib; const char *name; char *address; ev_uint16_t port; if (!streaming_initialized) { DPRINTF(E_LOG, L_STREAMING, "Got mp3 streaming request, but cannot encode to mp3\n"); evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); return -1; } evcon = evhttp_request_get_connection(req); evhttp_connection_get_peer(evcon, &address, &port); DPRINTF(E_INFO, L_STREAMING, "Beginning mp3 streaming to %s:%d\n", address, (int)port); lib = cfg_getsec(cfg, "library"); name = cfg_getstr(lib, "name"); output_headers = evhttp_request_get_output_headers(req); evhttp_add_header(output_headers, "Content-Type", "audio/mpeg"); evhttp_add_header(output_headers, "Server", "forked-daapd/" VERSION); evhttp_add_header(output_headers, "Cache-Control", "no-cache"); evhttp_add_header(output_headers, "Pragma", "no-cache"); evhttp_add_header(output_headers, "Expires", "Mon, 31 Aug 2015 06:00:00 GMT"); evhttp_add_header(output_headers, "icy-name", name); // TODO ICY metaint evhttp_send_reply_start(req, HTTP_OK, "OK"); session = malloc(sizeof(struct streaming_session)); if (!session) { DPRINTF(E_LOG, L_STREAMING, "Out of memory for streaming request\n"); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return -1; } if (!streaming_sessions) event_add(streamingev, &streaming_silence_tv); session->req = req; session->next = streaming_sessions; streaming_sessions = session; evhttp_connection_set_timeout(evcon, STREAMING_CONNECTION_TIMEOUT); evhttp_connection_set_closecb(evcon, streaming_fail_cb, session); return 0; } int streaming_init(void) { struct decode_ctx *decode_ctx; struct decoded_frame *decoded; int remaining; int ret; decode_ctx = transcode_decode_setup_raw(); if (!decode_ctx) { DPRINTF(E_LOG, L_STREAMING, "Could not create decoding context\n"); return -1; } streaming_encode_ctx = transcode_encode_setup(decode_ctx, XCODE_MP3, NULL); transcode_decode_cleanup(decode_ctx); if (!streaming_encode_ctx) { DPRINTF(E_LOG, L_STREAMING, "Will not be able to stream mp3, libav does not support mp3 encoding\n"); return -1; } // Non-blocking because otherwise httpd and player thread may deadlock #ifdef HAVE_PIPE2 ret = pipe2(streaming_pipe, O_CLOEXEC | O_NONBLOCK); #else if ( pipe(streaming_pipe) < 0 || fcntl(streaming_pipe[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 || fcntl(streaming_pipe[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ) ret = -1; else ret = 0; #endif if (ret < 0) { DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno)); goto pipe_fail; } // Listen to playback changes so we don't have to poll to check for pausing ret = listener_add(player_change_cb, LISTENER_PLAYER); if (ret < 0) { DPRINTF(E_FATAL, L_STREAMING, "Could not add listener\n"); goto listener_fail; } // Initialize buffer for encoded mp3 audio and event for pipe reading streaming_encoded_data = evbuffer_new(); streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL); if (!streaming_encoded_data || !streamingev) { DPRINTF(E_LOG, L_STREAMING, "Out of memory for encoded_data or event\n"); goto event_fail; } // Encode some silence which will be used for playback pause and put in a permanent buffer remaining = STREAMING_SILENCE_INTERVAL * STOB(44100); while (remaining > STREAMING_RAWBUF_SIZE) { decoded = transcode_raw2frame(streaming_rawbuf, STREAMING_RAWBUF_SIZE); if (!decoded) { DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); goto silence_fail; } ret = transcode_encode(streaming_encoded_data, decoded, streaming_encode_ctx); transcode_decoded_free(decoded); if (ret < 0) { DPRINTF(E_LOG, L_STREAMING, "Could not encode silence buffer\n"); goto silence_fail; } remaining -= STREAMING_RAWBUF_SIZE; } streaming_silence_size = evbuffer_get_length(streaming_encoded_data); if (streaming_silence_size == 0) { DPRINTF(E_LOG, L_STREAMING, "The encoder didn't encode any silence\n"); goto silence_fail; } streaming_silence_data = malloc(streaming_silence_size); if (!streaming_silence_data) { DPRINTF(E_LOG, L_STREAMING, "Out of memory for streaming_silence_data\n"); goto silence_fail; } ret = evbuffer_remove(streaming_encoded_data, streaming_silence_data, streaming_silence_size); if (ret != streaming_silence_size) { DPRINTF(E_LOG, L_STREAMING, "Unknown error while copying silence buffer\n"); free(streaming_silence_data); goto silence_fail; } // All done streaming_initialized = 1; return 0; silence_fail: event_free(streamingev); evbuffer_free(streaming_encoded_data); event_fail: listener_remove(player_change_cb); listener_fail: close(streaming_pipe[0]); close(streaming_pipe[1]); pipe_fail: transcode_encode_cleanup(streaming_encode_ctx); return -1; } void streaming_deinit(void) { struct streaming_session *session; struct streaming_session *next; if (!streaming_initialized) return; session = streaming_sessions; streaming_sessions = NULL; // Stops writing and sending next = NULL; while (session) { evhttp_send_reply_end(session->req); next = session->next; free(session); session = next; } event_free(streamingev); listener_remove(player_change_cb); close(streaming_pipe[0]); close(streaming_pipe[1]); transcode_encode_cleanup(streaming_encode_ctx); evbuffer_free(streaming_encoded_data); free(streaming_silence_data); } forked-daapd-25.0/src/httpd_streaming.h000066400000000000000000000011551313447753700201350ustar00rootroot00000000000000 #ifndef __HTTPD_STREAMING_H__ #define __HTTPD_STREAMING_H__ #include /* httpd_streaming takes care of incoming requests to /stream.mp3 * It will receive decoded audio from the player, and encode it, and * stream it to one or more clients. It will not be available * if a suitable ffmpeg/libav encoder is not present at runtime. */ void streaming_write(uint8_t *buf, uint64_t rtptime); int streaming_is_request(struct evhttp_request *req, char *uri); int streaming_request(struct evhttp_request *req); int streaming_init(void); void streaming_deinit(void); #endif /* !__HTTPD_STREAMING_H__ */ forked-daapd-25.0/src/input.c000066400000000000000000000305341313447753700160760ustar00rootroot00000000000000/* * Copyright (C) 2017 Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include "misc.h" #include "logger.h" #include "input.h" // Disallow further writes to the buffer when its size is larger than this threshold #define INPUT_BUFFER_THRESHOLD STOB(88200) // How long (in sec) to wait for player read before looping in playback thread #define INPUT_LOOP_TIMEOUT 1 #define DEBUG 1 //TODO disable extern struct input_definition input_file; extern struct input_definition input_http; extern struct input_definition input_pipe; #ifdef HAVE_SPOTIFY_H extern struct input_definition input_spotify; #endif // Must be in sync with enum input_types static struct input_definition *inputs[] = { &input_file, &input_http, &input_pipe, #ifdef HAVE_SPOTIFY_H &input_spotify, #endif NULL }; struct input_buffer { // Raw pcm stream data struct evbuffer *evbuf; // If non-zero, remaining length of buffer until EOF size_t eof; // If non-zero, remaining length of buffer until read error occurred size_t error; // If non-zero, remaining length of buffer until (possible) new metadata size_t metadata; // Optional callback to player if buffer is full input_cb full_cb; // Locks for sharing the buffer between input and player thread pthread_mutex_t mutex; pthread_cond_t cond; }; /* --- Globals --- */ // Input thread static pthread_t tid_input; // Input buffer static struct input_buffer input_buffer; // Timeout waiting in playback loop static struct timespec input_loop_timeout = { INPUT_LOOP_TIMEOUT, 0 }; #ifdef DEBUG static size_t debug_elapsed; #endif /* ------------------------------ MISC HELPERS ---------------------------- */ static short flags_set(size_t len) { short flags = 0; if (input_buffer.error) { if (len >= input_buffer.error) { flags |= INPUT_FLAG_ERROR; input_buffer.error = 0; } else input_buffer.error -= len; } if (input_buffer.eof) { if (len >= input_buffer.eof) { flags |= INPUT_FLAG_EOF; input_buffer.eof = 0; } else input_buffer.eof -= len; } if (input_buffer.metadata) { if (len >= input_buffer.metadata) { flags |= INPUT_FLAG_METADATA; input_buffer.metadata = 0; } else input_buffer.metadata -= len; } return flags; } static int map_data_kind(int data_kind) { switch (data_kind) { case DATA_KIND_FILE: return INPUT_TYPE_FILE; case DATA_KIND_HTTP: return INPUT_TYPE_HTTP; case DATA_KIND_PIPE: return INPUT_TYPE_PIPE; #ifdef HAVE_SPOTIFY_H case DATA_KIND_SPOTIFY: return INPUT_TYPE_SPOTIFY; #endif default: return -1; } } static int source_check_and_map(struct player_source *ps, const char *action, char check_setup) { int type; #ifdef DEBUG DPRINTF(E_DBG, L_PLAYER, "Action is %s\n", action); #endif if (!ps) { DPRINTF(E_LOG, L_PLAYER, "Stream %s called with invalid player source\n", action); return -1; } if (check_setup && !ps->setup_done) { DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, %s not possible\n", action); return -1; } type = map_data_kind(ps->data_kind); if (type < 0) { DPRINTF(E_LOG, L_PLAYER, "Unsupported input type, %s not possible\n", action); return -1; } return type; } /* ----------------------------- PLAYBACK LOOP ---------------------------- */ /* Thread: input */ // TODO Thread safety of ps? static void * playback(void *arg) { struct player_source *ps = arg; int type; int ret; type = source_check_and_map(ps, "start", 1); if ((type < 0) || (inputs[type]->disabled)) goto thread_exit; // Loops until input_loop_break is set or no more input, e.g. EOF ret = inputs[type]->start(ps); if (ret < 0) input_write(NULL, INPUT_FLAG_ERROR); #ifdef DEBUG DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d, ret %d)\n", input_loop_break, ret); #endif thread_exit: pthread_exit(NULL); } void input_wait(void) { struct timespec ts; pthread_mutex_lock(&input_buffer.mutex); ts = timespec_reltoabs(input_loop_timeout); pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); pthread_mutex_unlock(&input_buffer.mutex); } // Called by input modules from within the playback loop int input_write(struct evbuffer *evbuf, short flags) { struct timespec ts; int ret; pthread_mutex_lock(&input_buffer.mutex); while ( (!input_loop_break) && (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) && evbuf ) { if (input_buffer.full_cb) { input_buffer.full_cb(); input_buffer.full_cb = NULL; } if (flags & INPUT_FLAG_NONBLOCK) { pthread_mutex_unlock(&input_buffer.mutex); return EAGAIN; } ts = timespec_reltoabs(input_loop_timeout); pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); } if (input_loop_break) { pthread_mutex_unlock(&input_buffer.mutex); return 0; } if (evbuf) ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); else ret = 0; if (ret < 0) DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer\n"); if (!input_buffer.error && (flags & INPUT_FLAG_ERROR)) input_buffer.error = evbuffer_get_length(input_buffer.evbuf); if (!input_buffer.eof && (flags & INPUT_FLAG_EOF)) input_buffer.eof = evbuffer_get_length(input_buffer.evbuf); if (!input_buffer.metadata && (flags & INPUT_FLAG_METADATA)) input_buffer.metadata = evbuffer_get_length(input_buffer.evbuf); pthread_mutex_unlock(&input_buffer.mutex); return ret; } /* -------------------- Interface towards player thread ------------------- */ /* Thread: player */ int input_read(void *data, size_t size, short *flags) { int len; *flags = 0; if (!tid_input) { DPRINTF(E_LOG, L_PLAYER, "Bug! Read called, but playback not running\n"); return -1; } pthread_mutex_lock(&input_buffer.mutex); #ifdef DEBUG debug_elapsed += size; if (debug_elapsed > STOB(441000)) // 10 sec { DPRINTF(E_DBG, L_PLAYER, "Input buffer has %zu bytes\n", evbuffer_get_length(input_buffer.evbuf)); debug_elapsed = 0; } #endif len = evbuffer_remove(input_buffer.evbuf, data, size); if (len < 0) { DPRINTF(E_LOG, L_PLAYER, "Error reading stream data from input buffer\n"); goto out_unlock; } *flags = flags_set(len); out_unlock: pthread_cond_signal(&input_buffer.cond); pthread_mutex_unlock(&input_buffer.mutex); return len; } void input_buffer_full_cb(input_cb cb) { pthread_mutex_lock(&input_buffer.mutex); input_buffer.full_cb = cb; pthread_mutex_unlock(&input_buffer.mutex); } int input_setup(struct player_source *ps) { int type; type = source_check_and_map(ps, "setup", 0); if ((type < 0) || (inputs[type]->disabled)) return -1; if (!inputs[type]->setup) return 0; return inputs[type]->setup(ps); } int input_start(struct player_source *ps) { int ret; if (tid_input) { DPRINTF(E_WARN, L_PLAYER, "Input start called, but playback already running\n"); return 0; } input_loop_break = 0; ret = pthread_create(&tid_input, NULL, playback, ps); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not spawn input thread: %s\n", strerror(errno)); return -1; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_input, "input"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_input, "input"); #endif return 0; } int input_pause(struct player_source *ps) { short flags; int ret; #ifdef DEBUG DPRINTF(E_DBG, L_PLAYER, "Pause called, stopping playback loop\n"); #endif if (!tid_input) return -1; pthread_mutex_lock(&input_buffer.mutex); input_loop_break = 1; pthread_cond_signal(&input_buffer.cond); pthread_mutex_unlock(&input_buffer.mutex); // TODO What if input thread is hanging waiting for source? Kill thread? ret = pthread_join(tid_input, NULL); if (ret != 0) { DPRINTF(E_LOG, L_PLAYER, "Could not join input thread: %s\n", strerror(errno)); return -1; } tid_input = 0; input_flush(&flags); return 0; } int input_stop(struct player_source *ps) { int type; if (tid_input) input_pause(ps); if (!ps) return 0; type = source_check_and_map(ps, "stop", 1); if ((type < 0) || (inputs[type]->disabled)) return -1; if (!inputs[type]->stop) return 0; return inputs[type]->stop(ps); } int input_seek(struct player_source *ps, int seek_ms) { int type; type = source_check_and_map(ps, "seek", 1); if ((type < 0) || (inputs[type]->disabled)) return -1; if (!inputs[type]->seek) return 0; if (tid_input) input_pause(ps); return inputs[type]->seek(ps, seek_ms); } void input_flush(short *flags) { size_t len; pthread_mutex_lock(&input_buffer.mutex); len = evbuffer_get_length(input_buffer.evbuf); evbuffer_drain(input_buffer.evbuf, len); *flags = flags_set(len); input_buffer.error = 0; input_buffer.eof = 0; input_buffer.metadata = 0; input_buffer.full_cb = NULL; pthread_mutex_unlock(&input_buffer.mutex); #ifdef DEBUG DPRINTF(E_DBG, L_PLAYER, "Flush with flags %d\n", *flags); #endif } int input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime) { int type; if (!metadata || !ps || !ps->stream_start || !ps->output_start) { DPRINTF(E_LOG, L_PLAYER, "Bug! Unhandled case in input_metadata_get()\n"); return -1; } memset(metadata, 0, sizeof(struct input_metadata)); metadata->item_id = ps->item_id; metadata->startup = startup; metadata->offset = ps->output_start - ps->stream_start; metadata->rtptime = ps->stream_start; // Note that the source may overwrite the above progress metadata type = source_check_and_map(ps, "metadata_get", 1); if ((type < 0) || (inputs[type]->disabled)) return -1; if (!inputs[type]->metadata_get) return 0; return inputs[type]->metadata_get(metadata, ps, rtptime); } void input_metadata_free(struct input_metadata *metadata, int content_only) { free(metadata->artist); free(metadata->title); free(metadata->album); free(metadata->genre); free(metadata->artwork_url); if (!content_only) free(metadata); else memset(metadata, 0, sizeof(struct input_metadata)); } int input_init(void) { int no_input; int ret; int i; // Prepare input buffer pthread_mutex_init(&input_buffer.mutex, NULL); pthread_cond_init(&input_buffer.cond, NULL); input_buffer.evbuf = evbuffer_new(); if (!input_buffer.evbuf) { DPRINTF(E_LOG, L_PLAYER, "Out of memory for input buffer\n"); return -1; } no_input = 1; for (i = 0; inputs[i]; i++) { if (inputs[i]->type != i) { DPRINTF(E_FATAL, L_PLAYER, "BUG! Input definitions are misaligned with input enum\n"); return -1; } if (!inputs[i]->init) { no_input = 0; continue; } ret = inputs[i]->init(); if (ret < 0) inputs[i]->disabled = 1; else no_input = 0; } if (no_input) return -1; return 0; } void input_deinit(void) { int i; input_stop(NULL); for (i = 0; inputs[i]; i++) { if (inputs[i]->disabled) continue; if (inputs[i]->deinit) inputs[i]->deinit(); } pthread_cond_destroy(&input_buffer.cond); pthread_mutex_destroy(&input_buffer.mutex); evbuffer_free(input_buffer.evbuf); } forked-daapd-25.0/src/input.h000066400000000000000000000136271313447753700161070ustar00rootroot00000000000000 #ifndef __INPUT_H__ #define __INPUT_H__ #ifdef HAVE_CONFIG_H # include #endif #include #include "transcode.h" // Must be in sync with inputs[] in input.c enum input_types { INPUT_TYPE_FILE, INPUT_TYPE_HTTP, INPUT_TYPE_PIPE, #ifdef HAVE_SPOTIFY_H INPUT_TYPE_SPOTIFY, #endif }; enum input_flags { // Write to input buffer must not block INPUT_FLAG_NONBLOCK = (1 << 0), // Flags end of file INPUT_FLAG_EOF = (1 << 1), // Flags error reading file INPUT_FLAG_ERROR = (1 << 2), // Flags possible new stream metadata INPUT_FLAG_METADATA = (1 << 3), }; struct player_source { /* Id of the file/item in the files database */ uint32_t id; /* Item-Id of the file/item in the queue */ uint32_t item_id; /* Length of the file/item in milliseconds */ uint32_t len_ms; enum data_kind data_kind; enum media_kind media_kind; char *path; /* Start time of the media item as rtp-time The stream-start is the rtp-time the media item did or would have started playing (after seek or pause), therefor the elapsed time of the media item is always: elapsed time = current rtptime - stream-start */ uint64_t stream_start; /* Output start time of the media item as rtp-time The output start time is the rtp-time of the first audio packet send to the audio outputs. It differs from stream-start especially after a seek, where the first audio packet has the next rtp-time as output start and stream start becomes the rtp-time the media item would have been started playing if the seek did not happen. */ uint64_t output_start; /* End time of media item as rtp-time The end time is set if the reading (source_read) of the media item reached end of file, until then it is 0. */ uint64_t end; /* Opaque pointer to data that the input sets up when called with setup(), and * which is cleaned up by the input with stop() */ void *input_ctx; /* Input has completed setup of the source */ int setup_done; struct player_source *play_next; }; typedef int (*input_cb)(void); struct input_metadata { uint32_t item_id; int startup; uint64_t rtptime; uint64_t offset; // The player will update queue_item with the below uint32_t song_length; char *artist; char *title; char *album; char *genre; char *artwork_url; }; struct input_definition { // Name of the input const char *name; // Type of input enum input_types type; // Set to 1 if the input initialization failed char disabled; // Prepare a playback session int (*setup)(struct player_source *ps); // Starts playback loop (must be defined) int (*start)(struct player_source *ps); // Cleans up when playback loop has ended int (*stop)(struct player_source *ps); // Changes the playback position int (*seek)(struct player_source *ps, int seek_ms); // Return metadata int (*metadata_get)(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime); // Initialization function called during startup int (*init)(void); // Deinitialization function called at shutdown void (*deinit)(void); }; /* * Input modules should use this to test if playback should end */ int input_loop_break; /* * Transfer stream data to the player's input buffer. The input evbuf will be * drained on succesful write. This is to avoid copying memory. If the player's * input buffer is full the function will block until the write can be made * (unless INPUT_FILE_NONBLOCK is set). * * @in evbuf Raw audio data to write * @in flags One or more INPUT_FLAG_* * @return 0 on success, EAGAIN if buffer was full (and _NONBLOCK is set), * -1 on error */ int input_write(struct evbuffer *evbuf, short flags); /* * Input modules can use this to wait in the playback loop (like input_write() * would have done) */ void input_wait(void); /* * Move a chunk of stream data from the player's input buffer to an output * buffer. Should only be called by the player thread. Will not block. * * @in data Output buffer * @in size How much data to move to the output buffer * @out flags Flags INPUT_FLAG_* * @return Number of bytes moved, -1 on error */ int input_read(void *data, size_t size, short *flags); /* * Player can set this to get a callback from the input when the input buffer * is full. The player may use this to resume playback after an underrun. * * @in cb The callback */ void input_buffer_full_cb(input_cb cb); /* * Initializes the given player source for playback */ int input_setup(struct player_source *ps); /* * Tells the input to start or resume playback, i.e. after calling this function * the input buffer will begin to fill up, and should be read periodically with * input_read(). Before calling this input_setup() must have been called. */ int input_start(struct player_source *ps); /* * Pauses playback of the given player source (stops playback loop) and flushes * the input buffer */ int input_pause(struct player_source *ps); /* * Stops playback loop (if running), flushes input buffer and cleans up the * player source */ int input_stop(struct player_source *ps); /* * Seeks playback position to seek_ms. Returns actual seek position, 0 on * unseekable, -1 on error. May block. */ int input_seek(struct player_source *ps, int seek_ms); /* * Flush input buffer. Output flags will be the same as input_read(). */ void input_flush(short *flags); /* * Gets metadata from the input, returns 0 if metadata is set, otherwise -1 */ int input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime); /* * Free the entire struct */ void input_metadata_free(struct input_metadata *metadata, int content_only); /* * Called by player_init (so will run in main thread) */ int input_init(void); /* * Called by player_deinit (so will run in main thread) */ void input_deinit(void); #endif /* !__INPUT_H__ */ forked-daapd-25.0/src/inputs/000077500000000000000000000000001313447753700161105ustar00rootroot00000000000000forked-daapd-25.0/src/inputs/file_http.c000066400000000000000000000064221313447753700202360ustar00rootroot00000000000000/* * Copyright (C) 2017 Espen Jurgensen * * 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 */ #include #include #include #include #include #include "transcode.h" #include "http.h" #include "misc.h" #include "input.h" static int setup(struct player_source *ps) { ps->input_ctx = transcode_setup(ps->data_kind, ps->path, ps->len_ms, XCODE_PCM16_NOHEADER, NULL); if (!ps->input_ctx) return -1; ps->setup_done = 1; return 0; } static int setup_http(struct player_source *ps) { char *url; if (http_stream_setup(&url, ps->path) < 0) return -1; free(ps->path); ps->path = url; return setup(ps); } static int start(struct player_source *ps) { struct evbuffer *evbuf; short flags; int ret; int icy_timer; evbuf = evbuffer_new(); ret = -1; flags = 0; while (!input_loop_break && !(flags & INPUT_FLAG_EOF)) { // We set "wanted" to 1 because the read size doesn't matter to us // TODO optimize? ret = transcode(evbuf, 1, ps->input_ctx, &icy_timer); if (ret < 0) break; flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) | (icy_timer ? INPUT_FLAG_METADATA : 0); ret = input_write(evbuf, flags); if (ret < 0) break; } evbuffer_free(evbuf); return ret; } static int stop(struct player_source *ps) { transcode_cleanup(ps->input_ctx); ps->input_ctx = NULL; ps->setup_done = 0; return 0; } static int seek(struct player_source *ps, int seek_ms) { return transcode_seek(ps->input_ctx, seek_ms); } static int metadata_get_http(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime) { struct http_icy_metadata *m; int changed; m = transcode_metadata(ps->input_ctx, &changed); if (!m) return -1; if (!changed) { http_icy_metadata_free(m, 0); return -1; // TODO Perhaps a problem since this prohibits the player updating metadata } if (m->artist) swap_pointers(&metadata->artist, &m->artist); // Note we map title to album, because clients should show stream name as titel if (m->title) swap_pointers(&metadata->album, &m->title); if (m->artwork_url) swap_pointers(&metadata->artwork_url, &m->artwork_url); http_icy_metadata_free(m, 0); return 0; } struct input_definition input_file = { .name = "file", .type = INPUT_TYPE_FILE, .disabled = 0, .setup = setup, .start = start, .stop = stop, .seek = seek, }; struct input_definition input_http = { .name = "http", .type = INPUT_TYPE_HTTP, .disabled = 0, .setup = setup_http, .start = start, .stop = stop, .metadata_get = metadata_get_http, }; forked-daapd-25.0/src/inputs/pipe.c000066400000000000000000000531641313447753700172220ustar00rootroot00000000000000/* * Copyright (C) 2017 Espen Jurgensen * * 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 * * * About pipe.c * -------------- * This module will read a PCM16 stream from a named pipe and write it to the * input buffer. The user may start/stop playback from a pipe by selecting it * through a client. If the user has configured pipe_autostart, then pipes in * the library will also be watched for data, and playback will start/stop * automatically. * * The module will also look for pipes with a .metadata suffix, and if found, * the metadata will be parsed and fed to the player. The metadata must be in * the format Shairport uses for this purpose. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "input.h" #include "misc.h" #include "logger.h" #include "db.h" #include "conffile.h" #include "listener.h" #include "player.h" #include "worker.h" #include "commands.h" #include "mxml-compat.h" // Maximum number of pipes to watch for data #define PIPE_MAX_WATCH 4 // Max number of bytes to read from a pipe at a time #define PIPE_READ_MAX 65536 // Max number of bytes to buffer from metadata pipes #define PIPE_METADATA_BUFLEN_MAX 262144 enum pipetype { PIPE_PCM, PIPE_METADATA, }; struct pipe { int id; // The mfi id of the pipe int fd; // File descriptor bool is_autostarted; // We autostarted the pipe (and we will autostop) char *path; // Path enum pipetype type; // PCM (audio) or metadata event_callback_fn cb; // Callback when there is data to read struct event *ev; // Event for the callback struct pipe *next; }; union pipe_arg { uint32_t id; struct pipe *pipelist; }; // The usual thread stuff static pthread_t tid_pipe; static struct event_base *evbase_pipe; static struct commands_base *cmdbase; // From config - should we watch library pipes for data or only start on request static int pipe_autostart; // The mfi id of the pipe autostarted by the pipe thread static int pipe_autostart_id; // Global list of pipes we are watching (if watching/autostart is enabled) static struct pipe *pipe_watch_list; // Single pipe that we start watching for metadata after playback starts static struct pipe *pipe_metadata; // We read metadata into this evbuffer static struct evbuffer *pipe_metadata_buf; // Parsed metadata goes here static struct input_metadata pipe_metadata_parsed; // Mutex to share the parsed metadata static pthread_mutex_t pipe_metadata_lock; // True if there is new metadata to push to the player static bool pipe_metadata_is_new; /* -------------------------------- HELPERS ------------------------------- */ static struct pipe * pipe_create(const char *path, int id, enum pipetype type, event_callback_fn cb) { struct pipe *pipe; pipe = calloc(1, sizeof(struct pipe)); pipe->path = strdup(path); pipe->id = id; pipe->fd = -1; pipe->type = type; pipe->cb = cb; return pipe; } static void pipe_free(struct pipe *pipe) { free(pipe->path); free(pipe); } static int pipe_open(const char *path, bool silent) { struct stat sb; int fd; DPRINTF(E_DBG, L_PLAYER, "(Re)opening pipe: '%s'\n", path); if (lstat(path, &sb) < 0) { if (!silent) DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", path, strerror(errno)); return -1; } if (!S_ISFIFO(sb.st_mode)) { DPRINTF(E_LOG, L_PLAYER, "Source type is pipe, but path is not a fifo: %s\n", path); return -1; } fd = open(path, O_RDONLY | O_NONBLOCK); if (fd < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not open pipe for reading '%s': %s\n", path, strerror(errno)); return -1; } return fd; } static void pipe_close(int fd) { if (fd >= 0) close(fd); } static int watch_add(struct pipe *pipe) { bool silent; silent = (pipe->type == PIPE_METADATA); pipe->fd = pipe_open(pipe->path, silent); if (pipe->fd < 0) return -1; pipe->ev = event_new(evbase_pipe, pipe->fd, EV_READ, pipe->cb, pipe); if (!pipe->ev) { DPRINTF(E_LOG, L_PLAYER, "Could not watch pipe for new data '%s'\n", pipe->path); pipe_close(pipe->fd); return -1; } event_add(pipe->ev, NULL); return 0; } static void watch_del(struct pipe *pipe) { if (pipe->ev) event_free(pipe->ev); pipe_close(pipe->fd); pipe->fd = -1; } // If a read on pipe returns 0 it is an EOF, and we must close it and reopen it // for renewed watching. The event will be freed and reallocated by this. static int watch_reset(struct pipe *pipe) { watch_del(pipe); return watch_add(pipe); } static void pipelist_add(struct pipe **list, struct pipe *pipe) { pipe->next = *list; *list = pipe; } static void pipelist_remove(struct pipe **list, struct pipe *pipe) { struct pipe *prev = NULL; struct pipe *p; for (p = *list; p; p = p->next) { if (p->id == pipe->id) break; prev = p; } if (!p) return; if (!prev) *list = pipe->next; else prev->next = pipe->next; pipe_free(pipe); } static struct pipe * pipelist_find(struct pipe *list, int id) { struct pipe *p; for (p = list; p; p = p->next) { if (id == p->id) return p; } return NULL; } // Convert to macro? static inline uint32_t dmapval(const char s[4]) { return ((s[0] << 24) | (s[1] << 16) | (s[2] << 8) | (s[3] << 0)); } static void parse_progress(struct input_metadata *m, char *progress) { char *s; char *ptr; uint64_t start; uint64_t pos; uint64_t end; if (!(s = strtok_r(progress, "/", &ptr))) return; safe_atou64(s, &start); if (!(s = strtok_r(NULL, "/", &ptr))) return; safe_atou64(s, &pos); if (!(s = strtok_r(NULL, "/", &ptr))) return; safe_atou64(s, &end); if (!start || !pos || !end) return; m->rtptime = start; // Not actually used - we have our own rtptime m->offset = (pos > start) ? (pos - start) : 0; m->song_length = (end - start) * 10 / 441; // Convert to ms based on 44100 } static void parse_volume(const char *volume) { char *volume_next; float airplay_volume; int local_volume; errno = 0; airplay_volume = strtof(volume, &volume_next); if ((errno == ERANGE) || (volume == volume_next)) { DPRINTF(E_LOG, L_PLAYER, "Invalid Shairport airplay volume in string (%s): %s\n", volume, (errno == ERANGE ? strerror(errno) : "First token is not a number.")); return; } if (strcmp(volume_next, ",0.00,0.00,0.00") != 0) { DPRINTF(E_DBG, L_PLAYER, "Not applying Shairport airplay volume while software volume control is enabled (%s)\n", volume); return; } if (((int) airplay_volume) == -144) { DPRINTF(E_DBG, L_PLAYER, "Applying Shairport airplay volume ('mute', value: %.2f)\n", airplay_volume); player_volume_set(0); } else if (airplay_volume >= -30.0 && airplay_volume <= 0.0) { local_volume = (int)(100.0 + (airplay_volume / 30.0 * 100.0)); DPRINTF(E_DBG, L_PLAYER, "Applying Shairport airplay volume (percent: %d, value: %.2f)\n", local_volume, airplay_volume); player_volume_set(local_volume); } else DPRINTF(E_LOG, L_PLAYER, "Shairport airplay volume out of range (-144.0, [-30.0 - 0.0]): %.2f\n", airplay_volume); } // returns 1 on metadata found, 0 on nothing, -1 on error static int parse_item(struct input_metadata *m, const char *item) { mxml_node_t *xml; mxml_node_t *haystack; mxml_node_t *needle; const char *s; uint32_t type; uint32_t code; char *progress; char *volume; char **data; int ret; ret = 0; xml = mxmlNewXML("1.0"); if (!xml) return -1; // DPRINTF(E_DBG, L_PLAYER, "Parsing %s\n", item); haystack = mxmlLoadString(xml, item, MXML_NO_CALLBACK); if (!haystack) { DPRINTF(E_LOG, L_PLAYER, "Could not parse pipe metadata\n"); goto out_error; } type = 0; if ( (needle = mxmlFindElement(haystack, haystack, "type", NULL, NULL, MXML_DESCEND)) && (s = mxmlGetText(needle, NULL)) ) sscanf(s, "%8x", &type); code = 0; if ( (needle = mxmlFindElement(haystack, haystack, "code", NULL, NULL, MXML_DESCEND)) && (s = mxmlGetText(needle, NULL)) ) sscanf(s, "%8x", &code); if (!type || !code) { DPRINTF(E_LOG, L_PLAYER, "No type (%d) or code (%d) in pipe metadata, aborting\n", type, code); goto out_error; } if (code == dmapval("asal")) data = &m->album; else if (code == dmapval("asar")) data = &m->artist; else if (code == dmapval("minm")) data = &m->title; else if (code == dmapval("asgn")) data = &m->genre; else if (code == dmapval("prgr")) data = &progress; else if (code == dmapval("pvol")) data = &volume; else goto out_nothing; if ( (needle = mxmlFindElement(haystack, haystack, "data", NULL, NULL, MXML_DESCEND)) && (s = mxmlGetText(needle, NULL)) ) { pthread_mutex_lock(&pipe_metadata_lock); if (data != &progress && data != &volume) free(*data); *data = b64_decode(s); DPRINTF(E_DBG, L_PLAYER, "Read Shairport metadata (type=%8x, code=%8x): '%s'\n", type, code, *data); if (data == &progress) { parse_progress(m, progress); free(*data); } else if (data == &volume) { parse_volume(volume); free(*data); } pthread_mutex_unlock(&pipe_metadata_lock); ret = 1; } out_nothing: mxmlDelete(xml); return ret; out_error: mxmlDelete(xml); return -1; } static char * extract_item(struct evbuffer *evbuf) { struct evbuffer_ptr evptr; size_t size; char *item; evptr = evbuffer_search(evbuf, "", strlen(""), NULL); if (evptr.pos < 0) return NULL; size = evptr.pos + strlen("") + 1; item = malloc(size); if (!item) return NULL; evbuffer_remove(evbuf, item, size - 1); item[size - 1] = '\0'; return item; } static int pipe_metadata_parse(struct input_metadata *m, struct evbuffer *evbuf) { char *item; int found; int ret; found = 0; while ((item = extract_item(evbuf))) { ret = parse_item(m, item); free(item); if (ret < 0) return -1; if (ret > 0) found = 1; } return found; } /* ----------------------------- PIPE WATCHING ---------------------------- */ /* Thread: pipe */ // Some data arrived on a pipe we watch - let's autostart playback static void pipe_read_cb(evutil_socket_t fd, short event, void *arg) { struct pipe *pipe = arg; struct player_status status; int ret; ret = player_get_status(&status); if (status.id == pipe->id) { DPRINTF(E_DBG, L_PLAYER, "Pipe '%s' already playing\n", pipe->path); return; // We are already playing the pipe } else if ((ret < 0) || (status.status == PLAY_PLAYING)) { DPRINTF(E_LOG, L_PLAYER, "Data arrived on pipe '%s' - ignoring, player is busy\n", pipe->path); // FIXME What to do in this situation? Can't re-add the event, since it // will trigger right away, but also not good to stop watching the pipe // like we do right now. return; } DPRINTF(E_INFO, L_PLAYER, "Autostarting pipe '%s' (fd %d)\n", pipe->path, fd); player_playback_stop(); ret = player_playback_start_byid(pipe->id); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Autostarting pipe '%s' (fd %d) failed\n", pipe->path, fd); return; } pipe_autostart_id = pipe->id; } static enum command_state pipe_watch_reset(void *arg, int *retval) { union pipe_arg *cmdarg = arg; struct pipe *pipe; pipe_autostart_id = 0; pipe = pipelist_find(pipe_watch_list, cmdarg->id); *retval = watch_reset(pipe); return COMMAND_END; } static enum command_state pipe_watch_update(void *arg, int *retval) { union pipe_arg *cmdarg = arg; struct pipe *pipelist; struct pipe *pipe; struct pipe *next; int count; if (cmdarg) pipelist = cmdarg->pipelist; else pipelist = NULL; // Removes pipes that are gone from the watchlist for (pipe = pipe_watch_list; pipe; pipe = next) { next = pipe->next; if (!pipelist_find(pipelist, pipe->id)) { DPRINTF(E_DBG, L_PLAYER, "Pipe watch deleted: '%s'\n", pipe->path); watch_del(pipe); pipelist_remove(&pipe_watch_list, pipe); // Will free pipe } } // Looks for new pipes and adds them to the watchlist for (pipe = pipelist, count = 0; pipe; pipe = next, count++) { next = pipe->next; if (count > PIPE_MAX_WATCH) { DPRINTF(E_LOG, L_PLAYER, "Max open pipes reached (%d), will not watch '%s'\n", PIPE_MAX_WATCH, pipe->path); pipe_free(pipe); continue; } if (!pipelist_find(pipe_watch_list, pipe->id)) { DPRINTF(E_DBG, L_PLAYER, "Pipe watch added: '%s'\n", pipe->path); watch_add(pipe); pipelist_add(&pipe_watch_list, pipe); // Changes pipe->next } else { DPRINTF(E_DBG, L_PLAYER, "Pipe watch exists: '%s'\n", pipe->path); pipe_free(pipe); } } *retval = 0; return COMMAND_END; } static void * pipe_thread_run(void *arg) { event_base_dispatch(evbase_pipe); pthread_exit(NULL); } /* -------------------------- METADATA PIPE HANDLING ---------------------- */ /* Thread: worker */ static void pipe_metadata_watch_del(void *arg) { if (!pipe_metadata) return; evbuffer_free(pipe_metadata_buf); watch_del(pipe_metadata); pipe_free(pipe_metadata); pipe_metadata = NULL; } // Some metadata arrived on a pipe we watch static void pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg) { int ret; ret = evbuffer_read(pipe_metadata_buf, pipe_metadata->fd, PIPE_READ_MAX); if (ret < 0) { if (errno != EAGAIN) pipe_metadata_watch_del(NULL); return; } else if (ret == 0) { // Reset the pipe ret = watch_reset(pipe_metadata); if (ret < 0) return; goto readd; } if (evbuffer_get_length(pipe_metadata_buf) > PIPE_METADATA_BUFLEN_MAX) { DPRINTF(E_LOG, L_PLAYER, "Can't process data from metadata pipe, reading will stop\n"); pipe_metadata_watch_del(NULL); return; } ret = pipe_metadata_parse(&pipe_metadata_parsed, pipe_metadata_buf); if (ret < 0) { pipe_metadata_watch_del(NULL); return; } else if (ret > 0) { // Trigger notification in playback loop pipe_metadata_is_new = 1; } readd: if (pipe_metadata && pipe_metadata->ev) event_add(pipe_metadata->ev, NULL); } static void pipe_metadata_watch_add(void *arg) { char *base_path = arg; char path[PATH_MAX]; int ret; ret = snprintf(path, sizeof(path), "%s.metadata", base_path); if ((ret < 0) || (ret > sizeof(path))) return; pipe_metadata = pipe_create(path, 0, PIPE_METADATA, pipe_metadata_read_cb); if (!pipe_metadata) return; pipe_metadata_buf = evbuffer_new(); ret = watch_add(pipe_metadata); if (ret < 0) { evbuffer_free(pipe_metadata_buf); pipe_free(pipe_metadata); pipe_metadata = NULL; return; } } /* ---------------------- PIPE WATCH THREAD START/STOP -------------------- */ /* Thread: filescanner */ static void pipe_thread_start(void) { CHECK_NULL(L_PLAYER, evbase_pipe = event_base_new()); CHECK_NULL(L_PLAYER, cmdbase = commands_base_new(evbase_pipe, NULL)); CHECK_ERR(L_PLAYER, pthread_create(&tid_pipe, NULL, pipe_thread_run, NULL)); #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_pipe, "pipe"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_pipe, "pipe"); #endif } static void pipe_thread_stop(void) { if (!tid_pipe) return; commands_exec_sync(cmdbase, pipe_watch_update, NULL, NULL); commands_base_destroy(cmdbase); pthread_join(tid_pipe, NULL); event_base_free(evbase_pipe); tid_pipe = 0; } // Makes a pipelist with pipe items from the db, returns NULL on no pipes static struct pipe * pipelist_create(void) { struct query_params qp; struct db_media_file_info dbmfi; struct pipe *head; struct pipe *pipe; char filter[32]; int id; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.filter = filter; snprintf(filter, sizeof(filter), "f.data_kind = %d", DATA_KIND_PIPE); ret = db_query_start(&qp); if (ret < 0) return NULL; head = NULL; while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { ret = safe_atoi32(dbmfi.id, &id); if (ret < 0) continue; pipe = pipe_create(dbmfi.path, id, PIPE_PCM, pipe_read_cb); pipelist_add(&head, pipe); } db_query_end(&qp); return head; } // Queries the db to see if any pipes are present in the library. If so, starts // the pipe thread to watch the pipes. If no pipes in library, it will shut down // the pipe thread. static void pipe_listener_cb(enum listener_event_type type) { union pipe_arg *cmdarg; cmdarg = malloc(sizeof(union pipe_arg)); if (!cmdarg) return; cmdarg->pipelist = pipelist_create(); if (!cmdarg->pipelist) { pipe_thread_stop(); free(cmdarg); return; } if (!tid_pipe) pipe_thread_start(); commands_exec_async(cmdbase, pipe_watch_update, cmdarg); } /* -------------------------- PIPE INPUT INTERFACE ------------------------ */ /* Thread: player/input */ static int setup(struct player_source *ps) { struct pipe *pipe; int fd; fd = pipe_open(ps->path, 0); if (fd < 0) return -1; CHECK_NULL(L_PLAYER, pipe = pipe_create(ps->path, ps->id, PIPE_PCM, NULL)); pipe->fd = fd; pipe->is_autostarted = (ps->id == pipe_autostart_id); worker_execute(pipe_metadata_watch_add, ps->path, strlen(ps->path) + 1, 0); ps->input_ctx = pipe; ps->setup_done = 1; return 0; } static int start(struct player_source *ps) { struct pipe *pipe = ps->input_ctx; struct evbuffer *evbuf; short flags; int ret; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_PLAYER, "Out of memory for pipe evbuf\n"); return -1; } ret = -1; while (!input_loop_break) { ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX); if ((ret == 0) && (pipe->is_autostarted)) { input_write(evbuf, INPUT_FLAG_EOF); // Autostop break; } else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN))) { input_wait(); continue; } else if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe '%s': %s\n", ps->path, strerror(errno)); break; } flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0); pipe_metadata_is_new = 0; ret = input_write(evbuf, flags); if (ret < 0) break; } evbuffer_free(evbuf); return ret; } static int stop(struct player_source *ps) { struct pipe *pipe = ps->input_ctx; union pipe_arg *cmdarg; DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n"); pipe_close(pipe->fd); // Reset the pipe and start watching it again for new data. Must be async or // we will deadlock from the stop in pipe_read_cb(). if (pipe_autostart && (cmdarg = malloc(sizeof(union pipe_arg)))) { cmdarg->id = pipe->id; commands_exec_async(cmdbase, pipe_watch_reset, cmdarg); } if (pipe_metadata) worker_execute(pipe_metadata_watch_del, NULL, 0, 0); pipe_free(pipe); ps->input_ctx = NULL; ps->setup_done = 0; return 0; } static int metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime) { pthread_mutex_lock(&pipe_metadata_lock); if (pipe_metadata_parsed.artist) swap_pointers(&metadata->artist, &pipe_metadata_parsed.artist); if (pipe_metadata_parsed.title) swap_pointers(&metadata->title, &pipe_metadata_parsed.title); if (pipe_metadata_parsed.album) swap_pointers(&metadata->album, &pipe_metadata_parsed.album); if (pipe_metadata_parsed.genre) swap_pointers(&metadata->genre, &pipe_metadata_parsed.genre); if (pipe_metadata_parsed.artwork_url) swap_pointers(&metadata->artwork_url, &pipe_metadata_parsed.artwork_url); if (pipe_metadata_parsed.song_length) { if (rtptime > ps->stream_start) metadata->rtptime = rtptime - pipe_metadata_parsed.offset; metadata->offset = pipe_metadata_parsed.offset; metadata->song_length = pipe_metadata_parsed.song_length; } input_metadata_free(&pipe_metadata_parsed, 1); pthread_mutex_unlock(&pipe_metadata_lock); return 0; } // Thread: main static int init(void) { CHECK_ERR(L_PLAYER, mutex_init(&pipe_metadata_lock)); pipe_autostart = cfg_getbool(cfg_getsec(cfg, "library"), "pipe_autostart"); if (pipe_autostart) CHECK_ERR(L_PLAYER, listener_add(pipe_listener_cb, LISTENER_DATABASE)); return 0; } static void deinit(void) { if (pipe_autostart) { listener_remove(pipe_listener_cb); pipe_thread_stop(); } CHECK_ERR(L_PLAYER, pthread_mutex_destroy(&pipe_metadata_lock)); } struct input_definition input_pipe = { .name = "pipe", .type = INPUT_TYPE_PIPE, .disabled = 0, .setup = setup, .start = start, .stop = stop, .metadata_get = metadata_get, .init = init, .deinit = deinit, }; forked-daapd-25.0/src/inputs/spotify.c000066400000000000000000000033661313447753700177610ustar00rootroot00000000000000/* * Copyright (C) 2017 Espen Jurgensen * * 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 */ #include #include #include #include #include "input.h" #include "spotify.h" static int setup(struct player_source *ps) { int ret; ret = spotify_playback_setup(ps->path); if (ret < 0) return -1; ps->setup_done = 1; return 0; } static int start(struct player_source *ps) { int ret; ret = spotify_playback_play(); if (ret < 0) return -1; while (!input_loop_break) { input_wait(); } ret = spotify_playback_pause(); return ret; } static int stop(struct player_source *ps) { int ret; ret = spotify_playback_stop(); if (ret < 0) return -1; ps->setup_done = 0; return 0; } static int seek(struct player_source *ps, int seek_ms) { int ret; ret = spotify_playback_seek(seek_ms); if (ret < 0) return -1; return ret; } struct input_definition input_spotify = { .name = "Spotify", .type = INPUT_TYPE_SPOTIFY, .disabled = 0, .setup = setup, .start = start, .stop = stop, .seek = seek, }; forked-daapd-25.0/src/lastfm.c000066400000000000000000000217471313447753700162330ustar00rootroot00000000000000/* * Copyright (C) 2014 Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "mxml-compat.h" #include "db.h" #include "lastfm.h" #include "logger.h" #include "misc.h" #include "http.h" // LastFM becomes disabled if we get a scrobble, try initialising session, // but can't (probably no session key in db because user does not use LastFM) static int lastfm_disabled = 0; /** * The API key and secret (not so secret being open source) is specific to * forked-daapd, and is used to identify forked-daapd and to sign requests */ static const char *lastfm_api_key = "579593f2ed3f49673c7364fd1c9c829b"; static const char *lastfm_secret = "ce45a1d275c10b3edf0ecfa27791cb2b"; static const char *api_url = "http://ws.audioscrobbler.com/2.0/"; static const char *auth_url = "https://ws.audioscrobbler.com/2.0/"; // Session key static char *lastfm_session_key = NULL; /* --------------------------------- HELPERS ------------------------------- */ /* Creates an md5 signature of the concatenated parameters and adds it to keyval */ static int param_sign(struct keyval *kv) { struct onekeyval *okv; char hash[33]; char ebuf[64]; uint8_t *hash_bytes; size_t hash_len; gcry_md_hd_t md_hdl; gpg_error_t gc_err; int ret; int i; gc_err = gcry_md_open(&md_hdl, GCRY_MD_MD5, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_LASTFM, "Could not open MD5: %s\n", ebuf); return -1; } for (okv = kv->head; okv; okv = okv->next) { gcry_md_write(md_hdl, okv->name, strlen(okv->name)); gcry_md_write(md_hdl, okv->value, strlen(okv->value)); } gcry_md_write(md_hdl, lastfm_secret, strlen(lastfm_secret)); hash_bytes = gcry_md_read(md_hdl, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_LASTFM, "Could not read MD5 hash\n"); return -1; } hash_len = gcry_md_get_algo_dlen(GCRY_MD_MD5); for (i = 0; i < hash_len; i++) sprintf(hash + (2 * i), "%02x", hash_bytes[i]); ret = keyval_add(kv, "api_sig", hash); gcry_md_close(md_hdl); return ret; } /* --------------------------------- MAIN --------------------------------- */ static void response_proces(struct http_client_ctx *ctx) { mxml_node_t *tree; mxml_node_t *s_node; mxml_node_t *e_node; char *body; char *errmsg; char *sk; // NULL-terminate the buffer evbuffer_add(ctx->input_body, "", 1); body = (char *)evbuffer_pullup(ctx->input_body, -1); if (!body || (strlen(body) == 0)) { DPRINTF(E_LOG, L_LASTFM, "Empty response\n"); return; } DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body); tree = mxmlLoadString(NULL, body, MXML_OPAQUE_CALLBACK); if (!tree) return; // Look for errors e_node = mxmlFindElement(tree, tree, "error", NULL, NULL, MXML_DESCEND); if (e_node) { errmsg = trimwhitespace(mxmlGetOpaque(e_node)); DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", errmsg); if (errmsg) free(errmsg); mxmlDelete(tree); return; } // Was it a scrobble request? Then do nothing. TODO: Check for error messages s_node = mxmlFindElement(tree, tree, "scrobbles", NULL, NULL, MXML_DESCEND); if (s_node) { DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n"); mxmlDelete(tree); return; } // Otherwise an auth request, so get the session key s_node = mxmlFindElement(tree, tree, "key", NULL, NULL, MXML_DESCEND); if (!s_node) { DPRINTF(E_LOG, L_LASTFM, "Session key not found\n"); mxmlDelete(tree); return; } sk = trimwhitespace(mxmlGetOpaque(s_node)); if (sk) { DPRINTF(E_LOG, L_LASTFM, "Got session key from LastFM: %s\n", sk); db_admin_set("lastfm_sk", sk); if (lastfm_session_key) free(lastfm_session_key); lastfm_session_key = sk; } mxmlDelete(tree); } static int request_post(char *method, struct keyval *kv, int auth) { struct http_client_ctx ctx; int ret; ret = keyval_add(kv, "method", method); if (ret < 0) return -1; if (!auth) ret = keyval_add(kv, "sk", lastfm_session_key); if (ret < 0) return -1; // API requires that we MD5 sign sorted param (without "format" param) keyval_sort(kv); ret = param_sign(kv); if (ret < 0) { DPRINTF(E_LOG, L_LASTFM, "Aborting request, param_sign failed\n"); return -1; } memset(&ctx, 0, sizeof(struct http_client_ctx)); ctx.output_body = http_form_urlencode(kv); if (ret < 0) { DPRINTF(E_LOG, L_LASTFM, "Aborting request, http_form_urlencode failed\n"); return -1; } ctx.url = auth ? auth_url : api_url; ctx.input_body = evbuffer_new(); ret = http_client_request(&ctx); if (ret < 0) goto out_free_ctx; response_proces(&ctx); out_free_ctx: free(ctx.output_body); evbuffer_free(ctx.input_body); return ret; } static int scrobble(int id) { struct media_file_info *mfi; struct keyval *kv; char duration[4]; char trackNumber[4]; char timestamp[16]; int ret; mfi = db_file_fetch_byid(id); if (!mfi) { DPRINTF(E_LOG, L_LASTFM, "Scrobble failed, track id %d is unknown\n", id); return -1; } // Don't scrobble songs which are shorter than 30 sec if (mfi->song_length < 30000) goto noscrobble; // Don't scrobble non-music and radio stations if ((mfi->media_kind != MEDIA_KIND_MUSIC) || (mfi->data_kind == DATA_KIND_HTTP)) goto noscrobble; // Don't scrobble songs with unknown artist if (strcmp(mfi->artist, "Unknown artist") == 0) goto noscrobble; kv = keyval_alloc(); if (!kv) goto noscrobble; snprintf(duration, sizeof(duration), "%" PRIu32, mfi->song_length); snprintf(trackNumber, sizeof(trackNumber), "%" PRIu32, mfi->track); snprintf(timestamp, sizeof(timestamp), "%" PRIi64, (int64_t)time(NULL)); ret = ( (keyval_add(kv, "api_key", lastfm_api_key) == 0) && (keyval_add(kv, "sk", lastfm_session_key) == 0) && (keyval_add(kv, "artist", mfi->artist) == 0) && (keyval_add(kv, "track", mfi->title) == 0) && (keyval_add(kv, "album", mfi->album) == 0) && (keyval_add(kv, "albumArtist", mfi->album_artist) == 0) && (keyval_add(kv, "duration", duration) == 0) && (keyval_add(kv, "trackNumber", trackNumber) == 0) && (keyval_add(kv, "timestamp", timestamp) == 0) ); free_mfi(mfi, 0); if (!ret) { keyval_clear(kv); free(kv); return -1; } DPRINTF(E_INFO, L_LASTFM, "Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist")); ret = request_post("track.scrobble", kv, 0); keyval_clear(kv); free(kv); return ret; noscrobble: free_mfi(mfi, 0); return -1; } /* ---------------------------- Our lastfm API --------------------------- */ /* Thread: filescanner */ void lastfm_login(char **arglist) { struct keyval *kv; int ret; DPRINTF(E_LOG, L_LASTFM, "LastFM credentials file OK, logging in with username %s\n", arglist[0]); // Delete any existing session key free(lastfm_session_key); lastfm_session_key = NULL; db_admin_delete("lastfm_sk"); // Enable LastFM now that we got a login attempt lastfm_disabled = 0; kv = keyval_alloc(); if (!kv) return; ret = ( (keyval_add(kv, "api_key", lastfm_api_key) == 0) && (keyval_add(kv, "username", arglist[0]) == 0) && (keyval_add(kv, "password", arglist[1]) == 0) ); if (!ret) goto out_free_kv; // Send the login request request_post("auth.getMobileSession", kv, 1); out_free_kv: keyval_clear(kv); free(kv); } /* Thread: worker */ int lastfm_scrobble(int id) { DPRINTF(E_DBG, L_LASTFM, "Got LastFM scrobble request\n"); // LastFM is disabled because we already tried looking for a session key, but failed if (lastfm_disabled) return -1; // No session key in mem or in db if (!lastfm_session_key) lastfm_session_key = db_admin_get("lastfm_sk"); if (!lastfm_session_key) { DPRINTF(E_INFO, L_LASTFM, "No valid LastFM session key\n"); lastfm_disabled = 1; return -1; } return scrobble(id); } forked-daapd-25.0/src/lastfm.h000066400000000000000000000002111313447753700162170ustar00rootroot00000000000000 #ifndef __LASTFM_H__ #define __LASTFM_H__ void lastfm_login(char **arglist); int lastfm_scrobble(int id); #endif /* !__LASTFM_H__ */ forked-daapd-25.0/src/library.c000066400000000000000000000474221313447753700164070ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include #include #include #include #include #include #include #include "library.h" #include "cache.h" #include "commands.h" #include "conffile.h" #include "db.h" #include "logger.h" #include "misc.h" #include "listener.h" #include "player.h" static struct commands_base *cmdbase; static pthread_t tid_library; struct event_base *evbase_lib; extern struct library_source filescanner; #ifdef HAVE_SPOTIFY_H extern struct library_source spotifyscanner; #endif static struct library_source *sources[] = { &filescanner, #ifdef HAVE_SPOTIFY_H &spotifyscanner, #endif NULL }; /* Flag for aborting scan on exit */ static bool scan_exit; /* Flag for scan in progress */ static bool scanning; // After being told by db that the library was updated through update_trigger(), // wait 60 seconds before notifying listeners of LISTENER_DATABASE. This is to // avoid bombarding the listeners while there are many db updates, and to make // sure they only get a single update (useful for the cache). static struct timeval library_update_wait = { 60, 0 }; static struct event *updateev; static void sort_tag_create(char **sort_tag, char *src_tag) { const uint8_t *i_ptr; const uint8_t *n_ptr; const uint8_t *number; uint8_t out[1024]; uint8_t *o_ptr; int append_number; ucs4_t puc; int numlen; size_t len; int charlen; /* Note: include terminating NUL in string length for u8_normalize */ if (*sort_tag) { DPRINTF(E_DBG, L_LIB, "Existing sort tag will be normalized: %s\n", *sort_tag); o_ptr = u8_normalize(UNINORM_NFD, (uint8_t *)*sort_tag, strlen(*sort_tag) + 1, NULL, &len); free(*sort_tag); *sort_tag = (char *)o_ptr; return; } if (!src_tag || ((len = strlen(src_tag)) == 0)) { *sort_tag = NULL; return; } // Set input pointer past article if present if ((strncasecmp(src_tag, "a ", 2) == 0) && (len > 2)) i_ptr = (uint8_t *)(src_tag + 2); else if ((strncasecmp(src_tag, "an ", 3) == 0) && (len > 3)) i_ptr = (uint8_t *)(src_tag + 3); else if ((strncasecmp(src_tag, "the ", 4) == 0) && (len > 4)) i_ptr = (uint8_t *)(src_tag + 4); else i_ptr = (uint8_t *)src_tag; // Poor man's natural sort. Makes sure we sort like this: a1, a2, a10, a11, a21, a111 // We do this by padding zeroes to (short) numbers. As an alternative we could have // made a proper natural sort algorithm in sqlext.c, but we don't, since we don't // want any risk of hurting response times memset(&out, 0, sizeof(out)); o_ptr = (uint8_t *)&out; number = NULL; append_number = 0; do { n_ptr = u8_next(&puc, i_ptr); if (uc_is_digit(puc)) { if (!number) // We have encountered the beginning of a number number = i_ptr; append_number = (n_ptr == NULL); // If last char in string append number now } else { if (number) append_number = 1; // A number has ended so time to append it else { charlen = u8_strmblen(i_ptr); if (charlen >= 0) o_ptr = u8_stpncpy(o_ptr, i_ptr, charlen); // No numbers in sight, just append char } } // Break if less than 100 bytes remain (prevent buffer overflow) if (sizeof(out) - u8_strlen(out) < 100) break; // Break if number is very large (prevent buffer overflow) if (number && (i_ptr - number > 50)) break; if (append_number) { numlen = i_ptr - number; if (numlen < 5) // Max pad width { u8_strcpy(o_ptr, (uint8_t *)"00000"); o_ptr += (5 - numlen); } o_ptr = u8_stpncpy(o_ptr, number, numlen + u8_strmblen(i_ptr)); number = NULL; append_number = 0; } i_ptr = n_ptr; } while (n_ptr); *sort_tag = (char *)u8_normalize(UNINORM_NFD, (uint8_t *)&out, u8_strlen(out) + 1, NULL, &len); } static void fixup_tags(struct media_file_info *mfi) { cfg_t *lib; size_t len; char *tag; char *sep = " - "; char *ca; if (mfi->genre && (strlen(mfi->genre) == 0)) { free(mfi->genre); mfi->genre = NULL; } if (mfi->artist && (strlen(mfi->artist) == 0)) { free(mfi->artist); mfi->artist = NULL; } if (mfi->title && (strlen(mfi->title) == 0)) { free(mfi->title); mfi->title = NULL; } /* * Default to mpeg4 video/audio for unknown file types * in an attempt to allow streaming of DRM-afflicted files */ if (mfi->codectype && strcmp(mfi->codectype, "unkn") == 0) { if (mfi->has_video) { strcpy(mfi->codectype, "mp4v"); strcpy(mfi->type, "m4v"); } else { strcpy(mfi->codectype, "mp4a"); strcpy(mfi->type, "m4a"); } } if (!mfi->artist) { if (mfi->orchestra && mfi->conductor) { len = strlen(mfi->orchestra) + strlen(sep) + strlen(mfi->conductor); tag = (char *)malloc(len + 1); if (tag) { sprintf(tag,"%s%s%s", mfi->orchestra, sep, mfi->conductor); mfi->artist = tag; } } else if (mfi->orchestra) { mfi->artist = strdup(mfi->orchestra); } else if (mfi->conductor) { mfi->artist = strdup(mfi->conductor); } } /* Handle TV shows, try to present prettier metadata */ if (mfi->tv_series_name && strlen(mfi->tv_series_name) != 0) { mfi->media_kind = MEDIA_KIND_TVSHOW; /* tv show */ /* Default to artist = series_name */ if (mfi->artist && strlen(mfi->artist) == 0) { free(mfi->artist); mfi->artist = NULL; } if (!mfi->artist) mfi->artist = strdup(mfi->tv_series_name); /* Default to album = ", Season " */ if (mfi->album && strlen(mfi->album) == 0) { free(mfi->album); mfi->album = NULL; } if (!mfi->album) { len = snprintf(NULL, 0, "%s, Season %d", mfi->tv_series_name, mfi->tv_season_num); mfi->album = (char *)malloc(len + 1); if (mfi->album) sprintf(mfi->album, "%s, Season %d", mfi->tv_series_name, mfi->tv_season_num); } } /* Check the 4 top-tags are filled */ if (!mfi->artist) mfi->artist = strdup("Unknown artist"); if (!mfi->album) mfi->album = strdup("Unknown album"); if (!mfi->genre) mfi->genre = strdup("Unknown genre"); if (!mfi->title) { /* fname is left untouched by unicode_fixup_mfi() for * obvious reasons, so ensure it is proper UTF-8 */ mfi->title = unicode_fixup_string(mfi->fname, "ascii"); if (mfi->title == mfi->fname) mfi->title = strdup(mfi->fname); } /* Ensure sort tags are filled, manipulated and normalized */ sort_tag_create(&mfi->artist_sort, mfi->artist); sort_tag_create(&mfi->album_sort, mfi->album); sort_tag_create(&mfi->title_sort, mfi->title); /* We need to set album_artist according to media type and config */ if (mfi->compilation) /* Compilation */ { lib = cfg_getsec(cfg, "library"); ca = cfg_getstr(lib, "compilation_artist"); if (ca && mfi->album_artist) { free(mfi->album_artist); mfi->album_artist = strdup(ca); } else if (ca && !mfi->album_artist) { mfi->album_artist = strdup(ca); } else if (!ca && !mfi->album_artist) { mfi->album_artist = strdup(""); mfi->album_artist_sort = strdup(""); } } else if (mfi->media_kind == MEDIA_KIND_PODCAST) /* Podcast */ { if (mfi->album_artist) free(mfi->album_artist); mfi->album_artist = strdup(""); mfi->album_artist_sort = strdup(""); } else if (!mfi->album_artist) /* Regular media without album_artist */ { mfi->album_artist = strdup(mfi->artist); } if (!mfi->album_artist_sort && (strcmp(mfi->album_artist, mfi->artist) == 0)) mfi->album_artist_sort = strdup(mfi->artist_sort); else sort_tag_create(&mfi->album_artist_sort, mfi->album_artist); /* Composer is not one of our mandatory tags, so take extra care */ if (mfi->composer_sort || mfi->composer) sort_tag_create(&mfi->composer_sort, mfi->composer); } void library_add_media(struct media_file_info *mfi) { if (!mfi->path || !mfi->fname) { DPRINTF(E_LOG, L_LIB, "Ignoring media file with missing values (path='%s', fname='%s', data_kind='%d')\n", mfi->path, mfi->fname, mfi->data_kind); return; } if (!mfi->directory_id || !mfi->virtual_path) { // Missing informations for virtual_path and directory_id (may) lead to misplaced appearance in mpd clients DPRINTF(E_WARN, L_LIB, "Media file with missing values (path='%s', directory='%d', virtual_path='%s')\n", mfi->path, mfi->directory_id, mfi->virtual_path); } if (!mfi->item_kind) mfi->item_kind = 2; /* music */ if (!mfi->media_kind) mfi->media_kind = MEDIA_KIND_MUSIC; /* music */ unicode_fixup_mfi(mfi); fixup_tags(mfi); if (mfi->id == 0) db_file_add(mfi); else db_file_update(mfi); } int library_scan_media(const char *path, struct media_file_info *mfi) { int i; int ret; DPRINTF(E_DBG, L_LIB, "Scan metadata for path '%s'\n", path); ret = LIBRARY_PATH_INVALID; for (i = 0; sources[i] && ret == LIBRARY_PATH_INVALID; i++) { if (sources[i]->disabled || !sources[i]->scan_metadata) { DPRINTF(E_DBG, L_LIB, "Library source '%s' is disabled or does not support scan_metadata\n", sources[i]->name); continue; } ret = sources[i]->scan_metadata(path, mfi); if (ret == LIBRARY_OK) DPRINTF(E_DBG, L_LIB, "Got metadata for path '%s' from library source '%s'\n", path, sources[i]->name); } if (ret == LIBRARY_OK) { if (!mfi->virtual_path) mfi->virtual_path = strdup(mfi->path); if (!mfi->item_kind) mfi->item_kind = 2; /* music */ if (!mfi->media_kind) mfi->media_kind = MEDIA_KIND_MUSIC; /* music */ unicode_fixup_mfi(mfi); fixup_tags(mfi); } else { DPRINTF(E_LOG, L_LIB, "Failed to read metadata for path '%s' (ret=%d)\n", path, ret); } return ret; } int library_add_playlist_info(const char *path, const char *title, const char *virtual_path, enum pl_type type, int parent_pl_id, int dir_id) { struct playlist_info *pli; int plid; int ret; pli = db_pl_fetch_bypath(path); if (pli) { DPRINTF(E_DBG, L_LIB, "Playlist found ('%s', link %s), updating\n", title, path); plid = pli->id; pli->type = type; free(pli->title); pli->title = strdup(title); if (pli->virtual_path) free(pli->virtual_path); pli->virtual_path = safe_strdup(virtual_path); pli->directory_id = dir_id; ret = db_pl_update(pli); if (ret < 0) { DPRINTF(E_LOG, L_LIB, "Error updating playlist ('%s', link %s)\n", title, path); free_pli(pli, 0); return -1; } db_pl_clear_items(plid); } else { DPRINTF(E_DBG, L_LIB, "Adding playlist ('%s', link %s)\n", title, path); pli = (struct playlist_info *)malloc(sizeof(struct playlist_info)); if (!pli) { DPRINTF(E_LOG, L_LIB, "Out of memory\n"); return -1; } memset(pli, 0, sizeof(struct playlist_info)); pli->type = type; pli->title = strdup(title); pli->path = strdup(path); pli->virtual_path = safe_strdup(virtual_path); pli->parent_id = parent_pl_id; pli->directory_id = dir_id; ret = db_pl_add(pli, &plid); if ((ret < 0) || (plid < 1)) { DPRINTF(E_LOG, L_LIB, "Error adding playlist ('%s', link %s, ret %d, plid %d)\n", title, path, ret, plid); free_pli(pli, 0); return -1; } } free_pli(pli, 0); return plid; } int library_add_queue_item(struct media_file_info *mfi) { struct db_queue_item queue_item; memset(&queue_item, 0, sizeof(struct db_queue_item)); if (mfi->id) queue_item.file_id = mfi->id; else queue_item.file_id = 9999999; queue_item.title = mfi->title; queue_item.artist = mfi->artist; queue_item.album_artist = mfi->album_artist; queue_item.album = mfi->album; queue_item.genre = mfi->genre; queue_item.artist_sort = mfi->artist_sort; queue_item.album_artist_sort = mfi->album_artist_sort; queue_item.album_sort = mfi->album_sort; queue_item.path = mfi->path; queue_item.virtual_path = mfi->virtual_path; queue_item.data_kind = mfi->data_kind; queue_item.media_kind = mfi->media_kind; queue_item.song_length = mfi->song_length; queue_item.seek = mfi->seek; queue_item.songalbumid = mfi->songalbumid; queue_item.time_modified = mfi->time_modified; queue_item.year = mfi->year; queue_item.track = mfi->track; queue_item.disc = mfi->disc; //queue_item.artwork_url return db_queue_add_item(&queue_item, 0, 0); } static void purge_cruft(time_t start) { DPRINTF(E_DBG, L_LIB, "Purging old library content\n"); db_purge_cruft(start); db_groups_cleanup(); db_queue_cleanup(); DPRINTF(E_DBG, L_LIB, "Purging old artwork content\n"); cache_artwork_purge_cruft(start); } static enum command_state rescan(void *arg, int *ret) { time_t starttime; time_t endtime; int i; DPRINTF(E_LOG, L_LIB, "Library rescan triggered\n"); starttime = time(NULL); for (i = 0; sources[i]; i++) { if (!sources[i]->disabled && sources[i]->rescan) { DPRINTF(E_INFO, L_LIB, "Rescan library source '%s'\n", sources[i]->name); sources[i]->rescan(); } else { DPRINTF(E_INFO, L_LIB, "Library source '%s' is disabled\n", sources[i]->name); } } purge_cruft(starttime); endtime = time(NULL); DPRINTF(E_LOG, L_LIB, "Library rescan completed in %.f sec\n", difftime(endtime, starttime)); scanning = false; *ret = 0; return COMMAND_END; } static enum command_state fullrescan(void *arg, int *ret) { time_t starttime; time_t endtime; int i; DPRINTF(E_LOG, L_LIB, "Library full-rescan triggered\n"); starttime = time(NULL); player_playback_stop(); db_queue_clear(0); db_purge_all(); // Clears files, playlists, playlistitems, inotify and groups for (i = 0; sources[i]; i++) { if (!sources[i]->disabled && sources[i]->fullrescan) { DPRINTF(E_INFO, L_LIB, "Full-rescan library source '%s'\n", sources[i]->name); sources[i]->fullrescan(); } else { DPRINTF(E_INFO, L_LIB, "Library source '%s' is disabled\n", sources[i]->name); } } endtime = time(NULL); DPRINTF(E_LOG, L_LIB, "Library full-rescan completed in %.f sec\n", difftime(endtime, starttime)); scanning = false; *ret = 0; return COMMAND_END; } static void update_trigger_cb(int fd, short what, void *arg) { listener_notify(LISTENER_DATABASE); } static enum command_state update_trigger(void *arg, int *retval) { evtimer_add(updateev, &library_update_wait); *retval = 0; return COMMAND_END; } /* --------------------------- LIBRARY INTERFACE -------------------------- */ void library_rescan() { if (scanning) { DPRINTF(E_INFO, L_LIB, "Scan already running, ignoring request to trigger a new init scan\n"); return; } scanning = true; // TODO Guard "scanning" with a mutex commands_exec_async(cmdbase, rescan, NULL); } void library_fullrescan() { if (scanning) { DPRINTF(E_INFO, L_LIB, "Scan already running, ignoring request to trigger a new full rescan\n"); return; } scanning = true; // TODO Guard "scanning" with a mutex commands_exec_async(cmdbase, fullrescan, NULL); } static void initscan() { time_t starttime; time_t endtime; bool clear_queue_disabled; int i; scanning = true; starttime = time(NULL); // Only clear the queue if enabled (default) in config clear_queue_disabled = cfg_getbool(cfg_getsec(cfg, "mpd"), "clear_queue_on_stop_disable"); if (!clear_queue_disabled) { db_queue_clear(0); } for (i = 0; sources[i]; i++) { if (!sources[i]->disabled && sources[i]->initscan) sources[i]->initscan(); } if (! (cfg_getbool(cfg_getsec(cfg, "library"), "filescan_disable"))) { purge_cruft(starttime); DPRINTF(E_DBG, L_LIB, "Running post library scan jobs\n"); db_hook_post_scan(); } endtime = time(NULL); DPRINTF(E_LOG, L_LIB, "Library init scan completed in %.f sec\n", difftime(endtime, starttime)); scanning = false; listener_notify(LISTENER_DATABASE); } /* * @return true if scan is running, otherwise false */ bool library_is_scanning() { return scanning; } /* * @param is_scanning true if scan is running, otherwise false */ void library_set_scanning(bool is_scanning) { scanning = is_scanning; } /* * @return true if a running scan should be aborted due to imminent shutdown, otherwise false */ bool library_is_exiting() { return scan_exit; } void library_update_trigger(void) { if (scanning) return; commands_exec_async(cmdbase, update_trigger, NULL); } /* * Execute the function 'func' with the given argument 'arg' in the library thread. * * The pointer passed as argument is freed in the library thread after func returned. * * @param func The function to be executed * @param arg Argument passed to func * @return 0 if triggering the function execution succeeded, -1 on failure. */ int library_exec_async(command_function func, void *arg) { return commands_exec_async(cmdbase, func, arg); } static void * library(void *arg) { int ret; #ifdef __linux__ struct sched_param param; /* Lower the priority of the thread so forked-daapd may still respond * during library scan on low power devices. Param must be 0 for the SCHED_BATCH * policy. */ memset(¶m, 0, sizeof(struct sched_param)); ret = pthread_setschedparam(pthread_self(), SCHED_BATCH, ¶m); if (ret != 0) { DPRINTF(E_LOG, L_LIB, "Warning: Could not set thread priority to SCHED_BATCH\n"); } #endif ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_LIB, "Error: DB init failed\n"); pthread_exit(NULL); } initscan(); event_base_dispatch(evbase_lib); if (!scan_exit) DPRINTF(E_FATAL, L_LIB, "Scan event loop terminated ahead of time!\n"); db_perthread_deinit(); pthread_exit(NULL); } /* Thread: main */ int library_init(void) { int i; int ret; scan_exit = false; scanning = false; CHECK_NULL(L_LIB, evbase_lib = event_base_new()); CHECK_NULL(L_LIB, updateev = evtimer_new(evbase_lib, update_trigger_cb, NULL)); for (i = 0; sources[i]; i++) { if (!sources[i]->init) continue; ret = sources[i]->init(); if (ret < 0) sources[i]->disabled = 1; } CHECK_NULL(L_LIB, cmdbase = commands_base_new(evbase_lib, NULL)); CHECK_ERR(L_LIB, pthread_create(&tid_library, NULL, library, NULL)); #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_library, "library"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_library, "library"); #endif return 0; } /* Thread: main */ void library_deinit() { int i; int ret; scan_exit = true; commands_base_destroy(cmdbase); ret = pthread_join(tid_library, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_LIB, "Could not join library thread: %s\n", strerror(errno)); return; } for (i = 0; sources[i]; i++) { if (sources[i]->deinit && !sources[i]->disabled) sources[i]->deinit(); } event_base_free(evbase_lib); } forked-daapd-25.0/src/library.h000066400000000000000000000050051313447753700164030ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ #ifndef SRC_LIBRARY_H_ #define SRC_LIBRARY_H_ #include #include #include #include "commands.h" #include "db.h" #define LIBRARY_OK 0 #define LIBRARY_ERROR -1 #define LIBRARY_PATH_INVALID -2 /* * Definition of a library source * * A library source is responsible for scanning items into the library db. */ struct library_source { char *name; int disabled; /* * Initialize library source (called from the main thread) */ int (*init)(void); /* * Shutdown library source (called from the main thread after * terminating the library thread) */ void (*deinit)(void); /* * Run initial scan after startup (called from the library thread) */ int (*initscan)(void); /* * Run rescan (called from the library thread) */ int (*rescan)(void); /* * Run a full rescan (purge library entries and rescan) (called from the library thread) */ int (*fullrescan)(void); /* * Scans metadata for the media file with the given path into the given mfi */ int (*scan_metadata)(const char *path, struct media_file_info *mfi); }; void library_add_media(struct media_file_info *mfi); int library_add_playlist_info(const char *path, const char *title, const char *virtual_path, enum pl_type type, int parent_pl_id, int dir_id); int library_scan_media(const char *path, struct media_file_info *mfi); int library_add_queue_item(struct media_file_info *mfi); void library_rescan(); void library_fullrescan(); bool library_is_scanning(); void library_set_scanning(bool is_scanning); bool library_is_exiting(); void library_update_trigger(void); int library_exec_async(command_function func, void *arg); int library_init(); void library_deinit(); #endif /* SRC_LIBRARY_H_ */ forked-daapd-25.0/src/library/000077500000000000000000000000001313447753700162325ustar00rootroot00000000000000forked-daapd-25.0/src/library/filescanner.c000066400000000000000000001076421313447753700207010ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * Bits and pieces from mt-daapd: * Copyright (C) 2003 Ron Pedde (ron@pedde.com) * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include #include #include #include #ifdef HAVE_REGEX_H # include #endif #include "logger.h" #include "db.h" #include "library/filescanner.h" #include "conffile.h" #include "misc.h" #include "remote_pairing.h" #include "player.h" #include "cache.h" #include "artwork.h" #include "commands.h" #include "library.h" #ifdef LASTFM # include "lastfm.h" #endif #ifdef HAVE_SPOTIFY_H # include "spotify.h" #endif #define F_SCAN_BULK (1 << 0) #define F_SCAN_RESCAN (1 << 1) #define F_SCAN_FAST (1 << 2) #define F_SCAN_MOVED (1 << 3) #define F_SCAN_TYPE_FILE (1 << 0) #define F_SCAN_TYPE_PODCAST (1 << 1) #define F_SCAN_TYPE_AUDIOBOOK (1 << 2) #define F_SCAN_TYPE_COMPILATION (1 << 3) enum file_type { FILE_UNKNOWN = 0, FILE_IGNORE, FILE_REGULAR, FILE_PLAYLIST, FILE_SMARTPL, FILE_ITUNES, FILE_ARTWORK, FILE_CTRL_REMOTE, FILE_CTRL_RAOP_VERIFICATION, FILE_CTRL_LASTFM, FILE_CTRL_SPOTIFY, FILE_CTRL_INITSCAN, FILE_CTRL_FULLSCAN, }; struct deferred_pl { char *path; time_t mtime; struct deferred_pl *next; int directory_id; }; struct stacked_dir { char *path; int parent_id; struct stacked_dir *next; }; static int inofd; static struct event *inoev; static struct deferred_pl *playlists; static struct stacked_dir *dirstack; /* From library.c */ extern struct event_base *evbase_lib; #ifndef __linux__ struct deferred_file { struct watch_info wi; char path[PATH_MAX]; struct deferred_file *next; /* variable sized, must be at the end */ struct inotify_event ie; }; static struct deferred_file *filestack; static struct event *deferred_inoev; #endif /* Count of files scanned during a bulk scan */ static int counter; /* When copying into the lib (eg. if a file is moved to the lib by copying into * a Samba network share) inotify might give us IN_CREATE -> n x IN_ATTRIB -> * IN_CLOSE_WRITE, but we don't want to do any scanning before the * IN_CLOSE_WRITE. So we register new files (by path hashes) in this ring buffer * when we get the IN_CREATE and then ignore the IN_ATTRIB for these files. */ #define INCOMINGFILES_BUFFER_SIZE 50 static int incomingfiles_idx; static uint32_t incomingfiles_buffer[INCOMINGFILES_BUFFER_SIZE]; /* Forward */ static void bulk_scan(int flags); static int inofd_event_set(void); static void inofd_event_unset(void); static int filescanner_initscan(); static int filescanner_rescan(); static int filescanner_fullrescan(); const char * filename_from_path(const char *path) { const char *filename; filename = strrchr(path, '/'); if ((!filename) || (strlen(filename) == 1)) filename = path; else filename++; return filename; } static int push_dir(struct stacked_dir **s, char *path, int parent_id) { struct stacked_dir *d; d = malloc(sizeof(struct stacked_dir)); if (!d) { DPRINTF(E_LOG, L_SCAN, "Could not stack directory %s; out of memory\n", path); return -1; } d->path = strdup(path); if (!d->path) { DPRINTF(E_LOG, L_SCAN, "Could not stack directory %s; out of memory for path\n", path); free(d); return -1; } d->parent_id = parent_id; d->next = *s; *s = d; return 0; } static struct stacked_dir * pop_dir(struct stacked_dir **s) { struct stacked_dir *d; if (!*s) return NULL; d = *s; *s = d->next; return d; } #ifdef HAVE_REGEX_H /* Checks if the file path is configured to be ignored */ static int file_path_ignore(const char *path) { cfg_t *lib; regex_t regex; int n; int i; int ret; lib = cfg_getsec(cfg, "library"); n = cfg_size(lib, "filepath_ignore"); for (i = 0; i < n; i++) { ret = regcomp(®ex, cfg_getnstr(lib, "filepath_ignore", i), 0); if (ret != 0) { DPRINTF(E_LOG, L_SCAN, "Could not compile regex for matching with file path\n"); return 0; } ret = regexec(®ex, path, 0, NULL, 0); regfree(®ex); if (ret == 0) { DPRINTF(E_DBG, L_SCAN, "Regex match: %s\n", path); return 1; } } return 0; } #endif /* Checks if the file extension is in the ignore list */ static int file_type_ignore(const char *ext) { cfg_t *lib; int n; int i; lib = cfg_getsec(cfg, "library"); n = cfg_size(lib, "filetypes_ignore"); for (i = 0; i < n; i++) { if (strcasecmp(ext, cfg_getnstr(lib, "filetypes_ignore", i)) == 0) return 1; } return 0; } static enum file_type file_type_get(const char *path) { const char *filename; const char *ext; filename = strrchr(path, '/'); if ((!filename) || (strlen(filename) == 1)) filename = path; else filename++; #ifdef HAVE_REGEX_H if (file_path_ignore(path)) return FILE_IGNORE; #endif ext = strrchr(path, '.'); if (!ext || (strlen(ext) == 1)) return FILE_REGULAR; if (file_type_ignore(ext)) return FILE_IGNORE; if ((strcasecmp(ext, ".m3u") == 0) || (strcasecmp(ext, ".pls") == 0)) return FILE_PLAYLIST; if (strcasecmp(ext, ".smartpl") == 0) return FILE_SMARTPL; if (artwork_file_is_artwork(filename)) return FILE_ARTWORK; if ((strcasecmp(ext, ".jpg") == 0) || (strcasecmp(ext, ".png") == 0)) return FILE_IGNORE; #ifdef ITUNES if (strcasecmp(ext, ".xml") == 0) return FILE_ITUNES; #endif if (strcasecmp(ext, ".remote") == 0) return FILE_CTRL_REMOTE; if (strcasecmp(ext, ".verification") == 0) return FILE_CTRL_RAOP_VERIFICATION; if (strcasecmp(ext, ".lastfm") == 0) return FILE_CTRL_LASTFM; if (strcasecmp(ext, ".spotify") == 0) return FILE_CTRL_SPOTIFY; if (strcasecmp(ext, ".init-rescan") == 0) return FILE_CTRL_INITSCAN; if (strcasecmp(ext, ".full-rescan") == 0) return FILE_CTRL_FULLSCAN; if (strcasecmp(ext, ".url") == 0) { DPRINTF(E_INFO, L_SCAN, "No support for .url, use .m3u or .pls\n"); return FILE_IGNORE; } if ((filename[0] == '_') || (filename[0] == '.')) return FILE_IGNORE; return FILE_REGULAR; } static void process_playlist(char *file, time_t mtime, int dir_id) { enum file_type ft; ft = file_type_get(file); if (ft == FILE_PLAYLIST) scan_playlist(file, mtime, dir_id); #ifdef ITUNES else if (ft == FILE_ITUNES) scan_itunes_itml(file); #endif } /* If we found a control file we want to kickoff some action */ static void kickoff(void (*kickoff_func)(char **arg), const char *file, int lines) { char **file_content; int i; file_content = m_readfile(file, lines); if (!file_content) return; kickoff_func(file_content); for (i = 0; i < lines; i++) free(file_content[i]); free(file_content); } /* Thread: scan */ static void defer_playlist(char *path, time_t mtime, int dir_id) { struct deferred_pl *pl; pl = (struct deferred_pl *)malloc(sizeof(struct deferred_pl)); if (!pl) { DPRINTF(E_WARN, L_SCAN, "Out of memory for deferred playlist\n"); return; } memset(pl, 0, sizeof(struct deferred_pl)); pl->path = strdup(path); if (!pl->path) { DPRINTF(E_WARN, L_SCAN, "Out of memory for deferred playlist\n"); free(pl); return; } pl->mtime = mtime; pl->directory_id = dir_id; pl->next = playlists; playlists = pl; DPRINTF(E_INFO, L_SCAN, "Deferred playlist %s\n", path); } /* Thread: scan (bulk only) */ static void process_deferred_playlists(void) { struct deferred_pl *pl; while ((pl = playlists)) { playlists = pl->next; process_playlist(pl->path, pl->mtime, pl->directory_id); free(pl->path); free(pl); if (library_is_exiting()) return; } } static void process_regular_file(char *file, struct stat *sb, int type, int flags, int dir_id) { time_t stamp; int id; bool is_bulkscan = (flags & F_SCAN_BULK); struct media_file_info mfi; char virtual_path[PATH_MAX]; int ret; // Do not rescan metadata if file did not change since last scan db_file_stamp_bypath(file, &stamp, &id); if (stamp && (stamp >= sb->st_mtime)) { db_file_ping(id); return; } // File is new or modified - (re)scan metadata and update file in library memset(&mfi, 0, sizeof(struct media_file_info)); mfi.id = id; mfi.fname = strdup(filename_from_path(file)); mfi.path = strdup(file); mfi.time_modified = sb->st_mtime; mfi.file_size = sb->st_size; snprintf(virtual_path, PATH_MAX, "/file:%s", file); mfi.virtual_path = strdup(virtual_path); mfi.directory_id = dir_id; if (S_ISFIFO(sb->st_mode)) { mfi.data_kind = DATA_KIND_PIPE; mfi.type = strdup("wav"); mfi.codectype = strdup("wav"); mfi.description = strdup("PCM16 pipe"); mfi.media_kind = MEDIA_KIND_MUSIC; } else { mfi.data_kind = DATA_KIND_FILE; if (type & F_SCAN_TYPE_AUDIOBOOK) mfi.media_kind = MEDIA_KIND_AUDIOBOOK; else if (type & F_SCAN_TYPE_PODCAST) mfi.media_kind = MEDIA_KIND_PODCAST; mfi.compilation = (type & F_SCAN_TYPE_COMPILATION); mfi.file_size = sb->st_size; ret = scan_metadata_ffmpeg(file, &mfi); if (ret < 0) { free_mfi(&mfi, 1); return; } } library_add_media(&mfi); cache_artwork_ping(file, sb->st_mtime, !is_bulkscan); // TODO [artworkcache] If entry in artwork cache exists for no artwork available, delete the entry if media file has embedded artwork free_mfi(&mfi, 1); } /* Thread: scan */ static void process_file(char *file, struct stat *sb, int type, int flags, int dir_id) { switch (file_type_get(file)) { case FILE_REGULAR: process_regular_file(file, sb, type, flags, dir_id); counter++; /* When in bulk mode, split transaction in pieces of 200 */ if ((flags & F_SCAN_BULK) && (counter % 200 == 0)) { DPRINTF(E_LOG, L_SCAN, "Scanned %d files...\n", counter); db_transaction_end(); db_transaction_begin(); } break; case FILE_PLAYLIST: case FILE_ITUNES: if (flags & F_SCAN_BULK) defer_playlist(file, sb->st_mtime, dir_id); else process_playlist(file, sb->st_mtime, dir_id); break; case FILE_SMARTPL: DPRINTF(E_DBG, L_SCAN, "Smart playlist file: %s\n", file); scan_smartpl(file, sb->st_mtime, dir_id); break; case FILE_ARTWORK: DPRINTF(E_DBG, L_SCAN, "Artwork file: %s\n", file); cache_artwork_ping(file, sb->st_mtime, !(flags & F_SCAN_BULK)); // TODO [artworkcache] If entry in artwork cache exists for no artwork available for a album with files in the same directory, delete the entry break; case FILE_CTRL_REMOTE: if (flags & F_SCAN_BULK) DPRINTF(E_LOG, L_SCAN, "Bulk scan will ignore '%s' (to process, add it after startup)\n", file); else kickoff(remote_pairing_kickoff, file, 1); break; case FILE_CTRL_RAOP_VERIFICATION: if (flags & F_SCAN_BULK) DPRINTF(E_LOG, L_SCAN, "Bulk scan will ignore '%s' (to process, add it after startup)\n", file); else kickoff(player_raop_verification_kickoff, file, 1); break; case FILE_CTRL_LASTFM: #ifdef LASTFM if (flags & F_SCAN_BULK) DPRINTF(E_LOG, L_SCAN, "Bulk scan will ignore '%s' (to process, add it after startup)\n", file); else kickoff(lastfm_login, file, 2); #else DPRINTF(E_LOG, L_SCAN, "Found '%s', but this version was built without LastFM support\n", file); #endif break; case FILE_CTRL_SPOTIFY: #ifdef HAVE_SPOTIFY_H if (flags & F_SCAN_BULK) DPRINTF(E_LOG, L_SCAN, "Bulk scan will ignore '%s' (to process, add it after startup)\n", file); else kickoff(spotify_login, file, 2); #else DPRINTF(E_LOG, L_SCAN, "Found '%s', but this version was built without Spotify support\n", file); #endif break; case FILE_CTRL_INITSCAN: if (flags & F_SCAN_BULK) break; DPRINTF(E_LOG, L_SCAN, "Startup rescan triggered, found init-rescan file: %s\n", file); library_rescan(); break; case FILE_CTRL_FULLSCAN: if (flags & F_SCAN_BULK) break; DPRINTF(E_LOG, L_SCAN, "Full rescan triggered, found full-rescan file: %s\n", file); library_fullrescan(); break; default: DPRINTF(E_WARN, L_SCAN, "Ignoring file: %s\n", file); } } /* Thread: scan */ static int check_speciallib(char *path, const char *libtype) { cfg_t *lib; int ndirs; int i; lib = cfg_getsec(cfg, "library"); ndirs = cfg_size(lib, libtype); for (i = 0; i < ndirs; i++) { if (strstr(path, cfg_getnstr(lib, libtype, i))) return 1; } return 0; } /* Thread: scan */ static int create_virtual_path(char *path, char *virtual_path, int virtual_path_len) { int ret; ret = snprintf(virtual_path, virtual_path_len, "/file:%s", path); if ((ret < 0) || (ret >= virtual_path_len)) { DPRINTF(E_LOG, L_SCAN, "Virtual path /file:%s, PATH_MAX exceeded\n", path); return -1; } return 0; } /* * Returns informations about the attributes of the file at the given 'path' in the structure * pointed to by 'sb'. * * If path is a symbolic link, the attributes in sb describe the file that the link points to * and resolved_path contains the resolved path (resolved_path must be of length PATH_MAX). * If path is not a symbolic link, resolved_path holds the same value as path. * * The return value is 0 if the operation is successful, or -1 on failure */ static int read_attributes(char *resolved_path, const char *path, struct stat *sb) { int ret; ret = lstat(path, sb); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Skipping %s, lstat() failed: %s\n", path, strerror(errno)); return -1; } if (S_ISLNK(sb->st_mode)) { if (!realpath(path, resolved_path)) { DPRINTF(E_LOG, L_SCAN, "Skipping %s, could not dereference symlink: %s\n", path, strerror(errno)); return -1; } ret = stat(resolved_path, sb); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Skipping %s, stat() failed: %s\n", resolved_path, strerror(errno)); return -1; } } else { strcpy(resolved_path, path); } return 0; } static void process_directory(char *path, int parent_id, int flags) { DIR *dirp; struct dirent *de; char entry[PATH_MAX]; char resolved_path[PATH_MAX]; struct stat sb; struct watch_info wi; int type; char virtual_path[PATH_MAX]; int dir_id; int ret; DPRINTF(E_DBG, L_SCAN, "Processing directory %s (flags = 0x%x)\n", path, flags); dirp = opendir(path); if (!dirp) { DPRINTF(E_LOG, L_SCAN, "Could not open directory %s: %s\n", path, strerror(errno)); return; } /* Add/update directories table */ ret = create_virtual_path(path, virtual_path, sizeof(virtual_path)); if (ret < 0) return; dir_id = db_directory_addorupdate(virtual_path, 0, parent_id); if (dir_id <= 0) { DPRINTF(E_LOG, L_SCAN, "Insert or update of directory failed '%s'\n", virtual_path); } /* Check if compilation and/or podcast directory */ type = 0; if (check_speciallib(path, "compilations")) type |= F_SCAN_TYPE_COMPILATION; if (check_speciallib(path, "podcasts")) type |= F_SCAN_TYPE_PODCAST; if (check_speciallib(path, "audiobooks")) type |= F_SCAN_TYPE_AUDIOBOOK; for (;;) { if (library_is_exiting()) break; errno = 0; de = readdir(dirp); if (errno) { DPRINTF(E_LOG, L_SCAN, "readdir error in %s: %s\n", path, strerror(errno)); break; } if (!de) break; if (de->d_name[0] == '.') continue; ret = snprintf(entry, sizeof(entry), "%s/%s", path, de->d_name); if ((ret < 0) || (ret >= sizeof(entry))) { DPRINTF(E_LOG, L_SCAN, "Skipping %s/%s, PATH_MAX exceeded\n", path, de->d_name); continue; } ret = read_attributes(resolved_path, entry, &sb); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Skipping %s, read_attributes() failed\n", entry); continue; } if (S_ISDIR(sb.st_mode)) { push_dir(&dirstack, resolved_path, dir_id); } else if (!(flags & F_SCAN_FAST)) { if (S_ISREG(sb.st_mode) || S_ISFIFO(sb.st_mode)) process_file(resolved_path, &sb, type, flags, dir_id); else DPRINTF(E_LOG, L_SCAN, "Skipping %s, not a directory, symlink, pipe nor regular file\n", entry); } } closedir(dirp); memset(&wi, 0, sizeof(struct watch_info)); // Add inotify watch (for FreeBSD we limit the flags so only dirs will be // opened, otherwise we will be opening way too many files) #ifdef __linux__ wi.wd = inotify_add_watch(inofd, path, IN_ATTRIB | IN_CREATE | IN_DELETE | IN_CLOSE_WRITE | IN_MOVE | IN_DELETE | IN_MOVE_SELF); #else wi.wd = inotify_add_watch(inofd, path, IN_CREATE | IN_DELETE | IN_MOVE); #endif if (wi.wd < 0) { DPRINTF(E_WARN, L_SCAN, "Could not create inotify watch for %s: %s\n", path, strerror(errno)); return; } if (!(flags & F_SCAN_MOVED)) { wi.cookie = 0; wi.path = path; db_watch_add(&wi); } } /* Thread: scan */ static int process_parent_directories(char *path) { char *ptr; int dir_id; char buf[PATH_MAX]; char virtual_path[PATH_MAX]; int ret; dir_id = DIR_FILE; ptr = path + 1; while (ptr && (ptr = strchr(ptr, '/'))) { if (strlen(ptr) <= 1) { // Do not process trailing '/' break; } strncpy(buf, path, (ptr - path)); buf[(ptr - path)] = '\0'; ret = create_virtual_path(buf, virtual_path, sizeof(virtual_path)); if (ret < 0) return 0; dir_id = db_directory_addorupdate(virtual_path, 0, dir_id); if (dir_id <= 0) { DPRINTF(E_LOG, L_SCAN, "Insert or update of directory failed '%s'\n", virtual_path); return 0; } ptr++; } return dir_id; } static void process_directories(char *root, int parent_id, int flags) { struct stacked_dir *dir; process_directory(root, parent_id, flags); if (library_is_exiting()) return; while ((dir = pop_dir(&dirstack))) { process_directory(dir->path, dir->parent_id, flags); free(dir->path); free(dir); if (library_is_exiting()) return; } } /* Thread: scan */ static void bulk_scan(int flags) { cfg_t *lib; int ndirs; char *path; char *deref; time_t start; time_t end; int parent_id; int i; char virtual_path[PATH_MAX]; int ret; start = time(NULL); playlists = NULL; dirstack = NULL; lib = cfg_getsec(cfg, "library"); ndirs = cfg_size(lib, "directories"); for (i = 0; i < ndirs; i++) { path = cfg_getnstr(lib, "directories", i); parent_id = process_parent_directories(path); deref = m_realpath(path); if (!deref) { DPRINTF(E_LOG, L_SCAN, "Skipping library directory %s, could not dereference: %s\n", path, strerror(errno)); /* Assume dir is mistakenly not mounted, so just disable everything and update timestamps */ db_file_disable_bymatch(path, "", 0); db_pl_disable_bymatch(path, "", 0); db_directory_disable_bymatch(path, "", 0); db_file_ping_bymatch(path, 1); db_pl_ping_bymatch(path, 1); ret = snprintf(virtual_path, sizeof(virtual_path), "/file:%s", path); if ((ret < 0) || (ret >= sizeof(virtual_path))) DPRINTF(E_LOG, L_SCAN, "Virtual path exceeds PATH_MAX (/file:%s)\n", path); else db_directory_ping_bymatch(virtual_path); continue; } counter = 0; db_transaction_begin(); process_directories(deref, parent_id, flags); db_transaction_end(); free(deref); if (library_is_exiting()) return; } if (!(flags & F_SCAN_FAST) && playlists) process_deferred_playlists(); if (library_is_exiting()) return; if (dirstack) DPRINTF(E_LOG, L_SCAN, "WARNING: unhandled leftover directories\n"); end = time(NULL); if (flags & F_SCAN_FAST) { DPRINTF(E_LOG, L_SCAN, "Bulk library scan completed in %.f sec (with file scan disabled)\n", difftime(end, start)); } else { DPRINTF(E_LOG, L_SCAN, "Bulk library scan completed in %.f sec\n", difftime(end, start)); } } static int get_parent_dir_id(const char *path) { char *pathcopy; char *parent_dir; char virtual_path[PATH_MAX]; int parent_id; int ret; pathcopy = strdup(path); parent_dir = dirname(pathcopy); ret = create_virtual_path(parent_dir, virtual_path, sizeof(virtual_path)); if (ret == 0) parent_id = db_directory_id_byvirtualpath(virtual_path); else parent_id = 0; free(pathcopy); return parent_id; } static int watches_clear(uint32_t wd, char *path) { struct watch_enum we; uint32_t rm_wd; int ret; inotify_rm_watch(inofd, wd); db_watch_delete_bywd(wd); memset(&we, 0, sizeof(struct watch_enum)); we.match = path; ret = db_watch_enum_start(&we); if (ret < 0) return -1; while ((db_watch_enum_fetchwd(&we, &rm_wd) == 0) && (rm_wd)) { inotify_rm_watch(inofd, rm_wd); } db_watch_enum_end(&we); db_watch_delete_bymatch(path); return 0; } /* Thread: scan */ static void process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie) { struct watch_enum we; uint32_t rm_wd; char *s; int flags = 0; int ret; int parent_id; DPRINTF(E_DBG, L_SCAN, "Directory event: 0x%x, cookie 0x%x, wd %d\n", ie->mask, ie->cookie, wi->wd); if (ie->mask & IN_UNMOUNT) { db_file_disable_bymatch(path, "", 0); db_pl_disable_bymatch(path, "", 0); db_directory_disable_bymatch(path, "", 0); } if (ie->mask & IN_MOVE_SELF) { /* A directory we know about, that got moved from a place * we know about to a place we know nothing about */ if (wi->cookie) { memset(&we, 0, sizeof(struct watch_enum)); we.cookie = wi->cookie; ret = db_watch_enum_start(&we); if (ret < 0) return; while ((db_watch_enum_fetchwd(&we, &rm_wd) == 0) && (rm_wd)) { inotify_rm_watch(inofd, rm_wd); } db_watch_enum_end(&we); db_watch_delete_bycookie(wi->cookie); } else { /* If the directory exists, it has been moved and we've * kept track of it successfully, so we're done */ ret = access(path, F_OK); if (ret == 0) return; /* Most probably a top-level dir is getting moved, * and we can't tell where it's going */ ret = watches_clear(ie->wd, path); if (ret < 0) return; db_file_disable_bymatch(path, "", 0); db_pl_disable_bymatch(path, "", 0); } } if (ie->mask & IN_MOVED_FROM) { db_watch_mark_bypath(path, path, ie->cookie); db_watch_mark_bymatch(path, path, ie->cookie); db_file_disable_bymatch(path, path, ie->cookie); db_pl_disable_bymatch(path, path, ie->cookie); db_directory_disable_bymatch(path, path, ie->cookie); } if (ie->mask & IN_MOVED_TO) { if (db_watch_cookie_known(ie->cookie)) { db_watch_move_bycookie(ie->cookie, path); db_file_enable_bycookie(ie->cookie, path); db_pl_enable_bycookie(ie->cookie, path); db_directory_enable_bycookie(ie->cookie, path); /* We'll rescan the directory tree to update playlists */ flags |= F_SCAN_MOVED; } ie->mask |= IN_CREATE; } if (ie->mask & IN_ATTRIB) { DPRINTF(E_DBG, L_SCAN, "Directory permissions changed (%s): %s\n", wi->path, path); // Find out if we are already watching the dir (ret will be 0) s = wi->path; wi->path = path; ret = db_watch_get_bypath(wi); if (ret == 0) free(wi->path); wi->path = s; #ifdef HAVE_EUIDACCESS if (euidaccess(path, (R_OK | X_OK)) < 0) #else if (access(path, (R_OK | X_OK)) < 0) #endif { DPRINTF(E_LOG, L_SCAN, "Directory access to '%s' failed: %s\n", path, strerror(errno)); if (ret == 0) watches_clear(wi->wd, path); db_file_disable_bymatch(path, "", 0); db_pl_disable_bymatch(path, "", 0); db_directory_disable_bymatch(path, "", 0); } else if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Directory access to '%s' achieved\n", path); ie->mask |= IN_CREATE; } else { DPRINTF(E_INFO, L_SCAN, "Directory event, but '%s' already being watched\n", path); } } if (ie->mask & IN_CREATE) { parent_id = get_parent_dir_id(path); process_directories(path, parent_id, flags); if (dirstack) DPRINTF(E_LOG, L_SCAN, "WARNING: unhandled leftover directories\n"); } } /* Thread: scan */ static void process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie) { struct stat sb; uint32_t path_hash; char *file = path; char resolved_path[PATH_MAX]; char *dir; char dir_vpath[PATH_MAX]; int type; int i; int dir_id; char *ptr; int ret; DPRINTF(E_DBG, L_SCAN, "File event: 0x%x, cookie 0x%x, wd %d\n", ie->mask, ie->cookie, wi->wd); path_hash = djb_hash(path, strlen(path)); if (ie->mask & IN_DELETE) { DPRINTF(E_DBG, L_SCAN, "File deleted: %s\n", path); db_file_delete_bypath(path); db_pl_delete_bypath(path); cache_artwork_delete_by_path(path); } if (ie->mask & IN_MOVED_FROM) { DPRINTF(E_DBG, L_SCAN, "File moved from: %s\n", path); db_file_disable_bypath(path, path, ie->cookie); db_pl_disable_bypath(path, path, ie->cookie); } if (ie->mask & IN_ATTRIB) { DPRINTF(E_DBG, L_SCAN, "File attributes changed: %s\n", path); // Ignore the IN_ATTRIB if we just got an IN_CREATE for (i = 0; i < INCOMINGFILES_BUFFER_SIZE; i++) { if (incomingfiles_buffer[i] == path_hash) return; } #ifdef HAVE_EUIDACCESS if (euidaccess(path, R_OK) < 0) #else if (access(path, R_OK) < 0) #endif { DPRINTF(E_LOG, L_SCAN, "File access to '%s' failed: %s\n", path, strerror(errno)); db_file_delete_bypath(path); cache_artwork_delete_by_path(path); } else if ((file_type_get(path) == FILE_REGULAR) && (db_file_id_bypath(path) <= 0)) // TODO Playlists { DPRINTF(E_LOG, L_SCAN, "File access to '%s' achieved\n", path); ie->mask |= IN_CLOSE_WRITE; } } if (ie->mask & IN_MOVED_TO) { DPRINTF(E_DBG, L_SCAN, "File moved to: %s\n", path); ret = db_file_enable_bycookie(ie->cookie, path); if (ret > 0) { // If file was successfully enabled, update the directory id dir = strdup(path); ptr = strrchr(dir, '/'); dir[(ptr - dir)] = '\0'; ret = create_virtual_path(dir, dir_vpath, sizeof(dir_vpath)); if (ret >= 0) { dir_id = db_directory_id_byvirtualpath(dir_vpath); if (dir_id > 0) { ret = db_file_update_directoryid(path, dir_id); if (ret < 0) DPRINTF(E_LOG, L_SCAN, "Error updating directory id for file: %s\n", path); } } free(dir); } else { /* It's not a known media file, so it's either a new file * or a playlist, known or not. * We want to scan the new file and we want to rescan the * playlist to update playlist items (relative items). */ ie->mask |= IN_CLOSE_WRITE; db_pl_enable_bycookie(ie->cookie, path); } } if (ie->mask & IN_CREATE) { DPRINTF(E_DBG, L_SCAN, "File created: %s\n", path); ret = lstat(path, &sb); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Could not lstat() '%s': %s\n", path, strerror(errno)); return; } // Add to the list of files where we ignore IN_ATTRIB until the file is closed again if (S_ISREG(sb.st_mode)) { DPRINTF(E_SPAM, L_SCAN, "Incoming file created '%s' (%d), index %d\n", path, (int)path_hash, incomingfiles_idx); incomingfiles_buffer[incomingfiles_idx] = path_hash; incomingfiles_idx = (incomingfiles_idx + 1) % INCOMINGFILES_BUFFER_SIZE; } else if (S_ISFIFO(sb.st_mode)) ie->mask |= IN_CLOSE_WRITE; } if (ie->mask & IN_CLOSE_WRITE) { DPRINTF(E_DBG, L_SCAN, "File closed: %s\n", path); // File has been closed so remove from the IN_ATTRIB ignore list for (i = 0; i < INCOMINGFILES_BUFFER_SIZE; i++) if (incomingfiles_buffer[i] == path_hash) { DPRINTF(E_SPAM, L_SCAN, "Incoming file closed '%s' (%d), index %d\n", path, (int)path_hash, i); incomingfiles_buffer[i] = 0; } ret = read_attributes(resolved_path, path, &sb); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Skipping %s, read_attributes() failed\n", path); return; } type = 0; if (check_speciallib(path, "compilations")) type |= F_SCAN_TYPE_COMPILATION; if (check_speciallib(path, "podcasts")) type |= F_SCAN_TYPE_PODCAST; if (check_speciallib(path, "audiobooks")) type |= F_SCAN_TYPE_AUDIOBOOK; dir_id = get_parent_dir_id(file); if (S_ISDIR(sb.st_mode)) { process_inotify_dir(wi, resolved_path, ie); return; } else if (S_ISREG(sb.st_mode) || S_ISFIFO(sb.st_mode)) { process_file(resolved_path, &sb, type, 0, dir_id); } else DPRINTF(E_LOG, L_SCAN, "Skipping %s, not a directory, symlink, pipe nor regular file\n", resolved_path); } } #ifndef __linux__ /* Since kexec based inotify doesn't really have inotify we only get * a IN_CREATE. That is a bit too soon to start scanning the file, * so we defer it for 10 seconds. */ static void inotify_deferred_cb(int fd, short what, void *arg) { struct deferred_file *f; struct deferred_file *next; for (f = filestack; f; f = next) { next = f->next; DPRINTF(E_DBG, L_SCAN, "Processing deferred file %s\n", f->path); process_inotify_file(&f->wi, f->path, &f->ie); free(f->wi.path); free(f); } filestack = NULL; } static void process_inotify_file_defer(struct watch_info *wi, char *path, struct inotify_event *ie) { struct deferred_file *f; struct timeval tv = { 10, 0 }; if (!(ie->mask & IN_CREATE)) { process_inotify_file(wi, path, ie); return; } DPRINTF(E_INFO, L_SCAN, "Deferring scan of newly created file %s\n", path); ie->mask = IN_CLOSE_WRITE; f = calloc(1, sizeof(struct deferred_file)); f->wi = *wi; f->wi.path = strdup(wi->path); /* ie->name not copied here, so don't use in process_inotify_* */ f->ie = *ie; strcpy(f->path, path); f->next = filestack; filestack = f; event_add(deferred_inoev, &tv); } #endif /* Thread: scan */ static void inotify_cb(int fd, short event, void *arg) { struct inotify_event *ie; struct watch_info wi; uint8_t *buf; uint8_t *ptr; char path[PATH_MAX]; int size; int namelen; int ret; /* Determine the amount of bytes to read from inotify */ ret = ioctl(fd, FIONREAD, &size); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Could not determine inotify queue size: %s\n", strerror(errno)); return; } buf = malloc(size); if (!buf) { DPRINTF(E_LOG, L_SCAN, "Could not allocate %d bytes for inotify events\n", size); return; } ret = read(fd, buf, size); if (ret < 0 || ret != size) { DPRINTF(E_LOG, L_SCAN, "inotify read failed: %s (ret was %d, size %d)\n", strerror(errno), ret, size); free(buf); return; } for (ptr = buf; ptr < buf + size; ptr += ie->len + sizeof(struct inotify_event)) { ie = (struct inotify_event *)ptr; memset(&wi, 0, sizeof(struct watch_info)); /* ie[0] contains the inotify event information * the memory space for ie[1+] contains the name of the file * see the inotify documentation */ wi.wd = ie->wd; ret = db_watch_get_bywd(&wi); if (ret < 0) { if (!(ie->mask & IN_IGNORED)) DPRINTF(E_LOG, L_SCAN, "No matching watch found, ignoring event (0x%x)\n", ie->mask); continue; } if (ie->mask & IN_IGNORED) { DPRINTF(E_DBG, L_SCAN, "%s deleted or backing filesystem unmounted!\n", wi.path); db_watch_delete_bywd(ie->wd); free(wi.path); continue; } path[0] = '\0'; ret = snprintf(path, PATH_MAX, "%s", wi.path); if ((ret < 0) || (ret >= PATH_MAX)) { DPRINTF(E_LOG, L_SCAN, "Skipping event under %s, PATH_MAX exceeded\n", wi.path); free(wi.path); continue; } if (ie->len > 0) { namelen = PATH_MAX - ret; ret = snprintf(path + ret, namelen, "/%s", ie->name); if ((ret < 0) || (ret >= namelen)) { DPRINTF(E_LOG, L_SCAN, "Skipping %s/%s, PATH_MAX exceeded\n", wi.path, ie->name); free(wi.path); continue; } } /* ie->len == 0 catches events on the subject of the watch itself. * As we only watch directories, this catches directories. * General watch events like IN_UNMOUNT and IN_IGNORED do not come * with the IN_ISDIR flag set. */ if ((ie->mask & IN_ISDIR) || (ie->len == 0)) process_inotify_dir(&wi, path, ie); else #ifdef __linux__ process_inotify_file(&wi, path, ie); #else process_inotify_file_defer(&wi, path, ie); #endif free(wi.path); } free(buf); event_add(inoev, NULL); } /* Thread: main & scan */ static int inofd_event_set(void) { inofd = inotify_init1(IN_CLOEXEC); if (inofd < 0) { DPRINTF(E_FATAL, L_SCAN, "Could not create inotify fd: %s\n", strerror(errno)); return -1; } inoev = event_new(evbase_lib, inofd, EV_READ, inotify_cb, NULL); #ifndef __linux__ deferred_inoev = evtimer_new(evbase_lib, inotify_deferred_cb, NULL); if (!deferred_inoev) { DPRINTF(E_LOG, L_SCAN, "Could not create deferred inotify event\n"); return -1; } #endif return 0; } /* Thread: main & scan */ static void inofd_event_unset(void) { #ifndef __linux__ event_free(deferred_inoev); #endif event_free(inoev); close(inofd); } /* Thread: scan */ static int filescanner_initscan() { int ret; ret = db_watch_clear(); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Error: could not clear old watches from DB\n"); return -1; } if (cfg_getbool(cfg_getsec(cfg, "library"), "filescan_disable")) bulk_scan(F_SCAN_BULK | F_SCAN_FAST); else bulk_scan(F_SCAN_BULK); if (!library_is_exiting()) { /* Enable inotify */ event_add(inoev, NULL); } return 0; } static int filescanner_rescan() { DPRINTF(E_LOG, L_SCAN, "Startup rescan triggered\n"); inofd_event_unset(); // Clears all inotify watches db_watch_clear(); inofd_event_set(); bulk_scan(F_SCAN_BULK | F_SCAN_RESCAN); return 0; } static int filescanner_fullrescan() { DPRINTF(E_LOG, L_SCAN, "Full rescan triggered\n"); inofd_event_unset(); // Clears all inotify watches inofd_event_set(); bulk_scan(F_SCAN_BULK); return 0; } static int scan_metadata(const char *path, struct media_file_info *mfi) { int ret; if (strncasecmp(path, "http://", strlen("http://")) == 0) { memset(mfi, 0, sizeof(struct media_file_info)); mfi->path = strdup(path); mfi->fname = strdup(filename_from_path(mfi->path)); mfi->data_kind = DATA_KIND_HTTP; mfi->directory_id = DIR_HTTP; ret = scan_metadata_ffmpeg(path, mfi); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Playlist URL '%s' is unavailable for probe/metadata, assuming MP3 encoding\n", path); mfi->type = strdup("mp3"); mfi->codectype = strdup("mpeg"); mfi->description = strdup("MPEG audio file"); } return LIBRARY_OK; } return LIBRARY_PATH_INVALID; } /* Thread: main */ static int filescanner_init(void) { int ret; ret = inofd_event_set(); if (ret < 0) { return -1; } return 0; } /* Thread: main */ static void filescanner_deinit(void) { inofd_event_unset(); } struct library_source filescanner = { .name = "filescanner", .disabled = 0, .init = filescanner_init, .deinit = filescanner_deinit, .initscan = filescanner_initscan, .rescan = filescanner_rescan, .fullrescan = filescanner_fullrescan, .scan_metadata = scan_metadata, }; forked-daapd-25.0/src/library/filescanner.h000066400000000000000000000006521313447753700206770ustar00rootroot00000000000000 #ifndef __FILESCANNER_H__ #define __FILESCANNER_H__ #include "db.h" /* Actual scanners */ int scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi); void scan_playlist(char *file, time_t mtime, int dir_id); void scan_smartpl(char *file, time_t mtime, int dir_id); #ifdef ITUNES void scan_itunes_itml(char *file); #endif const char * filename_from_path(const char *path); #endif /* !__FILESCANNER_H__ */ forked-daapd-25.0/src/library/filescanner_ffmpeg.c000066400000000000000000000662611313447753700222260ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include "db.h" #include "logger.h" #include "misc.h" #include "http.h" /* Mapping between the metadata name(s) and the offset * of the equivalent metadata field in struct media_file_info */ struct metadata_map { char *key; int as_int; size_t offset; int (*handler_function)(struct media_file_info *, char *); }; // Used for passing errors to DPRINTF (can't count on av_err2str being present) static char errbuf[64]; static inline char * err2str(int errnum) { av_strerror(errnum, errbuf, sizeof(errbuf)); return errbuf; } static int parse_slash_separated_ints(char *string, uint32_t *firstval, uint32_t *secondval) { int numvals = 0; char *ptr; ptr = strchr(string, '/'); if (ptr) { *ptr = '\0'; if (safe_atou32(ptr + 1, secondval) == 0) numvals++; } if (safe_atou32(string, firstval) == 0) numvals++; return numvals; } static int parse_track(struct media_file_info *mfi, char *track_string) { uint32_t *track = (uint32_t *) ((char *) mfi + mfi_offsetof(track)); uint32_t *total_tracks = (uint32_t *) ((char *) mfi + mfi_offsetof(total_tracks)); return parse_slash_separated_ints(track_string, track, total_tracks); } static int parse_disc(struct media_file_info *mfi, char *disc_string) { uint32_t *disc = (uint32_t *) ((char *) mfi + mfi_offsetof(disc)); uint32_t *total_discs = (uint32_t *) ((char *) mfi + mfi_offsetof(total_discs)); return parse_slash_separated_ints(disc_string, disc, total_discs); } static int parse_date(struct media_file_info *mfi, char *date_string) { char year_string[21]; uint32_t *year = (uint32_t *) ((char *) mfi + mfi_offsetof(year)); uint32_t *date_released = (uint32_t *) ((char *) mfi + mfi_offsetof(date_released)); struct tm tm = { 0 }; int ret = 0; if ((*year == 0) && (safe_atou32(date_string, year) == 0)) ret++; if ( strptime(date_string, "%FT%T%z", &tm) // ISO 8601, %F=%Y-%m-%d, %T=%H:%M:%S || strptime(date_string, "%F %T", &tm) || strptime(date_string, "%F %H:%M", &tm) || strptime(date_string, "%F", &tm) ) { *date_released = (uint32_t)mktime(&tm); ret++; } if ((*date_released == 0) && (*year != 0)) { snprintf(year_string, sizeof(year_string), "%" PRIu32 "-01-01T12:00:00", *year); if (strptime(year_string, "%FT%T", &tm)) { *date_released = (uint32_t)mktime(&tm); ret++; } } return ret; } /* Lookup is case-insensitive, first occurrence takes precedence */ static const struct metadata_map md_map_generic[] = { { "title", 0, mfi_offsetof(title), NULL }, { "artist", 0, mfi_offsetof(artist), NULL }, { "author", 0, mfi_offsetof(artist), NULL }, { "album_artist", 0, mfi_offsetof(album_artist), NULL }, { "album", 0, mfi_offsetof(album), NULL }, { "genre", 0, mfi_offsetof(genre), NULL }, { "composer", 0, mfi_offsetof(composer), NULL }, { "grouping", 0, mfi_offsetof(grouping), NULL }, { "orchestra", 0, mfi_offsetof(orchestra), NULL }, { "conductor", 0, mfi_offsetof(conductor), NULL }, { "comment", 0, mfi_offsetof(comment), NULL }, { "description", 0, mfi_offsetof(comment), NULL }, { "track", 1, mfi_offsetof(track), parse_track }, { "disc", 1, mfi_offsetof(disc), parse_disc }, { "year", 1, mfi_offsetof(year), NULL }, { "date", 1, mfi_offsetof(date_released), parse_date }, { "title-sort", 0, mfi_offsetof(title_sort), NULL }, { "artist-sort", 0, mfi_offsetof(artist_sort), NULL }, { "album-sort", 0, mfi_offsetof(album_sort), NULL }, { "compilation", 1, mfi_offsetof(compilation), NULL }, { NULL, 0, 0, NULL } }; static const struct metadata_map md_map_tv[] = { { "stik", 1, mfi_offsetof(media_kind), NULL }, { "show", 0, mfi_offsetof(tv_series_name), NULL }, { "episode_id", 0, mfi_offsetof(tv_episode_num_str), NULL }, { "network", 0, mfi_offsetof(tv_network_name), NULL }, { "episode_sort", 1, mfi_offsetof(tv_episode_sort), NULL }, { "season_number",1, mfi_offsetof(tv_season_num), NULL }, { NULL, 0, 0, NULL } }; /* NOTE about VORBIS comments: * Only a small set of VORBIS comment fields are officially designated. Most * common tags are at best de facto standards. Currently, metadata conversion * functionality in ffmpeg only adds support for a couple of tags. Specifically, * ALBUMARTIST and TRACKNUMBER are handled as of Feb 1, 2010 (rev 21587). Tags * with names that already match the generic ffmpeg scheme--TITLE and ARTIST, * for example--are of course handled. The rest of these tags are reported to * have been used by various programs in the wild. */ static const struct metadata_map md_map_vorbis[] = { { "albumartist", 0, mfi_offsetof(album_artist), NULL }, { "album artist", 0, mfi_offsetof(album_artist), NULL }, { "tracknumber", 1, mfi_offsetof(track), NULL }, { "tracktotal", 1, mfi_offsetof(total_tracks), NULL }, { "totaltracks", 1, mfi_offsetof(total_tracks), NULL }, { "discnumber", 1, mfi_offsetof(disc), NULL }, { "disctotal", 1, mfi_offsetof(total_discs), NULL }, { "totaldiscs", 1, mfi_offsetof(total_discs), NULL }, { NULL, 0, 0, NULL } }; /* NOTE about ID3 tag names: * metadata conversion for ID3v2 tags was added in ffmpeg in september 2009 * (rev 20073) for ID3v2.3; support for ID3v2.2 tag names was added in december * 2009 (rev 20839). * * ID3v2.x tags will be removed from the map once a version of ffmpeg containing * the changes listed above will be generally available. The more entries in the * map, the slower the filescanner gets. */ static const struct metadata_map md_map_id3[] = { { "TT2", 0, mfi_offsetof(title), NULL }, /* ID3v2.2 */ { "TIT2", 0, mfi_offsetof(title), NULL }, /* ID3v2.3 */ { "TP1", 0, mfi_offsetof(artist), NULL }, /* ID3v2.2 */ { "TPE1", 0, mfi_offsetof(artist), NULL }, /* ID3v2.3 */ { "TP2", 0, mfi_offsetof(album_artist), NULL }, /* ID3v2.2 */ { "TPE2", 0, mfi_offsetof(album_artist), NULL }, /* ID3v2.3 */ { "TAL", 0, mfi_offsetof(album), NULL }, /* ID3v2.2 */ { "TALB", 0, mfi_offsetof(album), NULL }, /* ID3v2.3 */ { "TCO", 0, mfi_offsetof(genre), NULL }, /* ID3v2.2 */ { "TCON", 0, mfi_offsetof(genre), NULL }, /* ID3v2.3 */ { "TCM", 0, mfi_offsetof(composer), NULL }, /* ID3v2.2 */ { "TCOM", 0, mfi_offsetof(composer), NULL }, /* ID3v2.3 */ { "TRK", 1, mfi_offsetof(track), parse_track }, /* ID3v2.2 */ { "TRCK", 1, mfi_offsetof(track), parse_track }, /* ID3v2.3 */ { "TPA", 1, mfi_offsetof(disc), parse_disc }, /* ID3v2.2 */ { "TPOS", 1, mfi_offsetof(disc), parse_disc }, /* ID3v2.3 */ { "TYE", 1, mfi_offsetof(year), NULL }, /* ID3v2.2 */ { "TYER", 1, mfi_offsetof(year), NULL }, /* ID3v2.3 */ { "TDA", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.2 */ { "TDAT", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.3 */ { "TDR", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.2 */ { "TDRL", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.4 */ { "TSOA", 0, mfi_offsetof(album_sort), NULL }, /* ID3v2.4 */ { "XSOA", 0, mfi_offsetof(album_sort), NULL }, /* ID3v2.3 */ { "TSOP", 0, mfi_offsetof(artist_sort), NULL }, /* ID3v2.4 */ { "XSOP", 0, mfi_offsetof(artist_sort), NULL }, /* ID3v2.3 */ { "TSOT", 0, mfi_offsetof(title_sort), NULL }, /* ID3v2.4 */ { "XSOT", 0, mfi_offsetof(title_sort), NULL }, /* ID3v2.3 */ { "TS2", 0, mfi_offsetof(album_artist_sort), NULL }, /* ID3v2.2 */ { "TSO2", 0, mfi_offsetof(album_artist_sort), NULL }, /* ID3v2.3 */ { "ALBUMARTISTSORT", 0, mfi_offsetof(album_artist_sort), NULL }, /* ID3v2.x */ { "TSC", 0, mfi_offsetof(composer_sort), NULL }, /* ID3v2.2 */ { "TSOC", 0, mfi_offsetof(composer_sort), NULL }, /* ID3v2.3 */ { NULL, 0, 0, NULL } }; static int #if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5) extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struct metadata_map *md_map) #else extract_metadata_core(struct media_file_info *mfi, AVMetadata *md, const struct metadata_map *md_map) #endif { #if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5) AVDictionaryEntry *mdt; #else AVMetadataTag *mdt; #endif char **strval; uint32_t *intval; int mdcount; int i; int ret; #if 0 /* Dump all the metadata reported by ffmpeg */ mdt = NULL; #if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5) while ((mdt = av_dict_get(md, "", mdt, AV_DICT_IGNORE_SUFFIX)) != NULL) #else while ((mdt = av_metadata_get(md, "", mdt, AV_METADATA_IGNORE_SUFFIX)) != NULL) #endif fprintf(stderr, " -> %s = %s\n", mdt->key, mdt->value); #endif mdcount = 0; /* Extract actual metadata */ for (i = 0; md_map[i].key != NULL; i++) { #if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5) mdt = av_dict_get(md, md_map[i].key, NULL, 0); #else mdt = av_metadata_get(md, md_map[i].key, NULL, 0); #endif if (mdt == NULL) continue; if ((mdt->value == NULL) || (strlen(mdt->value) == 0)) continue; if (md_map[i].handler_function) { mdcount += md_map[i].handler_function(mfi, mdt->value); continue; } mdcount++; if (!md_map[i].as_int) { strval = (char **) ((char *) mfi + md_map[i].offset); if (*strval == NULL) *strval = strdup(mdt->value); } else { intval = (uint32_t *) ((char *) mfi + md_map[i].offset); if (*intval == 0) { ret = safe_atou32(mdt->value, intval); if (ret < 0) continue; } } } return mdcount; } static int extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *audio_stream, AVStream *video_stream, const struct metadata_map *md_map) { int mdcount; int ret; mdcount = 0; if (ctx->metadata) { ret = extract_metadata_core(mfi, ctx->metadata, md_map); mdcount += ret; DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from file metadata\n", ret); } if (audio_stream->metadata) { ret = extract_metadata_core(mfi, audio_stream->metadata, md_map); mdcount += ret; DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from audio stream metadata\n", ret); } if (video_stream && video_stream->metadata) { ret = extract_metadata_core(mfi, video_stream->metadata, md_map); mdcount += ret; DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from video stream metadata\n", ret); } return mdcount; } /* * Fills metadata read with ffmpeg/libav from the given path into the given mfi * * Following attributes from the given mfi are read to control how to read metadata: * - data_kind: if data_kind is http, icy metadata is used, if the path points to a playlist the first stream-uri in that playlist is used * - media_kind: if media_kind is podcast or audiobook, video streams in the file are ignored * - compilation: like podcast/audiobook video streams are ignored for compilations * - file_size: if bitrate could not be read through ffmpeg/libav, file_size is used to estimate the bitrate * - fname: (filename) used as fallback for artist */ int scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi) { AVFormatContext *ctx; AVDictionary *options; const struct metadata_map *extra_md_map; struct http_icy_metadata *icy_metadata; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) enum AVCodecID codec_id; enum AVCodecID video_codec_id; enum AVCodecID audio_codec_id; #else enum CodecID codec_id; enum CodecID video_codec_id; enum CodecID audio_codec_id; #endif AVStream *video_stream; AVStream *audio_stream; char *path; int mdcount; int i; int ret; ctx = NULL; options = NULL; path = strdup(file); #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3) if (mfi->data_kind == DATA_KIND_HTTP) { # ifndef HAVE_FFMPEG // Without this, libav is slow to probe some internet streams ctx = avformat_alloc_context(); ctx->probesize = 64000; # endif free(path); ret = http_stream_setup(&path, file); if (ret < 0) return -1; av_dict_set(&options, "icy", "1", 0); mfi->artwork = ARTWORK_HTTP; } ret = avformat_open_input(&ctx, path, NULL, &options); if (options) av_dict_free(&options); #else ret = av_open_input_file(&ctx, path, NULL, 0, NULL); #endif if (ret != 0) { DPRINTF(E_WARN, L_SCAN, "Cannot open media file '%s': %s\n", path, err2str(ret)); free(path); return -1; } free(path); #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3) ret = avformat_find_stream_info(ctx, NULL); #else ret = av_find_stream_info(ctx); #endif if (ret < 0) { DPRINTF(E_WARN, L_SCAN, "Cannot get stream info of '%s': %s\n", path, err2str(ret)); #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21) avformat_close_input(&ctx); #else av_close_input_file(ctx); #endif return -1; } #if 0 /* Dump input format as determined by ffmpeg */ av_dump_format(ctx, 0, file, 0); #endif DPRINTF(E_DBG, L_SCAN, "File has %d streams\n", ctx->nb_streams); /* Extract codec IDs, check for video */ #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) video_codec_id = AV_CODEC_ID_NONE; video_stream = NULL; audio_codec_id = AV_CODEC_ID_NONE; audio_stream = NULL; #else video_codec_id = CODEC_ID_NONE; video_stream = NULL; audio_codec_id = CODEC_ID_NONE; audio_stream = NULL; #endif for (i = 0; i < ctx->nb_streams; i++) { switch (ctx->streams[i]->codec->codec_type) { #if LIBAVCODEC_VERSION_MAJOR >= 53 || (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR >= 64) case AVMEDIA_TYPE_VIDEO: #else case CODEC_TYPE_VIDEO: #endif #if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6) if (ctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) { DPRINTF(E_DBG, L_SCAN, "Found embedded artwork (stream %d)\n", i); mfi->artwork = ARTWORK_EMBEDDED; break; } #endif // We treat these as audio no matter what if (mfi->compilation || (mfi->media_kind & (MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK))) break; if (!video_stream) { DPRINTF(E_DBG, L_SCAN, "File has video (stream %d)\n", i); mfi->has_video = 1; video_stream = ctx->streams[i]; video_codec_id = video_stream->codec->codec_id; } break; #if LIBAVCODEC_VERSION_MAJOR >= 53 || (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR >= 64) case AVMEDIA_TYPE_AUDIO: #else case CODEC_TYPE_AUDIO: #endif if (!audio_stream) { audio_stream = ctx->streams[i]; audio_codec_id = audio_stream->codec->codec_id; } break; default: break; } } #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) if (audio_codec_id == AV_CODEC_ID_NONE) #else if (audio_codec_id == CODEC_ID_NONE) #endif { DPRINTF(E_DBG, L_SCAN, "File has no audio streams, discarding\n"); #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21) avformat_close_input(&ctx); #else av_close_input_file(ctx); #endif return -1; } /* Common media information */ if (ctx->duration > 0) mfi->song_length = ctx->duration / (AV_TIME_BASE / 1000); /* ms */ if (ctx->bit_rate > 0) mfi->bitrate = ctx->bit_rate / 1000; else if (ctx->duration > AV_TIME_BASE) /* guesstimate */ mfi->bitrate = ((mfi->file_size * 8) / (ctx->duration / AV_TIME_BASE)) / 1000; DPRINTF(E_DBG, L_SCAN, "Duration %d ms, bitrate %d kbps\n", mfi->song_length, mfi->bitrate); /* Try to extract ICY metadata if http stream */ if (mfi->data_kind == DATA_KIND_HTTP) { icy_metadata = http_icy_metadata_get(ctx, 0); if (icy_metadata && icy_metadata->name) { DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, name is '%s'\n", icy_metadata->name); if (mfi->title) free(mfi->title); if (mfi->artist) free(mfi->artist); if (mfi->album_artist) free(mfi->album_artist); mfi->title = strdup(icy_metadata->name); mfi->artist = strdup(icy_metadata->name); mfi->album_artist = strdup(icy_metadata->name); } if (icy_metadata && icy_metadata->description) { DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, description is '%s'\n", icy_metadata->description); if (mfi->album) free(mfi->album); mfi->album = strdup(icy_metadata->description); } if (icy_metadata && icy_metadata->genre) { DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, genre is '%s'\n", icy_metadata->genre); if (mfi->genre) free(mfi->genre); mfi->genre = strdup(icy_metadata->genre); } if (icy_metadata) http_icy_metadata_free(icy_metadata, 0); } /* Get some more information on the audio stream */ if (audio_stream) { if (audio_stream->codec->sample_rate != 0) mfi->samplerate = audio_stream->codec->sample_rate; /* Try sample format first */ #if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 4) mfi->bits_per_sample = 8 * av_get_bytes_per_sample(audio_stream->codec->sample_fmt); #elif LIBAVCODEC_VERSION_MAJOR >= 53 mfi->bits_per_sample = av_get_bits_per_sample_fmt(audio_stream->codec->sample_fmt); #else mfi->bits_per_sample = av_get_bits_per_sample_format(audio_stream->codec->sample_fmt); #endif if (mfi->bits_per_sample == 0) { /* Try codec */ mfi->bits_per_sample = av_get_bits_per_sample(audio_codec_id); } DPRINTF(E_DBG, L_SCAN, "samplerate %d, bps %d\n", mfi->samplerate, mfi->bits_per_sample); } /* Check codec */ extra_md_map = NULL; codec_id = (mfi->has_video) ? video_codec_id : audio_codec_id; switch (codec_id) { #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_AAC: #else case CODEC_ID_AAC: #endif DPRINTF(E_DBG, L_SCAN, "AAC\n"); mfi->type = strdup("m4a"); mfi->codectype = strdup("mp4a"); mfi->description = strdup("AAC audio file"); break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_ALAC: #else case CODEC_ID_ALAC: #endif DPRINTF(E_DBG, L_SCAN, "ALAC\n"); mfi->type = strdup("m4a"); mfi->codectype = strdup("alac"); mfi->description = strdup("Apple Lossless audio file"); break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_FLAC: #else case CODEC_ID_FLAC: #endif DPRINTF(E_DBG, L_SCAN, "FLAC\n"); mfi->type = strdup("flac"); mfi->codectype = strdup("flac"); mfi->description = strdup("FLAC audio file"); extra_md_map = md_map_vorbis; break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_APE: #else case CODEC_ID_APE: #endif DPRINTF(E_DBG, L_SCAN, "APE\n"); mfi->type = strdup("ape"); mfi->codectype = strdup("ape"); mfi->description = strdup("Monkey's audio"); break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_MUSEPACK7: case AV_CODEC_ID_MUSEPACK8: #else case CODEC_ID_MUSEPACK7: case CODEC_ID_MUSEPACK8: #endif DPRINTF(E_DBG, L_SCAN, "Musepack\n"); mfi->type = strdup("mpc"); mfi->codectype = strdup("mpc"); mfi->description = strdup("Musepack audio file"); break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_MPEG4: /* Video */ case AV_CODEC_ID_H264: #else case CODEC_ID_MPEG4: /* Video */ case CODEC_ID_H264: #endif DPRINTF(E_DBG, L_SCAN, "MPEG4 video\n"); mfi->type = strdup("m4v"); mfi->codectype = strdup("mp4v"); mfi->description = strdup("MPEG-4 video file"); extra_md_map = md_map_tv; break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_MP3: #else case CODEC_ID_MP3: #endif DPRINTF(E_DBG, L_SCAN, "MP3\n"); mfi->type = strdup("mp3"); mfi->codectype = strdup("mpeg"); mfi->description = strdup("MPEG audio file"); extra_md_map = md_map_id3; break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_VORBIS: #else case CODEC_ID_VORBIS: #endif DPRINTF(E_DBG, L_SCAN, "VORBIS\n"); mfi->type = strdup("ogg"); mfi->codectype = strdup("ogg"); mfi->description = strdup("Ogg Vorbis audio file"); extra_md_map = md_map_vorbis; break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_WMAV1: case AV_CODEC_ID_WMAV2: case AV_CODEC_ID_WMAVOICE: #else case CODEC_ID_WMAV1: case CODEC_ID_WMAV2: case CODEC_ID_WMAVOICE: #endif DPRINTF(E_DBG, L_SCAN, "WMA Voice\n"); mfi->type = strdup("wma"); mfi->codectype = strdup("wmav"); mfi->description = strdup("WMA audio file"); break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_WMAPRO: #else case CODEC_ID_WMAPRO: #endif DPRINTF(E_DBG, L_SCAN, "WMA Pro\n"); mfi->type = strdup("wmap"); mfi->codectype = strdup("wma"); mfi->description = strdup("WMA audio file"); break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_WMALOSSLESS: #else case CODEC_ID_WMALOSSLESS: #endif DPRINTF(E_DBG, L_SCAN, "WMA Lossless\n"); mfi->type = strdup("wma"); mfi->codectype = strdup("wmal"); mfi->description = strdup("WMA audio file"); break; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) case AV_CODEC_ID_PCM_S16LE ... AV_CODEC_ID_PCM_F64LE: #else case CODEC_ID_PCM_S16LE ... CODEC_ID_PCM_F64LE: #endif if (strcmp(ctx->iformat->name, "aiff") == 0) { DPRINTF(E_DBG, L_SCAN, "AIFF\n"); mfi->type = strdup("aif"); mfi->codectype = strdup("aif"); mfi->description = strdup("AIFF audio file"); break; } else if (strcmp(ctx->iformat->name, "wav") == 0) { DPRINTF(E_DBG, L_SCAN, "WAV\n"); mfi->type = strdup("wav"); mfi->codectype = strdup("wav"); mfi->description = strdup("WAV audio file"); break; } /* WARNING: will fallthrough to default case, don't move */ /* FALLTHROUGH */ default: DPRINTF(E_DBG, L_SCAN, "Unknown codec 0x%x (video: %s), format %s (%s)\n", codec_id, (mfi->has_video) ? "yes" : "no", ctx->iformat->name, ctx->iformat->long_name); mfi->type = strdup("unkn"); mfi->codectype = strdup("unkn"); if (mfi->has_video) { mfi->description = strdup("Unknown video file format"); extra_md_map = md_map_tv; } else mfi->description = strdup("Unknown audio file format"); break; } mdcount = 0; if ((!ctx->metadata) && (!audio_stream->metadata) && (video_stream && !video_stream->metadata)) { DPRINTF(E_WARN, L_SCAN, "ffmpeg reports no metadata\n"); goto skip_extract; } if (extra_md_map) { ret = extract_metadata(mfi, ctx, audio_stream, video_stream, extra_md_map); mdcount += ret; DPRINTF(E_DBG, L_SCAN, "Picked up %d tags with extra md_map\n", ret); } ret = extract_metadata(mfi, ctx, audio_stream, video_stream, md_map_generic); mdcount += ret; DPRINTF(E_DBG, L_SCAN, "Picked up %d tags with generic md_map, %d tags total\n", ret, mdcount); /* fix up TV metadata */ if (mfi->media_kind == 10) { /* I have no idea why this is, but iTunes reports a media kind of 64 for stik==10 (?!) */ mfi->media_kind = MEDIA_KIND_TVSHOW; } /* Unspecified video files are "Movies", media_kind 2 */ else if (mfi->has_video == 1) { mfi->media_kind = MEDIA_KIND_MOVIE; } skip_extract: #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21) avformat_close_input(&ctx); #else av_close_input_file(ctx); #endif if (mdcount == 0) DPRINTF(E_WARN, L_SCAN, "ffmpeg/libav could not extract any metadata\n"); /* Just in case there's no title set ... */ if (mfi->title == NULL) mfi->title = strdup(mfi->fname); /* All done */ return 0; } forked-daapd-25.0/src/library/filescanner_itunes.c000066400000000000000000000453501313447753700222650ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "db.h" #include "conffile.h" #include "misc.h" /* Mapping between iTunes library IDs and our DB IDs using a "hash" table of * size ID_MAP_SIZE */ #define ID_MAP_SIZE 16384 struct itml_to_db_map { uint64_t itml_id; uint32_t db_id; struct itml_to_db_map *next; }; struct itml_to_db_map **id_map; /* Mapping between iTunes library metadata keys and the offset * of the equivalent metadata field in struct media_file_info */ struct metadata_map { char *key; plist_type type; size_t offset; }; static struct metadata_map md_map[] = { { "Name", PLIST_STRING, mfi_offsetof(title) }, { "Artist", PLIST_STRING, mfi_offsetof(artist) }, { "Album Artist", PLIST_STRING, mfi_offsetof(album_artist) }, { "Composer", PLIST_STRING, mfi_offsetof(composer) }, { "Album", PLIST_STRING, mfi_offsetof(album) }, { "Genre", PLIST_STRING, mfi_offsetof(genre) }, { "Comments", PLIST_STRING, mfi_offsetof(comment) }, { "Track Count", PLIST_UINT, mfi_offsetof(total_tracks) }, { "Track Number", PLIST_UINT, mfi_offsetof(track) }, { "Disc Count", PLIST_UINT, mfi_offsetof(total_discs) }, { "Disc Number", PLIST_UINT, mfi_offsetof(disc) }, { "Year", PLIST_UINT, mfi_offsetof(year) }, { "Total Time", PLIST_UINT, mfi_offsetof(song_length) }, { "Bit Rate", PLIST_UINT, mfi_offsetof(bitrate) }, { "Sample Rate", PLIST_UINT, mfi_offsetof(samplerate) }, { "BPM", PLIST_UINT, mfi_offsetof(bpm) }, { "Rating", PLIST_UINT, mfi_offsetof(rating) }, { "Compilation", PLIST_BOOLEAN, mfi_offsetof(compilation) }, { "Date Added", PLIST_DATE, mfi_offsetof(time_added) }, { NULL, 0, 0 } }; static void id_map_free(void) { struct itml_to_db_map *map; int i; for (i = 0; i < ID_MAP_SIZE; i++) { if (!id_map[i]) continue; for (map = id_map[i]; id_map[i]; map = id_map[i]) { id_map[i] = map->next; free(map); } } free(id_map); } /* Inserts a linked list item into "hash" position in the id_table */ static int id_map_add(uint64_t itml_id, uint32_t db_id) { struct itml_to_db_map *new_map; struct itml_to_db_map *cur_map; int i; new_map = malloc(sizeof(struct itml_to_db_map)); if (!new_map) return -1; new_map->itml_id = itml_id; new_map->db_id = db_id; i = itml_id % ID_MAP_SIZE; cur_map = id_map[i]; new_map->next = cur_map; id_map[i] = new_map; return 0; } static uint32_t id_map_get(uint64_t itml_id) { struct itml_to_db_map *map; int i; i = itml_id % ID_MAP_SIZE; for (map = id_map[i]; map; map = map->next) { if (itml_id == map->itml_id) return map->db_id; } return 0; } /* plist helpers */ static int get_dictval_int_from_key(plist_t dict, const char *key, uint64_t *val) { plist_t node; node = plist_dict_get_item(dict, key); if (!node) return -1; if (plist_get_node_type(node) != PLIST_UINT) return -1; plist_get_uint_val(node, val); return 0; } static int get_dictval_date_from_key(plist_t dict, const char *key, uint32_t *val) { plist_t node; int32_t secs; int32_t dummy; node = plist_dict_get_item(dict, key); if (!node) return -1; if (plist_get_node_type(node) != PLIST_DATE) return -1; plist_get_date_val(node, &secs, &dummy); *val = (uint32_t) secs; return 0; } static int get_dictval_bool_from_key(plist_t dict, const char *key, uint8_t *val) { plist_t node; node = plist_dict_get_item(dict, key); /* Not present means false */ if (!node) { *val = 0; return 0; } if (plist_get_node_type(node) != PLIST_BOOLEAN) return -1; plist_get_bool_val(node, val); return 0; } static int get_dictval_string_from_key(plist_t dict, const char *key, char **val) { plist_t node; node = plist_dict_get_item(dict, key); if (!node) return -1; if (plist_get_node_type(node) != PLIST_STRING) return -1; plist_get_string_val(node, val); return 0; } static int get_dictval_dict_from_key(plist_t dict, const char *key, plist_t *val) { plist_t node; node = plist_dict_get_item(dict, key); if (!node) return -1; if (plist_get_node_type(node) != PLIST_DICT) return -1; *val = node; return 0; } static int get_dictval_array_from_key(plist_t dict, const char *key, plist_t *val) { plist_t node; node = plist_dict_get_item(dict, key); if (!node) return -1; if (plist_get_node_type(node) != PLIST_ARRAY) return -1; *val = node; return 0; } /* We don't actually check anything (yet) despite the name */ static int check_meta(plist_t dict) { char *appver; char *folder; uint64_t major; uint64_t minor; int ret; ret = get_dictval_int_from_key(dict, "Major Version", &major); if (ret < 0) return -1; ret = get_dictval_int_from_key(dict, "Minor Version", &minor); if (ret < 0) return -1; ret = get_dictval_string_from_key(dict, "Application Version", &appver); if (ret < 0) return -1; ret = get_dictval_string_from_key(dict, "Music Folder", &folder); if (ret < 0) { free(appver); return -1; } DPRINTF(E_INFO, L_SCAN, "iTunes XML playlist Major:%" PRIu64 " Minor:%" PRIu64 " Application:%s Folder:%s\n", major, minor, appver, folder); free(appver); free(folder); return 0; } static int find_track_file(char *location) { int ret; int plen; int mfi_id; char *entry; char *ptr; location = evhttp_decode_uri(location); if (!location) { DPRINTF(E_LOG, L_SCAN, "Could not decode iTunes XML playlist url.\n"); return 0; } plen = strlen("file://"); /* Not a local file ... */ if (strncmp(location, "file://", plen) != 0) return 0; /* Now search for the library item where the path has closest match to playlist item */ /* Succes is when we find an unambiguous match, or when we no longer can expand the */ /* the path to refine our search. */ entry = NULL; do { ptr = strrchr(location, '/'); if (entry) *(entry - 1) = '/'; if (ptr) { *ptr = '\0'; entry = ptr + 1; } else entry = location; DPRINTF(E_SPAM, L_SCAN, "iTunes XML playlist entry is now %s\n", entry); ret = db_files_get_count_bymatch(entry); } while (ptr && (ret > 1)); if (ret > 0) { mfi_id = db_file_id_bymatch(entry); DPRINTF(E_DBG, L_SCAN, "Found iTunes XML playlist entry match, id is %d, entry is %s\n", mfi_id, entry); free(location); return mfi_id; } else { DPRINTF(E_DBG, L_SCAN, "No match for iTunes XML playlist entry %s\n", entry); free(location); return 0; } } static int process_track_file(plist_t trk) { char *location; struct media_file_info *mfi; char *string; uint64_t integer; char **strval; uint32_t *intval; char *chrval; uint8_t boolean; int mfi_id; int i; int ret; ret = get_dictval_string_from_key(trk, "Location", &location); if (ret < 0) { DPRINTF(E_WARN, L_SCAN, "Track type File with no Location\n"); return 0; } mfi_id = find_track_file(location); if (mfi_id <= 0) { DPRINTF(E_INFO, L_SCAN, "Could not match location '%s' to any known file\n", location); free(location); return 0; } free(location); if (!cfg_getbool(cfg_getsec(cfg, "library"), "itunes_overrides")) return mfi_id; /* Override our metadata with what's provided by iTunes */ mfi = db_file_fetch_byid(mfi_id); if (!mfi) { DPRINTF(E_LOG, L_SCAN, "Could not retrieve file info for file id %d\n", mfi_id); return mfi_id; } for (i = 0; md_map[i].key != NULL; i++) { switch (md_map[i].type) { case PLIST_UINT: ret = get_dictval_int_from_key(trk, md_map[i].key, &integer); if (ret < 0) break; intval = (uint32_t *) ((char *) mfi + md_map[i].offset); *intval = (uint32_t)integer; break; case PLIST_STRING: ret = get_dictval_string_from_key(trk, md_map[i].key, &string); if (ret < 0) break; strval = (char **) ((char *) mfi + md_map[i].offset); if (*strval) free(*strval); *strval = string; break; case PLIST_BOOLEAN: ret = get_dictval_bool_from_key(trk, md_map[i].key, &boolean); if (ret < 0) break; chrval = (char *) mfi + md_map[i].offset; *chrval = boolean; break; case PLIST_DATE: intval = (uint32_t *) ((char *) mfi + md_map[i].offset); get_dictval_date_from_key(trk, md_map[i].key, intval); break; default: DPRINTF(E_WARN, L_SCAN, "Unhandled metadata type %d\n", md_map[i].type); break; } } /* Set media_kind to 4 (Podcast) if Podcast is true */ ret = get_dictval_bool_from_key(trk, "Podcast", &boolean); if ((ret == 0) && boolean) { mfi->media_kind = MEDIA_KIND_PODCAST; } /* Don't let album_artist set to "Unknown artist" if we've * filled artist from the iTunes data in the meantime */ if (strcmp(mfi->album_artist, "Unknown artist") == 0) { free(mfi->album_artist); mfi->album_artist = strdup(mfi->artist); } unicode_fixup_mfi(mfi); db_file_update(mfi); free_mfi(mfi, 0); return mfi_id; } static int process_track_stream(plist_t trk) { char *url; int ret; ret = get_dictval_string_from_key(trk, "Location", &url); if (ret < 0) { DPRINTF(E_WARN, L_SCAN, "Track type URL with no Location entry!\n"); return 0; } ret = db_file_id_byurl(url); free(url); return ret; } static int process_tracks(plist_t tracks) { plist_t trk; plist_dict_iter iter; char *str; uint64_t trk_id; uint8_t disabled; int ntracks; int mfi_id; int ret; if (plist_dict_get_size(tracks) == 0) { DPRINTF(E_WARN, L_SCAN, "No tracks in iTunes library\n"); return 0; } ntracks = 0; iter = NULL; plist_dict_new_iter(tracks, &iter); plist_dict_next_item(tracks, iter, NULL, &trk); while (trk) { if (plist_get_node_type(trk) != PLIST_DICT) { plist_dict_next_item(tracks, iter, NULL, &trk); continue; } ret = get_dictval_int_from_key(trk, "Track ID", &trk_id); if (ret < 0) { DPRINTF(E_WARN, L_SCAN, "Track ID not found!\n"); plist_dict_next_item(tracks, iter, NULL, &trk); continue; } ret = get_dictval_bool_from_key(trk, "Disabled", &disabled); if (ret < 0) { DPRINTF(E_WARN, L_SCAN, "Malformed track record (id %" PRIu64 ")\n", trk_id); plist_dict_next_item(tracks, iter, NULL, &trk); continue; } if (disabled) { DPRINTF(E_INFO, L_SCAN, "Track %" PRIu64 " disabled; skipping\n", trk_id); plist_dict_next_item(tracks, iter, NULL, &trk); continue; } ret = get_dictval_string_from_key(trk, "Track Type", &str); if (ret < 0) { DPRINTF(E_WARN, L_SCAN, "Track %" PRIu64 " has no track type\n", trk_id); plist_dict_next_item(tracks, iter, NULL, &trk); continue; } if (strcmp(str, "URL") == 0) mfi_id = process_track_stream(trk); else if (strcmp(str, "File") == 0) mfi_id = process_track_file(trk); else { DPRINTF(E_LOG, L_SCAN, "Unknown track type: %s\n", str); free(str); plist_dict_next_item(tracks, iter, NULL, &trk); continue; } free(str); if (mfi_id <= 0) { plist_dict_next_item(tracks, iter, NULL, &trk); continue; } ntracks++; ret = id_map_add(trk_id, mfi_id); if (ret < 0) DPRINTF(E_LOG, L_SCAN, "Out of memory for itml -> db mapping\n"); plist_dict_next_item(tracks, iter, NULL, &trk); } free(iter); return ntracks; } static void process_pl_items(plist_t items, int pl_id) { plist_t trk; uint64_t itml_id; uint32_t db_id; uint32_t alen; uint32_t i; int ret; alen = plist_array_get_size(items); for (i = 0; i < alen; i++) { trk = plist_array_get_item(items, i); if (plist_get_node_type(trk) != PLIST_DICT) continue; ret = get_dictval_int_from_key(trk, "Track ID", &itml_id); if (ret < 0) { DPRINTF(E_WARN, L_SCAN, "No Track ID found for playlist item %u\n", i); continue; } db_id = id_map_get(itml_id); if (!db_id) { DPRINTF(E_INFO, L_SCAN, "Track ID %" PRIu64 " dropped\n", itml_id); continue; } ret = db_pl_add_item_byid(pl_id, db_id); if (ret < 0) DPRINTF(E_WARN, L_SCAN, "Could not add ID %d to playlist\n", db_id); } } static int ignore_pl(plist_t pl, char *name) { uint64_t kind; int smart; uint8_t master; uint8_t party; kind = 0; smart = 0; master = 0; party = 0; /* Special (builtin) playlists */ get_dictval_int_from_key(pl, "Distinguished Kind", &kind); /* Import smart playlists (optional) */ if (!cfg_getbool(cfg_getsec(cfg, "library"), "itunes_smartpl") && (plist_dict_get_item(pl, "Smart Info") || plist_dict_get_item(pl, "Smart Criteria"))) smart = 1; /* Not interested in the Master playlist */ get_dictval_bool_from_key(pl, "Master", &master); /* Not interested in Party Shuffle playlists */ get_dictval_bool_from_key(pl, "Party Shuffle", &party); if ((kind > 0) || smart || party || master) { DPRINTF(E_INFO, L_SCAN, "Ignoring playlist '%s' (k %" PRIu64 " s%d p%d m%d)\n", name, kind, smart, party, master); return 1; } return 0; } static void process_pls(plist_t playlists, char *file) { plist_t pl; plist_t items; struct playlist_info *pli; char *name; uint64_t id; int pl_id; uint32_t alen; uint32_t i; char virtual_path[PATH_MAX]; int ret; alen = plist_array_get_size(playlists); for (i = 0; i < alen; i++) { pl = plist_array_get_item(playlists, i); if (plist_get_node_type(pl) != PLIST_DICT) continue; ret = get_dictval_int_from_key(pl, "Playlist ID", &id); if (ret < 0) { DPRINTF(E_DBG, L_SCAN, "Playlist ID not found!\n"); continue; } ret = get_dictval_string_from_key(pl, "Name", &name); if (ret < 0) { DPRINTF(E_DBG, L_SCAN, "Name not found!\n"); continue; } if (ignore_pl(pl, name)) { free(name); continue; } pli = db_pl_fetch_bytitlepath(name, file); if (pli) { pl_id = pli->id; free_pli(pli, 0); db_pl_ping(pl_id); db_pl_clear_items(pl_id); } else pl_id = 0; ret = get_dictval_array_from_key(pl, "Playlist Items", &items); if (ret < 0) { DPRINTF(E_INFO, L_SCAN, "Playlist '%s' has no items\n", name); free(name); continue; } if (pl_id == 0) { pli = (struct playlist_info *)malloc(sizeof(struct playlist_info)); if (!pli) { DPRINTF(E_LOG, L_SCAN, "Out of memory\n"); return; } memset(pli, 0, sizeof(struct playlist_info)); pli->type = PL_PLAIN; pli->title = strdup(name); pli->path = strdup(file); snprintf(virtual_path, PATH_MAX, "/file:%s", file); pli->virtual_path = strdup(virtual_path); ret = db_pl_add(pli, &pl_id); free_pli(pli, 0); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Error adding iTunes playlist '%s' (%s)\n", name, file); free(name); continue; } DPRINTF(E_INFO, L_SCAN, "Added playlist as id %d\n", pl_id); } free(name); process_pl_items(items, pl_id); } } void scan_itunes_itml(char *file) { struct stat sb; char *itml_xml; char *ptr; plist_t itml; plist_t node; int fd; int size; int ret; DPRINTF(E_LOG, L_SCAN, "Processing iTunes library: %s\n", file); fd = open(file, O_RDONLY); if (fd < 0) { DPRINTF(E_LOG, L_SCAN, "Could not open iTunes library '%s': %s\n", file, strerror(errno)); return; } ret = fstat(fd, &sb); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Could not stat iTunes library '%s': %s\n", file, strerror(errno)); close(fd); return; } itml_xml = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0); if (itml_xml == MAP_FAILED) { DPRINTF(E_LOG, L_SCAN, "Could not map iTunes library: %s\n", strerror(errno)); close(fd); return; } itml = NULL; plist_from_xml(itml_xml, sb.st_size, &itml); ret = munmap(itml_xml, sb.st_size); if (ret < 0) DPRINTF(E_LOG, L_SCAN, "Could not unmap iTunes library: %s\n", strerror(errno)); close(fd); if (!itml) { DPRINTF(E_LOG, L_SCAN, "iTunes XML playlist '%s' failed to parse\n", file); return; } if (plist_get_node_type(itml) != PLIST_DICT) { DPRINTF(E_LOG, L_SCAN, "Malformed iTunes XML playlist '%s'\n", file); plist_free(itml); return; } /* Meta data */ ret = check_meta(itml); if (ret < 0) { plist_free(itml); return; } /* Tracks */ ret = get_dictval_dict_from_key(itml, "Tracks", &node); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Could not find Tracks dict\n"); plist_free(itml); return; } size = ID_MAP_SIZE * sizeof(struct itml_to_db_map *); id_map = malloc(size); if (!id_map) { DPRINTF(E_FATAL, L_SCAN, "iTunes library parser could not allocate ID map\n"); plist_free(itml); return; } memset(id_map, 0, size); ptr = strrchr(file, '/'); if (!ptr) { DPRINTF(E_FATAL, L_SCAN, "Invalid filename\n"); id_map_free(); plist_free(itml); return; } *ptr = '\0'; ret = process_tracks(node); if (ret <= 0) { DPRINTF(E_LOG, L_SCAN, "No tracks loaded\n"); id_map_free(); plist_free(itml); return; } *ptr = '/'; DPRINTF(E_INFO, L_SCAN, "Loaded %d tracks from iTunes library\n", ret); /* Playlists */ ret = get_dictval_array_from_key(itml, "Playlists", &node); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Could not find Playlists dict\n"); id_map_free(); plist_free(itml); return; } process_pls(node, file); id_map_free(); plist_free(itml); } forked-daapd-25.0/src/library/filescanner_playlist.c000066400000000000000000000206161313447753700226150ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * Rewritten from mt-daapd code: * Copyright (C) 2003 Ron Pedde (ron@pedde.com) * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "db.h" #include "library/filescanner.h" #include "misc.h" #include "library.h" /* Formats we can read so far */ #define PLAYLIST_PLS 1 #define PLAYLIST_M3U 2 /* Get metadata from the EXTINF tag */ static int extinf_get(char *string, struct media_file_info *mfi, int *extinf) { char *ptr; if (strncmp(string, "#EXTINF:", strlen("#EXTINF:")) != 0) return 0; ptr = strchr(string, ','); if (!ptr || strlen(ptr) < 2) return 0; /* New extinf found, so clear old data */ free_mfi(mfi, 1); *extinf = 1; mfi->artist = strdup(ptr + 1); ptr = strstr(mfi->artist, " -"); if (ptr && strlen(ptr) > 3) mfi->title = strdup(ptr + 3); else mfi->title = strdup(""); if (ptr) *ptr = '\0'; return 1; } static int process_url(const char *path, time_t mtime, int extinf, struct media_file_info *mfi, char **filename) { char virtual_path[PATH_MAX]; time_t stamp; int id; int ret; *filename = strdup(path); db_file_stamp_bypath(path, &stamp, &id); if (stamp && (stamp >= mtime)) { db_file_ping(id); return 0; } if (extinf) DPRINTF(E_INFO, L_SCAN, "Playlist has EXTINF metadata, artist is '%s', title is '%s'\n", mfi->artist, mfi->title); mfi->id = id; mfi->path = strdup(path); mfi->fname = strdup(filename_from_path(path)); mfi->data_kind = DATA_KIND_HTTP; mfi->time_modified = mtime; mfi->directory_id = DIR_HTTP; ret = scan_metadata_ffmpeg(path, mfi); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Playlist URL '%s' is unavailable for probe/metadata, assuming MP3 encoding\n", path); mfi->type = strdup("mp3"); mfi->codectype = strdup("mpeg"); mfi->description = strdup("MPEG audio file"); } if (!mfi->title) mfi->title = strdup(mfi->fname); snprintf(virtual_path, PATH_MAX, "/http:/%s", mfi->title); mfi->virtual_path = strdup(virtual_path); library_add_media(mfi); return 0; } static int process_regular_file(char **filename, char *path) { int i; int mfi_id; char *ptr; char *entry; int ret; /* Playlist might be from Windows so we change backslash to forward slash */ for (i = 0; i < strlen(path); i++) { if (path[i] == '\\') path[i] = '/'; } /* Now search for the library item where the path has closest match to playlist item */ /* Succes is when we find an unambiguous match, or when we no longer can expand the */ /* the path to refine our search. */ entry = NULL; do { ptr = strrchr(path, '/'); if (entry) *(entry - 1) = '/'; if (ptr) { *ptr = '\0'; entry = ptr + 1; } else entry = path; DPRINTF(E_SPAM, L_SCAN, "Playlist entry is now %s\n", entry); ret = db_files_get_count_bymatch(entry); } while (ptr && (ret > 1)); if (ret > 0) { mfi_id = db_file_id_bymatch(entry); DPRINTF(E_DBG, L_SCAN, "Found playlist entry match, id is %d, entry is %s\n", mfi_id, entry); *filename = db_file_path_byid(mfi_id); if (!(*filename)) { DPRINTF(E_LOG, L_SCAN, "Playlist entry %s matches file id %d, but file path is missing.\n", entry, mfi_id); return -1; } } else { DPRINTF(E_DBG, L_SCAN, "No match for playlist entry %s\n", entry); return -1; } return 0; } void scan_playlist(char *file, time_t mtime, int dir_id) { FILE *fp; struct media_file_info mfi; struct playlist_info *pli; struct stat sb; char buf[PATH_MAX]; char *path; const char *filename; char *ptr; size_t len; int extinf; int pl_id; int pl_format; int ret; char virtual_path[PATH_MAX]; char *plitem_path; DPRINTF(E_LOG, L_SCAN, "Processing static playlist: %s\n", file); ptr = strrchr(file, '.'); if (!ptr) return; if (strcasecmp(ptr, ".m3u") == 0) pl_format = PLAYLIST_M3U; else if (strcasecmp(ptr, ".pls") == 0) pl_format = PLAYLIST_PLS; else return; filename = filename_from_path(file); ret = stat(file, &sb); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Could not stat() '%s': %s\n", file, strerror(errno)); return; } fp = fopen(file, "r"); if (!fp) { DPRINTF(E_LOG, L_SCAN, "Could not open playlist '%s': %s\n", file, strerror(errno)); return; } /* Fetch or create playlist */ pli = db_pl_fetch_bypath(file); if (pli) { DPRINTF(E_DBG, L_SCAN, "Found playlist '%s', updating\n", file); pl_id = pli->id; db_pl_ping(pl_id); db_pl_clear_items(pl_id); } else { pli = (struct playlist_info *)malloc(sizeof(struct playlist_info)); if (!pli) { DPRINTF(E_LOG, L_SCAN, "Out of memory\n"); return; } memset(pli, 0, sizeof(struct playlist_info)); pli->type = PL_PLAIN; /* Get only the basename, to be used as the playlist title */ ptr = strrchr(filename, '.'); if (ptr) *ptr = '\0'; pli->title = strdup(filename); /* Restore the full filename */ if (ptr) *ptr = '.'; pli->path = strdup(file); snprintf(virtual_path, PATH_MAX, "/file:%s", file); ptr = strrchr(virtual_path, '.'); if (ptr) *ptr = '\0'; pli->virtual_path = strdup(virtual_path); pli->directory_id = dir_id; ret = db_pl_add(pli, &pl_id); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Error adding playlist '%s'\n", file); free_pli(pli, 0); return; } DPRINTF(E_INFO, L_SCAN, "Added playlist as id %d\n", pl_id); } free_pli(pli, 0); extinf = 0; memset(&mfi, 0, sizeof(struct media_file_info)); while (fgets(buf, sizeof(buf), fp) != NULL) { len = strlen(buf); /* rtrim and check that length is sane (ignore blank lines) */ while ((len > 0) && isspace(buf[len - 1])) { len--; buf[len] = '\0'; } if (len < 1) continue; /* Saves metadata in mfi if EXTINF metadata line */ if ((pl_format == PLAYLIST_M3U) && extinf_get(buf, &mfi, &extinf)) continue; /* For pls files we are only interested in the part after the FileX= entry */ path = NULL; if ((pl_format == PLAYLIST_PLS) && (strncasecmp(buf, "file", strlen("file")) == 0)) path = strchr(buf, '=') + 1; else if (pl_format == PLAYLIST_M3U) path = buf; if (!path) continue; /* Check that first char is sane for a path */ if ((!isalnum(path[0])) && (path[0] != '/') && (path[0] != '.')) continue; /* Check if line is an URL, will be added to library */ if (strncasecmp(path, "http://", strlen("http://")) == 0) { DPRINTF(E_DBG, L_SCAN, "Playlist contains URL entry: '%s'\n", path); ret = process_url(path, sb.st_mtime, extinf, &mfi, &plitem_path); } /* Regular file, should already be in library */ else { ret = process_regular_file(&plitem_path, path); } if (ret == 0) { ret = db_pl_add_item_bypath(pl_id, plitem_path); if (ret < 0) DPRINTF(E_WARN, L_SCAN, "Could not add %s to playlist\n", plitem_path); /* Clean up in preparation for next item */ extinf = 0; free_mfi(&mfi, 1); free(plitem_path); } } /* We had some extinf that we never got to use, free it now */ if (extinf) free_mfi(&mfi, 1); if (!feof(fp)) { DPRINTF(E_LOG, L_SCAN, "Error reading playlist '%s': %s\n", file, strerror(errno)); fclose(fp); return; } fclose(fp); DPRINTF(E_INFO, L_SCAN, "Done processing playlist\n"); } forked-daapd-25.0/src/library/filescanner_smartpl.c000066400000000000000000000131141313447753700224310ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "db.h" #include "misc.h" #include "SMARTPLLexer.h" #include "SMARTPLParser.h" #include "SMARTPL2SQL.h" static int smartpl_parse_file(const char *file, struct playlist_info *pli) { pANTLR3_INPUT_STREAM input; pSMARTPLLexer lxr; pANTLR3_COMMON_TOKEN_STREAM tstream; pSMARTPLParser psr; SMARTPLParser_playlist_return qtree; pANTLR3_COMMON_TREE_NODE_STREAM nodes; pSMARTPL2SQL sqlconv; SMARTPL2SQL_playlist_return plreturn; int ret; #if ANTLR3C_NEW_INPUT input = antlr3FileStreamNew((pANTLR3_UINT8) file, ANTLR3_ENC_8BIT); #else input = antlr3AsciiFileStreamNew((pANTLR3_UINT8) file); #endif // The input will be created successfully, providing that there is enough memory and the file exists etc if (input == NULL) { DPRINTF(E_LOG, L_SCAN, "Unable to open smart playlist file %s\n", file); return -1; } lxr = SMARTPLLexerNew(input); // Need to check for errors if (lxr == NULL) { DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL lexer\n"); ret = -1; goto lxr_fail; } tstream = antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lxr)); if (tstream == NULL) { DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL token stream\n"); ret = -1; goto tkstream_fail; } // Finally, now that we have our lexer constructed, we can create the parser psr = SMARTPLParserNew(tstream); // CParserNew is generated by ANTLR3 if (tstream == NULL) { DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL parser\n"); ret = -1; goto psr_fail; } qtree = psr->playlist(psr); /* Check for parser errors */ if (psr->pParser->rec->state->errorCount > 0) { DPRINTF(E_LOG, L_SCAN, "SMARTPL query parser terminated with %d errors\n", psr->pParser->rec->state->errorCount); ret = -1; goto psr_error; } DPRINTF(E_DBG, L_SCAN, "SMARTPL query AST:\n\t%s\n", qtree.tree->toStringTree(qtree.tree)->chars); nodes = antlr3CommonTreeNodeStreamNewTree(qtree.tree, ANTLR3_SIZE_HINT); if (!nodes) { DPRINTF(E_LOG, L_SCAN, "Could not create node stream\n"); ret = -1; goto psr_error; } sqlconv = SMARTPL2SQLNew(nodes); if (!sqlconv) { DPRINTF(E_LOG, L_SCAN, "Could not create SQL converter\n"); ret = -1; goto sql_fail; } plreturn = sqlconv->playlist(sqlconv); /* Check for tree parser errors */ if (sqlconv->pTreeParser->rec->state->errorCount > 0) { DPRINTF(E_LOG, L_SCAN, "SMARTPL query tree parser terminated with %d errors\n", sqlconv->pTreeParser->rec->state->errorCount); ret = -1; goto sql_error; } if (plreturn.title && plreturn.query) { DPRINTF(E_DBG, L_SCAN, "SMARTPL SQL title '%s' query: -%s-\n", plreturn.title->chars, plreturn.query->chars); if (pli->title) free(pli->title); pli->title = strdup((char *)plreturn.title->chars); if (pli->query) free(pli->query); pli->query = strdup((char *)plreturn.query->chars); ret = 0; } else { DPRINTF(E_LOG, L_SCAN, "Invalid SMARTPL query\n"); ret = -1; } sql_error: sqlconv->free(sqlconv); sql_fail: nodes->free(nodes); psr_error: psr->free(psr); psr_fail: tstream->free(tstream); tkstream_fail: lxr->free(lxr); lxr_fail: input->close(input); return ret; } void scan_smartpl(char *file, time_t mtime, int dir_id) { struct playlist_info *pli; int pl_id; char virtual_path[PATH_MAX]; char *ptr; int ret; /* Fetch or create playlist */ pli = db_pl_fetch_bypath(file); if (!pli) { pli = calloc(1, sizeof(struct playlist_info)); if (!pli) { DPRINTF(E_LOG, L_SCAN, "Out of memory\n"); return; } pli->path = strdup(file); snprintf(virtual_path, PATH_MAX, "/file:%s", file); ptr = strrchr(virtual_path, '.'); if (ptr) *ptr = '\0'; pli->virtual_path = strdup(virtual_path); pli->type = PL_SMART; } pli->directory_id = dir_id; ret = smartpl_parse_file(file, pli); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Error parsing smart playlist '%s'\n", file); free_pli(pli, 0); return; } if (pli->id) { pl_id = pli->id; ret = db_pl_update(pli); } else { ret = db_pl_add(pli, &pl_id); } if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Error adding smart playlist '%s'\n", file); free_pli(pli, 0); return; } DPRINTF(E_INFO, L_SCAN, "Added or updated smart playlist as id %d\n", pl_id); free_pli(pli, 0); DPRINTF(E_INFO, L_SCAN, "Done processing smart playlist\n"); } forked-daapd-25.0/src/listener.c000066400000000000000000000037651313447753700165720ustar00rootroot00000000000000/* * Copyright (C) 2015 Christian Meffert * * 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 */ #include #include #include #include #include "listener.h" struct listener { notify notify_cb; short events; struct listener *next; }; struct listener *listener_list = NULL; int listener_add(notify notify_cb, short events) { struct listener *listener; listener = (struct listener*)malloc(sizeof(struct listener)); if (!listener) { return -1; } listener->notify_cb = notify_cb; listener->events = events; listener->next = listener_list; listener_list = listener; return 0; } int listener_remove(notify notify_cb) { struct listener *listener; struct listener *prev; prev = NULL; for (listener = listener_list; listener; listener = listener->next) { if (listener->notify_cb == notify_cb) break; prev = listener; } if (!listener) { return -1; } if (prev) prev->next = listener->next; else listener_list = listener->next; free(listener); return 0; } void listener_notify(enum listener_event_type type) { struct listener *listener; listener = listener_list; while (listener) { if (type & listener->events) listener->notify_cb(type); listener = listener->next; } } forked-daapd-25.0/src/listener.h000066400000000000000000000027101313447753700165640ustar00rootroot00000000000000 #ifndef __LISTENER_H__ #define __LISTENER_H__ enum listener_event_type { /* The player has been started, stopped or seeked */ LISTENER_PLAYER = (1 << 0), /* The current playback queue has been modified */ LISTENER_QUEUE = (1 << 1), /* The volume has been changed */ LISTENER_VOLUME = (1 << 2), /* A speaker has been enabled or disabled */ LISTENER_SPEAKER = (1 << 3), /* Options like repeat, random has been changed */ LISTENER_OPTIONS = (1 << 4), /* The library has been modified */ LISTENER_DATABASE = (1 << 5), }; typedef void (*notify)(enum listener_event_type type); /* * Registers the given callback function to the given event types. * This function is not thread safe. Listeners must be added once at startup. * * @param notify_cb Callback function * @param events Event mask, one or more of LISTENER_* * @return 0 on success, -1 on failure */ int listener_add(notify notify_cb, short events); /* * Removes the given callback function * This function is not thread safe. Listeners must be removed once at shutdown. * * @param notify_cb Callback function * @return 0 on success, -1 if the callback was not registered */ int listener_remove(notify notify_cb); /* * Calls the callback function of the registered listeners listening for the * given type of event. * * @param type The event type, on of the LISTENER_* values * */ void listener_notify(enum listener_event_type type); #endif /* !__LISTENER_H__ */ forked-daapd-25.0/src/logger.c000066400000000000000000000154731313447753700162230ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include "logger.h" #include #include #include #include #include #include #include #include #include #include #include "conffile.h" #include "misc.h" static pthread_mutex_t logger_lck; static int logger_initialized; static int logdomains; static int threshold; static int console = 1; static char *logfilename; static FILE *logfile; static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd", "stream", "cast", "fifo", "lib" }; static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" }; /* We need our own check to avoid nested locking or recursive calls */ #define LOGGER_CHECK_ERR(f) \ do { int lerr; lerr = f; if (lerr != 0) { \ vlogger_fatal("%s failed at line %d, err %d (%s)\n", #f, __LINE__, \ lerr, strerror(lerr)); \ abort(); \ } } while(0) static int set_logdomains(char *domains) { char *ptr; char *d; int i; logdomains = 0; while ((d = strtok_r(domains, " ,", &ptr))) { domains = NULL; for (i = 0; i < N_LOGDOMAINS; i++) { if (strcmp(d, labels[i]) == 0) { logdomains |= (1 << i); break; } } if (i == N_LOGDOMAINS) { fprintf(stderr, "Error: unknown log domain '%s'\n", d); return -1; } } return 0; } static void vlogger_writer(int severity, int domain, const char *fmt, va_list args) { va_list ap; char stamp[32]; time_t t; int ret; if (logfile) { t = time(NULL); ret = strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", localtime(&t)); if (ret == 0) stamp[0] = '\0'; fprintf(logfile, "[%s] [%5s] %8s: ", stamp, severities[severity], labels[domain]); va_copy(ap, args); vfprintf(logfile, fmt, ap); va_end(ap); fflush(logfile); } if (console) { fprintf(stderr, "[%5s] %8s: ", severities[severity], labels[domain]); va_copy(ap, args); vfprintf(stderr, fmt, ap); va_end(ap); } } static void vlogger_fatal(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlogger_writer(E_FATAL, L_MISC, fmt, ap); va_end(ap); } static void vlogger(int severity, int domain, const char *fmt, va_list args) { if(! logger_initialized) { /* lock not initialized, use stderr */ vlogger_writer(severity, domain, fmt, args); return; } if (!((1 << domain) & logdomains) || (severity > threshold)) return; LOGGER_CHECK_ERR(pthread_mutex_lock(&logger_lck)); if (!logfile && !console) { LOGGER_CHECK_ERR(pthread_mutex_unlock(&logger_lck)); return; } vlogger_writer(severity, domain, fmt, args); LOGGER_CHECK_ERR(pthread_mutex_unlock(&logger_lck)); } void DPRINTF(int severity, int domain, const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlogger(severity, domain, fmt, ap); va_end(ap); } void logger_ffmpeg(void *ptr, int level, const char *fmt, va_list ap) { int severity; if (level <= AV_LOG_FATAL) severity = E_LOG; else if (level <= AV_LOG_WARNING) severity = E_WARN; else if (level <= AV_LOG_VERBOSE) severity = E_INFO; else if (level <= AV_LOG_DEBUG) severity = E_DBG; else severity = E_SPAM; vlogger(severity, L_FFMPEG, fmt, ap); } void logger_libevent(int severity, const char *msg) { switch (severity) { case EVENT_LOG_DEBUG: severity = E_DBG; break; case EVENT_LOG_ERR: severity = E_LOG; break; case EVENT_LOG_WARN: severity = E_WARN; break; case EVENT_LOG_MSG: severity = E_INFO; break; default: severity = E_LOG; break; } DPRINTF(severity, L_EVENT, "%s\n", msg); } #ifdef HAVE_ALSA void logger_alsa(const char *file, int line, const char *function, int err, const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlogger(E_LOG, L_LAUDIO, fmt, ap); va_end(ap); } #endif /* HAVE_ALSA */ void logger_reinit(void) { FILE *fp; if (!logfile) return; LOGGER_CHECK_ERR(pthread_mutex_lock(&logger_lck)); fp = fopen(logfilename, "a"); if (!fp) { fprintf(logfile, "Could not reopen logfile: %s\n", strerror(errno)); goto out; } fclose(logfile); logfile = fp; out: LOGGER_CHECK_ERR(pthread_mutex_unlock(&logger_lck)); } /* The functions below are used at init time with a single thread running */ void logger_domains(void) { int i; fprintf(stdout, "%s", labels[0]); for (i = 1; i < N_LOGDOMAINS; i++) fprintf(stdout, ", %s", labels[i]); fprintf(stdout, "\n"); } void logger_detach(void) { console = 0; } int logger_init(char *file, char *domains, int severity) { int ret; if ((sizeof(labels) / sizeof(labels[0])) != N_LOGDOMAINS) { fprintf(stderr, "WARNING: log domains do not match\n"); return -1; } console = 1; threshold = severity; if (domains) { ret = set_logdomains(domains); if (ret < 0) return ret; } else logdomains = ~0; if (!file) return 0; logfile = fopen(file, "a"); if (!logfile) { fprintf(stderr, "Could not open logfile %s: %s\n", file, strerror(errno)); return -1; } ret = fchown(fileno(logfile), runas_uid, 0); if (ret < 0) fprintf(stderr, "Failed to set ownership on logfile: %s\n", strerror(errno)); ret = fchmod(fileno(logfile), 0644); if (ret < 0) fprintf(stderr, "Failed to set permissions on logfile: %s\n", strerror(errno)); logfilename = file; /* logging w/o locks before initialized complete */ CHECK_ERR(L_MISC, mutex_init(&logger_lck)); logger_initialized = 1; return 0; } void logger_deinit(void) { if (logfile) { fclose(logfile); logfile = NULL; } if(logger_initialized) { /* logging w/o locks to stderr now */ logger_initialized = 0; console = 1; CHECK_ERR(L_MISC, pthread_mutex_destroy(&logger_lck)); } } forked-daapd-25.0/src/logger.h000066400000000000000000000027761313447753700162320ustar00rootroot00000000000000 #ifndef __LOGGER_H__ #define __LOGGER_H__ #include /* Log domains */ #define L_CONF 0 #define L_DAAP 1 #define L_DB 2 #define L_HTTPD 3 #define L_HTTP 4 #define L_MAIN 5 #define L_MDNS 6 #define L_MISC 7 #define L_RSP 8 #define L_SCAN 9 #define L_XCODE 10 /* libevent logging */ #define L_EVENT 11 #define L_REMOTE 12 #define L_DACP 13 #define L_FFMPEG 14 #define L_ART 15 #define L_PLAYER 16 #define L_RAOP 17 #define L_LAUDIO 18 #define L_DMAP 19 #define L_DBPERF 20 #define L_SPOTIFY 21 #define L_LASTFM 22 #define L_CACHE 23 #define L_MPD 24 #define L_STREAMING 25 #define L_CAST 26 #define L_FIFO 27 #define L_LIB 28 #define N_LOGDOMAINS 29 /* Severities */ #define E_FATAL 0 #define E_LOG 1 #define E_WARN 2 #define E_INFO 3 #define E_DBG 4 #define E_SPAM 5 void DPRINTF(int severity, int domain, const char *fmt, ...) __attribute__((format(printf, 3, 4))); void logger_ffmpeg(void *ptr, int level, const char *fmt, va_list ap); void logger_libevent(int severity, const char *msg); #ifdef HAVE_ALSA void logger_alsa(const char *file, int line, const char *function, int err, const char *fmt, ...); #endif void logger_reinit(void); void logger_domains(void); void logger_detach(void); int logger_init(char *file, char *domains, int severity); void logger_deinit(void); #endif /* !__LOGGER_H__ */ forked-daapd-25.0/src/main.c000066400000000000000000000514461313447753700156700ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * Pieces from mt-daapd: * Copyright (C) 2003 Ron Pedde (ron@pedde.com) * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_SIGNALFD # include #else # include # include #endif #include #include #ifdef HAVE_LIBEVENT_PTHREADS # include #endif #include #include #include #include #include GCRY_THREAD_OPTION_PTHREAD_IMPL; #include "conffile.h" #include "db.h" #include "logger.h" #include "misc.h" #include "cache.h" #include "httpd.h" #include "mpd.h" #include "mdns.h" #include "remote_pairing.h" #include "player.h" #include "worker.h" #include "library.h" #ifdef HAVE_LIBCURL # include #endif #define PIDFILE STATEDIR "/run/" PACKAGE ".pid" struct event_base *evbase_main; static struct event *sig_event; static int main_exit; static void version(void) { fprintf(stdout, "Forked Media Server: Version %s\n", VERSION); fprintf(stdout, "Copyright (C) 2009-2015 Julien BLACHE \n"); fprintf(stdout, "Based on mt-daapd, Copyright (C) 2003-2007 Ron Pedde \n"); fprintf(stdout, "Released under the GNU General Public License version 2 or later\n"); } static void usage(char *program) { version(); printf("\n"); printf("Usage: %s [options]\n\n", program); printf("Options:\n"); printf(" -d Log level (0-5)\n"); printf(" -D Log domains\n"); printf(" -c Use as the configfile\n"); printf(" -P Write PID to specified file\n"); printf(" -f Run in foreground\n"); printf(" -b ffid to be broadcast\n"); printf(" -v Display version information\n"); printf("\n\n"); printf("Available log domains:\n"); logger_domains(); printf("\n\n"); } static int daemonize(int background, char *pidfile) { FILE *fp; pid_t childpid; pid_t pid_ret; int fd; int ret; char *runas; if (background) { fp = fopen(pidfile, "w"); if (!fp) { DPRINTF(E_LOG, L_MAIN, "Error opening pidfile (%s): %s\n", pidfile, strerror(errno)); return -1; } fd = open("/dev/null", O_RDWR, 0); if (fd < 0) { DPRINTF(E_LOG, L_MAIN, "Error opening /dev/null: %s\n", strerror(errno)); fclose(fp); return -1; } signal(SIGTTOU, SIG_IGN); signal(SIGTTIN, SIG_IGN); signal(SIGTSTP, SIG_IGN); childpid = fork(); if (childpid > 0) exit(EXIT_SUCCESS); else if (childpid < 0) { DPRINTF(E_FATAL, L_MAIN, "Fork failed: %s\n", strerror(errno)); close(fd); fclose(fp); return -1; } pid_ret = setsid(); if (pid_ret == (pid_t) -1) { DPRINTF(E_FATAL, L_MAIN, "setsid() failed: %s\n", strerror(errno)); close(fd); fclose(fp); return -1; } logger_detach(); dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if (fd > 2) close(fd); ret = chdir("/"); if (ret < 0) DPRINTF(E_WARN, L_MAIN, "chdir() failed: %s\n", strerror(errno)); umask(0); fprintf(fp, "%d\n", getpid()); fclose(fp); DPRINTF(E_DBG, L_MAIN, "PID: %d\n", getpid()); } if (geteuid() == (uid_t) 0) { runas = cfg_getstr(cfg_getsec(cfg, "general"), "uid"); ret = initgroups(runas, runas_gid); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "initgroups() failed: %s\n", strerror(errno)); return -1; } ret = setegid(runas_gid); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "setegid() failed: %s\n", strerror(errno)); return -1; } ret = seteuid(runas_uid); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "seteuid() failed: %s\n", strerror(errno)); return -1; } } return 0; } static int register_services(char *ffid, int no_rsp, int no_daap, int mdns_no_mpd) { cfg_t *lib; cfg_t *mpd; char *libname; char *password; char *txtrecord[10]; char records[9][128]; int port; int mpd_port; uint32_t hash; int i; int ret; srand((unsigned int)time(NULL)); lib = cfg_getsec(cfg, "library"); libname = cfg_getstr(lib, "name"); hash = djb_hash(libname, strlen(libname)); for (i = 0; i < (sizeof(records) / sizeof(records[0])); i++) { memset(records[i], 0, 128); txtrecord[i] = records[i]; } txtrecord[9] = NULL; snprintf(txtrecord[0], 128, "txtvers=1"); snprintf(txtrecord[1], 128, "Database ID=%0X", hash); snprintf(txtrecord[2], 128, "Machine ID=%0X", hash); snprintf(txtrecord[3], 128, "Machine Name=%s", libname); snprintf(txtrecord[4], 128, "mtd-version=%s", VERSION); snprintf(txtrecord[5], 128, "iTSh Version=131073"); /* iTunes 6.0.4 */ snprintf(txtrecord[6], 128, "Version=196610"); /* iTunes 6.0.4 */ password = cfg_getstr(lib, "password"); snprintf(txtrecord[7], 128, "Password=%s", (password) ? "true" : "false"); if (ffid) snprintf(txtrecord[8], 128, "ffid=%s", ffid); else snprintf(txtrecord[8], 128, "ffid=%08x", rand()); DPRINTF(E_INFO, L_MAIN, "Registering rendezvous names\n"); port = cfg_getint(lib, "port"); /* Register web server service - disabled since we have no web interface */ /* ret = mdns_register(libname, "_http._tcp", port, txtrecord); if (ret < 0) return ret; */ /* Register RSP service */ if (!no_rsp) { ret = mdns_register(libname, "_rsp._tcp", port, txtrecord); if (ret < 0) return ret; } /* Register DAAP service */ if (!no_daap) { ret = mdns_register(libname, "_daap._tcp", port, txtrecord); if (ret < 0) return ret; } for (i = 0; i < (sizeof(records) / sizeof(records[0])); i++) { memset(records[i], 0, 128); } snprintf(txtrecord[0], 128, "txtvers=1"); snprintf(txtrecord[1], 128, "DbId=%016" PRIX64, libhash); snprintf(txtrecord[2], 128, "DvTy=iTunes"); snprintf(txtrecord[3], 128, "DvSv=2306"); /* Magic number! Yay! */ snprintf(txtrecord[4], 128, "Ver=131073"); /* iTunes 6.0.4 */ snprintf(txtrecord[5], 128, "OSsi=0x1F5"); /* Magic number! Yay! */ snprintf(txtrecord[6], 128, "CtlN=%s", libname); /* Terminator */ txtrecord[7] = NULL; /* The group name for the touch-able service advertising is a 64bit hash * but is different from the DbId in iTunes. For now we'll use a hash of * the library name for both, and we'll change that if needed. */ /* Use as scratch space for the hash */ snprintf(records[7], 128, "%016" PRIX64, libhash); /* Register touch-able service, for Remote.app */ ret = mdns_register(records[7], "_touch-able._tcp", port, txtrecord); if (ret < 0) return ret; /* Register MPD serivce */ mpd = cfg_getsec(cfg, "mpd"); mpd_port = cfg_getint(mpd, "port"); if (!mdns_no_mpd && mpd_port > 0) { ret = mdns_register(libname, "_mpd._tcp", mpd_port, NULL); if (ret < 0) return ret; } return 0; } #ifdef HAVE_SIGNALFD static void signal_signalfd_cb(int fd, short event, void *arg) { struct signalfd_siginfo info; int status; while (read(fd, &info, sizeof(struct signalfd_siginfo)) > 0) { switch (info.ssi_signo) { case SIGCHLD: DPRINTF(E_LOG, L_MAIN, "Got SIGCHLD\n"); while (waitpid(-1, &status, WNOHANG) > 0) /* Nothing. */ ; break; case SIGINT: case SIGTERM: DPRINTF(E_LOG, L_MAIN, "Got SIGTERM or SIGINT\n"); main_exit = 1; break; case SIGHUP: DPRINTF(E_LOG, L_MAIN, "Got SIGHUP\n"); if (!main_exit) logger_reinit(); break; } } if (main_exit) event_base_loopbreak(evbase_main); else event_add(sig_event, NULL); } #else static void signal_kqueue_cb(int fd, short event, void *arg) { struct timespec ts; struct kevent ke; int status; ts.tv_sec = 0; ts.tv_nsec = 0; while (kevent(fd, NULL, 0, &ke, 1, &ts) > 0) { switch (ke.ident) { case SIGCHLD: DPRINTF(E_LOG, L_MAIN, "Got SIGCHLD\n"); while (waitpid(-1, &status, WNOHANG) > 0) /* Nothing. */ ; break; case SIGINT: case SIGTERM: DPRINTF(E_LOG, L_MAIN, "Got SIGTERM or SIGINT\n"); main_exit = 1; break; case SIGHUP: DPRINTF(E_LOG, L_MAIN, "Got SIGHUP\n"); if (!main_exit) logger_reinit(); break; } } if (main_exit) event_base_loopbreak(evbase_main); else event_add(sig_event, NULL); } #endif static int ffmpeg_lockmgr(void **pmutex, enum AVLockOp op) { switch (op) { case AV_LOCK_CREATE: *pmutex = malloc(sizeof(pthread_mutex_t)); if (!*pmutex) return 1; CHECK_ERR(L_MAIN, mutex_init(*pmutex)); return 0; case AV_LOCK_OBTAIN: CHECK_ERR(L_MAIN, pthread_mutex_lock(*pmutex)); return 0; case AV_LOCK_RELEASE: CHECK_ERR(L_MAIN, pthread_mutex_unlock(*pmutex)); return 0; case AV_LOCK_DESTROY: CHECK_ERR(L_MAIN, pthread_mutex_destroy(*pmutex)); free(*pmutex); *pmutex = NULL; return 0; } return 1; } int main(int argc, char **argv) { int option; char *configfile; int background; int mdns_no_rsp; int mdns_no_daap; int mdns_no_mpd; int loglevel; char *logdomains; char *logfile; char *ffid; char *pidfile; char buildopts[256]; const char *gcry_version; sigset_t sigs; int sigfd; #ifdef HAVE_KQUEUE struct kevent ke_sigs[4]; #endif int ret; struct option option_map[] = { { "ffid", 1, NULL, 'b' }, { "debug", 1, NULL, 'd' }, { "logdomains", 1, NULL, 'D' }, { "foreground", 0, NULL, 'f' }, { "config", 1, NULL, 'c' }, { "pidfile", 1, NULL, 'P' }, { "version", 0, NULL, 'v' }, { "mdns-no-rsp", 0, NULL, 512 }, { "mdns-no-daap", 0, NULL, 513 }, { NULL, 0, NULL, 0 } }; configfile = CONFFILE; pidfile = PIDFILE; loglevel = -1; logdomains = NULL; logfile = NULL; background = 1; ffid = NULL; mdns_no_rsp = 0; mdns_no_daap = 0; while ((option = getopt_long(argc, argv, "D:d:c:P:fb:v", option_map, NULL)) != -1) { switch (option) { case 512: mdns_no_rsp = 1; break; case 513: mdns_no_daap = 1; break; case 'b': ffid = optarg; break; case 'd': ret = safe_atoi32(optarg, &option); if (ret < 0) fprintf(stderr, "Error: loglevel must be an integer in '-d %s'\n", optarg); else loglevel = option; break; case 'D': logdomains = optarg; break; case 'f': background = 0; break; case 'c': configfile = optarg; break; case 'P': pidfile = optarg; break; case 'v': version(); return EXIT_SUCCESS; break; default: usage(argv[0]); return EXIT_FAILURE; break; } } ret = logger_init(NULL, NULL, (loglevel < 0) ? E_LOG : loglevel); if (ret != 0) { fprintf(stderr, "Could not initialize log facility\n"); return EXIT_FAILURE; } ret = conffile_load(configfile); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Config file errors; please fix your config\n"); logger_deinit(); return EXIT_FAILURE; } logger_deinit(); /* Reinit log facility with configfile values */ if (loglevel < 0) loglevel = cfg_getint(cfg_getsec(cfg, "general"), "loglevel"); logfile = cfg_getstr(cfg_getsec(cfg, "general"), "logfile"); ret = logger_init(logfile, logdomains, loglevel); if (ret != 0) { fprintf(stderr, "Could not reinitialize log facility with config file settings\n"); conffile_unload(); return EXIT_FAILURE; } /* Set up libevent logging callback */ event_set_log_callback(logger_libevent); DPRINTF(E_LOG, L_MAIN, "Forked Media Server Version %s taking off\n", VERSION); /* Remember to check the size of buildopts when adding new opts */ strcpy(buildopts, ""); #ifdef ITUNES strcat(buildopts, " --enable-itunes"); #endif #ifdef SPOTIFY strcat(buildopts, " --enable-spotify"); #endif #ifdef LASTFM strcat(buildopts, " --enable-lastfm"); #endif #ifdef CHROMECAST strcat(buildopts, " --enable-chromecast"); #endif #ifdef MPD strcat(buildopts, " --enable-mpd"); #endif #ifdef RAOP_VERIFICATION strcat(buildopts, " --enable-verification"); #endif #ifdef HAVE_ALSA strcat(buildopts, " --with-alsa"); #endif #ifdef HAVE_LIBPULSE strcat(buildopts, " --with-pulseaudio"); #endif DPRINTF(E_LOG, L_MAIN, "Built %s with:%s\n", __DATE__, buildopts); ret = av_lockmgr_register(ffmpeg_lockmgr); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not register ffmpeg lock manager callback\n"); ret = EXIT_FAILURE; goto ffmpeg_init_fail; } av_register_all(); avfilter_register_all(); #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 13) avformat_network_init(); #endif av_log_set_callback(logger_ffmpeg); #ifdef HAVE_LIBCURL /* Initialize libcurl */ curl_global_init(CURL_GLOBAL_DEFAULT); #endif /* Initialize libgcrypt */ gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread); gcry_version = gcry_check_version(GCRYPT_VERSION); if (!gcry_version) { DPRINTF(E_FATAL, L_MAIN, "libgcrypt version mismatch\n"); ret = EXIT_FAILURE; goto gcrypt_init_fail; } /* We aren't handling anything sensitive, so give up on secure * memory, which is a scarce system resource. */ gcry_control(GCRYCTL_DISABLE_SECMEM, 0); gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); DPRINTF(E_DBG, L_MAIN, "Initialized with gcrypt %s\n", gcry_version); /* Block signals for all threads except the main one */ sigemptyset(&sigs); sigaddset(&sigs, SIGINT); sigaddset(&sigs, SIGHUP); sigaddset(&sigs, SIGCHLD); sigaddset(&sigs, SIGTERM); sigaddset(&sigs, SIGPIPE); ret = pthread_sigmask(SIG_BLOCK, &sigs, NULL); if (ret != 0) { DPRINTF(E_LOG, L_MAIN, "Error setting signal set\n"); ret = EXIT_FAILURE; goto signal_block_fail; } /* Daemonize and drop privileges */ ret = daemonize(background, pidfile); if (ret < 0) { DPRINTF(E_LOG, L_MAIN, "Could not initialize server\n"); ret = EXIT_FAILURE; goto daemon_fail; } /* Initialize event base (after forking) */ CHECK_NULL(L_MAIN, evbase_main = event_base_new()); #ifdef HAVE_LIBEVENT_PTHREADS CHECK_ERR(L_MAIN, evthread_use_pthreads()); #endif DPRINTF(E_LOG, L_MAIN, "mDNS init\n"); ret = mdns_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "mDNS init failed\n"); ret = EXIT_FAILURE; goto mdns_fail; } /* Initialize the database before starting */ DPRINTF(E_INFO, L_MAIN, "Initializing database\n"); ret = db_init(); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Database init failed\n"); ret = EXIT_FAILURE; goto db_fail; } /* Open a DB connection for the main thread */ ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not perform perthread DB init for main\n"); ret = EXIT_FAILURE; goto db_fail; } /* Spawn worker thread */ ret = worker_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Worker thread failed to start\n"); ret = EXIT_FAILURE; goto worker_fail; } /* Spawn cache thread */ ret = cache_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Cache thread failed to start\n"); ret = EXIT_FAILURE; goto cache_fail; } /* Spawn library scan thread */ ret = library_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Library thread failed to start\n"); ret = EXIT_FAILURE; goto library_fail; } /* Spawn player thread */ ret = player_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Player thread failed to start\n"); ret = EXIT_FAILURE; goto player_fail; } /* Spawn HTTPd thread */ ret = httpd_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "HTTPd thread failed to start\n"); ret = EXIT_FAILURE; goto httpd_fail; } #ifdef MPD /* Spawn MPD thread */ ret = mpd_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "MPD thread failed to start\n"); ret = EXIT_FAILURE; goto mpd_fail; } mdns_no_mpd = 0; #else mdns_no_mpd = 1; #endif /* Start Remote pairing service */ ret = remote_pairing_init(); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Remote pairing service failed to start\n"); ret = EXIT_FAILURE; goto remote_fail; } /* Register mDNS services */ ret = register_services(ffid, mdns_no_rsp, mdns_no_daap, mdns_no_mpd); if (ret < 0) { ret = EXIT_FAILURE; goto mdns_reg_fail; } /* Register this CNAME with mDNS for OAuth */ mdns_cname("forked-daapd.local"); #ifdef HAVE_SIGNALFD /* Set up signal fd */ sigfd = signalfd(-1, &sigs, SFD_NONBLOCK | SFD_CLOEXEC); if (sigfd < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not setup signalfd: %s\n", strerror(errno)); ret = EXIT_FAILURE; goto signalfd_fail; } sig_event = event_new(evbase_main, sigfd, EV_READ, signal_signalfd_cb, NULL); #else sigfd = kqueue(); if (sigfd < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not setup kqueue: %s\n", strerror(errno)); ret = EXIT_FAILURE; goto signalfd_fail; } EV_SET(&ke_sigs[0], SIGINT, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); EV_SET(&ke_sigs[1], SIGTERM, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); EV_SET(&ke_sigs[2], SIGHUP, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); EV_SET(&ke_sigs[3], SIGCHLD, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); ret = kevent(sigfd, ke_sigs, 4, NULL, 0, NULL); if (ret < 0) { DPRINTF(E_FATAL, L_MAIN, "Could not register signal events: %s\n", strerror(errno)); ret = EXIT_FAILURE; goto signalfd_fail; } sig_event = event_new(evbase_main, sigfd, EV_READ, signal_kqueue_cb, NULL); #endif if (!sig_event) { DPRINTF(E_FATAL, L_MAIN, "Could not create signal event\n"); ret = EXIT_FAILURE; goto sig_event_fail; } event_add(sig_event, NULL); /* Run the loop */ event_base_dispatch(evbase_main); DPRINTF(E_LOG, L_MAIN, "Stopping gracefully\n"); ret = EXIT_SUCCESS; /* * On a clean shutdown, bring mDNS down first to give a chance * to the clients to perform a clean shutdown on their end */ DPRINTF(E_LOG, L_MAIN, "mDNS deinit\n"); mdns_deinit(); sig_event_fail: signalfd_fail: mdns_reg_fail: DPRINTF(E_LOG, L_MAIN, "Remote pairing deinit\n"); remote_pairing_deinit(); remote_fail: #ifdef MPD DPRINTF(E_LOG, L_MAIN, "MPD deinit\n"); mpd_deinit(); mpd_fail: #endif DPRINTF(E_LOG, L_MAIN, "HTTPd deinit\n"); httpd_deinit(); httpd_fail: DPRINTF(E_LOG, L_MAIN, "Player deinit\n"); player_deinit(); player_fail: DPRINTF(E_LOG, L_MAIN, "Library scaner deinit\n"); library_deinit(); library_fail: DPRINTF(E_LOG, L_MAIN, "Cache deinit\n"); cache_deinit(); cache_fail: DPRINTF(E_LOG, L_MAIN, "Worker deinit\n"); worker_deinit(); worker_fail: DPRINTF(E_LOG, L_MAIN, "Database deinit\n"); db_perthread_deinit(); db_deinit(); db_fail: if (ret == EXIT_FAILURE) { DPRINTF(E_LOG, L_MAIN, "mDNS deinit\n"); mdns_deinit(); } mdns_fail: daemon_fail: if (background) { ret = seteuid(0); if (ret < 0) DPRINTF(E_LOG, L_MAIN, "seteuid() failed: %s\n", strerror(errno)); else { ret = unlink(pidfile); if (ret < 0) DPRINTF(E_LOG, L_MAIN, "Could not unlink PID file %s: %s\n", pidfile, strerror(errno)); } } signal_block_fail: gcrypt_init_fail: #ifdef HAVE_LIBCURL curl_global_cleanup(); #endif #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 13) avformat_network_deinit(); #endif av_lockmgr_register(NULL); ffmpeg_init_fail: DPRINTF(E_LOG, L_MAIN, "Exiting.\n"); conffile_unload(); logger_deinit(); return ret; } forked-daapd-25.0/src/mdns.h000066400000000000000000000033701313447753700157030ustar00rootroot00000000000000 #ifndef __MDNS_H__ #define __MDNS_H__ #include "misc.h" typedef void (* mdns_browse_cb)(const char *name, const char *type, const char *domain, const char *hostname, int family, const char *address, int port, struct keyval *txt); /* * Start a mDNS client * Call only from the main thread! * * @return 0 on success, -1 on error */ int mdns_init(void); /* * Removes registered services, stops service browsers and stop the mDNS client * Call only from the main thread! * */ void mdns_deinit(void); /* * Register (announce) a service with mDNS * Call only from the main thread! * * @in name Name of service, e.g. "My Music on Debian" * @in type Type of service to announce, e.g. "_daap._tcp" * @in port Port of the service * @in txt Pointer to array of strings with txt key/values ("Version=1") * for DNS-SD TXT. The array must be terminated by a NULL pointer. * @return 0 on success, -1 on error */ int mdns_register(char *name, char *type, int port, char **txt); /* * Register a CNAME record, it will be an alias for hostname * Call only from the main thread! * * @in name The CNAME alias, e.g. "forked-daapd.local" * @return 0 on success, -1 on error */ int mdns_cname(char *name); /* * Start a service browser, a callback will be made when the service changes state * Call only from the main thread! * * @in type Type of service to look for, e.g. "_raop._tcp" * @in family AF_INET to browse for ipv4 services, AF_INET6 for ipv6, AF_UNSPEC for both * @in cb Callback when service state changes (e.g. appears/disappears) * @return 0 on success, -1 on error */ int mdns_browse(char *type, int family, mdns_browse_cb cb); #endif /* !__MDNS_H__ */ forked-daapd-25.0/src/mdns_avahi.c000066400000000000000000000575321313447753700170570ustar00rootroot00000000000000/* * Avahi mDNS backend, with libevent polling * * Copyright (C) 2009-2011 Julien BLACHE * * Pieces coming from mt-daapd: * Copyright (C) 2005 Sebastian Dröge * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Hack for FreeBSD, don't want to bother with sysconf() #ifndef HOST_NAME_MAX # include # define HOST_NAME_MAX _POSIX_HOST_NAME_MAX #endif #include "logger.h" #include "mdns.h" #define MDNSERR avahi_strerror(avahi_client_errno(mdns_client)) /* Main event base, from main.c */ extern struct event_base *evbase_main; static AvahiClient *mdns_client = NULL; static AvahiEntryGroup *mdns_group = NULL; struct AvahiWatch { struct event *ev; AvahiWatchCallback cb; void *userdata; AvahiWatch *next; }; struct AvahiTimeout { struct event *ev; AvahiTimeoutCallback cb; void *userdata; AvahiTimeout *next; }; static AvahiWatch *all_w; static AvahiTimeout *all_t; /* libevent callbacks */ static void evcb_watch(int fd, short ev_events, void *arg) { AvahiWatch *w; AvahiWatchEvent a_events; w = (AvahiWatch *)arg; a_events = 0; if (ev_events & EV_READ) a_events |= AVAHI_WATCH_IN; if (ev_events & EV_WRITE) a_events |= AVAHI_WATCH_OUT; event_add(w->ev, NULL); w->cb(w, fd, a_events, w->userdata); } static void evcb_timeout(int fd, short ev_events, void *arg) { AvahiTimeout *t; t = (AvahiTimeout *)arg; t->cb(t, t->userdata); } /* AvahiPoll implementation for libevent */ static int _ev_watch_add(AvahiWatch *w, int fd, AvahiWatchEvent a_events) { short ev_events; ev_events = 0; if (a_events & AVAHI_WATCH_IN) ev_events |= EV_READ; if (a_events & AVAHI_WATCH_OUT) ev_events |= EV_WRITE; if (w->ev) event_free(w->ev); w->ev = event_new(evbase_main, fd, ev_events, evcb_watch, w); if (!w->ev) { DPRINTF(E_LOG, L_MDNS, "Could not make new event in _ev_watch_add\n"); return -1; } return event_add(w->ev, NULL); } static AvahiWatch * ev_watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent a_events, AvahiWatchCallback cb, void *userdata) { AvahiWatch *w; int ret; w = calloc(1, sizeof(AvahiWatch)); if (!w) return NULL; w->cb = cb; w->userdata = userdata; ret = _ev_watch_add(w, fd, a_events); if (ret != 0) { free(w); return NULL; } w->next = all_w; all_w = w; return w; } static void ev_watch_update(AvahiWatch *w, AvahiWatchEvent a_events) { if (w->ev) event_del(w->ev); _ev_watch_add(w, (int)event_get_fd(w->ev), a_events); } static AvahiWatchEvent ev_watch_get_events(AvahiWatch *w) { AvahiWatchEvent a_events; a_events = 0; if (event_pending(w->ev, EV_READ, NULL)) a_events |= AVAHI_WATCH_IN; if (event_pending(w->ev, EV_WRITE, NULL)) a_events |= AVAHI_WATCH_OUT; return a_events; } static void ev_watch_free(AvahiWatch *w) { AvahiWatch *prev; AvahiWatch *cur; if (w->ev) { event_free(w->ev); w->ev = NULL; } prev = NULL; for (cur = all_w; cur; prev = cur, cur = cur->next) { if (cur != w) continue; if (prev == NULL) all_w = w->next; else prev->next = w->next; break; } free(w); } static int _ev_timeout_add(AvahiTimeout *t, const struct timeval *tv) { struct timeval e_tv; struct timeval now; int ret; if (t->ev) event_free(t->ev); t->ev = evtimer_new(evbase_main, evcb_timeout, t); if (!t->ev) { DPRINTF(E_LOG, L_MDNS, "Could not make event in _ev_timeout_add - out of memory?\n"); return -1; } if ((tv->tv_sec == 0) && (tv->tv_usec == 0)) { evutil_timerclear(&e_tv); } else { ret = gettimeofday(&now, NULL); if (ret != 0) return -1; evutil_timersub(tv, &now, &e_tv); } return evtimer_add(t->ev, &e_tv); } static AvahiTimeout * ev_timeout_new(const AvahiPoll *api, const struct timeval *tv, AvahiTimeoutCallback cb, void *userdata) { AvahiTimeout *t; int ret; t = calloc(1, sizeof(AvahiTimeout)); if (!t) return NULL; t->cb = cb; t->userdata = userdata; if (tv != NULL) { ret = _ev_timeout_add(t, tv); if (ret != 0) { free(t); return NULL; } } t->next = all_t; all_t = t; return t; } static void ev_timeout_update(AvahiTimeout *t, const struct timeval *tv) { if (t->ev) event_del(t->ev); if (tv) _ev_timeout_add(t, tv); } static void ev_timeout_free(AvahiTimeout *t) { AvahiTimeout *prev; AvahiTimeout *cur; if (t->ev) { event_free(t->ev); t->ev = NULL; } prev = NULL; for (cur = all_t; cur; prev = cur, cur = cur->next) { if (cur != t) continue; if (prev == NULL) all_t = t->next; else prev->next = t->next; break; } free(t); } static struct AvahiPoll ev_poll_api = { .userdata = NULL, .watch_new = ev_watch_new, .watch_update = ev_watch_update, .watch_get_events = ev_watch_get_events, .watch_free = ev_watch_free, .timeout_new = ev_timeout_new, .timeout_update = ev_timeout_update, .timeout_free = ev_timeout_free }; /* Avahi client callbacks & helpers */ struct mdns_browser { char *type; AvahiProtocol protocol; mdns_browse_cb cb; struct mdns_browser *next; }; struct mdns_record_browser { struct mdns_browser *mb; char *name; char *domain; struct keyval txt_kv; int port; }; struct mdns_resolver { char *name; AvahiServiceResolver *resolver; AvahiProtocol proto; struct mdns_resolver *next; }; enum publish { MDNS_PUBLISH_SERVICE, MDNS_PUBLISH_CNAME, }; struct mdns_group_entry { enum publish publish; char *name; char *type; int port; AvahiStringList *txt; struct mdns_group_entry *next; }; static struct mdns_browser *browser_list; static struct mdns_resolver *resolver_list; static struct mdns_group_entry *group_entries; #define IPV4LL_NETWORK 0xA9FE0000 #define IPV4LL_NETMASK 0xFFFF0000 #define IPV6LL_NETWORK 0xFE80 #define IPV6LL_NETMASK 0xFFC0 static int is_v4ll(const AvahiIPv4Address *addr) { return ((ntohl(addr->address) & IPV4LL_NETMASK) == IPV4LL_NETWORK); } static int is_v6ll(const AvahiIPv6Address *addr) { return ((((addr->address[0] << 8) | addr->address[1]) & IPV6LL_NETMASK) == IPV6LL_NETWORK); } static int avahi_address_make(AvahiAddress *addr, AvahiProtocol proto, const void *rdata, size_t size) { memset(addr, 0, sizeof(AvahiAddress)); addr->proto = proto; if (proto == AVAHI_PROTO_INET) { if (size != sizeof(AvahiIPv4Address)) { DPRINTF(E_LOG, L_MDNS, "Got RR type A size %zu (should be %zu)\n", size, sizeof(AvahiIPv4Address)); return -1; } memcpy(&addr->data.ipv4.address, rdata, size); return 0; } if (proto == AVAHI_PROTO_INET6) { if (size != sizeof(AvahiIPv6Address)) { DPRINTF(E_LOG, L_MDNS, "Got RR type AAAA size %zu (should be %zu)\n", size, sizeof(AvahiIPv6Address)); return -1; } memcpy(&addr->data.ipv6.address, rdata, size); return 0; } DPRINTF(E_LOG, L_MDNS, "Error: Unknown protocol\n"); return -1; } // Frees all resolvers for a given service name static void resolvers_cleanup(const char *name, AvahiProtocol proto) { struct mdns_resolver *r; struct mdns_resolver *prev; struct mdns_resolver *next; prev = NULL; for (r = resolver_list; r; r = next) { next = r->next; if ((strcmp(name, r->name) != 0) || (proto != r->proto)) { prev = r; continue; } if (!prev) resolver_list = r->next; else prev->next = r->next; avahi_service_resolver_free(r->resolver); free(r->name); free(r); } } static void browse_record_callback(AvahiRecordBrowser *b, AvahiIfIndex intf, AvahiProtocol proto, AvahiBrowserEvent event, const char *hostname, uint16_t clazz, uint16_t type, const void *rdata, size_t size, AvahiLookupResultFlags flags, void *userdata) { struct mdns_record_browser *rb_data; AvahiAddress addr; char address[AVAHI_ADDRESS_STR_MAX]; int family; int ret; rb_data = (struct mdns_record_browser *)userdata; if (event == AVAHI_BROWSER_CACHE_EXHAUSTED) DPRINTF(E_DBG, L_MDNS, "Avahi Record Browser (%s, proto %d): no more results (CACHE_EXHAUSTED)\n", hostname, proto); else if (event == AVAHI_BROWSER_ALL_FOR_NOW) DPRINTF(E_DBG, L_MDNS, "Avahi Record Browser (%s, proto %d): no more results (ALL_FOR_NOW)\n", hostname, proto); else if (event == AVAHI_BROWSER_FAILURE) DPRINTF(E_LOG, L_MDNS, "Avahi Record Browser (%s, proto %d) failure: %s\n", hostname, proto, MDNSERR); else if (event == AVAHI_BROWSER_REMOVE) return; // Not handled - record browser lifetime too short for this to happen if (event != AVAHI_BROWSER_NEW) goto out_free_record_browser; ret = avahi_address_make(&addr, proto, rdata, size); // Not an avahi function despite the name if (ret < 0) return; family = avahi_proto_to_af(proto); avahi_address_snprint(address, sizeof(address), &addr); // Avahi will sometimes give us link-local addresses in 169.254.0.0/16 or // fe80::/10, which (most of the time) are useless // - see also https://lists.freedesktop.org/archives/avahi/2012-September/002183.html if ((proto == AVAHI_PROTO_INET && is_v4ll(&addr.data.ipv4)) || (proto == AVAHI_PROTO_INET6 && is_v6ll(&addr.data.ipv6))) { DPRINTF(E_WARN, L_MDNS, "Ignoring announcement from %s, address %s is link-local\n", hostname, address); return; } DPRINTF(E_DBG, L_MDNS, "Avahi Record Browser (%s, proto %d): NEW record %s for service type '%s'\n", hostname, proto, address, rb_data->mb->type); // Execute callback (mb->cb) with all the data rb_data->mb->cb(rb_data->name, rb_data->mb->type, rb_data->domain, hostname, family, address, rb_data->port, &rb_data->txt_kv); // Stop record browser out_free_record_browser: keyval_clear(&rb_data->txt_kv); free(rb_data->name); free(rb_data->domain); free(rb_data); avahi_record_browser_free(b); } static void browse_resolve_callback(AvahiServiceResolver *r, AvahiIfIndex intf, AvahiProtocol proto, AvahiResolverEvent event, const char *name, const char *type, const char *domain, const char *hostname, const AvahiAddress *addr, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void *userdata) { AvahiRecordBrowser *rb; struct mdns_browser *mb; struct mdns_record_browser *rb_data; char *key; char *value; uint16_t dns_type; int family; int ret; mb = (struct mdns_browser *)userdata; if (event != AVAHI_RESOLVER_FOUND) { if (event == AVAHI_RESOLVER_FAILURE) DPRINTF(E_LOG, L_MDNS, "Avahi Resolver failure: service '%s' type '%s' proto %d: %s\n", name, type, proto, MDNSERR); else DPRINTF(E_LOG, L_MDNS, "Avahi Resolver empty callback\n"); family = avahi_proto_to_af(proto); if (family != AF_UNSPEC) mb->cb(name, type, domain, NULL, family, NULL, -1, NULL); // We don't clean up resolvers because we want a notification from them if // the service reappears (e.g. if device was switched off and then on) return; } DPRINTF(E_DBG, L_MDNS, "Avahi Resolver: resolved service '%s' type '%s' proto %d, host %s\n", name, type, proto, hostname); CHECK_NULL(L_MDNS, rb_data = calloc(1, sizeof(struct mdns_record_browser))); rb_data->name = strdup(name); rb_data->domain = strdup(domain); rb_data->mb = mb; rb_data->port = port; while (txt) { ret = avahi_string_list_get_pair(txt, &key, &value, NULL); txt = avahi_string_list_get_next(txt); if (ret < 0) continue; if (value) { keyval_add(&rb_data->txt_kv, key, value); avahi_free(value); } avahi_free(key); } if (proto == AVAHI_PROTO_INET6) dns_type = AVAHI_DNS_TYPE_AAAA; else dns_type = AVAHI_DNS_TYPE_A; // We need to implement a record browser because the announcement from some // devices (e.g. ApEx 1 gen) will include multiple records, and we need to // filter out those records that won't work (notably link-local). The value of // *addr given by browse_resolve_callback is just the first record. rb = avahi_record_browser_new(mdns_client, intf, proto, hostname, AVAHI_DNS_CLASS_IN, dns_type, 0, browse_record_callback, rb_data); if (!rb) DPRINTF(E_LOG, L_MDNS, "Could not create record browser for host %s: %s\n", hostname, MDNSERR); } static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex intf, AvahiProtocol proto, AvahiBrowserEvent event, const char *name, const char *type, const char *domain, AvahiLookupResultFlags flags, void *userdata) { struct mdns_browser *mb; struct mdns_resolver *r; int family; mb = (struct mdns_browser *)userdata; switch (event) { case AVAHI_BROWSER_FAILURE: DPRINTF(E_LOG, L_MDNS, "Avahi Browser failure: %s\n", MDNSERR); avahi_service_browser_free(b); b = avahi_service_browser_new(mdns_client, AVAHI_IF_UNSPEC, mb->protocol, mb->type, NULL, 0, browse_callback, mb); if (!b) { DPRINTF(E_LOG, L_MDNS, "Failed to recreate service browser (service type %s): %s\n", mb->type, MDNSERR); return; } break; case AVAHI_BROWSER_NEW: DPRINTF(E_DBG, L_MDNS, "Avahi Browser: NEW service '%s' type '%s' proto %d\n", name, type, proto); CHECK_NULL(L_MDNS, r = calloc(1, sizeof(struct mdns_resolver))); r->resolver = avahi_service_resolver_new(mdns_client, intf, proto, name, type, domain, proto, 0, browse_resolve_callback, mb); if (!r->resolver) { DPRINTF(E_LOG, L_MDNS, "Failed to create service resolver: %s\n", MDNSERR); free(r); return; } r->name = strdup(name); r->proto = proto; r->next = resolver_list; resolver_list = r; break; case AVAHI_BROWSER_REMOVE: DPRINTF(E_DBG, L_MDNS, "Avahi Browser: REMOVE service '%s' type '%s' proto %d\n", name, type, proto); family = avahi_proto_to_af(proto); if (family != AF_UNSPEC) mb->cb(name, type, domain, NULL, family, NULL, -1, NULL); resolvers_cleanup(name, proto); break; case AVAHI_BROWSER_ALL_FOR_NOW: case AVAHI_BROWSER_CACHE_EXHAUSTED: DPRINTF(E_DBG, L_MDNS, "Avahi Browser (%s): no more results (%s)\n", mb->type, (event == AVAHI_BROWSER_CACHE_EXHAUSTED) ? "CACHE_EXHAUSTED" : "ALL_FOR_NOW"); break; } } static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata) { if (!g || (g != mdns_group)) return; switch (state) { case AVAHI_ENTRY_GROUP_ESTABLISHED: DPRINTF(E_DBG, L_MDNS, "Successfully added mDNS services\n"); break; case AVAHI_ENTRY_GROUP_COLLISION: DPRINTF(E_DBG, L_MDNS, "Group collision\n"); break; case AVAHI_ENTRY_GROUP_FAILURE: DPRINTF(E_DBG, L_MDNS, "Group failure\n"); break; case AVAHI_ENTRY_GROUP_UNCOMMITED: DPRINTF(E_DBG, L_MDNS, "Group uncommitted\n"); break; case AVAHI_ENTRY_GROUP_REGISTERING: DPRINTF(E_DBG, L_MDNS, "Group registering\n"); break; } } static int create_group_entry(struct mdns_group_entry *ge, int commit) { char hostname[HOST_NAME_MAX + 1]; char rdata[HOST_NAME_MAX + 6 + 1]; // Includes room for ".local" and 0-terminator int count; int i; int ret; if (!mdns_group) { mdns_group = avahi_entry_group_new(mdns_client, entry_group_callback, NULL); if (!mdns_group) { DPRINTF(E_WARN, L_MDNS, "Could not create Avahi EntryGroup: %s\n", MDNSERR); return -1; } } if (ge->publish == MDNS_PUBLISH_SERVICE) { DPRINTF(E_DBG, L_MDNS, "Adding service %s/%s\n", ge->name, ge->type); ret = avahi_entry_group_add_service_strlst(mdns_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, ge->name, ge->type, NULL, NULL, ge->port, ge->txt); if (ret < 0) { DPRINTF(E_LOG, L_MDNS, "Could not add mDNS service %s/%s: %s\n", ge->name, ge->type, avahi_strerror(ret)); return -1; } } else if (ge->publish == MDNS_PUBLISH_CNAME) { DPRINTF(E_DBG, L_MDNS, "Adding CNAME record %s\n", ge->name); ret = gethostname(hostname, HOST_NAME_MAX); if (ret < 0) { DPRINTF(E_LOG, L_MDNS, "Could not add CNAME %s, gethostname failed\n", ge->name); return -1; } // Note, gethostname does not guarantee 0-termination hostname[HOST_NAME_MAX] = 0; ret = snprintf(rdata, sizeof(rdata), ".%s.local", hostname); if (!(ret > 0 && ret < sizeof(rdata))) { DPRINTF(E_LOG, L_MDNS, "Could not add CNAME %s, hostname is invalid\n", ge->name); return -1; } // Convert to dns string: .forked-daapd.local -> \12forked-daapd\6local count = 0; for (i = ret - 1; i >= 0; i--) { if (rdata[i] == '.') { rdata[i] = count; count = 0; } else count++; } // ret + 1 should be the string length of rdata incl. 0-terminator ret = avahi_entry_group_add_record(mdns_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_USE_MULTICAST | AVAHI_PUBLISH_ALLOW_MULTIPLE, ge->name, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_CNAME, AVAHI_DEFAULT_TTL, rdata, ret + 1); if (ret < 0) { DPRINTF(E_LOG, L_MDNS, "Could not add CNAME record %s: %s\n", ge->name, avahi_strerror(ret)); return -1; } } if (!commit) return 0; ret = avahi_entry_group_commit(mdns_group); if (ret < 0) { DPRINTF(E_LOG, L_MDNS, "Could not commit mDNS services: %s\n", MDNSERR); return -1; } return 0; } static void create_all_group_entries(void) { struct mdns_group_entry *ge; int ret; if (!group_entries) { DPRINTF(E_DBG, L_MDNS, "No entries yet... skipping service create\n"); return; } if (mdns_group) avahi_entry_group_reset(mdns_group); DPRINTF(E_INFO, L_MDNS, "Re-registering mDNS groups (services and records)\n"); for (ge = group_entries; ge; ge = ge->next) { create_group_entry(ge, 0); if (!mdns_group) return; } ret = avahi_entry_group_commit(mdns_group); if (ret < 0) DPRINTF(E_WARN, L_MDNS, "Could not commit mDNS services: %s\n", MDNSERR); } static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata) { struct mdns_browser *mb; AvahiServiceBrowser *b; int error; switch (state) { case AVAHI_CLIENT_S_RUNNING: DPRINTF(E_LOG, L_MDNS, "Avahi state change: Client running\n"); if (!mdns_group) create_all_group_entries(); for (mb = browser_list; mb; mb = mb->next) { b = avahi_service_browser_new(mdns_client, AVAHI_IF_UNSPEC, mb->protocol, mb->type, NULL, 0, browse_callback, mb); if (!b) DPRINTF(E_LOG, L_MDNS, "Failed to recreate service browser (service type %s): %s\n", mb->type, MDNSERR); } break; case AVAHI_CLIENT_S_COLLISION: DPRINTF(E_LOG, L_MDNS, "Avahi state change: Client collision\n"); if(mdns_group) avahi_entry_group_reset(mdns_group); break; case AVAHI_CLIENT_FAILURE: DPRINTF(E_LOG, L_MDNS, "Avahi state change: Client failure\n"); error = avahi_client_errno(c); if (error == AVAHI_ERR_DISCONNECTED) { DPRINTF(E_LOG, L_MDNS, "Avahi Server disconnected, reconnecting\n"); avahi_client_free(mdns_client); mdns_group = NULL; mdns_client = avahi_client_new(&ev_poll_api, AVAHI_CLIENT_NO_FAIL, client_callback, NULL, &error); if (!mdns_client) DPRINTF(E_LOG, L_MDNS, "Failed to create new Avahi client: %s\n", avahi_strerror(error)); } else { DPRINTF(E_LOG, L_MDNS, "Avahi client failure: %s\n", avahi_strerror(error)); } break; case AVAHI_CLIENT_S_REGISTERING: DPRINTF(E_LOG, L_MDNS, "Avahi state change: Client registering\n"); if (mdns_group) avahi_entry_group_reset(mdns_group); break; case AVAHI_CLIENT_CONNECTING: DPRINTF(E_LOG, L_MDNS, "Avahi state change: Client connecting\n"); break; } } /* mDNS interface - to be called only from the main thread */ int mdns_init(void) { int error; DPRINTF(E_DBG, L_MDNS, "Initializing Avahi mDNS\n"); all_w = NULL; all_t = NULL; group_entries = NULL; browser_list = NULL; mdns_client = avahi_client_new(&ev_poll_api, AVAHI_CLIENT_NO_FAIL, client_callback, NULL, &error); if (!mdns_client) { DPRINTF(E_WARN, L_MDNS, "mdns_init: Could not create Avahi client: %s\n", MDNSERR); return -1; } return 0; } void mdns_deinit(void) { struct mdns_group_entry *ge; struct mdns_browser *mb; AvahiWatch *w; AvahiTimeout *t; for (t = all_t; t; t = t->next) if (t->ev) { event_free(t->ev); t->ev = NULL; } for (w = all_w; w; w = w->next) if (w->ev) { event_free(w->ev); w->ev = NULL; } for (ge = group_entries; group_entries; ge = group_entries) { group_entries = ge->next; free(ge->name); free(ge->type); avahi_string_list_free(ge->txt); free(ge); } for (mb = browser_list; browser_list; mb = browser_list) { browser_list = mb->next; free(mb->type); free(mb); } if (mdns_client) avahi_client_free(mdns_client); } int mdns_register(char *name, char *type, int port, char **txt) { struct mdns_group_entry *ge; AvahiStringList *txt_sl; int i; ge = calloc(1, sizeof(struct mdns_group_entry)); if (!ge) { DPRINTF(E_LOG, L_MDNS, "Out of memory for mdns register\n"); return -1; } ge->publish = MDNS_PUBLISH_SERVICE; ge->name = strdup(name); ge->type = strdup(type); ge->port = port; txt_sl = NULL; if (txt) { for (i = 0; txt[i]; i++) { txt_sl = avahi_string_list_add(txt_sl, txt[i]); DPRINTF(E_DBG, L_MDNS, "Added key %s\n", txt[i]); } } ge->txt = txt_sl; ge->next = group_entries; group_entries = ge; create_all_group_entries(); // TODO why is this required? return 0; } int mdns_cname(char *name) { struct mdns_group_entry *ge; ge = calloc(1, sizeof(struct mdns_group_entry)); if (!ge) { DPRINTF(E_LOG, L_MDNS, "Out of memory for mDNS CNAME\n"); return -1; } ge->publish = MDNS_PUBLISH_CNAME; ge->name = strdup(name); ge->next = group_entries; group_entries = ge; create_all_group_entries(); return 0; } int mdns_browse(char *type, int family, mdns_browse_cb cb) { struct mdns_browser *mb; AvahiServiceBrowser *b; DPRINTF(E_DBG, L_MDNS, "Adding service browser for type %s\n", type); mb = calloc(1, sizeof(struct mdns_browser)); if (!mb) { DPRINTF(E_LOG, L_MDNS, "Out of memory for new mdns browser\n"); return -1; } mb->protocol = avahi_af_to_proto(family); mb->type = strdup(type); mb->cb = cb; mb->next = browser_list; browser_list = mb; b = avahi_service_browser_new(mdns_client, AVAHI_IF_UNSPEC, mb->protocol, mb->type, NULL, 0, browse_callback, mb); if (!b) { DPRINTF(E_LOG, L_MDNS, "Failed to create service browser: %s\n", MDNSERR); browser_list = mb->next; free(mb->type); free(mb); return -1; } return 0; } forked-daapd-25.0/src/mdns_dnssd.c000066400000000000000000000562761313447753700171060ustar00rootroot00000000000000/* * Bonjour mDNS backend, with libevent polling * * Copyright (c) Scott Shambarger * Copyright (C) 2009-2011 Julien BLACHE * Copyright (C) 2005 Sebastian Dr�ge * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include "mdns.h" #include #include #include #include #include #include #include #include #include #include #include /* timeout for service resolves */ #define MDNS_RESOLVE_TIMEOUT_SECS 5 // Hack for FreeBSD, don't want to bother with sysconf() #ifndef HOST_NAME_MAX # include # define HOST_NAME_MAX _POSIX_HOST_NAME_MAX #endif #include "logger.h" /* Main event base, from main.c */ extern struct event_base *evbase_main; static DNSServiceRef mdns_sdref_main; static struct event *mdns_ev_main; /* registered services last the life of the program */ struct mdns_service { struct mdns_service *next; /* allocated */ DNSServiceRef sdref; TXTRecordRef txtRecord; }; static struct mdns_service *mdns_services = NULL; /* we keep records forever to display names in logs when registered or renamed */ struct mdns_record { struct mdns_record *next; /* allocated */ char *name; /* references */ DNSRecordRef recRef; uint16_t rrtype; }; static struct mdns_record *mdns_records = NULL; struct mdns_addr_lookup { struct mdns_addr_lookup *next; /* allocated */ DNSServiceRef sdref; struct keyval txt_kv; /* references */ u_int16_t port; struct mdns_resolver *rs; }; /* resolvers and address lookups clean themselves up */ struct mdns_resolver { struct mdns_resolver *next; /* allocated */ DNSServiceRef sdref; char *service; char *regtype; char *domain; struct event *timer; struct mdns_addr_lookup *lookups; /* references */ void *uuid; uint32_t interface; struct mdns_browser *mb; }; /* browsers keep running for the life of the program */ struct mdns_browser { struct mdns_browser *next; /* allocated */ DNSServiceRef sdref; struct mdns_resolver *resolvers; char *regtype; /* references */ mdns_browse_cb cb; DNSServiceProtocol protocol; void *res_uuid; }; static struct mdns_browser *mdns_browsers = NULL; #define IPV4LL_NETWORK 0xA9FE0000 #define IPV4LL_NETMASK 0xFFFF0000 #define IPV6LL_NETWORK 0xFE80 #define IPV6LL_NETMASK 0xFFC0 static int is_v4ll(struct in_addr *addr) { return ((ntohl(addr->s_addr) & IPV4LL_NETMASK) == IPV4LL_NETWORK); } static int is_v6ll(struct in6_addr *addr) { return ((((addr->s6_addr[0] << 8) | addr->s6_addr[1]) & IPV6LL_NETMASK) == IPV6LL_NETWORK); } /* mDNS interface - to be called only from the main thread */ static int mdns_service_free(struct mdns_service *s) { if(! s) return -1; /* free sdref, then everything else */ if(s->sdref) DNSServiceRefDeallocate(s->sdref); TXTRecordDeallocate(&(s->txtRecord)); free(s); return -1; } static int mdns_addr_lookup_free(struct mdns_addr_lookup *lu) { if (! lu) return -1; if(lu->sdref) DNSServiceRefDeallocate(lu->sdref); keyval_clear(&lu->txt_kv); free(lu); return -1; } static int mdns_resolver_free(struct mdns_resolver *rs) { struct mdns_addr_lookup *lu; if (! rs) return -1; /* free/cancel all lookups */ for(lu = rs->lookups; lu; lu = rs->lookups) { rs->lookups = lu->next; mdns_addr_lookup_free(lu); } if(rs->timer) event_free(rs->timer); if(rs->sdref) DNSServiceRefDeallocate(rs->sdref); free(rs->service); free(rs->regtype); free(rs->domain); free(rs); return -1; } static int mdns_browser_free(struct mdns_browser *mb) { struct mdns_resolver *rs; if(! mb) return -1; /* free all resolvers */ for(rs = mb->resolvers; rs; rs = mb->resolvers) { mb->resolvers = rs->next; mdns_resolver_free(rs); } /* free sdref, then everything else */ if(mb->sdref) DNSServiceRefDeallocate(mb->sdref); free(mb->regtype); free(mb); return -1; } static int mdns_record_free(struct mdns_record *r) { if (! r) return -1; free(r->name); free(r); return -1; } static int mdns_main_free(void) { struct mdns_service *s; struct mdns_browser *mb; struct mdns_record *r; for(s = mdns_services; mdns_services; s = mdns_services) { mdns_services = s->next; mdns_service_free(s); } for (mb = mdns_browsers; mdns_browsers; mb = mdns_browsers) { mdns_browsers = mb->next; mdns_browser_free(mb); } for (r = mdns_records; mdns_records; r = mdns_records) { mdns_records = r->next; mdns_record_free(r); } if(mdns_ev_main) event_free(mdns_ev_main); mdns_ev_main = NULL; if(mdns_sdref_main) DNSServiceRefDeallocate(mdns_sdref_main); mdns_sdref_main = NULL; return -1; } void mdns_deinit(void) { mdns_main_free(); } static void mdns_event_cb(evutil_socket_t fd, short flags, void *data) { DNSServiceErrorType err; err = DNSServiceProcessResult(mdns_sdref_main); if (err != kDNSServiceErr_NoError) DPRINTF(E_LOG, L_MDNS, "DNSServiceProcessResult error %d\n", err); } int mdns_init(void) { DNSServiceErrorType err; int fd; int ret; DPRINTF(E_DBG, L_MDNS, "Initializing DNS_SD mDNS\n"); mdns_services = NULL; mdns_browsers = NULL; mdns_records = NULL; mdns_sdref_main = NULL; mdns_ev_main = NULL; err = DNSServiceCreateConnection(&mdns_sdref_main); if (err != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Could not create mDNS connection\n"); return -1; } fd = DNSServiceRefSockFD(mdns_sdref_main); if (fd == -1) { DPRINTF(E_LOG, L_MDNS, "DNSServiceRefSockFD failed\n"); return mdns_main_free(); } mdns_ev_main = event_new(evbase_main, fd, EV_PERSIST | EV_READ, mdns_event_cb, NULL); if (! mdns_ev_main) { DPRINTF(E_LOG, L_MDNS, "Could not make new event in mdns\n"); return mdns_main_free(); } ret = event_add(mdns_ev_main, NULL); if (ret != 0) { DPRINTF(E_LOG, L_MDNS, "Could not add new event in mdns\n"); return mdns_main_free(); } return 0; } static void mdns_register_callback(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, const char *name, const char *regtype, const char *domain, void *context) { switch (errorCode) { case kDNSServiceErr_NoError: DPRINTF(E_DBG, L_MDNS, "Successfully added mDNS service '%s.%s'\n", name, regtype); break; case kDNSServiceErr_NameConflict: DPRINTF(E_DBG, L_MDNS, "Name collision for service '%s.%s' - automatically assigning new name\n", name, regtype); break; case kDNSServiceErr_NoMemory: DPRINTF(E_DBG, L_MDNS, "Out of memory registering service %s\n", name); break; default: DPRINTF(E_DBG, L_MDNS, "Unspecified error registering service %s, error %d\n", name, errorCode); } } int mdns_register(char *name, char *regtype, int port, char **txt) { struct mdns_service *s; DNSServiceErrorType err; int i; char *eq; DPRINTF(E_DBG, L_MDNS, "Adding mDNS service '%s.%s'\n", name, regtype); s = calloc(1, sizeof(*s)); if (!s) { DPRINTF(E_LOG, L_MDNS, "Out of memory registering service.\n"); return -1; } TXTRecordCreate(&(s->txtRecord), 0, NULL); for (i = 0; txt && txt[i]; i++) { if ((eq = strchr(txt[i], '='))) { *eq = '\0'; eq++; err = TXTRecordSetValue(&(s->txtRecord), txt[i], strlen(eq) * sizeof(char), eq); *(--eq) = '='; if (err != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Could not set TXT record value\n"); return mdns_service_free(s); } } } s->sdref = mdns_sdref_main; err = DNSServiceRegister(&(s->sdref), kDNSServiceFlagsShareConnection, 0, name, regtype, NULL, NULL, htons(port), TXTRecordGetLength(&(s->txtRecord)), TXTRecordGetBytesPtr(&(s->txtRecord)), mdns_register_callback, NULL); if (err != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Error registering service '%s.%s'\n", name, regtype); s->sdref = NULL; return mdns_service_free(s); } s->next = mdns_services; mdns_services = s; return 0; } static void mdns_record_callback(DNSServiceRef sdRef, DNSRecordRef RecordRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, void *context) { struct mdns_record *r; r = context; switch (errorCode) { case kDNSServiceErr_NoError: DPRINTF(E_DBG, L_MDNS, "Successfully added mDNS record %s\n", r->name); break; case kDNSServiceErr_NameConflict: DPRINTF(E_DBG, L_MDNS, "Record ame collision - automatically assigning new name\n"); break; case kDNSServiceErr_NoMemory: DPRINTF(E_DBG, L_MDNS, "Out of memory registering record %s\n", r->name); break; default: DPRINTF(E_DBG, L_MDNS, "Unspecified error registering record %s, error %d\n", r->name, errorCode); } } static int mdns_register_record(uint16_t rrtype, const char *name, uint16_t rdlen, const void *rdata) { struct mdns_record *r; DNSServiceErrorType err; DPRINTF(E_DBG, L_MDNS, "Adding mDNS record %s/%u\n", name, rrtype); r = calloc(1, sizeof(*r)); if (!r) { DPRINTF(E_LOG, L_MDNS, "Out of memory adding record.\n"); return -1; } r->name = strdup(name); if (!(r->name)) { DPRINTF(E_LOG, L_MDNS, "Out of memory adding record.\n"); return mdns_record_free(r); } r->rrtype = rrtype; err = DNSServiceRegisterRecord(mdns_sdref_main, &r->recRef, kDNSServiceFlagsShared, 0, r->name, r->rrtype, kDNSServiceClass_IN, rdlen, rdata, 0, mdns_record_callback, r); if (err != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Error registering record %s, error %d\n", name, err); return mdns_record_free(r); } /* keep these around so we can display r->name in the callback */ r->next = mdns_records; mdns_records = r; return 0; } int mdns_cname(char *name) { char hostname[HOST_NAME_MAX + 1]; // Includes room for "..local" and 0-terminator char rdata[HOST_NAME_MAX + 8]; int count; int i; int ret; ret = gethostname(hostname, HOST_NAME_MAX); if (ret < 0) { DPRINTF(E_LOG, L_MDNS, "Could not add CNAME %s, gethostname failed\n", name); return -1; } // Note, gethostname does not guarantee 0-termination hostname[HOST_NAME_MAX] = 0; ret = snprintf(rdata, sizeof(rdata), ".%s.local", hostname); if (!(ret > 0 && ret < sizeof(rdata))) { DPRINTF(E_LOG, L_MDNS, "Could not add CNAME %s, hostname is invalid\n", name); return -1; } // Convert to dns string: .forked-daapd.local -> \12forked-daapd\6local count = 0; for (i = ret - 1; i >= 0; i--) { if (rdata[i] == '.') { rdata[i] = count; count = 0; } else count++; } return mdns_register_record(kDNSServiceType_CNAME, name, (uint16_t)ret, rdata); } static void mdns_browse_call_cb(struct mdns_addr_lookup *lu, const char *hostname, const struct sockaddr *address) { char addr_str[INET6_ADDRSTRLEN]; if (address->sa_family == AF_INET) { struct sockaddr_in *addr = (struct sockaddr_in *)address; if (!inet_ntop(AF_INET, &addr->sin_addr, addr_str, sizeof(addr_str))) { DPRINTF(E_LOG, L_MDNS, "Could not print IPv4 address: %s\n", strerror(errno)); return; } if (!(lu->rs->mb->protocol & kDNSServiceProtocol_IPv4)) { DPRINTF(E_DBG, L_MDNS, "Discarding IPv4, not interested (service %s)\n", lu->rs->service); return; } else if (is_v4ll(&addr->sin_addr)) { DPRINTF(E_WARN, L_MDNS, "Ignoring announcement from %s, address %s is link-local\n", hostname, addr_str); return; } } else if (address->sa_family == AF_INET6) { struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)address; if (!inet_ntop(AF_INET6, &addr6->sin6_addr, addr_str, sizeof(addr_str))) { DPRINTF(E_LOG, L_MDNS, "Could not print IPv6 address: %s\n", strerror(errno)); return; } if (!(lu->rs->mb->protocol & kDNSServiceProtocol_IPv6)) { DPRINTF(E_DBG, L_MDNS, "Discarding IPv6, not interested (service %s)\n", lu->rs->service); return; } else if (is_v6ll(&addr6->sin6_addr)) { DPRINTF(E_WARN, L_MDNS, "Ignoring announcement from %s, address %s is link-local\n", hostname, addr_str); return; } } DPRINTF(E_DBG, L_MDNS, "Service %s, hostname %s resolved to %s\n", lu->rs->service, hostname, addr_str); /* Execute callback (mb->cb) with all the data */ lu->rs->mb->cb(lu->rs->service, lu->rs->regtype, lu->rs->domain, hostname, address->sa_family, addr_str, lu->port, &lu->txt_kv); } static void mdns_lookup_callback(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *hostname, const struct sockaddr *address, uint32_t ttl, void *context) { struct mdns_addr_lookup *lu; lu = context; if (errorCode != kDNSServiceErr_NoError ) { DPRINTF(E_LOG, L_MDNS, "Error resolving hostname '%s', error %d\n", hostname, errorCode); return; } if (flags & kDNSServiceFlagsAdd) mdns_browse_call_cb(lu, hostname, address); } static int mdns_addr_lookup_start(struct mdns_resolver *rs, uint32_t interfaceIndex, const char *hosttarget, uint16_t port, uint16_t txtLen, const unsigned char *txtRecord) { struct mdns_addr_lookup *lu; DNSServiceErrorType err; char key[256]; int i; uint8_t valueLen; const char *value; int ret; lu = calloc(1, sizeof(*lu)); if (!lu) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating address lookup.\n"); return -1; } lu->port = port; lu->rs = rs; for (i=0; TXTRecordGetItemAtIndex(txtLen, txtRecord, i, sizeof(key), key, &valueLen, (const void **)&value) != kDNSServiceErr_Invalid; i++) { ret = keyval_add_size(&lu->txt_kv, key, value, valueLen); if (ret < 0) { DPRINTF(E_LOG, L_MDNS, "Could not build TXT record keyval\n"); return mdns_addr_lookup_free(lu); } } lu->sdref = mdns_sdref_main; err = DNSServiceGetAddrInfo(&lu->sdref, kDNSServiceFlagsShareConnection, interfaceIndex, rs->mb->protocol, hosttarget, mdns_lookup_callback, lu); if (err != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Failed to create service resolver.\n"); lu->sdref = NULL; return mdns_addr_lookup_free(lu); } /* resolver now owns the lookup */ lu->next = rs->lookups; rs->lookups = lu; return 0; } static void mdns_resolver_remove(struct mdns_resolver *rs) { struct mdns_resolver *cur; /* remove from browser's resolver list */ if(rs->mb->resolvers == rs) rs->mb->resolvers = rs->next; else { for(cur = rs->mb->resolvers; cur; cur = cur->next) if (cur->next == rs) { cur->next = rs->next; break; } } /* free resolver (which cancels resolve) */ mdns_resolver_free(rs); } static void mdns_resolve_timeout_cb(evutil_socket_t fd, short flags, void *uuid) { struct mdns_browser *mb; struct mdns_resolver *rs = NULL; for(mb = mdns_browsers; mb && !rs; mb = mb->next) for(rs = mb->resolvers; rs; rs = rs->next) if(rs->uuid == uuid) { DPRINTF(E_DBG, L_MDNS, "Resolve finished for '%s' type '%s' interface %d\n", rs->service, rs->regtype, rs->interface); mdns_resolver_remove(rs); break; } } static void mdns_resolve_callback(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullname, const char *hosttarget, uint16_t port, uint16_t txtLen, const unsigned char *txtRecord, void *context) { struct mdns_resolver *rs; rs = context; /* convert port to host order */ port = ntohs(port); if (errorCode != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Error resolving service '%s', error %d\n", rs->service, errorCode); } else { DPRINTF(E_DBG, L_MDNS, "Bonjour resolved '%s' as '%s:%u' on interface %d\n", fullname, hosttarget, port, interfaceIndex); mdns_addr_lookup_start(rs, interfaceIndex, hosttarget, port, txtLen, txtRecord); } } static int mdns_resolve_start(struct mdns_browser *mb, uint32_t interfaceIndex, const char *serviceName, const char *regtype, const char *replyDomain) { DNSServiceErrorType err; struct mdns_resolver *rs; struct timeval tv; rs = calloc(1, sizeof(*rs)); if (!rs) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating service resolver.\n"); return -1; } rs->service = strdup(serviceName); if (!rs->service) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating service resolver.\n"); return mdns_resolver_free(rs); } rs->regtype = strdup(regtype); if (!rs->regtype) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating service resolver.\n"); return mdns_resolver_free(rs); } rs->domain = strdup(replyDomain); if (!rs->domain) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating service resolver.\n"); return mdns_resolver_free(rs); } rs->mb = mb; rs->interface = interfaceIndex; /* create a timer with a uuid, so we can search for resolver without leaking */ rs->uuid = ++(mb->res_uuid); rs->timer = evtimer_new(evbase_main, mdns_resolve_timeout_cb, rs->uuid); if(! rs->timer) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating service resolver timer.\n"); return mdns_resolver_free(rs); } rs->sdref = mdns_sdref_main; err = DNSServiceResolve(&(rs->sdref), kDNSServiceFlagsShareConnection, interfaceIndex, serviceName, regtype, replyDomain, mdns_resolve_callback, rs); if (err != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Failed to create service resolver.\n"); rs->sdref = NULL; return mdns_resolver_free(rs); } /* add to browser's resolvers */ rs->next = mb->resolvers; mb->resolvers = rs; /* setup a timeout to cancel the resolve */ tv.tv_sec = MDNS_RESOLVE_TIMEOUT_SECS; tv.tv_usec = 0; evtimer_add(rs->timer, &tv); return 0; } static void mdns_resolve_cancel(const struct mdns_browser *mb, uint32_t interfaceIndex, const char *serviceName, const char *regtype, const char *replyDomain) { struct mdns_resolver *rs; for(rs = mb->resolvers; rs; rs = rs->next) { if ((rs->interface == interfaceIndex) && (! strcasecmp(rs->service, serviceName)) && (! strcmp(rs->regtype, regtype)) && (! strcasecmp(rs->domain, replyDomain))) { /* remove from resolvers, and free (which cancels resolve) */ DPRINTF(E_DBG, L_MDNS, "Cancelling resolve for '%s'\n", rs->service); mdns_resolver_remove(rs); break; } } return; } static void mdns_browse_callback(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *serviceName, const char *regtype, const char *replyDomain, void *context) { struct mdns_browser *mb; if (errorCode != kDNSServiceErr_NoError) { // FIXME: if d/c, we sould recreate the browser? DPRINTF(E_LOG, L_MDNS, "Bonjour browsing error %d\n", errorCode); return; } mb = context; if (flags & kDNSServiceFlagsAdd) { DPRINTF(E_DBG, L_MDNS, "Bonjour Browser: NEW service '%s' type '%s' interface %d\n", serviceName, regtype, interfaceIndex); mdns_resolve_start(mb, interfaceIndex, serviceName, regtype, replyDomain); } else { DPRINTF(E_DBG, L_MDNS, "Bonjour Browser: REMOVE service '%s' type '%s' interface %d\n", serviceName, regtype, interfaceIndex); mdns_resolve_cancel(mb, interfaceIndex, serviceName, regtype, replyDomain); mb->cb(serviceName, regtype, replyDomain, NULL, 0, NULL, -1, NULL); } } int mdns_browse(char *regtype, int family, mdns_browse_cb cb) { struct mdns_browser *mb; DNSServiceErrorType err; DPRINTF(E_DBG, L_MDNS, "Adding service browser for type %s\n", regtype); mb = calloc(1, sizeof(*mb)); if (!mb) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating service browser.\n"); return -1; } mb->cb = cb; /* flags are ignored in DNS-SD implementation */ switch(family) { case AF_UNSPEC: mb->protocol = kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6; break; case AF_INET: mb->protocol = kDNSServiceProtocol_IPv4; break; case AF_INET6: mb->protocol = kDNSServiceProtocol_IPv6; break; default: DPRINTF(E_LOG, L_MDNS, "Unrecognized protocol family %d.\n", family); return mdns_browser_free(mb); } mb->regtype = strdup(regtype); if (!mb->regtype) { DPRINTF(E_LOG, L_MDNS, "Out of memory creating service browser.\n"); return mdns_browser_free(mb); } mb->sdref = mdns_sdref_main; err = DNSServiceBrowse(&(mb->sdref), kDNSServiceFlagsShareConnection, 0, regtype, NULL, mdns_browse_callback, mb); if (err != kDNSServiceErr_NoError) { DPRINTF(E_LOG, L_MDNS, "Failed to create service browser.\n"); mb->sdref = NULL; return mdns_browser_free(mb); } mb->next = mdns_browsers; mdns_browsers = mb; return 0; } forked-daapd-25.0/src/misc.c000066400000000000000000000532441313447753700156750ustar00rootroot00000000000000/* * Copyright (C) 2009-2010 Julien BLACHE * * Some code included below is in the public domain, check comments * in the file. * * Pieces of code adapted from mt-daapd: * Copyright (C) 2003-2007 Ron Pedde (ron@pedde.com) * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #ifndef CLOCK_REALTIME #include #endif #include #include #include "logger.h" #include "misc.h" int safe_atoi32(const char *str, int32_t *val) { char *end; long intval; *val = 0; errno = 0; intval = strtol(str, &end, 10); if (((errno == ERANGE) && ((intval == LONG_MAX) || (intval == LONG_MIN))) || ((errno != 0) && (intval == 0))) { DPRINTF(E_DBG, L_MISC, "Invalid integer in string (%s): %s\n", str, strerror(errno)); return -1; } if (end == str) { DPRINTF(E_DBG, L_MISC, "No integer found in string (%s)\n", str); return -1; } if (intval > INT32_MAX) { DPRINTF(E_DBG, L_MISC, "Integer value too large (%s)\n", str); return -1; } *val = (int32_t)intval; return 0; } int safe_atou32(const char *str, uint32_t *val) { char *end; unsigned long intval; *val = 0; errno = 0; intval = strtoul(str, &end, 10); if (((errno == ERANGE) && (intval == ULONG_MAX)) || ((errno != 0) && (intval == 0))) { DPRINTF(E_DBG, L_MISC, "Invalid integer in string (%s): %s\n", str, strerror(errno)); return -1; } if (end == str) { DPRINTF(E_DBG, L_MISC, "No integer found in string (%s)\n", str); return -1; } if (intval > UINT32_MAX) { DPRINTF(E_DBG, L_MISC, "Integer value too large (%s)\n", str); return -1; } *val = (uint32_t)intval; return 0; } int safe_hextou32(const char *str, uint32_t *val) { char *end; unsigned long intval; *val = 0; /* A hex shall begin with 0x */ if (strncmp(str, "0x", 2) != 0) return safe_atou32(str, val); errno = 0; intval = strtoul(str, &end, 16); if (((errno == ERANGE) && (intval == ULONG_MAX)) || ((errno != 0) && (intval == 0))) { DPRINTF(E_DBG, L_MISC, "Invalid integer in string (%s): %s\n", str, strerror(errno)); return -1; } if (end == str) { DPRINTF(E_DBG, L_MISC, "No integer found in string (%s)\n", str); return -1; } if (intval > UINT32_MAX) { DPRINTF(E_DBG, L_MISC, "Integer value too large (%s)\n", str); return -1; } *val = (uint32_t)intval; return 0; } int safe_atoi64(const char *str, int64_t *val) { char *end; long long intval; *val = 0; errno = 0; intval = strtoll(str, &end, 10); if (((errno == ERANGE) && ((intval == LLONG_MAX) || (intval == LLONG_MIN))) || ((errno != 0) && (intval == 0))) { DPRINTF(E_DBG, L_MISC, "Invalid integer in string (%s): %s\n", str, strerror(errno)); return -1; } if (end == str) { DPRINTF(E_DBG, L_MISC, "No integer found in string (%s)\n", str); return -1; } if (intval > INT64_MAX) { DPRINTF(E_DBG, L_MISC, "Integer value too large (%s)\n", str); return -1; } *val = (int64_t)intval; return 0; } int safe_atou64(const char *str, uint64_t *val) { char *end; unsigned long long intval; *val = 0; errno = 0; intval = strtoull(str, &end, 10); if (((errno == ERANGE) && (intval == ULLONG_MAX)) || ((errno != 0) && (intval == 0))) { DPRINTF(E_DBG, L_MISC, "Invalid integer in string (%s): %s\n", str, strerror(errno)); return -1; } if (end == str) { DPRINTF(E_DBG, L_MISC, "No integer found in string (%s)\n", str); return -1; } if (intval > UINT64_MAX) { DPRINTF(E_DBG, L_MISC, "Integer value too large (%s)\n", str); return -1; } *val = (uint64_t)intval; return 0; } int safe_hextou64(const char *str, uint64_t *val) { char *end; unsigned long long intval; *val = 0; errno = 0; intval = strtoull(str, &end, 16); if (((errno == ERANGE) && (intval == ULLONG_MAX)) || ((errno != 0) && (intval == 0))) { DPRINTF(E_DBG, L_MISC, "Invalid integer in string (%s): %s\n", str, strerror(errno)); return -1; } if (end == str) { DPRINTF(E_DBG, L_MISC, "No integer found in string (%s)\n", str); return -1; } if (intval > UINT64_MAX) { DPRINTF(E_DBG, L_MISC, "Integer value too large (%s)\n", str); return -1; } *val = (uint64_t)intval; return 0; } char * safe_strdup(const char *str) { if (str == NULL) return NULL; return strdup(str); } /* * Wrapper function for vasprintf by Intel Corporation * Published under the L-GPL 2.1 licence as part of clr-boot-manager * * https://github.com/clearlinux/clr-boot-manager */ char * safe_asprintf(const char *fmt, ...) { char *ret = NULL; va_list va; va_start(va, fmt); if (vasprintf(&ret, fmt, va) < 0) { DPRINTF(E_FATAL, L_MISC, "Out of memory for safe_asprintf\n"); abort(); } va_end(va); return ret; } /* Key/value functions */ struct keyval * keyval_alloc(void) { struct keyval *kv; kv = (struct keyval *)malloc(sizeof(struct keyval)); if (!kv) { DPRINTF(E_LOG, L_MISC, "Out of memory for keyval alloc\n"); return NULL; } memset(kv, 0, sizeof(struct keyval)); return kv; } int keyval_add_size(struct keyval *kv, const char *name, const char *value, size_t size) { struct onekeyval *okv; const char *val; if (!kv) return -1; /* Check for duplicate key names */ val = keyval_get(kv, name); if (val) { /* Same value, fine */ if (strcmp(val, value) == 0) return 0; else /* Different value, bad */ return -1; } okv = (struct onekeyval *)malloc(sizeof(struct onekeyval)); if (!okv) { DPRINTF(E_LOG, L_MISC, "Out of memory for new keyval\n"); return -1; } okv->name = strdup(name); if (!okv->name) { DPRINTF(E_LOG, L_MISC, "Out of memory for new keyval name\n"); free(okv); return -1; } okv->value = (char *)malloc(size + 1); if (!okv->value) { DPRINTF(E_LOG, L_MISC, "Out of memory for new keyval value\n"); free(okv->name); free(okv); return -1; } memcpy(okv->value, value, size); okv->value[size] = '\0'; okv->next = NULL; if (!kv->head) kv->head = okv; if (kv->tail) kv->tail->next = okv; kv->tail = okv; return 0; } int keyval_add(struct keyval *kv, const char *name, const char *value) { return keyval_add_size(kv, name, value, strlen(value)); } void keyval_remove(struct keyval *kv, const char *name) { struct onekeyval *okv; struct onekeyval *pokv; if (!kv) return; for (pokv = NULL, okv = kv->head; okv; pokv = okv, okv = okv->next) { if (strcasecmp(okv->name, name) == 0) break; } if (!okv) return; if (okv == kv->head) kv->head = okv->next; if (okv == kv->tail) kv->tail = pokv; if (pokv) pokv->next = okv->next; free(okv->name); free(okv->value); free(okv); } const char * keyval_get(struct keyval *kv, const char *name) { struct onekeyval *okv; if (!kv) return NULL; for (okv = kv->head; okv; okv = okv->next) { if (strcasecmp(okv->name, name) == 0) return okv->value; } return NULL; } void keyval_clear(struct keyval *kv) { struct onekeyval *hokv; struct onekeyval *okv; if (!kv) return; hokv = kv->head; for (okv = hokv; hokv; okv = hokv) { hokv = okv->next; free(okv->name); free(okv->value); free(okv); } kv->head = NULL; kv->tail = NULL; } void keyval_sort(struct keyval *kv) { struct onekeyval *head; struct onekeyval *okv; struct onekeyval *sokv; if (!kv || !kv->head) return; head = kv->head; for (okv = kv->head; okv; okv = okv->next) { okv->sort = NULL; for (sokv = kv->head; sokv; sokv = sokv->next) { // We try to find a name which is greater than okv->name // but less than our current candidate (okv->sort->name) if ( (strcmp(sokv->name, okv->name) > 0) && ((okv->sort == NULL) || (strcmp(sokv->name, okv->sort->name) < 0)) ) okv->sort = sokv; } // Find smallest name, which will be the new head if (strcmp(okv->name, head->name) < 0) head = okv; } while ((okv = kv->head)) { kv->head = okv->next; okv->next = okv->sort; } kv->head = head; for (okv = kv->head; okv; okv = okv->next) kv->tail = okv; DPRINTF(E_DBG, L_MISC, "Keyval sorted. New head: %s. New tail: %s.\n", kv->head->name, kv->tail->name); } char * m_realpath(const char *pathname) { char buf[PATH_MAX]; char *ret; ret = realpath(pathname, buf); if (!ret) return NULL; ret = strdup(buf); if (!ret) { DPRINTF(E_LOG, L_MISC, "Out of memory for realpath\n"); return NULL; } return ret; } char ** m_readfile(const char *path, int num_lines) { char buf[256]; FILE *fp; char **lines; char *line; int i; // Alloc array of char pointers lines = calloc(num_lines, sizeof(char *)); if (!lines) return NULL; fp = fopen(path, "rb"); if (!fp) { DPRINTF(E_LOG, L_MISC, "Could not open file '%s' for reading: %s\n", path, strerror(errno)); free(lines); return NULL; } for (i = 0; i < num_lines; i++) { line = fgets(buf, sizeof(buf), fp); if (!line) { DPRINTF(E_LOG, L_MISC, "File '%s' has fewer lines than expected (found %d, expected %d)\n", path, i, num_lines); goto error; } lines[i] = trimwhitespace(line); if (!lines[i] || (strlen(lines[i]) == 0)) { DPRINTF(E_LOG, L_MISC, "Line %d in '%s' is invalid\n", i+1, path); goto error; } } fclose(fp); return lines; error: for (i = 0; i < num_lines; i++) free(lines[i]); free(lines); fclose(fp); return NULL; } char * unicode_fixup_string(char *str, const char *fromcode) { uint8_t *ret; size_t len; if (!str) return NULL; len = strlen(str); /* String is valid UTF-8 */ if (!u8_check((uint8_t *)str, len)) { if (len >= 3) { /* Check for and strip byte-order mark */ if (memcmp("\xef\xbb\xbf", str, 3) == 0) memmove(str, str + 3, len - 3 + 1); } return str; } ret = u8_strconv_from_encoding(str, fromcode, iconveh_question_mark); if (!ret) { DPRINTF(E_LOG, L_MISC, "Could not convert string '%s' to UTF-8: %s\n", str, strerror(errno)); return NULL; } return (char *)ret; } char * trimwhitespace(const char *str) { char *ptr; char *start; char *out; if (!str) return NULL; // Find the beginning while (isspace(*str)) str++; if (*str == 0) // All spaces? return strdup(""); // Make copy, because we will need to insert a null terminator start = strdup(str); if (!start) return NULL; // Find the end ptr = start + strlen(start) - 1; while (ptr > start && isspace(*ptr)) ptr--; // Insert null terminator *(ptr+1) = 0; out = strdup(start); free(start); return out; } void swap_pointers(char **a, char **b) { char *t = *a; *a = *b; *b = t; } uint32_t djb_hash(const void *data, size_t len) { const unsigned char *bytes = data; uint32_t hash = 5381; while (len--) { hash = ((hash << 5) + hash) + *bytes; bytes++; } return hash; } static unsigned char b64_decode_table[256]; char * b64_decode(const char *b64) { char *str; const unsigned char *iptr; unsigned char *optr; unsigned char c; int len; int i; if (b64_decode_table[0] == 0) { memset(b64_decode_table, 0xff, sizeof(b64_decode_table)); /* Base64 encoding: A-Za-z0-9+/ */ for (i = 0; i < 26; i++) { b64_decode_table['A' + i] = i; b64_decode_table['a' + i] = i + 26; } for (i = 0; i < 10; i++) b64_decode_table['0' + i] = i + 52; b64_decode_table['+'] = 62; b64_decode_table['/'] = 63; /* Stop on '=' */ b64_decode_table['='] = 100; /* > 63 */ } len = strlen(b64); str = (char *)malloc(len); if (!str) return NULL; memset(str, 0, len); iptr = (const unsigned char *)b64; optr = (unsigned char *)str; i = 0; while (len) { if (*iptr == '=') break; c = b64_decode_table[*iptr]; if (c > 63) { iptr++; len--; continue; } switch (i) { case 0: optr[0] = c << 2; break; case 1: optr[0] |= c >> 4; optr[1] = c << 4; break; case 2: optr[1] |= c >> 2; optr[2] = c << 6; break; case 3: optr[2] |= c; break; } i++; if (i == 4) { optr += 3; i = 0; } len--; iptr++; } return str; } static const char b64_encode_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static void b64_encode_block(const uint8_t *in, char *out, int len) { out[0] = b64_encode_table[in[0] >> 2]; out[2] = out[3] = '='; if (len == 1) out[1] = b64_encode_table[((in[0] & 0x03) << 4)]; else { out[1] = b64_encode_table[((in[0] & 0x03) << 4) | ((in[1] & 0xf0) >> 4)]; if (len == 2) out[2] = b64_encode_table[((in[1] & 0x0f) << 2)]; else { out[2] = b64_encode_table[((in[1] & 0x0f) << 2) | ((in[2] & 0xc0) >> 6)]; out[3] = b64_encode_table[in[2] & 0x3f]; } } } static void b64_encode_full_block(const uint8_t *in, char *out) { out[0] = b64_encode_table[in[0] >> 2]; out[1] = b64_encode_table[((in[0] & 0x03) << 4) | ((in[1] & 0xf0) >> 4)]; out[2] = b64_encode_table[((in[1] & 0x0f) << 2) | ((in[2] & 0xc0) >> 6)]; out[3] = b64_encode_table[in[2] & 0x3f]; } char * b64_encode(const uint8_t *in, size_t len) { char *encoded; char *out; /* 3 in chars -> 4 out chars */ encoded = (char *)malloc(len + (len / 3) + 4 + 1); if (!encoded) return NULL; out = encoded; while (len >= 3) { b64_encode_full_block(in, out); len -= 3; in += 3; out += 4; } if (len > 0) { b64_encode_block(in, out, len); out += 4; } out[0] = '\0'; return encoded; } /* * MurmurHash2, 64-bit versions, by Austin Appleby * * Code released under the public domain, as per * * as of 2010-01-03. */ #if SIZEOF_VOID_P == 8 /* 64bit platforms */ uint64_t murmur_hash64(const void *key, int len, uint32_t seed) { const int r = 47; const uint64_t m = 0xc6a4a7935bd1e995; const uint64_t *data; const uint64_t *end; const unsigned char *data_tail; uint64_t h; uint64_t k; h = seed ^ (len * m); data = (const uint64_t *)key; end = data + (len / 8); while (data != end) { k = *data++; k *= m; k ^= k >> r; k *= m; h ^= k; h *= m; } data_tail = (const unsigned char *)data; switch (len & 7) { case 7: h ^= (uint64_t)(data_tail[6]) << 48; /* FALLTHROUGH */ case 6: h ^= (uint64_t)(data_tail[5]) << 40; /* FALLTHROUGH */ case 5: h ^= (uint64_t)(data_tail[4]) << 32; /* FALLTHROUGH */ case 4: h ^= (uint64_t)(data_tail[3]) << 24; /* FALLTHROUGH */ case 3: h ^= (uint64_t)(data_tail[2]) << 16; /* FALLTHROUGH */ case 2: h ^= (uint64_t)(data_tail[1]) << 8; /* FALLTHROUGH */ case 1: h ^= (uint64_t)(data_tail[0]); h *= m; } h ^= h >> r; h *= m; h ^= h >> r; return h; } #elif SIZEOF_VOID_P == 4 /* 32bit platforms */ uint64_t murmur_hash64(const void *key, int len, uint32_t seed) { const int r = 24; const uint32_t m = 0x5bd1e995; const uint32_t *data; const unsigned char *data_tail; uint32_t k1; uint32_t h1; uint32_t k2; uint32_t h2; uint64_t h; h1 = seed ^ len; h2 = 0; data = (const uint32_t *)key; while (len >= 8) { k1 = *data++; k1 *= m; k1 ^= k1 >> r; k1 *= m; h1 *= m; h1 ^= k1; k2 = *data++; k2 *= m; k2 ^= k2 >> r; k2 *= m; h2 *= m; h2 ^= k2; len -= 8; } if (len >= 4) { k1 = *data++; k1 *= m; k1 ^= k1 >> r; k1 *= m; h1 *= m; h1 ^= k1; len -= 4; } data_tail = (const unsigned char *)data; switch(len) { case 3: h2 ^= (uint32_t)(data_tail[2]) << 16; case 2: h2 ^= (uint32_t)(data_tail[1]) << 8; case 1: h2 ^= (uint32_t)(data_tail[0]); h2 *= m; }; h1 ^= h2 >> 18; h1 *= m; h2 ^= h1 >> 22; h2 *= m; h1 ^= h2 >> 17; h1 *= m; h2 ^= h1 >> 19; h2 *= m; h = h1; h = (h << 32) | h2; return h; } #else # error Platform not supported #endif int clock_gettime_with_res(clockid_t clock_id, struct timespec *tp, struct timespec *res) { int ret; if ((!tp) || (!res)) return -1; ret = clock_gettime(clock_id, tp); /* this will only work for sub-second resolutions. */ if (ret == 0 && res->tv_nsec > 1) tp->tv_nsec = (tp->tv_nsec/res->tv_nsec)*res->tv_nsec; return ret; } struct timespec timespec_add(struct timespec time1, struct timespec time2) { struct timespec result; result.tv_sec = time1.tv_sec + time2.tv_sec; result.tv_nsec = time1.tv_nsec + time2.tv_nsec; if (result.tv_nsec >= 1000000000L) { result.tv_sec++; result.tv_nsec -= 1000000000L; } return result; } int timespec_cmp(struct timespec time1, struct timespec time2) { /* Less than. */ if (time1.tv_sec < time2.tv_sec) return -1; /* Greater than. */ else if (time1.tv_sec > time2.tv_sec) return 1; /* Less than. */ else if (time1.tv_nsec < time2.tv_nsec) return -1; /* Greater than. */ else if (time1.tv_nsec > time2.tv_nsec) return 1; /* Equal. */ else return 0; } struct timespec timespec_reltoabs(struct timespec relative) { struct timespec absolute; #ifdef CLOCK_REALTIME clock_gettime(CLOCK_REALTIME, &absolute); #else struct timeval tv; gettimeofday(&tv, NULL); TIMEVAL_TO_TIMESPEC(&tv, &absolute); #endif return timespec_add(absolute, relative); } #if defined(HAVE_MACH_CLOCK) || defined(HAVE_MACH_TIMER) #include /* mach_absolute_time */ #include /* host_get_clock_service */ #include /* clock_get_time */ /* mach monotonic clock port */ extern mach_port_t clock_port; #ifndef HAVE_CLOCK_GETTIME int clock_gettime(clockid_t clock_id, struct timespec *tp) { static int clock_init = 0; static clock_serv_t clock; mach_timespec_t mts; int ret; if (! clock_init) { clock_init = 1; if (host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &clock)) abort(); /* unlikely */ } if(! tp) return -1; switch (clock_id) { case CLOCK_REALTIME: /* query mach for calendar time */ ret = clock_get_time(clock, &mts); if (! ret) { tp->tv_sec = mts.tv_sec; tp->tv_nsec = mts.tv_nsec; } break; case CLOCK_MONOTONIC: /* query mach for monotinic time */ ret = clock_get_time(clock_port, &mts); if (! ret) { tp->tv_sec = mts.tv_sec; tp->tv_nsec = mts.tv_nsec; } break; default: ret = -1; break; } return ret; } int clock_getres(clockid_t clock_id, struct timespec *res) { if (! res) return -1; /* hardcode ms resolution */ res->tv_sec = 0; res->tv_nsec = 1000; return 0; } #endif /* HAVE_CLOCK_GETTIME */ #ifndef HAVE_TIMER_SETTIME #include /* ITIMER_REAL */ int timer_create(clockid_t clock_id, void *sevp, timer_t *timer_id) { if (clock_id != CLOCK_MONOTONIC) return -1; if (sevp) return -1; /* setitimer only supports one timer */ *timer_id = 0; return 0; } int timer_delete(timer_t timer_id) { struct itimerval timerval; if (timer_id != 0) return -1; memset(&timerval, 0, sizeof(struct itimerval)); return setitimer(ITIMER_REAL, &timerval, NULL); } int timer_settime(timer_t timer_id, int flags, const struct itimerspec *tp, struct itimerspec *old) { struct itimerval tv; if (timer_id != 0 || ! tp || old) return -1; TIMESPEC_TO_TIMEVAL(&(tv.it_value), &(tp->it_value)); TIMESPEC_TO_TIMEVAL(&(tv.it_interval), &(tp->it_interval)); return setitimer(ITIMER_REAL, &tv, NULL); } int timer_getoverrun(timer_t timer_id) { /* since we don't know if there have been signals that weren't delivered, assume none */ return 0; } #endif /* HAVE_TIMER_SETTIME */ #endif /* HAVE_MACH_CLOCK */ int mutex_init(pthread_mutex_t *mutex) { pthread_mutexattr_t mattr; int err; CHECK_ERR(L_MISC, pthread_mutexattr_init(&mattr)); CHECK_ERR(L_MISC, pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_ERRORCHECK)); err = pthread_mutex_init(mutex, &mattr); CHECK_ERR(L_MISC, pthread_mutexattr_destroy(&mattr)); return err; } void log_fatal_err(int domain, const char *func, int line, int err) { DPRINTF(E_FATAL, domain, "%s failed at line %d, error %d (%s)\n", func, line, err, strerror(err)); abort(); } void log_fatal_errno(int domain, const char *func, int line) { DPRINTF(E_FATAL, domain, "%s failed at line %d, error %d (%s)\n", func, line, errno, strerror(errno)); abort(); } void log_fatal_null(int domain, const char *func, int line) { DPRINTF(E_FATAL, domain, "%s returned NULL at line %d\n", func, line); abort(); } forked-daapd-25.0/src/misc.h000066400000000000000000000107161313447753700156770ustar00rootroot00000000000000 #ifndef __MISC_H__ #define __MISC_H__ #ifdef HAVE_CONFIG_H # include #endif #include #include #include /* Samples to bytes, bytes to samples */ #define STOB(s) ((s) * 4) #define BTOS(b) ((b) / 4) struct onekeyval { char *name; char *value; struct onekeyval *next; struct onekeyval *sort; }; struct keyval { struct onekeyval *head; struct onekeyval *tail; }; int safe_atoi32(const char *str, int32_t *val); int safe_atou32(const char *str, uint32_t *val); int safe_hextou32(const char *str, uint32_t *val); int safe_atoi64(const char *str, int64_t *val); int safe_atou64(const char *str, uint64_t *val); int safe_hextou64(const char *str, uint64_t *val); char * safe_strdup(const char *str); char * safe_asprintf(const char *fmt, ...); /* Key/value functions */ struct keyval * keyval_alloc(void); int keyval_add(struct keyval *kv, const char *name, const char *value); int keyval_add_size(struct keyval *kv, const char *name, const char *value, size_t size); void keyval_remove(struct keyval *kv, const char *name); const char * keyval_get(struct keyval *kv, const char *name); void keyval_clear(struct keyval *kv); void keyval_sort(struct keyval *kv); char * m_realpath(const char *pathname); char ** m_readfile(const char *path, int num_lines); char * unicode_fixup_string(char *str, const char *fromcode); char * trimwhitespace(const char *str); void swap_pointers(char **a, char **b); uint32_t djb_hash(const void *data, size_t len); char * b64_decode(const char *b64); char * b64_encode(const uint8_t *in, size_t len); uint64_t murmur_hash64(const void *key, int len, uint32_t seed); #ifndef HAVE_CLOCK_GETTIME #ifndef CLOCK_REALTIME # define CLOCK_REALTIME 0 #endif #ifndef CLOCK_MONOTONIC # define CLOCK_MONOTONIC 1 #endif typedef int clockid_t; int clock_gettime(clockid_t clock_id, struct timespec *tp); int clock_getres(clockid_t clock_id, struct timespec *res); #endif #ifndef HAVE_TIMER_SETTIME struct itimerspec { struct timespec it_interval; struct timespec it_value; }; typedef uint64_t timer_t; int timer_create(clockid_t clock_id, void *sevp, timer_t *timer_id); int timer_delete(timer_t timer_id); int timer_settime(timer_t timer_id, int flags, const struct itimerspec *tp, struct itimerspec *old); int timer_getoverrun(timer_t timer_id); #endif /* Timer function for platforms without hi-res timers */ int clock_gettime_with_res(clockid_t clock_id, struct timespec *tp, struct timespec *res); struct timespec timespec_add(struct timespec time1, struct timespec time2); int timespec_cmp(struct timespec time1, struct timespec time2); struct timespec timespec_reltoabs(struct timespec relative); /* initialize mutex with error checking (not default on all platforms) */ int mutex_init(pthread_mutex_t *mutex); /* Check that the function returns 0, logging a fatal error referencing returned error (type errno) if it fails, and aborts the process. Example: CHECK_ERR(L_MAIN, my_function()); */ #define CHECK_ERR(d, f) \ do { int chk_err; \ if ( (chk_err = (f)) != 0) \ log_fatal_err(d, #f, __LINE__, chk_err); \ } while(0) /* Check that the function returns 0 or okval, logging a fatal error referencing returned erro (type errno) if not, and aborts the process. Example: int err; CHECK_ERR_EXCEPT(L_MAIN, my_wait(), err, ETIMEDOUT); */ #define CHECK_ERR_EXCEPT(d, f, var, okval) \ do { (var) = (f); \ if (! (((var) == (okval)) || ((var) == 0))) \ log_fatal_err(d, #f, __LINE__, (var)); \ } while(0) /* Check that the function returns value >= 0, logging a fatal error referencing errno if it not, and aborts the process. Example: int ret; CHECK_ERRNO(L_MAIN, ret = my_function()); */ #define CHECK_ERRNO(d, f) \ do { \ if ( (f) < 0 ) \ log_fatal_errno(d, #f, __LINE__); \ } while(0) /* Check that the function returns non-NULL, logging a fatal error if not, and aborts the process. Example: void *ptr; CHECK_NULL(L_MAIN, ptr = my_create()); */ #define CHECK_NULL(d, f) \ do { \ if ( (f) == NULL ) \ log_fatal_null(d, #f, __LINE__); \ } while(0) /* Used by CHECK_*() macros */ void log_fatal_err(int domain, const char *func, int line, int err) __attribute__((__noreturn__)); void log_fatal_errno(int domain, const char *func, int line) __attribute__((__noreturn__)); void log_fatal_null(int domain, const char *func, int line) __attribute__((__noreturn__)); #endif /* !__MISC_H__ */ forked-daapd-25.0/src/mpd.c000066400000000000000000003371311313447753700155220ustar00rootroot00000000000000/* * Copyright (C) 2016 Christian Meffert * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include #include #include #include #include #include #include #include #include #include #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) # include #endif #include "artwork.h" #include "commands.h" #include "conffile.h" #include "db.h" #include "httpd.h" #include "library.h" #include "listener.h" #include "logger.h" #include "misc.h" #include "player.h" #include "remote_pairing.h" static pthread_t tid_mpd; static struct event_base *evbase_mpd; static struct commands_base *cmdbase; static struct evhttp *evhttpd; struct evconnlistener *listener; #define COMMAND_ARGV_MAX 37 /* MPD error codes (taken from ack.h) */ enum ack { ACK_ERROR_NOT_LIST = 1, ACK_ERROR_ARG = 2, ACK_ERROR_PASSWORD = 3, ACK_ERROR_PERMISSION = 4, ACK_ERROR_UNKNOWN = 5, ACK_ERROR_NO_EXIST = 50, ACK_ERROR_PLAYLIST_MAX = 51, ACK_ERROR_SYSTEM = 52, ACK_ERROR_PLAYLIST_LOAD = 53, ACK_ERROR_UPDATE_ALREADY = 54, ACK_ERROR_PLAYER_SYNC = 55, ACK_ERROR_EXIST = 56, }; enum command_list_type { COMMAND_LIST = 1, COMMAND_LIST_OK = 2, COMMAND_LIST_NONE = 3 }; /** * This lists for ffmpeg suffixes and mime types are taken from the ffmpeg decoder plugin from mpd * (FfmpegDecoderPlugin.cxx, git revision 9fb351a139a56fc7b1ece549894f8fc31fa887cd). * * forked-daapd does not support different decoders and always uses ffmpeg or libav for decoding. * Some clients rely on a response for the decoder commands (e.g. ncmpccp) therefor return something * valid for this command. */ static const char * const ffmpeg_suffixes[] = { "16sv", "3g2", "3gp", "4xm", "8svx", "aa3", "aac", "ac3", "afc", "aif", "aifc", "aiff", "al", "alaw", "amr", "anim", "apc", "ape", "asf", "atrac", "au", "aud", "avi", "avm2", "avs", "bap", "bfi", "c93", "cak", "cin", "cmv", "cpk", "daud", "dct", "divx", "dts", "dv", "dvd", "dxa", "eac3", "film", "flac", "flc", "fli", "fll", "flx", "flv", "g726", "gsm", "gxf", "iss", "m1v", "m2v", "m2t", "m2ts", "m4a", "m4b", "m4v", "mad", "mj2", "mjpeg", "mjpg", "mka", "mkv", "mlp", "mm", "mmf", "mov", "mp+", "mp1", "mp2", "mp3", "mp4", "mpc", "mpeg", "mpg", "mpga", "mpp", "mpu", "mve", "mvi", "mxf", "nc", "nsv", "nut", "nuv", "oga", "ogm", "ogv", "ogx", "oma", "ogg", "omg", "psp", "pva", "qcp", "qt", "r3d", "ra", "ram", "rl2", "rm", "rmvb", "roq", "rpl", "rvc", "shn", "smk", "snd", "sol", "son", "spx", "str", "swf", "tgi", "tgq", "tgv", "thp", "ts", "tsp", "tta", "xa", "xvid", "uv", "uv2", "vb", "vid", "vob", "voc", "vp6", "vmd", "wav", "webm", "wma", "wmv", "wsaud", "wsvga", "wv", "wve", NULL }; static const char * const ffmpeg_mime_types[] = { "application/flv", "application/m4a", "application/mp4", "application/octet-stream", "application/ogg", "application/x-ms-wmz", "application/x-ms-wmd", "application/x-ogg", "application/x-shockwave-flash", "application/x-shorten", "audio/8svx", "audio/16sv", "audio/aac", "audio/ac3", "audio/aiff", "audio/amr", "audio/basic", "audio/flac", "audio/m4a", "audio/mp4", "audio/mpeg", "audio/musepack", "audio/ogg", "audio/qcelp", "audio/vorbis", "audio/vorbis+ogg", "audio/x-8svx", "audio/x-16sv", "audio/x-aac", "audio/x-ac3", "audio/x-aiff", "audio/x-alaw", "audio/x-au", "audio/x-dca", "audio/x-eac3", "audio/x-flac", "audio/x-gsm", "audio/x-mace", "audio/x-matroska", "audio/x-monkeys-audio", "audio/x-mpeg", "audio/x-ms-wma", "audio/x-ms-wax", "audio/x-musepack", "audio/x-ogg", "audio/x-vorbis", "audio/x-vorbis+ogg", "audio/x-pn-realaudio", "audio/x-pn-multirate-realaudio", "audio/x-speex", "audio/x-tta", "audio/x-voc", "audio/x-wav", "audio/x-wma", "audio/x-wv", "video/anim", "video/quicktime", "video/msvideo", "video/ogg", "video/theora", "video/webm", "video/x-dv", "video/x-flv", "video/x-matroska", "video/x-mjpeg", "video/x-mpeg", "video/x-ms-asf", "video/x-msvideo", "video/x-ms-wmv", "video/x-ms-wvx", "video/x-ms-wm", "video/x-ms-wmx", "video/x-nut", "video/x-pva", "video/x-theora", "video/x-vid", "video/x-wmv", "video/x-xvid", /* special value for the "ffmpeg" input plugin: all streams by the "ffmpeg" input plugin shall be decoded by this plugin */ "audio/x-mpd-ffmpeg", NULL }; struct output { unsigned short shortid; uint64_t id; char *name; unsigned selected; struct output *next; }; struct outputs { unsigned int count; unsigned int active; struct output *outputs; }; static void free_outputs(struct output *outputs) { struct output *temp; struct output *next; temp = outputs; next = outputs ? outputs->next : NULL; while (temp) { free(temp->name); free(temp); temp = next; next = next ? next->next : NULL; } } struct idle_client { struct evbuffer *evbuffer; short events; struct idle_client *next; }; struct idle_client *idle_clients; /* Thread: mpd */ static void * mpd(void *arg) { int ret; ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error: DB init failed\n"); pthread_exit(NULL); } event_base_dispatch(evbase_mpd); db_perthread_deinit(); pthread_exit(NULL); } static void mpd_time(char *buffer, size_t bufferlen, time_t t) { struct tm tm; const struct tm *tm2 = gmtime_r(&t, &tm); if (tm2 == NULL) return; strftime(buffer, bufferlen, "%FT%TZ", tm2); } /* * Parses a rage argument of the form START:END (the END item is not included in the range) * into its start and end position. * * @param range the range argument * @param start_pos set by this method to the start position * @param end_pos set by this method to the end postion * @return 0 on success, -1 on failure */ static int mpd_pars_range_arg(char *range, int *start_pos, int *end_pos) { int ret; if (strchr(range, ':')) { ret = sscanf(range, "%d:%d", start_pos, end_pos); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error parsing range argument '%s' (return code = %d)\n", range, ret); return -1; } } else { ret = safe_atoi32(range, start_pos); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error parsing integer argument '%s' (return code = %d)\n", range, ret); return -1; } *end_pos = (*start_pos) + 1; } return 0; } /* * Returns the next unquoted string argument from the input string */ static char* mpd_pars_unquoted(char **input) { char *arg; arg = *input; while (**input != 0) { if (**input == ' ') { **input = '\0'; (*input)++; return arg; } (*input)++; } return arg; } /* * Returns the next quoted string argument from the input string * with the quotes removed */ static char* mpd_pars_quoted(char **input) { char *arg; // skip double quote character (*input)++; arg = *input; while (**input != '"') { // A backslash character escapes the following character if (**input == '\\') { (*input)++; } if (**input == 0) { // Error handling for missing double quote at end of parameter DPRINTF(E_LOG, L_MPD, "Error missing closing double quote in argument\n"); return NULL; } (*input)++; } **input = '\0'; (*input)++; return arg; } /* * Parses the argument string into an array of strings. * Arguments are seperated by a whitespace character and may be wrapped in double quotes. * * @param args the arguments * @param argc the number of arguments in the argument string * @param argv the array containing the found arguments */ static int mpd_parse_args(char *args, int *argc, char **argv) { char *input; input = args; *argc = 0; while (*input != 0) { // Ignore whitespace characters if (*input == ' ') { input++; continue; } // Check if the parameter is wrapped in double quotes if (*input == '"') { argv[*argc] = mpd_pars_quoted(&input); if (argv[*argc] == NULL) { return -1; } *argc = *argc + 1; } else { argv[*argc] = mpd_pars_unquoted(&input); *argc = *argc + 1; } } return 0; } /* * Adds the informations (path, id, tags, etc.) for the given song to the given buffer * with additional information for the position of this song in the playqueue. * * Example output: * file: foo/bar/song.mp3 * Last-Modified: 2013-07-14T06:57:59Z * Time: 172 * Artist: foo * AlbumArtist: foo * ArtistSort: foo * AlbumArtistSort: foo * Title: song * Album: bar * Track: 1/11 * Date: 2012-09-11 * Genre: Alternative * Disc: 1/1 * MUSICBRAINZ_ALBUMARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ALBUMID: 812f4b87-8ad9-41bd-be79-38151f17a2b4 * MUSICBRAINZ_TRACKID: fde95c39-ee51-48f6-a7f9-b5631c2ed156 * Pos: 0 * Id: 1 * * @param evbuf the response event buffer * @param queue_item queue item information * @return the number of bytes added if successful, or -1 if an error occurred. */ static int mpd_add_db_queue_item(struct evbuffer *evbuf, struct db_queue_item *queue_item) { char modified[32]; int ret; mpd_time(modified, sizeof(modified), queue_item->time_modified); ret = evbuffer_add_printf(evbuf, "file: %s\n" "Last-Modified: %s\n" "Time: %d\n" "Artist: %s\n" "AlbumArtist: %s\n" "ArtistSort: %s\n" "AlbumArtistSort: %s\n" "Album: %s\n" "Title: %s\n" "Track: %d\n" "Date: %d\n" "Genre: %s\n" "Disc: %d\n" "Pos: %d\n" "Id: %d\n", (queue_item->virtual_path + 1), modified, (queue_item->song_length / 1000), queue_item->artist, queue_item->album_artist, queue_item->artist_sort, queue_item->album_artist_sort, queue_item->album, queue_item->title, queue_item->track, queue_item->year, queue_item->genre, queue_item->disc, queue_item->pos, queue_item->id); return ret; } /* * Adds the informations (path, id, tags, etc.) for the given song to the given buffer. * * Example output: * file: foo/bar/song.mp3 * Last-Modified: 2013-07-14T06:57:59Z * Time: 172 * Artist: foo * AlbumArtist: foo * ArtistSort: foo * AlbumArtistSort: foo * Title: song * Album: bar * Track: 1/11 * Date: 2012-09-11 * Genre: Alternative * Disc: 1/1 * MUSICBRAINZ_ALBUMARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 * MUSICBRAINZ_ALBUMID: 812f4b87-8ad9-41bd-be79-38151f17a2b4 * MUSICBRAINZ_TRACKID: fde95c39-ee51-48f6-a7f9-b5631c2ed156 * * @param evbuf the response event buffer * @param mfi media information * @return the number of bytes added if successful, or -1 if an error occurred. */ static int mpd_add_db_media_file_info(struct evbuffer *evbuf, struct db_media_file_info *dbmfi) { char modified[32]; uint32_t time_modified; uint32_t songlength; int ret; if (safe_atou32(dbmfi->time_modified, &time_modified) != 0) { DPRINTF(E_LOG, L_MPD, "Error converting time modified to uint32_t: %s\n", dbmfi->time_modified); return -1; } mpd_time(modified, sizeof(modified), time_modified); if (safe_atou32(dbmfi->song_length, &songlength) != 0) { DPRINTF(E_LOG, L_MPD, "Error converting song length to uint32_t: %s\n", dbmfi->song_length); return -1; } ret = evbuffer_add_printf(evbuf, "file: %s\n" "Last-Modified: %s\n" "Time: %d\n" "Artist: %s\n" "AlbumArtist: %s\n" "ArtistSort: %s\n" "AlbumArtistSort: %s\n" "Album: %s\n" "Title: %s\n" "Track: %s\n" "Date: %s\n" "Genre: %s\n" "Disc: %s\n", (dbmfi->virtual_path + 1), modified, (songlength / 1000), dbmfi->artist, dbmfi->album_artist, dbmfi->artist_sort, dbmfi->album_artist_sort, dbmfi->album, dbmfi->title, dbmfi->track, dbmfi->year, dbmfi->genre, dbmfi->disc); return ret; } /* * Command handler function for 'currentsong' */ static int mpd_command_currentsong(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct player_status status; struct db_queue_item *queue_item; int ret; player_get_status(&status); if (status.status == PLAY_STOPPED) { // Return empty evbuffer if there is no current playing song return 0; } queue_item = db_queue_fetch_byitemid(status.item_id); if (!queue_item) { *errmsg = safe_asprintf("Error adding queue item info for file with id: %d", status.item_id); return ACK_ERROR_UNKNOWN; } ret = mpd_add_db_queue_item(evbuf, queue_item); free_queue_item(queue_item, 0); if (ret < 0) { *errmsg = safe_asprintf("Error adding media info for file with id: %d", status.id); return ACK_ERROR_UNKNOWN; } return 0; } /* * * Example input: * idle "database" "mixer" "options" "output" "player" "playlist" "sticker" "update" */ static int mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct idle_client *client; int i; client = (struct idle_client*)malloc(sizeof(struct idle_client)); if (!client) { DPRINTF(E_LOG, L_MPD, "Out of memory for idle_client\n"); return ACK_ERROR_UNKNOWN; } client->evbuffer = evbuf; client->events = 0; client->next = idle_clients; if (argc > 1) { for (i = 1; i < argc; i++) { if (0 == strcmp(argv[i], "database")) { client->events |= LISTENER_DATABASE; } else if (0 == strcmp(argv[i], "player")) { client->events |= LISTENER_PLAYER; } else if (0 == strcmp(argv[i], "playlist")) { client->events |= LISTENER_QUEUE; } else if (0 == strcmp(argv[i], "mixer")) { client->events |= LISTENER_VOLUME; } else if (0 == strcmp(argv[i], "output")) { client->events |= LISTENER_SPEAKER; } else if (0 == strcmp(argv[i], "options")) { client->events |= LISTENER_OPTIONS; } else { DPRINTF(E_DBG, L_MPD, "Idle command for '%s' not supported\n", argv[i]); } } } else client->events = LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS; idle_clients = client; return 0; } static void mpd_remove_idle_client(struct evbuffer *evbuf) { struct idle_client *client; struct idle_client *prev; client = idle_clients; prev = NULL; while (client) { if (client->evbuffer == evbuf) { DPRINTF(E_DBG, L_MPD, "Removing idle client for evbuffer\n"); if (prev) prev->next = client->next; else idle_clients = client->next; free(client); break; } prev = client; client = client->next; } } static int mpd_command_noidle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { mpd_remove_idle_client(evbuf); return 0; } /* * Command handler function for 'status' * * Example output: * volume: -1 * repeat: 0 * random: 0 * single: 0 * consume: 0 * playlist: 2 * playlistlength: 34 * mixrampdb: 0.000000 * state: stop * song: 0 * songid: 1 * time: 28:306 * elapsed: 28.178 * bitrate: 278 * audio: 44100:f:2 * nextsong: 1 * nextsongid: 2 */ static int mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct player_status status; int queue_length; int queue_version; char *state; int pos_pl; struct db_queue_item *next_item; player_get_status(&status); switch (status.status) { case PLAY_PAUSED: state = "pause"; break; case PLAY_PLAYING: state = "play"; break; default: state = "stop"; break; } queue_version = db_queue_get_version(); queue_length = db_queue_get_count(); evbuffer_add_printf(evbuf, "volume: %d\n" "repeat: %d\n" "random: %d\n" "single: %d\n" "consume: %d\n" "playlist: %d\n" "playlistlength: %d\n" "mixrampdb: 0.000000\n" "state: %s\n", status.volume, (status.repeat == REPEAT_OFF ? 0 : 1), status.shuffle, (status.repeat == REPEAT_SONG ? 1 : 0), status.consume, queue_version, queue_length, state); if (status.status != PLAY_STOPPED) { pos_pl = db_queue_get_pos(status.item_id, 0); evbuffer_add_printf(evbuf, "song: %d\n" "songid: %d\n" "time: %d:%d\n" "elapsed: %#.3f\n" "bitrate: 128\n" "audio: 44100:16:2\n", pos_pl, status.item_id, (status.pos_ms / 1000), (status.len_ms / 1000), (status.pos_ms / 1000.0)); } if (library_is_scanning()) { evbuffer_add(evbuf, "updating_db: 1\n", 15); } if (status.status != PLAY_STOPPED) { next_item = db_queue_fetch_next(status.item_id, status.shuffle); if (next_item) { evbuffer_add_printf(evbuf, "nextsong: %d\n" "nextsongid: %d\n", next_item->id, next_item->pos); free_queue_item(next_item, 0); } } return 0; } /* * Command handler function for 'stats' */ static int mpd_command_stats(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct filecount_info fci; int artists; int albums; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_COUNT_ITEMS; ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } ret = db_query_fetch_count(&qp, &fci); if (ret < 0) { db_query_end(&qp); *errmsg = safe_asprintf("Could not fetch query count"); return ACK_ERROR_UNKNOWN; } db_query_end(&qp); artists = db_files_get_artist_count(); albums = db_files_get_album_count(); //TODO [mpd] Implement missing stats attributes (uptime, db_update, playtime) evbuffer_add_printf(evbuf, "artists: %d\n" "albums: %d\n" "songs: %d\n" "uptime: %d\n" //in seceonds "db_playtime: %" PRIu64 "\n" "db_update: %d\n" "playtime: %d\n", artists, albums, fci.count, 4, (fci.length / 1000), 6, 7); return 0; } /* * Command handler function for 'consume' * Sets the consume mode, expects argument argv[1] to be an integer with * 0 = disable consume * 1 = enable consume */ static int mpd_command_consume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int enable; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'consume'"); return ACK_ERROR_ARG; } ret = safe_atoi32(argv[1], &enable); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } player_consume_set(enable); return 0; } /* * Command handler function for 'random' * Sets the shuffle mode, expects argument argv[1] to be an integer with * 0 = disable shuffle * 1 = enable shuffle */ static int mpd_command_random(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int enable; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'random'"); return ACK_ERROR_ARG; } ret = safe_atoi32(argv[1], &enable); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } player_shuffle_set(enable); return 0; } /* * Command handler function for 'repeat' * Sets the repeat mode, expects argument argv[1] to be an integer with * 0 = repeat off * 1 = repeat all */ static int mpd_command_repeat(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int enable; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'repeat'"); return ACK_ERROR_ARG; } ret = safe_atoi32(argv[1], &enable); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } if (enable == 0) player_repeat_set(REPEAT_OFF); else player_repeat_set(REPEAT_ALL); return 0; } /* * Command handler function for 'setvol' * Sets the volume, expects argument argv[1] to be an integer 0-100 */ static int mpd_command_setvol(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int volume; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'setvol'"); return ACK_ERROR_ARG; } ret = safe_atoi32(argv[1], &volume); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } player_volume_set(volume); return 0; } /* * Command handler function for 'single' * Sets the repeat mode, expects argument argv[1] to be an integer. * forked-daapd only allows single-mode in combination with repeat, therefor the command * single translates (depending on the current repeat mode) into: * a) if repeat off: * 0 = repeat off * 1 = repeat song * b) if repeat all: * 0 = repeat all * 1 = repeat song * c) if repeat song: * 0 = repeat all * 1 = repeat song */ static int mpd_command_single(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int enable; struct player_status status; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'single'"); return ACK_ERROR_ARG; } ret = safe_atoi32(argv[1], &enable); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } player_get_status(&status); if (enable == 0 && status.repeat != REPEAT_OFF) player_repeat_set(REPEAT_ALL); else if (enable == 0) player_repeat_set(REPEAT_OFF); else player_repeat_set(REPEAT_SONG); return 0; } /* * Command handler function for 'replay_gain_status' * forked-daapd does not support replay gain, therefor this function returns always * "replay_gain_mode: off". */ static int mpd_command_replay_gain_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { evbuffer_add(evbuf, "replay_gain_mode: off\n", 22); return 0; } /* * Command handler function for 'volume' * Changes the volume by the given amount, expects argument argv[1] to be an integer * * According to the mpd protocoll specification this function is deprecated. */ static int mpd_command_volume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct player_status status; int volume; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'volume'"); return ACK_ERROR_ARG; } ret = safe_atoi32(argv[1], &volume); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } player_get_status(&status); volume += status.volume; player_volume_set(volume); return 0; } /* * Command handler function for 'next' * Skips to the next song in the playqueue */ static int mpd_command_next(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int ret; ret = player_playback_next(); if (ret < 0) { *errmsg = safe_asprintf("Failed to skip to next song"); return ACK_ERROR_UNKNOWN; } ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Player returned an error for start after nextitem"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'pause' * Toggles pause/play, if the optional argument argv[1] is present, it must be an integer with * 0 = play * 1 = pause */ static int mpd_command_pause(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int pause; struct player_status status; int ret; pause = 1; if (argc > 1) { ret = safe_atoi32(argv[1], &pause); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } } else { player_get_status(&status); if (status.status != PLAY_PLAYING) pause = 0; } if (pause == 1) ret = player_playback_pause(); else ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Failed to pause playback"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'play' * Starts playback, the optional argument argv[1] represents the position in the playqueue * where to start playback. */ static int mpd_command_play(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int songpos; struct player_status status; struct db_queue_item *queue_item; int ret; songpos = 0; if (argc > 1) { ret = safe_atoi32(argv[1], &songpos); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } } player_get_status(&status); if (status.status == PLAY_PLAYING && songpos < 0) { DPRINTF(E_DBG, L_MPD, "Ignoring play command with parameter '%s', player is already playing.\n", argv[1]); return 0; } if (status.status == PLAY_PLAYING) { // Stop playback, if player is already playing and a valid song position is given (it will be restarted for the given song position) player_playback_stop(); } if (songpos > 0) { queue_item = db_queue_fetch_bypos(songpos, 0); if (!queue_item) { *errmsg = safe_asprintf("Failed to start playback"); return ACK_ERROR_UNKNOWN; } ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); } else ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Failed to start playback"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'playid' * Starts playback, the optional argument argv[1] represents the songid of the song * where to start playback. */ static int mpd_command_playid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { uint32_t id; struct player_status status; struct db_queue_item *queue_item; int ret; player_get_status(&status); id = 0; if (argc > 1) { //TODO [mpd] mpd allows passing "-1" as argument and simply ignores it, forked-daapd fails to convert "-1" to an unsigned int ret = safe_atou32(argv[1], &id); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } } if (status.status == PLAY_PLAYING) { // Stop playback, if player is already playing and a valid item id is given (it will be restarted for the given song) player_playback_stop(); } if (id > 0) { queue_item = db_queue_fetch_byitemid(id); if (!queue_item) { *errmsg = safe_asprintf("Failed to start playback"); return ACK_ERROR_UNKNOWN; } ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); } else ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Failed to start playback"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'previous' * Skips to the previous song in the playqueue */ static int mpd_command_previous(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int ret; ret = player_playback_prev(); if (ret < 0) { *errmsg = safe_asprintf("Failed to skip to previous song"); return ACK_ERROR_UNKNOWN; } ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Player returned an error for start after previtem"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'seekid' * Seeks to song at the given position in argv[1] to the position in seconds given in argument argv[2] * (fractions allowed). */ static int mpd_command_seek(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { uint32_t songpos; float seek_target_sec; int seek_target_msec; int ret; if (argc < 3) { *errmsg = safe_asprintf("Missing argument for command 'seek'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &songpos); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } //TODO Allow seeking in songs not currently playing seek_target_sec = strtof(argv[2], NULL); seek_target_msec = seek_target_sec * 1000; ret = player_playback_seek(seek_target_msec); if (ret < 0) { *errmsg = safe_asprintf("Failed to seek current song to time %d msec", seek_target_msec); return ACK_ERROR_UNKNOWN; } ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Player returned an error for start after seekcur"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'seekid' * Seeks to song with id given in argv[1] to the position in seconds given in argument argv[2] * (fractions allowed). */ static int mpd_command_seekid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct player_status status; uint32_t id; float seek_target_sec; int seek_target_msec; int ret; if (argc < 3) { *errmsg = safe_asprintf("Missing argument for command 'seekcur'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &id); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } //TODO Allow seeking in songs not currently playing player_get_status(&status); if (status.item_id != id) { *errmsg = safe_asprintf("Given song is not the current playing one, seeking is not supported"); return ACK_ERROR_UNKNOWN; } seek_target_sec = strtof(argv[2], NULL); seek_target_msec = seek_target_sec * 1000; ret = player_playback_seek(seek_target_msec); if (ret < 0) { *errmsg = safe_asprintf("Failed to seek current song to time %d msec", seek_target_msec); return ACK_ERROR_UNKNOWN; } ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Player returned an error for start after seekcur"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'seekcur' * Seeks the current song to the position in seconds given in argument argv[1] (fractions allowed). */ static int mpd_command_seekcur(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { float seek_target_sec; int seek_target_msec; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'seekcur'"); return ACK_ERROR_ARG; } seek_target_sec = strtof(argv[1], NULL); seek_target_msec = seek_target_sec * 1000; // TODO If prefixed by '+' or '-', then the time is relative to the current playing position. ret = player_playback_seek(seek_target_msec); if (ret < 0) { *errmsg = safe_asprintf("Failed to seek current song to time %d msec", seek_target_msec); return ACK_ERROR_UNKNOWN; } ret = player_playback_start(); if (ret < 0) { *errmsg = safe_asprintf("Player returned an error for start after seekcur"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'stop' * Stop playback. */ static int mpd_command_stop(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int ret; ret = player_playback_stop(); if (ret != 0) { *errmsg = safe_asprintf("Failed to stop playback"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Add media file item with given virtual path to the queue * * @param path The virtual path * @param recursive If 0 add only item with exact match, otherwise add all items virtual path start with the given path * @return The queue item id of the last inserted item or -1 on failure */ static int mpd_queue_add(char *path, int recursive) { struct query_params qp; struct player_status status; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.idx_type = I_NONE; qp.sort = S_ARTIST; if (recursive) { qp.filter = sqlite3_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q%%'", path); if (!qp.filter) DPRINTF(E_DBG, L_PLAYER, "Out of memory\n"); } else { qp.filter = sqlite3_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q'", path); if (!qp.filter) DPRINTF(E_DBG, L_PLAYER, "Out of memory\n"); } player_get_status(&status); ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id); sqlite3_free(qp.filter); return ret; } /* * Command handler function for 'add' * Adds the all songs under the given path to the end of the playqueue (directories add recursively). * Expects argument argv[1] to be a path to a single file or directory. */ static int mpd_command_add(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct media_file_info mfi; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'add'"); return ACK_ERROR_ARG; } ret = mpd_queue_add(argv[1], 1); if (ret < 0) { *errmsg = safe_asprintf("Failed to add song '%s' to playlist", argv[1]); return ACK_ERROR_UNKNOWN; } if (ret == 0) { // Given path is not in the library, check if it is possible to add as a non-library queue item ret = library_scan_media(argv[1], &mfi); if (ret != LIBRARY_OK) { *errmsg = safe_asprintf("Failed to add song '%s' to playlist (unkown path)", argv[1]); return ACK_ERROR_UNKNOWN; } library_add_queue_item(&mfi); free_mfi(&mfi, 1); } return 0; } /* * Command handler function for 'addid' * Adds the song under the given path to the end or to the given position of the playqueue. * Expects argument argv[1] to be a path to a single file. argv[2] is optional, if present * it must be an integer representing the position in the playqueue. */ static int mpd_command_addid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct media_file_info mfi; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'addid'"); return ACK_ERROR_ARG; } //TODO if argc > 2 add song at position argv[2] if (argc > 2) { DPRINTF(E_LOG, L_MPD, "Adding at a specified position not supported for 'addid', adding songs at end of queue.\n"); } ret = mpd_queue_add(argv[1], 0); if (ret == 0) { // Given path is not in the library, directly add it as a new queue item ret = library_scan_media(argv[1], &mfi); if (ret != LIBRARY_OK) { *errmsg = safe_asprintf("Failed to add song '%s' to playlist (unkown path)", argv[1]); return ACK_ERROR_UNKNOWN; } ret = library_add_queue_item(&mfi); free_mfi(&mfi, 1); } if (ret < 0) { *errmsg = safe_asprintf("Failed to add song '%s' to playlist", argv[1]); return ACK_ERROR_UNKNOWN; } evbuffer_add_printf(evbuf, "Id: %d\n", ret); // mpd_queue_add returns the item_id of the last inserted queue item return 0; } /* * Command handler function for 'clear' * Stops playback and removes all songs from the playqueue */ static int mpd_command_clear(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int ret; ret = player_playback_stop(); if (ret != 0) { DPRINTF(E_DBG, L_MPD, "Failed to stop playback\n"); } db_queue_clear(0); return 0; } /* * Command handler function for 'delete' * Removes songs from the playqueue. Expects argument argv[1] (optional) to be an integer or * an integer range {START:END} representing the position of the songs in the playlist, that * should be removed. */ static int mpd_command_delete(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int start_pos; int end_pos; int count; int ret; // If argv[1] is ommited clear the whole queue if (argc < 2) { db_queue_clear(0); return 0; } // If argument argv[1] is present remove only the specified songs ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer or range: '%s'", argv[1]); return ACK_ERROR_ARG; } count = end_pos - start_pos; ret = db_queue_delete_bypos(start_pos, count); if (ret < 0) { *errmsg = safe_asprintf("Failed to remove %d songs starting at position %d", count, start_pos); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'deleteid' * Removes the song with given id from the playqueue. Expects argument argv[1] to be an integer (song id). */ static int mpd_command_deleteid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { uint32_t songid; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'deleteid'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &songid); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } ret = db_queue_delete_byitemid(songid); if (ret < 0) { *errmsg = safe_asprintf("Failed to remove song with id '%s'", argv[1]); return ACK_ERROR_UNKNOWN; } return 0; } //Moves the song at FROM or range of songs at START:END to TO in the playlist. static int mpd_command_move(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int start_pos; int end_pos; int count; uint32_t to_pos; int ret; if (argc < 3) { *errmsg = safe_asprintf("Missing argument for command 'move'"); return ACK_ERROR_ARG; } ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer or range: '%s'", argv[1]); return ACK_ERROR_ARG; } count = end_pos - start_pos; if (count > 1) DPRINTF(E_WARN, L_MPD, "Moving ranges is not supported, only the first item will be moved\n"); ret = safe_atou32(argv[2], &to_pos); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); return ACK_ERROR_ARG; } ret = db_queue_move_bypos(start_pos, to_pos); if (ret < 0) { *errmsg = safe_asprintf("Failed to move song at position %d to %d", start_pos, to_pos); return ACK_ERROR_UNKNOWN; } return 0; } static int mpd_command_moveid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { uint32_t songid; uint32_t to_pos; int ret; if (argc < 3) { *errmsg = safe_asprintf("Missing argument for command 'moveid'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &songid); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } ret = safe_atou32(argv[2], &to_pos); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); return ACK_ERROR_ARG; } ret = db_queue_move_byitemid(songid, to_pos, 0); if (ret < 0) { *errmsg = safe_asprintf("Failed to move song with id '%s' to index '%s'", argv[1], argv[2]); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'playlistid' * Displays a list of all songs in the queue, or if the optional argument is given, displays information * only for the song with ID. * * The order of the songs is always the not shuffled order. */ static int mpd_command_playlistid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params query_params; struct db_queue_item queue_item; uint32_t songid; int ret; songid = 0; if (argc > 1) { ret = safe_atou32(argv[1], &songid); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } } memset(&query_params, 0, sizeof(struct query_params)); if (songid > 0) query_params.filter = sqlite3_mprintf("id = %d", songid); ret = db_queue_enum_start(&query_params); if (ret < 0) { sqlite3_free(query_params.filter); *errmsg = safe_asprintf("Failed to start queue enum for command playlistid: '%s'", argv[1]); return ACK_ERROR_ARG; } while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(evbuf, &queue_item); if (ret < 0) { *errmsg = safe_asprintf("Error adding media info for file with id: %d", queue_item.file_id); db_queue_enum_end(&query_params); sqlite3_free(query_params.filter); return ACK_ERROR_UNKNOWN; } } db_queue_enum_end(&query_params); sqlite3_free(query_params.filter); return 0; } /* * Command handler function for 'playlistinfo' * Displays a list of all songs in the queue, or if the optional argument is given, displays information * only for the song SONGPOS or the range of songs START:END given in argv[1]. * * The order of the songs is always the not shuffled order. */ static int mpd_command_playlistinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params query_params; struct db_queue_item queue_item; int start_pos; int end_pos; int ret; start_pos = 0; end_pos = 0; memset(&query_params, 0, sizeof(struct query_params)); if (argc > 1) { ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer or range: '%s'", argv[1]); return ACK_ERROR_ARG; } if (start_pos < 0) DPRINTF(E_DBG, L_MPD, "Command 'playlistinfo' called with pos < 0 (arg = '%s'), ignore arguments and return whole queue\n", argv[1]); else query_params.filter = sqlite3_mprintf("pos >= %d AND pos < %d", start_pos, end_pos); } ret = db_queue_enum_start(&query_params); if (ret < 0) { sqlite3_free(query_params.filter); *errmsg = safe_asprintf("Failed to start queue enum for command playlistinfo: '%s'", argv[1]); return ACK_ERROR_ARG; } while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(evbuf, &queue_item); if (ret < 0) { *errmsg = safe_asprintf("Error adding media info for file with id: %d", queue_item.file_id); db_queue_enum_end(&query_params); sqlite3_free(query_params.filter); return ACK_ERROR_UNKNOWN; } } db_queue_enum_end(&query_params); sqlite3_free(query_params.filter); return 0; } /* * Command handler function for 'plchanges' * Lists all changed songs in the queue since the given playlist version in argv[1]. */ static int mpd_command_plchanges(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params query_params; struct db_queue_item queue_item; int ret; /* * forked-daapd does not keep track of changes in the queue based on the playlist version, * therefor plchanges returns all songs in the queue as changed ignoring the given version. */ memset(&query_params, 0, sizeof(struct query_params)); ret = db_queue_enum_start(&query_params); if (ret < 0) { *errmsg = safe_asprintf("Failed to start queue enum for command plchanges"); return ACK_ERROR_ARG; } while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(evbuf, &queue_item); if (ret < 0) { *errmsg = safe_asprintf("Error adding media info for file with id: %d", queue_item.file_id); db_queue_enum_end(&query_params); return ACK_ERROR_UNKNOWN; } } db_queue_enum_end(&query_params); return 0; } /* * Command handler function for 'plchangesposid' * Lists all changed songs in the queue since the given playlist version in argv[1] without metadata. */ static int mpd_command_plchangesposid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params query_params; struct db_queue_item queue_item; int ret; /* * forked-daapd does not keep track of changes in the queue based on the playlist version, * therefor plchangesposid returns all songs in the queue as changed ignoring the given version. */ memset(&query_params, 0, sizeof(struct query_params)); ret = db_queue_enum_start(&query_params); if (ret < 0) { *errmsg = safe_asprintf("Failed to start queue enum for command plchangesposid"); return ACK_ERROR_ARG; } while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) { evbuffer_add_printf(evbuf, "cpos: %d\n" "Id: %d\n", queue_item.pos, queue_item.id); } db_queue_enum_end(&query_params); return 0; } /* * Command handler function for 'listplaylist' * Lists all songs in the playlist given by virtual-path in argv[1]. */ static int mpd_command_listplaylist(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { char path[PATH_MAX]; struct playlist_info *pli; struct query_params qp; struct db_media_file_info dbmfi; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'listplaylist'"); return ACK_ERROR_ARG; } if (strncmp(argv[1], "/", 1) == 0) ret = snprintf(path, sizeof(path), "%s", argv[1]); else ret = snprintf(path, sizeof(path), "/%s", argv[1]); if (ret >= sizeof(path)) { *errmsg = safe_asprintf("Length of path exceeds the PATH_MAX value '%s'", argv[1]); return ACK_ERROR_ARG; } pli = db_pl_fetch_byvirtualpath(path); if (!pli) { *errmsg = safe_asprintf("Playlist not found for path '%s'", argv[1]); return ACK_ERROR_ARG; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_PLITEMS; qp.idx_type = I_NONE; qp.id = pli->id; ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); free_pli(pli, 0); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { evbuffer_add_printf(evbuf, "file: %s\n", (dbmfi.virtual_path + 1)); } db_query_end(&qp); free_pli(pli, 0); return 0; } /* * Command handler function for 'listplaylistinfo' * Lists all songs in the playlist given by virtual-path in argv[1] with metadata. */ static int mpd_command_listplaylistinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { char path[PATH_MAX]; struct playlist_info *pli; struct query_params qp; struct db_media_file_info dbmfi; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'listplaylistinfo'"); return ACK_ERROR_ARG; } if (strncmp(argv[1], "/", 1) == 0) ret = snprintf(path, sizeof(path), "%s", argv[1]); else ret = snprintf(path, sizeof(path), "/%s", argv[1]); if (ret >= sizeof(path)) { *errmsg = safe_asprintf("Length of path exceeds the PATH_MAX value '%s'", argv[1]); return ACK_ERROR_ARG; } pli = db_pl_fetch_byvirtualpath(path); if (!pli) { *errmsg = safe_asprintf("Playlist not found for path '%s'", argv[1]); return ACK_ERROR_NO_EXIST; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_PLITEMS; qp.idx_type = I_NONE; qp.id = pli->id; ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); free_pli(pli, 0); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { ret = mpd_add_db_media_file_info(evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } } db_query_end(&qp); free_pli(pli, 0); return 0; } /* * Command handler function for 'listplaylists' * Lists all playlists with their last modified date. */ static int mpd_command_listplaylists(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct db_playlist_info dbpli; char modified[32]; uint32_t time_modified; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_PL; qp.sort = S_PLAYLIST; qp.idx_type = I_NONE; qp.filter = sqlite3_mprintf("(f.type = %d OR f.type = %d)", PL_PLAIN, PL_SMART); ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } while (((ret = db_query_fetch_pl(&qp, &dbpli, 0)) == 0) && (dbpli.id)) { if (safe_atou32(dbpli.db_timestamp, &time_modified) != 0) { *errmsg = safe_asprintf("Error converting time modified to uint32_t: %s\n", dbpli.db_timestamp); return ACK_ERROR_UNKNOWN; } mpd_time(modified, sizeof(modified), time_modified); evbuffer_add_printf(evbuf, "playlist: %s\n" "Last-Modified: %s\n", (dbpli.virtual_path + 1), modified); } db_query_end(&qp); sqlite3_free(qp.filter); return 0; } /* * Command handler function for 'load' * Adds the playlist given by virtual-path in argv[1] to the queue. */ static int mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { char path[PATH_MAX]; struct playlist_info *pli; struct player_status status; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'load'"); return ACK_ERROR_ARG; } if (strncmp(argv[1], "/", 1) == 0) ret = snprintf(path, sizeof(path), "%s", argv[1]); else ret = snprintf(path, sizeof(path), "/%s", argv[1]); if (ret >= sizeof(path)) { *errmsg = safe_asprintf("Length of path exceeds the PATH_MAX value '%s'", argv[1]); return ACK_ERROR_ARG; } pli = db_pl_fetch_byvirtualpath(path); if (!pli) { *errmsg = safe_asprintf("Playlist not found for path '%s'", argv[1]); return ACK_ERROR_ARG; } //TODO If a second parameter is given only add the specified range of songs to the playqueue player_get_status(&status); ret = db_queue_add_by_playlistid(pli->id, status.shuffle, status.item_id); free_pli(pli, 0); if (ret < 0) { *errmsg = safe_asprintf("Failed to add song '%s' to playlist", argv[1]); return ACK_ERROR_UNKNOWN; } return 0; } static int mpd_get_query_params_find(int argc, char **argv, struct query_params *qp) { char *c1; char *c2; int start_pos; int end_pos; int i; uint32_t num; int ret; c1 = NULL; c2 = NULL; for (i = 0; i < argc; i += 2) { if (0 == strcasecmp(argv[i], "any")) { c1 = sqlite3_mprintf("(f.artist LIKE '%%%q%%' OR f.album LIKE '%%%q%%' OR f.title LIKE '%%%q%%')", argv[i + 1], argv[i + 1], argv[i + 1]); } else if (0 == strcasecmp(argv[i], "file")) { c1 = sqlite3_mprintf("(f.virtual_path = '/%q')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "base")) { c1 = sqlite3_mprintf("(f.virtual_path LIKE '/%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "modified-since")) { DPRINTF(E_WARN, L_MPD, "Special parameter 'modified-since' is not supported by forked-daapd and will be ignored\n"); } else if (0 == strcasecmp(argv[i], "window")) { ret = mpd_pars_range_arg(argv[i + 1], &start_pos, &end_pos); if (ret == 0) { qp->idx_type = I_SUB; qp->limit = end_pos - start_pos; qp->offset = start_pos; } else { DPRINTF(E_LOG, L_MPD, "Window argument doesn't convert to integer or range: '%s'\n", argv[i + 1]); } } else if (0 == strcasecmp(argv[i], "artist")) { c1 = sqlite3_mprintf("(f.artist = '%q')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "albumartist")) { c1 = sqlite3_mprintf("(f.album_artist = '%q')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "album")) { c1 = sqlite3_mprintf("(f.album = '%q')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "title")) { c1 = sqlite3_mprintf("(f.title = '%q')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "genre")) { c1 = sqlite3_mprintf("(f.genre = '%q')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "disc")) { ret = safe_atou32(argv[i + 1], &num); if (ret < 0) DPRINTF(E_WARN, L_MPD, "Disc parameter '%s' is not an integer and will be ignored\n", argv[i + 1]); else c1 = sqlite3_mprintf("(f.disc = %d)", num); } else if (0 == strcasecmp(argv[i], "track")) { ret = safe_atou32(argv[i + 1], &num); if (ret < 0) DPRINTF(E_WARN, L_MPD, "Track parameter '%s' is not an integer and will be ignored\n", argv[i + 1]); else c1 = sqlite3_mprintf("(f.track = %d)", num); } else if (0 == strcasecmp(argv[i], "date")) { ret = safe_atou32(argv[i + 1], &num); if (ret < 0) c1 = sqlite3_mprintf("(f.year = 0 OR f.year IS NULL)"); else c1 = sqlite3_mprintf("(f.year = %d)", num); } else if (i == 0 && argc == 1) { // Special case: a single token is allowed if listing albums for an artist c1 = sqlite3_mprintf("(f.album_artist = '%q')", argv[i]); } else { DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported by forked-daapd and will be ignored\n", argv[i]); } if (c1) { if (qp->filter) c2 = sqlite3_mprintf("%s AND %s", qp->filter, c1); else c2 = sqlite3_mprintf("%s", c1); if (qp->filter) sqlite3_free(qp->filter); qp->filter = c2; c2 = NULL; sqlite3_free(c1); c1 = NULL; } } return 0; } static int mpd_command_count(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct filecount_info fci; int ret; if (argc < 3 || ((argc - 1) % 2) != 0) { *errmsg = safe_asprintf("Missing argument(s) for command 'find'"); return ACK_ERROR_ARG; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_COUNT_ITEMS; mpd_get_query_params_find(argc - 1, argv + 1, &qp); ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); sqlite3_free(qp.filter); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } ret = db_query_fetch_count(&qp, &fci); if (ret < 0) { db_query_end(&qp); sqlite3_free(qp.filter); *errmsg = safe_asprintf("Could not fetch query count"); return ACK_ERROR_UNKNOWN; } evbuffer_add_printf(evbuf, "songs: %d\n" "playtime: %" PRIu64 "\n", fci.count, (fci.length / 1000)); db_query_end(&qp); sqlite3_free(qp.filter); return 0; } static int mpd_command_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct db_media_file_info dbmfi; int ret; if (argc < 3 || ((argc - 1) % 2) != 0) { *errmsg = safe_asprintf("Missing argument(s) for command 'find'"); return ACK_ERROR_ARG; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.sort = S_NAME; qp.idx_type = I_NONE; mpd_get_query_params_find(argc - 1, argv + 1, &qp); ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); sqlite3_free(qp.filter); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { ret = mpd_add_db_media_file_info(evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } } db_query_end(&qp); sqlite3_free(qp.filter); return 0; } static int mpd_command_findadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct player_status status; int ret; if (argc < 3 || ((argc - 1) % 2) != 0) { *errmsg = safe_asprintf("Missing argument(s) for command 'findadd'"); return ACK_ERROR_ARG; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.sort = S_ARTIST; qp.idx_type = I_NONE; mpd_get_query_params_find(argc - 1, argv + 1, &qp); player_get_status(&status); ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id); sqlite3_free(qp.filter); if (ret < 0) { *errmsg = safe_asprintf("Failed to add songs to playlist"); return ACK_ERROR_UNKNOWN; } return 0; } static int mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct db_group_info dbgri; char *type; char *browse_item; char *sort_item; int ret; if (argc < 2 || ((argc % 2) != 0)) { if (argc != 3 || (0 != strcasecmp(argv[1], "album"))) { *errmsg = safe_asprintf("Missing argument(s) for command 'list'"); return ACK_ERROR_ARG; } } memset(&qp, 0, sizeof(struct query_params)); if (0 == strcasecmp(argv[1], "artist")) { qp.type = Q_GROUP_ARTISTS; qp.sort = S_ARTIST; type = "Artist: "; } else if (0 == strcasecmp(argv[1], "albumartist")) { qp.type = Q_GROUP_ARTISTS; qp.sort = S_ARTIST; type = "AlbumArtist: "; } else if (0 == strcasecmp(argv[1], "album")) { qp.type = Q_GROUP_ALBUMS; qp.sort = S_ALBUM; type = "Album: "; } else if (0 == strcasecmp(argv[1], "date")) { qp.type = Q_BROWSE_YEARS; qp.sort = S_YEAR; type = "Date: "; } else if (0 == strcasecmp(argv[1], "genre")) { qp.type = Q_BROWSE_GENRES; qp.sort = S_GENRE; type = "Genre: "; } else if (0 == strcasecmp(argv[1], "disc")) { qp.type = Q_BROWSE_DISCS; qp.sort = S_DISC; type = "Disc: "; } else if (0 == strcasecmp(argv[1], "track")) { qp.type = Q_BROWSE_TRACKS; qp.sort = S_TRACK; type = "Track: "; } else if (0 == strcasecmp(argv[1], "file")) { qp.type = Q_BROWSE_VPATH; qp.sort = S_VPATH; type = "file: "; } else { DPRINTF(E_WARN, L_MPD, "Unsupported type argument for command 'list': %s\n", argv[1]); return 0; } qp.idx_type = I_NONE; if (argc > 2) { mpd_get_query_params_find(argc - 2, argv + 2, &qp); } ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); sqlite3_free(qp.filter); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } if (qp.type & Q_F_BROWSE) { if (qp.type == Q_BROWSE_VPATH) { while (((ret = db_query_fetch_string_sort(&qp, &browse_item, &sort_item)) == 0) && (browse_item)) { // Remove the first "/" from the virtual_path evbuffer_add_printf(evbuf, "%s%s\n", type, (browse_item + 1)); } } else { while (((ret = db_query_fetch_string_sort(&qp, &browse_item, &sort_item)) == 0) && (browse_item)) { evbuffer_add_printf(evbuf, "%s%s\n", type, browse_item); } } } else { while ((ret = db_query_fetch_group(&qp, &dbgri)) == 0) { evbuffer_add_printf(evbuf, "%s%s\n", type, dbgri.itemname); } } db_query_end(&qp); sqlite3_free(qp.filter); return 0; } static int mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int listinfo, char **errmsg) { struct directory_info subdir; struct query_params qp; struct directory_enum dir_enum; struct db_playlist_info dbpli; char modified[32]; uint32_t time_modified; struct db_media_file_info dbmfi; int ret; // Load playlists for dir-id memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_PL; qp.sort = S_PLAYLIST; qp.idx_type = I_NONE; qp.filter = sqlite3_mprintf("(f.directory_id = %d AND (f.type = %d OR f.type = %d))", directory_id, PL_PLAIN, PL_SMART); ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } while (((ret = db_query_fetch_pl(&qp, &dbpli, 0)) == 0) && (dbpli.id)) { if (safe_atou32(dbpli.db_timestamp, &time_modified) != 0) { DPRINTF(E_LOG, L_MPD, "Error converting time modified to uint32_t: %s\n", dbpli.db_timestamp); return -1; } if (listinfo) { mpd_time(modified, sizeof(modified), time_modified); evbuffer_add_printf(evbuf, "playlist: %s\n" "Last-Modified: %s\n", (dbpli.virtual_path + 1), modified); } else { evbuffer_add_printf(evbuf, "playlist: %s\n", (dbpli.virtual_path + 1)); } } db_query_end(&qp); sqlite3_free(qp.filter); // Load sub directories for dir-id memset(&dir_enum, 0, sizeof(struct directory_enum)); dir_enum.parent_id = directory_id; ret = db_directory_enum_start(&dir_enum); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Failed to start directory enum for parent_id %d\n", directory_id); return -1; } while ((ret = db_directory_enum_fetch(&dir_enum, &subdir)) == 0 && subdir.id > 0) { if (listinfo) { evbuffer_add_printf(evbuf, "directory: %s\n" "Last-Modified: %s\n", (subdir.virtual_path + 1), "2015-12-01 00:00"); } else { evbuffer_add_printf(evbuf, "directory: %s\n", (subdir.virtual_path + 1)); } if (listall) { mpd_add_directory(evbuf, subdir.id, listall, listinfo, errmsg); } } db_directory_enum_end(&dir_enum); // Load files for dir-id memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.sort = S_ARTIST; qp.idx_type = I_NONE; qp.filter = sqlite3_mprintf("(f.directory_id = %d)", directory_id); ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { if (listinfo) { ret = mpd_add_db_media_file_info(evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } } else { evbuffer_add_printf(evbuf, "file: %s\n", (dbmfi.virtual_path + 1)); } } db_query_end(&qp); return 0; } static int mpd_command_listall(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int dir_id; char parent[PATH_MAX]; int ret; if (argc < 2 || strlen(argv[1]) == 0 || (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } else if (strncmp(argv[1], "/", 1) == 0) { ret = snprintf(parent, sizeof(parent), "%s/", argv[1]); } else { ret = snprintf(parent, sizeof(parent), "/%s", argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) { *errmsg = safe_asprintf("Parent path exceeds PATH_MAX"); return ACK_ERROR_UNKNOWN; } // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) { *errmsg = safe_asprintf("Directory info not found for virtual-path '%s'", parent); return ACK_ERROR_NO_EXIST; } ret = mpd_add_directory(evbuf, dir_id, 1, 0, errmsg); return ret; } static int mpd_command_listallinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int dir_id; char parent[PATH_MAX]; int ret; if (argc < 2 || strlen(argv[1]) == 0 || (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } else if (strncmp(argv[1], "/", 1) == 0) { ret = snprintf(parent, sizeof(parent), "%s/", argv[1]); } else { ret = snprintf(parent, sizeof(parent), "/%s", argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) { *errmsg = safe_asprintf("Parent path exceeds PATH_MAX"); return ACK_ERROR_UNKNOWN; } // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) { *errmsg = safe_asprintf("Directory info not found for virtual-path '%s'", parent); return ACK_ERROR_NO_EXIST; } ret = mpd_add_directory(evbuf, dir_id, 1, 1, errmsg); return ret; } /* * Command handler function for 'lsinfo' * Lists the contents of the directory given in argv[1]. */ static int mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int dir_id; char parent[PATH_MAX]; int print_playlists; int ret; if (argc < 2 || strlen(argv[1]) == 0 || (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } else if (strncmp(argv[1], "/", 1) == 0) { ret = snprintf(parent, sizeof(parent), "%s/", argv[1]); } else { ret = snprintf(parent, sizeof(parent), "/%s", argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) { *errmsg = safe_asprintf("Parent path exceeds PATH_MAX"); return ACK_ERROR_UNKNOWN; } print_playlists = 0; if ((strncmp(parent, "/", 1) == 0 && strlen(parent) == 1)) { /* * Special handling necessary if the root directory '/' is given. * In this case additional to the directory contents the stored playlists will be returned. * This behavior is deprecated in the mpd protocol but clients like ncmpccp or ympd uses it. */ print_playlists = 1; } // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) { *errmsg = safe_asprintf("Directory info not found for virtual-path '%s'", parent); return ACK_ERROR_NO_EXIST; } ret = mpd_add_directory(evbuf, dir_id, 0, 1, errmsg); // If the root directory was passed as argument add the stored playlists to the response if (ret == 0 && print_playlists) { return mpd_command_listplaylists(evbuf, argc, argv, errmsg); } return ret; } static int mpd_get_query_params_search(int argc, char **argv, struct query_params *qp) { char *c1; char *c2; int start_pos; int end_pos; int i; uint32_t num; int ret; c1 = NULL; c2 = NULL; for (i = 0; i < argc; i += 2) { if (0 == strcasecmp(argv[i], "any")) { c1 = sqlite3_mprintf("(f.artist LIKE '%%%q%%' OR f.album LIKE '%%%q%%' OR f.title LIKE '%%%q%%')", argv[i + 1], argv[i + 1], argv[i + 1]); } else if (0 == strcasecmp(argv[i], "file")) { c1 = sqlite3_mprintf("(f.virtual_path LIKE '%%%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "base")) { c1 = sqlite3_mprintf("(f.virtual_path LIKE '/%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "modified-since")) { DPRINTF(E_WARN, L_MPD, "Special parameter 'modified-since' is not supported by forked-daapd and will be ignored\n"); } else if (0 == strcasecmp(argv[i], "window")) { ret = mpd_pars_range_arg(argv[i + 1], &start_pos, &end_pos); if (ret == 0) { qp->idx_type = I_SUB; qp->limit = end_pos - start_pos; qp->offset = start_pos; } else { DPRINTF(E_LOG, L_MPD, "Window argument doesn't convert to integer or range: '%s'\n", argv[i + 1]); } } else if (0 == strcasecmp(argv[i], "artist")) { c1 = sqlite3_mprintf("(f.artist LIKE '%%%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "albumartist")) { c1 = sqlite3_mprintf("(f.album_artist LIKE '%%%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "album")) { c1 = sqlite3_mprintf("(f.album LIKE '%%%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "title")) { c1 = sqlite3_mprintf("(f.title LIKE '%%%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "genre")) { c1 = sqlite3_mprintf("(f.genre LIKE '%%%q%%')", argv[i + 1]); } else if (0 == strcasecmp(argv[i], "disc")) { ret = safe_atou32(argv[i + 1], &num); if (ret < 0) DPRINTF(E_WARN, L_MPD, "Disc parameter '%s' is not an integer and will be ignored\n", argv[i + 1]); else c1 = sqlite3_mprintf("(f.disc = %d)", num); } else if (0 == strcasecmp(argv[i], "track")) { ret = safe_atou32(argv[i + 1], &num); if (ret < 0) DPRINTF(E_WARN, L_MPD, "Track parameter '%s' is not an integer and will be ignored\n", argv[i + 1]); else c1 = sqlite3_mprintf("(f.track = %d)", num); } else if (0 == strcasecmp(argv[i], "date")) { ret = safe_atou32(argv[i + 1], &num); if (ret < 0) c1 = sqlite3_mprintf("(f.year = 0 OR f.year IS NULL)"); else c1 = sqlite3_mprintf("(f.year = %d)", num); } else { DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported by forked-daapd and will be ignored\n", argv[i]); } if (c1) { if (qp->filter) c2 = sqlite3_mprintf("%s AND %s", qp->filter, c1); else c2 = sqlite3_mprintf("%s", c1); if (qp->filter) sqlite3_free(qp->filter); qp->filter = c2; c2 = NULL; sqlite3_free(c1); c1 = NULL; } } return 0; } /* * Command handler function for 'search' * Lists any song that matches the given list of arguments. Arguments are pairs of TYPE and WHAT, where * TYPE is the tag that contains WHAT (case insensitiv). * * TYPE can also be one of the special parameter: * - any: checks all tags * - file: checks the virtual_path * - base: restricts result to the given directory * - modified-since (not supported) * - window: limits result to the given range of "START:END" * * Example request: "search artist foo album bar" */ static int mpd_command_search(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct db_media_file_info dbmfi; int ret; if (argc < 3 || ((argc - 1) % 2) != 0) { *errmsg = safe_asprintf("Missing argument(s) for command 'search'"); return ACK_ERROR_ARG; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.sort = S_NAME; qp.idx_type = I_NONE; mpd_get_query_params_search(argc - 1, argv + 1, &qp); ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); sqlite3_free(qp.filter); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { ret = mpd_add_db_media_file_info(evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); } } db_query_end(&qp); sqlite3_free(qp.filter); return 0; } static int mpd_command_searchadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct query_params qp; struct player_status status; int ret; if (argc < 3 || ((argc - 1) % 2) != 0) { *errmsg = safe_asprintf("Missing argument(s) for command 'search'"); return ACK_ERROR_ARG; } memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.sort = S_ARTIST; qp.idx_type = I_NONE; mpd_get_query_params_search(argc - 1, argv + 1, &qp); player_get_status(&status); ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id); sqlite3_free(qp.filter); if (ret < 0) { *errmsg = safe_asprintf("Failed to add songs to playlist"); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'update' * Initiates an init-rescan (scans for new files) */ static int mpd_command_update(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { if (argc > 1 && strlen(argv[1]) > 0) { *errmsg = safe_asprintf("Update for specific uri not supported for command 'update'"); return ACK_ERROR_ARG; } library_rescan(); evbuffer_add(evbuf, "updating_db: 1\n", 15); return 0; } /* static int mpd_command_rescan(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int ret; if (argc > 1) { DPRINTF(E_LOG, L_MPD, "Rescan for specific uri not supported for command 'rescan'\n"); *errmsg = safe_asprintf("Rescan for specific uri not supported for command 'rescan'"); return ACK_ERROR_ARG; } filescanner_trigger_fullrescan(); evbuffer_add(evbuf, "updating_db: 1\n", 15); return 0; } */ /* * Callback function for the 'player_speaker_enumerate' function. * Adds a new struct output to the given struct outputs in *arg for the given speaker (id, name, etc.). */ static void outputs_enum_cb(uint64_t id, const char *name, int relvol, int absvol, struct spk_flags flags, void *arg) { struct outputs *outputs; struct output *output; outputs = (struct outputs *)arg; output = (struct output*)malloc(sizeof(struct output)); output->id = id; output->shortid = (unsigned short) id; output->name = strdup(name); output->selected = flags.selected; output->next = outputs->outputs; outputs->outputs = output; outputs->count++; if (flags.selected) outputs->active++; DPRINTF(E_DBG, L_MPD, "Output enum: shortid %d, id %" PRIu64 ", name '%s', selected %d\n", output->shortid, output->id, output->name, output->selected); } /* * Command handler function for 'disableoutput' * Expects argument argv[1] to be the id of the speaker to disable. */ static int mpd_command_disableoutput(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct outputs outputs; struct output *output; uint32_t num; uint64_t *ids; int nspk; int i; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'disableoutput'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &num); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } outputs.count = 0; outputs.active = 0; outputs.outputs = NULL; player_speaker_enumerate(outputs_enum_cb, &outputs); nspk = outputs.active; output = outputs.outputs; while (output) { if (output->shortid == num && output->selected) { nspk--; break; } output = output->next; } if (nspk == outputs.active) { DPRINTF(E_LOG, L_MPD, "No speaker to deactivate\n"); free_outputs(outputs.outputs); return 0; } ids = (uint64_t *)malloc((nspk + 1) * sizeof(uint64_t)); ids[0] = nspk; i = 1; output = outputs.outputs; while (output) { if (output->shortid != num && output->selected) { ids[i] = output->id; i++; } output = output->next; } ret = player_speaker_set(ids); free(ids); free_outputs(outputs.outputs); if (ret < 0) { *errmsg = safe_asprintf("Speakers deactivation failed: %d", num); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'enableoutput' * Expects argument argv[1] to be the id of the speaker to enable. */ static int mpd_command_enableoutput(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct outputs outputs; struct output *output; uint32_t num; uint64_t *ids; int nspk; int i; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'enableoutput'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &num); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } outputs.count = 0; outputs.active = 0; outputs.outputs = NULL; player_speaker_enumerate(outputs_enum_cb, &outputs); nspk = outputs.active; output = outputs.outputs; while (output) { if (output->shortid == num && !output->selected) { nspk++; break; } output = output->next; } if (nspk == outputs.active) { DPRINTF(E_LOG, L_MPD, "No speaker to activate\n"); free_outputs(outputs.outputs); return 0; } ids = (uint64_t *)malloc((nspk + 1) * sizeof(uint64_t)); ids[0] = nspk; i = 1; output = outputs.outputs; while (output) { if (output->shortid == num || output->selected) { ids[i] = output->id; i++; } output = output->next; } ret = player_speaker_set(ids); if (ids) free(ids); free_outputs(outputs.outputs); if (ret < 0) { *errmsg = safe_asprintf("Speakers activation failed: %d", num); return ACK_ERROR_UNKNOWN; } return 0; } /* * Command handler function for 'toggleoutput' * Expects argument argv[1] to be the id of the speaker to enable/disable. */ static int mpd_command_toggleoutput(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { struct outputs outputs; struct output *output; uint32_t num; uint64_t *ids; int nspk; int i; int ret; if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'toggleoutput'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &num); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } outputs.count = 0; outputs.active = 0; outputs.outputs = NULL; player_speaker_enumerate(outputs_enum_cb, &outputs); nspk = outputs.active; output = outputs.outputs; while (output) { if (output->shortid == num && !output->selected) { nspk++; break; } else if (output->shortid == num && output->selected) { nspk--; break; } output = output->next; } if (nspk == outputs.active) { DPRINTF(E_LOG, L_MPD, "No speaker to de/activate\n"); free_outputs(outputs.outputs); return 0; } ids = (uint64_t *)malloc((nspk + 1) * sizeof(uint64_t)); ids[0] = nspk; i = 1; output = outputs.outputs; while (output) { if ((output->shortid == num && !output->selected) || (output->shortid != num && output->selected)) { ids[i] = output->id; i++; } output = output->next; } ret = player_speaker_set(ids); if (ids) free(ids); free_outputs(outputs.outputs); if (ret < 0) { *errmsg = safe_asprintf("Speakers de/activation failed: %d", num); return ACK_ERROR_UNKNOWN; } return 0; } /* * Callback function for the 'outputs' command. * Gets called for each available speaker and prints the speaker information to the evbuffer given in *arg. * * Example output: * outputid: 0 * outputname: Computer * outputenabled: 1 * outputvolume: 50 */ static void speaker_enum_cb(uint64_t id, const char *name, int relvol, int absvol, struct spk_flags flags, void *arg) { struct evbuffer *evbuf; evbuf = (struct evbuffer *)arg; evbuffer_add_printf(evbuf, "outputid: %d\n" "outputname: %s\n" "outputenabled: %d\n" "outputvolume: %d\n", (unsigned short) id, name, flags.selected, absvol); } /* * Command handler function for 'outputs' * Returns a lists with the avaiable speakers. */ static int mpd_command_outputs(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { player_speaker_enumerate(speaker_enum_cb, evbuf); return 0; } static int outputvolume_set(uint32_t shortid, int volume, char **errmsg) { struct outputs outputs; struct output *output; int ret; outputs.count = 0; outputs.active = 0; outputs.outputs = NULL; player_speaker_enumerate(outputs_enum_cb, &outputs); output = outputs.outputs; while (output) { if (output->shortid == shortid) { break; } output = output->next; } if (!output) { free_outputs(outputs.outputs); *errmsg = safe_asprintf("No speaker found for short id: %d", shortid); return ACK_ERROR_UNKNOWN; } ret = player_volume_setabs_speaker(output->id, volume); free_outputs(outputs.outputs); if (ret < 0) { *errmsg = safe_asprintf("Setting volume to %d for speaker with short-id %d failed", volume, shortid); return ACK_ERROR_UNKNOWN; } return 0; } static int mpd_command_outputvolume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { uint32_t shortid; int volume; int ret; if (argc < 3) { *errmsg = safe_asprintf("Missing argument for command 'outputvolume'"); return ACK_ERROR_ARG; } ret = safe_atou32(argv[1], &shortid); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); return ACK_ERROR_ARG; } ret = safe_atoi32(argv[2], &volume); if (ret < 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); return ACK_ERROR_ARG; } ret = outputvolume_set(shortid, volume, errmsg); return ret; } static void channel_outputvolume(const char *message) { uint32_t shortid; int volume; char *tmp; char *ptr; char *errmsg = NULL; int ret; tmp = strdup(message); ptr = strrchr(tmp, ':'); if (!ptr) { free(tmp); DPRINTF(E_LOG, L_MPD, "Failed to parse output id and volume from message '%s' (expected format: \"output-id:volume\"\n", message); return; } *ptr = '\0'; ret = safe_atou32(tmp, &shortid); if (ret < 0) { free(tmp); DPRINTF(E_LOG, L_MPD, "Failed to parse output id from message: '%s'\n", message); return; } ret = safe_atoi32((ptr + 1), &volume); if (ret < 0) { free(tmp); DPRINTF(E_LOG, L_MPD, "Failed to parse volume from message: '%s'\n", message); return; } outputvolume_set(shortid, volume, &errmsg); if (errmsg) DPRINTF(E_LOG, L_MPD, "Failed to set output volume from message: '%s' (error='%s')\n", message, errmsg); free(tmp); } static void channel_pairing(const char *message) { remote_pairing_kickoff((char **)&message); } static void channel_verification(const char *message) { player_raop_verification_kickoff((char **)&message); } struct mpd_channel { /* The channel name */ const char *channel; /* * The function to execute the sendmessage command for a specific channel * * @param message message received on this channel */ void (*handler)(const char *message); }; static struct mpd_channel mpd_channels[] = { { .channel = "outputvolume", .handler = channel_outputvolume }, { .channel = "pairing", .handler = channel_pairing }, { .channel = "verification", .handler = channel_verification }, { .channel = NULL, .handler = NULL }, }; /* * Finds the channel handler for the given channel name * * @param name channel name from sendmessage command * @return the channel or NULL if it is an unknown/unsupported channel */ static struct mpd_channel * mpd_find_channel(const char *name) { int i; for (i = 0; mpd_channels[i].handler; i++) { if (0 == strcmp(name, mpd_channels[i].channel)) { return &mpd_channels[i]; } } return NULL; } static int mpd_command_channels(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int i; for (i = 0; mpd_channels[i].handler; i++) { evbuffer_add_printf(evbuf, "channel: %s\n", mpd_channels[i].channel); } return 0; } static int mpd_command_sendmessage(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { const char *channelname; const char *message; struct mpd_channel *channel; if (argc < 3) { *errmsg = safe_asprintf("Missing argument for command 'sendmessage'"); return ACK_ERROR_ARG; } channelname = argv[1]; message = argv[2]; channel = mpd_find_channel(channelname); if (!channel) { // Just ignore the message, only log an error message DPRINTF(E_LOG, L_MPD, "Unsupported channel '%s'\n", channelname); return 0; } channel->handler(message); return 0; } /* * Dummy function to handle commands that are not supported by forked-daapd and should * not raise an error. */ static int mpd_command_ignore(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { //do nothing DPRINTF(E_DBG, L_MPD, "Ignore command %s\n", argv[0]); return 0; } static int mpd_command_commands(struct evbuffer *evbuf, int argc, char **argv, char **errmsg); /* * Command handler function for 'tagtypes' * Returns a lists with supported tags in the form: * tagtype: Artist */ static int mpd_command_tagtypes(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { evbuffer_add_printf(evbuf, "tagtype: Artist\n" "tagtype: AlbumArtist\n" "tagtype: ArtistSort\n" "tagtype: AlbumArtistSort\n" "tagtype: Album\n" "tagtype: Title\n" "tagtype: Track\n" "tagtype: Genre\n" "tagtype: Disc\n"); return 0; } /* * Command handler function for 'decoders' * MPD returns the decoder plugins with their supported suffix and mime types. * * forked-daapd only uses libav/ffmepg for decoding and does not support decoder plugins, * therefor the function reports only ffmpeg as available. */ static int mpd_command_decoders(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int i; evbuffer_add_printf(evbuf, "plugin: ffmpeg\n"); for (i = 0; ffmpeg_suffixes[i]; i++) { evbuffer_add_printf(evbuf, "suffix: %s\n", ffmpeg_suffixes[i]); } for (i = 0; ffmpeg_mime_types[i]; i++) { evbuffer_add_printf(evbuf, "mime_type: %s\n", ffmpeg_mime_types[i]); } return 0; } struct mpd_command { /* The command name */ const char *mpdcommand; /* * The function to execute the command * * @param evbuf the response event buffer * @param argc number of arguments in argv * @param argv argument array, first entry is the commandname * @param errmsg error message set by this function if an error occured * @return 0 if successful, one of ack values if an error occured */ int (*handler)(struct evbuffer *evbuf, int argc, char **argv, char **errmsg); }; static struct mpd_command mpd_handlers[] = { /* * Commands for querying status */ { .mpdcommand = "clearerror", .handler = mpd_command_ignore }, { .mpdcommand = "currentsong", .handler = mpd_command_currentsong }, { .mpdcommand = "idle", .handler = mpd_command_idle }, { .mpdcommand = "noidle", .handler = mpd_command_noidle }, { .mpdcommand = "status", .handler = mpd_command_status }, { .mpdcommand = "stats", .handler = mpd_command_stats }, /* * Playback options */ { .mpdcommand = "consume", .handler = mpd_command_consume }, { .mpdcommand = "crossfade", .handler = mpd_command_ignore }, { .mpdcommand = "mixrampdb", .handler = mpd_command_ignore }, { .mpdcommand = "mixrampdelay", .handler = mpd_command_ignore }, { .mpdcommand = "random", .handler = mpd_command_random }, { .mpdcommand = "repeat", .handler = mpd_command_repeat }, { .mpdcommand = "setvol", .handler = mpd_command_setvol }, { .mpdcommand = "single", .handler = mpd_command_single }, { .mpdcommand = "replay_gain_mode", .handler = mpd_command_ignore }, { .mpdcommand = "replay_gain_status", .handler = mpd_command_replay_gain_status }, { .mpdcommand = "volume", .handler = mpd_command_volume }, /* * Controlling playback */ { .mpdcommand = "next", .handler = mpd_command_next }, { .mpdcommand = "pause", .handler = mpd_command_pause }, { .mpdcommand = "play", .handler = mpd_command_play }, { .mpdcommand = "playid", .handler = mpd_command_playid }, { .mpdcommand = "previous", .handler = mpd_command_previous }, { .mpdcommand = "seek", .handler = mpd_command_seek }, { .mpdcommand = "seekid", .handler = mpd_command_seekid }, { .mpdcommand = "seekcur", .handler = mpd_command_seekcur }, { .mpdcommand = "stop", .handler = mpd_command_stop }, /* * The current playlist */ { .mpdcommand = "add", .handler = mpd_command_add }, { .mpdcommand = "addid", .handler = mpd_command_addid }, { .mpdcommand = "clear", .handler = mpd_command_clear }, { .mpdcommand = "delete", .handler = mpd_command_delete }, { .mpdcommand = "deleteid", .handler = mpd_command_deleteid }, { .mpdcommand = "move", .handler = mpd_command_move }, { .mpdcommand = "moveid", .handler = mpd_command_moveid }, // According to the mpd protocol the use of "playlist" is deprecated { .mpdcommand = "playlist", .handler = mpd_command_playlistinfo }, /* { .mpdcommand = "playlistfind", .handler = mpd_command_playlistfind }, */ { .mpdcommand = "playlistid", .handler = mpd_command_playlistid }, { .mpdcommand = "playlistinfo", .handler = mpd_command_playlistinfo }, /* { .mpdcommand = "playlistsearch", .handler = mpd_command_playlistsearch }, */ { .mpdcommand = "plchanges", .handler = mpd_command_plchanges }, { .mpdcommand = "plchangesposid", .handler = mpd_command_plchangesposid }, /* { .mpdcommand = "prio", .handler = mpd_command_prio }, { .mpdcommand = "prioid", .handler = mpd_command_prioid }, { .mpdcommand = "rangeid", .handler = mpd_command_rangeid }, { .mpdcommand = "shuffle", .handler = mpd_command_shuffle }, { .mpdcommand = "swap", .handler = mpd_command_swap }, { .mpdcommand = "swapid", .handler = mpd_command_swapid }, { .mpdcommand = "addtagid", .handler = mpd_command_addtagid }, { .mpdcommand = "cleartagid", .handler = mpd_command_cleartagid }, */ /* * Stored playlists */ { .mpdcommand = "listplaylist", .handler = mpd_command_listplaylist }, { .mpdcommand = "listplaylistinfo", .handler = mpd_command_listplaylistinfo }, { .mpdcommand = "listplaylists", .handler = mpd_command_listplaylists }, { .mpdcommand = "load", .handler = mpd_command_load }, /* { .mpdcommand = "playlistadd", .handler = mpd_command_playlistadd }, { .mpdcommand = "playlistclear", .handler = mpd_command_playlistclear }, { .mpdcommand = "playlistdelete", .handler = mpd_command_playlistdelete }, { .mpdcommand = "playlistmove", .handler = mpd_command_playlistmove }, { .mpdcommand = "rename", .handler = mpd_command_rename }, { .mpdcommand = "rm", .handler = mpd_command_rm }, { .mpdcommand = "save", .handler = mpd_command_save }, */ /* * The music database */ { .mpdcommand = "count", .handler = mpd_command_count }, { .mpdcommand = "find", .handler = mpd_command_find }, { .mpdcommand = "findadd", .handler = mpd_command_findadd }, { .mpdcommand = "list", .handler = mpd_command_list }, { .mpdcommand = "listall", .handler = mpd_command_listall }, { .mpdcommand = "listallinfo", .handler = mpd_command_listallinfo }, /* { .mpdcommand = "listfiles", .handler = mpd_command_listfiles }, */ { .mpdcommand = "lsinfo", .handler = mpd_command_lsinfo }, /* { .mpdcommand = "readcomments", .handler = mpd_command_readcomments }, */ { .mpdcommand = "search", .handler = mpd_command_search }, { .mpdcommand = "searchadd", .handler = mpd_command_searchadd }, /* { .mpdcommand = "searchaddpl", .handler = mpd_command_searchaddpl }, */ { .mpdcommand = "update", .handler = mpd_command_update }, /* { .mpdcommand = "rescan", .handler = mpd_command_rescan }, */ /* * Mounts and neighbors */ /* { .mpdcommand = "mount", .handler = mpd_command_mount }, { .mpdcommand = "unmount", .handler = mpd_command_unmount }, { .mpdcommand = "listmounts", .handler = mpd_command_listmounts }, { .mpdcommand = "listneighbors", .handler = mpd_command_listneighbors }, */ /* * Stickers */ { .mpdcommand = "sticker", .handler = mpd_command_ignore }, /* * Connection settings */ { .mpdcommand = "close", .handler = mpd_command_ignore }, /* { .mpdcommand = "kill", .handler = mpd_command_kill }, { .mpdcommand = "password", .handler = mpd_command_password }, */ { .mpdcommand = "ping", .handler = mpd_command_ignore }, /* * Audio output devices */ { .mpdcommand = "disableoutput", .handler = mpd_command_disableoutput }, { .mpdcommand = "enableoutput", .handler = mpd_command_enableoutput }, { .mpdcommand = "toggleoutput", .handler = mpd_command_toggleoutput }, { .mpdcommand = "outputs", .handler = mpd_command_outputs }, /* * Reflection */ /* { .mpdcommand = "config", .handler = mpd_command_config }, */ { .mpdcommand = "commands", .handler = mpd_command_commands }, { .mpdcommand = "notcommands", .handler = mpd_command_ignore }, { .mpdcommand = "tagtypes", .handler = mpd_command_tagtypes }, { .mpdcommand = "urlhandlers", .handler = mpd_command_ignore }, { .mpdcommand = "decoders", .handler = mpd_command_decoders }, /* * Client to client */ { .mpdcommand = "subscribe", .handler = mpd_command_ignore }, { .mpdcommand = "unsubscribe", .handler = mpd_command_ignore }, { .mpdcommand = "channels", .handler = mpd_command_channels }, { .mpdcommand = "readmessages", .handler = mpd_command_ignore }, { .mpdcommand = "sendmessage", .handler = mpd_command_sendmessage }, /* * Forked-daapd commands (not supported by mpd) */ { .mpdcommand = "outputvolume", .handler = mpd_command_outputvolume }, /* * NULL command to terminate loop */ { .mpdcommand = NULL, .handler = NULL } }; /* * Finds the command handler for the given command name * * @param name the name of the command * @return the command or NULL if it is an unknown/unsupported command */ static struct mpd_command* mpd_find_command(const char *name) { int i; for (i = 0; mpd_handlers[i].handler; i++) { if (0 == strcmp(name, mpd_handlers[i].mpdcommand)) { return &mpd_handlers[i]; } } return NULL; } static int mpd_command_commands(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { int i; for (i = 0; mpd_handlers[i].handler; i++) { evbuffer_add_printf(evbuf, "command: %s\n", mpd_handlers[i].mpdcommand); } return 0; } /* * The read callback function is invoked if a complete command sequence was received from the client * (see mpd_input_filter function). * * @param bev the buffer event * @param ctx (not used) */ static void mpd_read_cb(struct bufferevent *bev, void *ctx) { struct evbuffer *input; struct evbuffer *output; int ret; int ncmd; char *line; char *errmsg; struct mpd_command *command; enum command_list_type listtype; int idle_cmd; int close_cmd; char *argv[COMMAND_ARGV_MAX]; int argc; /* Get the input evbuffer, contains the command sequence received from the client */ input = bufferevent_get_input(bev); /* Get the output evbuffer, used to send the server response to the client */ output = bufferevent_get_output(bev); DPRINTF(E_SPAM, L_MPD, "Received MPD command sequence\n"); idle_cmd = 0; close_cmd = 0; listtype = COMMAND_LIST_NONE; ncmd = 0; ret = -1; while ((line = evbuffer_readln(input, NULL, EVBUFFER_EOL_ANY))) { DPRINTF(E_DBG, L_MPD, "MPD message: %s\n", line); // Split the read line into command name and arguments ret = mpd_parse_args(line, &argc, argv); if (ret != 0 || argc <= 0) { // Error handling for argument parsing error DPRINTF(E_LOG, L_MPD, "Error parsing arguments for MPD message: %s\n", line); errmsg = safe_asprintf("Error parsing arguments"); ret = ACK_ERROR_ARG; evbuffer_add_printf(output, "ACK [%d@%d] {%s} %s\n", ret, ncmd, "unkown", errmsg); free(errmsg); free(line); break; } /* * Check if it is a list command */ if (0 == strcmp(argv[0], "command_list_ok_begin")) { listtype = COMMAND_LIST_OK; free(line); continue; } else if (0 == strcmp(argv[0], "command_list_begin")) { listtype = COMMAND_LIST; free(line); continue; } else if (0 == strcmp(argv[0], "command_list_end")) { free(line); break; } else if (0 == strcmp(argv[0], "idle")) idle_cmd = 1; else if (0 == strcmp(argv[0], "noidle")) idle_cmd = 0; else if (0 == strcmp(argv[0], "close")) close_cmd = 1; /* * Find the command handler and execute the command function */ command = mpd_find_command(argv[0]); if (command == NULL) { errmsg = safe_asprintf("Unsupported command '%s'", argv[0]); ret = ACK_ERROR_UNKNOWN; } else ret = command->handler(output, argc, argv, &errmsg); /* * If an error occurred, add the ACK line to the response buffer and exit the loop */ if (ret != 0) { DPRINTF(E_LOG, L_MPD, "Error executing command '%s': %s\n", argv[0], errmsg); evbuffer_add_printf(output, "ACK [%d@%d] {%s} %s\n", ret, ncmd, argv[0], errmsg); free(errmsg); free(line); break; } /* * If the command sequence started with command_list_ok_begin, add a list_ok line to the * response buffer after each command output. */ if (listtype == COMMAND_LIST_OK) { evbuffer_add(output, "list_OK\n", 8); } free(line); ncmd++; } DPRINTF(E_SPAM, L_MPD, "Finished MPD command sequence: %d\n", ret); /* * If everything was successful add OK line to signal clients end of message. * If an error occured the necessary ACK line should already be added to the response buffer. */ if (ret == 0 && idle_cmd == 0 && close_cmd == 0) { evbuffer_add(output, "OK\n", 3); } } /* * Callback when an event occurs on the bufferevent */ static void mpd_event_cb(struct bufferevent *bev, short events, void *ctx) { struct evbuffer *evbuf; if (events & BEV_EVENT_ERROR) { DPRINTF(E_LOG, L_MPD, "Error from bufferevent: %s\n", evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR())); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { evbuf = bufferevent_get_output(bev); mpd_remove_idle_client(evbuf); bufferevent_free(bev); } } /* * The input filter buffer callback checks if the data received from the client is a complete command sequence. * A command sequence has end with '\n' and if it starts with "command_list_begin\n" or "command_list_ok_begin\n" * the last line has to be "command_list_end\n". * * @param src evbuffer to read data from (contains the data received from the client) * @param dst evbuffer to write data to (this is the evbuffer for the read callback) * @param lim the upper bound of bytes to add to destination * @param state write mode * @param ctx (not used) * @return BEV_OK if a complete command sequence was received otherwise BEV_NEED_MORE */ static enum bufferevent_filter_result mpd_input_filter(struct evbuffer *src, struct evbuffer *dst, ev_ssize_t lim, enum bufferevent_flush_mode state, void *ctx) { struct evbuffer_ptr p; char *line; int ret; while ((line = evbuffer_readln(src, NULL, EVBUFFER_EOL_ANY))) { ret = evbuffer_add_printf(dst, "%s\n", line); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding line to buffer: '%s'\n", line); free(line); return BEV_ERROR; } free(line); } if (evbuffer_get_length(src) > 0) { DPRINTF(E_DBG, L_MPD, "Message incomplete, waiting for more data\n"); return BEV_NEED_MORE; } p = evbuffer_search(dst, "command_list_begin", 18, NULL); if (p.pos < 0) { p = evbuffer_search(dst, "command_list_ok_begin", 21, NULL); } if (p.pos >= 0) { p = evbuffer_search(dst, "command_list_end", 16, NULL); if (p.pos < 0) { DPRINTF(E_DBG, L_MPD, "Message incomplete (missing command_list_end), waiting for more data\n"); return BEV_NEED_MORE; } } return BEV_OK; } /* * The connection listener callback function is invoked when a new connection was received. * * @param listener the connection listener that received the connection * @param sock the new socket * @param address the address from which the connection was received * @param socklen the length of that address * @param ctx (not used) */ static void mpd_accept_conn_cb(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *address, int socklen, void *ctx) { /* * For each new connection setup a new buffer event and wrap it around a filter event. * The filter event ensures, that the read callback on the buffer event is only invoked if a complete * command sequence from the client was received. */ struct event_base *base = evconnlistener_get_base(listener); struct bufferevent *bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE); bev = bufferevent_filter_new(bev, mpd_input_filter, NULL, BEV_OPT_CLOSE_ON_FREE, NULL, NULL); bufferevent_setcb(bev, mpd_read_cb, NULL, mpd_event_cb, bev); bufferevent_enable(bev, EV_READ | EV_WRITE); /* * According to the mpd protocol send "OK MPD \n" to the client, where version is the version * of the supported mpd protocol and not the server version. */ evbuffer_add(bufferevent_get_output(bev), "OK MPD 0.18.0\n", 14); } /* * Error callback that gets called whenever an accept() call fails on the listener * @param listener the connection listener that received the connection * @param ctx (not used) */ static void mpd_accept_error_cb(struct evconnlistener *listener, void *ctx) { int err; err = EVUTIL_SOCKET_ERROR(); DPRINTF(E_LOG, L_MPD, "Error occured %d (%s) on the listener.\n", err, evutil_socket_error_to_string(err)); } static int mpd_notify_idle_client(struct idle_client *client, enum listener_event_type type) { if (!(client->events & type)) { DPRINTF(E_DBG, L_MPD, "Client not listening for event: %d\n", type); return 1; } switch (type) { case LISTENER_PLAYER: evbuffer_add(client->evbuffer, "changed: player\n", 16); break; case LISTENER_QUEUE: evbuffer_add(client->evbuffer, "changed: playlist\n", 18); break; case LISTENER_VOLUME: evbuffer_add(client->evbuffer, "changed: mixer\n", 15); break; case LISTENER_SPEAKER: evbuffer_add(client->evbuffer, "changed: output\n", 16); break; case LISTENER_OPTIONS: evbuffer_add(client->evbuffer, "changed: options\n", 17); break; default: DPRINTF(E_WARN, L_MPD, "Unsupported event type (%d) in notify idle clients.\n", type); return -1; } evbuffer_add(client->evbuffer, "OK\n", 3); return 0; } static enum command_state mpd_notify_idle(void *arg, int *retval) { enum listener_event_type type; struct idle_client *client; struct idle_client *prev; struct idle_client *next; int i; int ret; type = *(enum listener_event_type *)arg; DPRINTF(E_DBG, L_MPD, "Notify clients waiting for idle results: %d\n", type); prev = NULL; next = NULL; i = 0; client = idle_clients; while (client) { DPRINTF(E_DBG, L_MPD, "Notify client #%d\n", i); next = client->next; ret = mpd_notify_idle_client(client, type); if (ret == 0) { if (prev) prev->next = next; else idle_clients = next; free(client); } else { prev = client; } client = next; i++; } *retval = 0; return COMMAND_END; } static void mpd_listener_cb(enum listener_event_type type) { enum listener_event_type *ptr; ptr = (enum listener_event_type *)malloc(sizeof(enum listener_event_type)); *ptr = type; DPRINTF(E_DBG, L_MPD, "Listener callback called with event type %d.\n", type); commands_exec_async(cmdbase, mpd_notify_idle, ptr); } /* * Callback function that handles http requests for artwork files * * Some MPD clients allow retrieval of local artwork by making http request for artwork * files. * * A request for the artwork of an item with virtual path "file:/path/to/example.mp3" looks * like: * GET http://:/path/to/cover.jpg * * Artwork is found by taking the uri and removing everything after the last '/'. The first * item in the library with a virtual path that matches *path/to* is used to read the artwork * file through the default forked-daapd artwork logic. */ static void artwork_cb(struct evhttp_request *req, void *arg) { struct evbuffer *evbuffer; struct evhttp_uri *decoded; const char *uri; const char *path; char *decoded_path; char *last_slash; int itemid; int format; if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) { DPRINTF(E_LOG, L_MPD, "Unsupported request type for artwork\n"); httpd_send_error(req, HTTP_BADMETHOD, "Method not allowed"); return; } uri = evhttp_request_get_uri(req); DPRINTF(E_DBG, L_MPD, "Got artwork request with uri '%s'\n", uri); decoded = evhttp_uri_parse(uri); if (!decoded) { DPRINTF(E_LOG, L_MPD, "Bad artwork request with uri '%s'\n", uri); httpd_send_error(req, HTTP_BADREQUEST, 0); return; } path = evhttp_uri_get_path(decoded); if (!path) { DPRINTF(E_LOG, L_MPD, "Invalid path from artwork request with uri '%s'\n", uri); httpd_send_error(req, HTTP_BADREQUEST, 0); evhttp_uri_free(decoded); return; } decoded_path = evhttp_uridecode(path, 0, NULL); if (!decoded_path) { DPRINTF(E_LOG, L_MPD, "Error decoding path from artwork request with uri '%s'\n", uri); httpd_send_error(req, HTTP_BADREQUEST, 0); evhttp_uri_free(decoded); return; } last_slash = strrchr(decoded_path, '/'); if (last_slash) *last_slash = '\0'; DPRINTF(E_DBG, L_MPD, "Artwork request for path: %s\n", decoded_path); itemid = db_file_id_by_virtualpath_match(decoded_path); if (!itemid) { DPRINTF(E_WARN, L_MPD, "No item found for path '%s' from request uri '%s'\n", decoded_path, uri); httpd_send_error(req, HTTP_NOTFOUND, "Document was not found"); evhttp_uri_free(decoded); free(decoded_path); return; } evbuffer = evbuffer_new(); if (!evbuffer) { DPRINTF(E_LOG, L_MPD, "Could not allocate an evbuffer for artwork request\n"); httpd_send_error(req, HTTP_INTERNAL, "Document was not found"); evhttp_uri_free(decoded); free(decoded_path); return; } format = artwork_get_item(evbuffer, itemid, 600, 600); if (format < 0) { httpd_send_error(req, HTTP_NOTFOUND, "Document was not found"); } else { switch (format) { case ART_FMT_PNG: evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "image/png"); break; default: evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "image/jpeg"); break; } httpd_send_reply(req, HTTP_OK, "OK", evbuffer, HTTPD_SEND_NO_GZIP); } evbuffer_free(evbuffer); evhttp_uri_free(decoded); free(decoded_path); } /* Thread: main */ int mpd_init(void) { struct sockaddr *saddr; size_t saddr_length; struct sockaddr_in sin; struct sockaddr_in6 sin6; unsigned short port; unsigned short http_port; const char *http_addr; int v6enabled; int ret; port = cfg_getint(cfg_getsec(cfg, "mpd"), "port"); if (port <= 0) { DPRINTF(E_INFO, L_MPD, "MPD not enabled\n"); return 0; } v6enabled = cfg_getbool(cfg_getsec(cfg, "general"), "ipv6"); evbase_mpd = event_base_new(); if (!evbase_mpd) { DPRINTF(E_LOG, L_MPD, "Could not create an event base\n"); goto evbase_fail; } cmdbase = commands_base_new(evbase_mpd, NULL); if (v6enabled) { saddr_length = sizeof(sin6); memset(&sin6, 0, saddr_length); sin6.sin6_family = AF_INET6; sin6.sin6_port = htons(port); saddr = (struct sockaddr *)&sin6; listener = evconnlistener_new_bind( evbase_mpd, mpd_accept_conn_cb, NULL, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, saddr, saddr_length); if (!listener) { DPRINTF(E_LOG, L_MPD, "Could not bind to port %d, falling back to IPv4\n", port); v6enabled = 0; } } if (!v6enabled) { saddr_length = sizeof(struct sockaddr_in); memset(&sin, 0, saddr_length); sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(0); sin.sin_port = htons(port); saddr = (struct sockaddr *)&sin; listener = evconnlistener_new_bind( evbase_mpd, mpd_accept_conn_cb, NULL, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, saddr, saddr_length); if (!listener) { DPRINTF(E_LOG, L_MPD, "Could not create connection listener for mpd clients on port %d\n", port); goto connew_fail; } } evconnlistener_set_error_cb(listener, mpd_accept_error_cb); http_port = cfg_getint(cfg_getsec(cfg, "mpd"), "http_port"); if (http_port > 0) { evhttpd = evhttp_new(evbase_mpd); if (!evhttpd) { DPRINTF(E_LOG, L_MPD, "Could not create HTTP artwork server\n"); goto evhttp_fail; } evhttp_set_gencb(evhttpd, artwork_cb, NULL); if (v6enabled) http_addr = "::"; else http_addr = "0.0.0.0"; ret = evhttp_bind_socket(evhttpd, http_addr, http_port); if (ret < 0) { DPRINTF(E_FATAL, L_MPD, "Could not bind HTTP artwork server at %s:%d\n", http_addr, http_port); goto bind_fail; } } DPRINTF(E_INFO, L_MPD, "mpd thread init\n"); ret = pthread_create(&tid_mpd, NULL, mpd, NULL); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Could not spawn cache thread: %s\n", strerror(errno)); goto thread_fail; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_mpd, "mpd"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_mpd, "mpd"); #endif idle_clients = NULL; listener_add(mpd_listener_cb, LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS); return 0; thread_fail: bind_fail: if (http_port > 0) evhttp_free(evhttpd); evhttp_fail: evconnlistener_free(listener); connew_fail: commands_base_free(cmdbase); event_base_free(evbase_mpd); evbase_mpd = NULL; evbase_fail: return -1; } /* Thread: main */ void mpd_deinit(void) { struct idle_client *temp; unsigned short port; unsigned short http_port; int ret; port = cfg_getint(cfg_getsec(cfg, "mpd"), "port"); if (port <= 0) { DPRINTF(E_INFO, L_MPD, "MPD not enabled\n"); return; } commands_base_destroy(cmdbase); ret = pthread_join(tid_mpd, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_MPD, "Could not join cache thread: %s\n", strerror(errno)); return; } listener_remove(mpd_listener_cb); while (idle_clients) { temp = idle_clients; idle_clients = idle_clients->next; free(temp); } http_port = cfg_getint(cfg_getsec(cfg, "mpd"), "http_port"); if (http_port > 0) evhttp_free(evhttpd); evconnlistener_free(listener); // Free event base (should free events too) event_base_free(evbase_mpd); } forked-daapd-25.0/src/mpd.h000066400000000000000000000001541313447753700155170ustar00rootroot00000000000000 #ifndef __MPD_H__ #define __MPD_H__ int mpd_init(void); void mpd_deinit(void); #endif /* !__MPD_H__ */ forked-daapd-25.0/src/mxml-compat.h000066400000000000000000000027571313447753700172100ustar00rootroot00000000000000#ifndef __MXML_COMPAT_H__ #define __MXML_COMPAT_H__ /* For compability with mxml 2.6 */ #ifndef HAVE_MXMLGETTEXT __attribute__((unused)) static const char * /* O - Text string or NULL */ mxmlGetText(mxml_node_t *node, /* I - Node to get */ int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */ { if (node->type == MXML_TEXT) return (node->value.text.string); else if (node->type == MXML_ELEMENT && node->child && node->child->type == MXML_TEXT) return (node->child->value.text.string); else return (NULL); } #endif #ifndef HAVE_MXMLGETOPAQUE __attribute__((unused)) static const char * /* O - Opaque string or NULL */ mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ { if (!node) return (NULL); if (node->type == MXML_OPAQUE) return (node->value.opaque); else if (node->type == MXML_ELEMENT && node->child && node->child->type == MXML_OPAQUE) return (node->child->value.opaque); else return (NULL); } #endif #ifndef HAVE_MXMLGETFIRSTCHILD __attribute__((unused)) static mxml_node_t * /* O - First child or NULL */ mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */ { if (!node || node->type != MXML_ELEMENT) return (NULL); return (node->child); } #endif #ifndef HAVE_MXMLGETTYPE __attribute__((unused)) static mxml_type_t /* O - Type of node */ mxmlGetType(mxml_node_t *node) /* I - Node to get */ { return (node->type); } #endif #endif /* !__MXML_COMPAT_H__ */ forked-daapd-25.0/src/outputs.c000066400000000000000000000161411313447753700164600ustar00rootroot00000000000000/* * Copyright (C) 2016 Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include "logger.h" #include "outputs.h" extern struct output_definition output_raop; extern struct output_definition output_streaming; extern struct output_definition output_dummy; extern struct output_definition output_fifo; #ifdef HAVE_ALSA extern struct output_definition output_alsa; #endif #ifdef HAVE_LIBPULSE extern struct output_definition output_pulse; #endif #ifdef CHROMECAST extern struct output_definition output_cast; #endif // Must be in sync with enum output_types static struct output_definition *outputs[] = { &output_raop, &output_streaming, &output_dummy, &output_fifo, #ifdef HAVE_ALSA &output_alsa, #endif #ifdef HAVE_LIBPULSE &output_pulse, #endif #ifdef CHROMECAST &output_cast, #endif NULL }; int outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) { if (outputs[device->type]->disabled) return -1; if (outputs[device->type]->device_start) return outputs[device->type]->device_start(device, cb, rtptime); else return -1; } void outputs_device_stop(struct output_session *session) { if (outputs[session->type]->disabled) return; if (outputs[session->type]->device_stop) outputs[session->type]->device_stop(session); } int outputs_device_probe(struct output_device *device, output_status_cb cb) { if (outputs[device->type]->disabled) return -1; if (outputs[device->type]->device_probe) return outputs[device->type]->device_probe(device, cb); else return -1; } void outputs_device_free(struct output_device *device) { if (!device) return; if (outputs[device->type]->disabled) DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device from a disabled output?\n"); if (device->session) DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device with active session?\n"); if (outputs[device->type]->device_free_extra) outputs[device->type]->device_free_extra(device); free(device->name); free(device->auth_key); free(device->v4_address); free(device->v6_address); free(device); } int outputs_device_volume_set(struct output_device *device, output_status_cb cb) { if (outputs[device->type]->disabled) return -1; if (outputs[device->type]->device_volume_set) return outputs[device->type]->device_volume_set(device, cb); else return -1; } void outputs_playback_start(uint64_t next_pkt, struct timespec *ts) { int i; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->playback_start) outputs[i]->playback_start(next_pkt, ts); } } void outputs_playback_stop(void) { int i; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->playback_stop) outputs[i]->playback_stop(); } } void outputs_write(uint8_t *buf, uint64_t rtptime) { int i; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->write) outputs[i]->write(buf, rtptime); } } int outputs_flush(output_status_cb cb, uint64_t rtptime) { int ret; int i; ret = 0; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->flush) ret += outputs[i]->flush(cb, rtptime); } return ret; } void outputs_status_cb(struct output_session *session, output_status_cb cb) { if (outputs[session->type]->disabled) return; if (outputs[session->type]->status_cb) outputs[session->type]->status_cb(session, cb); } struct output_metadata * outputs_metadata_prepare(int id) { struct output_metadata *omd; struct output_metadata *new; void *metadata; int i; omd = NULL; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (!outputs[i]->metadata_prepare) continue; metadata = outputs[i]->metadata_prepare(id); if (!metadata) continue; new = calloc(1, sizeof(struct output_metadata)); if (!new) return omd; if (omd) new->next = omd; omd = new; omd->type = i; omd->metadata = metadata; } return omd; } void outputs_metadata_send(struct output_metadata *omd, uint64_t rtptime, uint64_t offset, int startup) { struct output_metadata *ptr; int i; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (!outputs[i]->metadata_send) continue; // Go through linked list to find appropriate metadata for type for (ptr = omd; ptr; ptr = ptr->next) if (ptr->type == i) break; if (!ptr) continue; outputs[i]->metadata_send(ptr->metadata, rtptime, offset, startup); } } void outputs_metadata_purge(void) { int i; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->metadata_purge) outputs[i]->metadata_purge(); } } void outputs_metadata_prune(uint64_t rtptime) { int i; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->metadata_prune) outputs[i]->metadata_prune(rtptime); } } void outputs_metadata_free(struct output_metadata *omd) { struct output_metadata *ptr; if (!omd) return; for (ptr = omd; omd; ptr = omd) { omd = ptr->next; free(ptr); } } void outputs_authorize(enum output_types type, const char *pin) { if (outputs[type]->disabled) return; if (outputs[type]->authorize) outputs[type]->authorize(pin); } int outputs_priority(struct output_device *device) { return outputs[device->type]->priority; } const char * outputs_name(enum output_types type) { return outputs[type]->name; } int outputs_init(void) { int no_output; int ret; int i; no_output = 1; for (i = 0; outputs[i]; i++) { if (outputs[i]->type != i) { DPRINTF(E_FATAL, L_PLAYER, "BUG! Output definitions are misaligned with output enum\n"); return -1; } if (!outputs[i]->init) { no_output = 0; continue; } ret = outputs[i]->init(); if (ret < 0) outputs[i]->disabled = 1; else no_output = 0; } if (no_output) return -1; return 0; } void outputs_deinit(void) { int i; for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->deinit) outputs[i]->deinit(); } } forked-daapd-25.0/src/outputs.h000066400000000000000000000171301313447753700164640ustar00rootroot00000000000000 #ifndef __OUTPUTS_H__ #define __OUTPUTS_H__ #include /* Outputs is a generic interface between the player and a media output method, * like for instance AirPlay (raop) or ALSA. The purpose of the interface is to * make it easier to add new outputs without messing too much with the player or * existing output methods. * * An output method will have a general type, and it will be able to detect * supported devices that are available for output. A device will be typically * be something like an AirPlay speaker. * * When a device is started the output backend will typically create a session. * This session is only passed around as an opaque object in this interface. * * Here is the sequence of commands from the player to the outputs, and the * callback from the output once the command has been executed. Commands marked * with * may make multiple callbacks if multiple sessions are affected. * (TODO should callbacks always be deferred?) * * PLAYER OUTPUT PLAYER CB * speaker_activate -> device_start -> device_activate_cb * -> (if playback) -> playback_start -> device_streaming_cb* (or no cb) * -> (else if playback not active) -> device_streaming_cb * -> (fail) -> device_stop -> device_lost_cb * speaker_activate -> device_probe -> device_probe_cb * speaker_deactivate -> device_stop -> device_shutdown_cb * volume_set -> device_volume_set -> device_command_cb * -> -> device_streaming_cb * (volume_setrel/abs_speaker is the same) * playback_start_item -> device_start -> device_restart_cb * -> (success) -> device_streaming_cb * -> (fail) -> device_stop -> device_lost_cb * playback_start_bh -> playback_start -> device_streaming_cb* (or no cb) * playback_stop -> flush -> device_command_cb* * -> -> device_streaming_cb* * -> -> playback_stop -> device_streaming_cb* * playback_pause -> flush -> device_command_cb* * -> -> device_streaming_cb* * -> -> playback_stop -> device_streaming_cb* * playback_abort -> playback_stop -> device_streaming_cb* (or no cb) * device_streaming_cb -> device_streaming_cb (re-add) * */ // Must be in sync with outputs[] in outputs.c enum output_types { OUTPUT_TYPE_RAOP, OUTPUT_TYPE_STREAMING, OUTPUT_TYPE_DUMMY, OUTPUT_TYPE_FIFO, #ifdef HAVE_ALSA OUTPUT_TYPE_ALSA, #endif #ifdef HAVE_LIBPULSE OUTPUT_TYPE_PULSE, #endif #ifdef CHROMECAST OUTPUT_TYPE_CAST, #endif }; /* Output session state */ enum output_device_state { // Device is stopped (no session) OUTPUT_STATE_STOPPED = 0, // Device is starting up OUTPUT_STATE_STARTUP = 1, // Session established (streaming ready and commands are possible) OUTPUT_STATE_CONNECTED = 2, // Media data is being sent OUTPUT_STATE_STREAMING = 3, // Session is failed, couldn't startup or error occurred OUTPUT_STATE_FAILED = -1, // Password issue: unknown password or bad password OUTPUT_STATE_PASSWORD = -2, }; /* Linked list of device info used by the player for each device */ struct output_device { // Device id uint64_t id; // Name of the device, e.g. "Living Room" char *name; // Type of the device, will be used to determine which output backend to call enum output_types type; // Type of output (string) const char *type_name; // Misc device flags unsigned selected:1; unsigned advertised:1; unsigned has_password:1; unsigned has_video:1; unsigned requires_auth:1; // Credentials if relevant const char *password; char *auth_key; // Device volume int volume; int relvol; // Address char *v4_address; char *v6_address; short v4_port; short v6_port; // Opaque pointers to device and session data void *extra_device_info; struct output_session *session; struct output_device *next; }; // Except for the type, sessions are opaque outside of the output backend struct output_session { enum output_types type; void *session; }; // Linked list of metadata prepared by each output backend struct output_metadata { enum output_types type; void *metadata; struct output_metadata *next; }; typedef void (*output_status_cb)(struct output_device *device, struct output_session *session, enum output_device_state status); struct output_definition { // Name of the output const char *name; // Type of output enum output_types type; // Priority to give this output when autoselecting an output, 1 is highest // 1 = highest priority, 0 = don't autoselect int priority; // Set to 1 if the output initialization failed int disabled; // Initialization function called during startup // Output must call device_cb when an output device becomes available/unavailable int (*init)(void); // Deinitialization function called at shutdown void (*deinit)(void); // Prepare a playback session on device and call back int (*device_start)(struct output_device *device, output_status_cb cb, uint64_t rtptime); // Close a session prepared by device_start void (*device_stop)(struct output_session *session); // Test the connection to a device and call back int (*device_probe)(struct output_device *device, output_status_cb cb); // Free the private device data void (*device_free_extra)(struct output_device *device); // Set the volume and call back int (*device_volume_set)(struct output_device *device, output_status_cb cb); // Start/stop playback on devices that were started void (*playback_start)(uint64_t next_pkt, struct timespec *ts); void (*playback_stop)(void); // Write stream data to the output devices void (*write)(uint8_t *buf, uint64_t rtptime); // Flush all sessions, the return must be number of sessions pending the flush int (*flush)(output_status_cb cb, uint64_t rtptime); // Authorize an output with a pin-code (probably coming from the filescanner) void (*authorize)(const char *pin); // Change the call back associated with a session void (*status_cb)(struct output_session *session, output_status_cb cb); // Metadata void *(*metadata_prepare)(int id); void (*metadata_send)(void *metadata, uint64_t rtptime, uint64_t offset, int startup); void (*metadata_purge)(void); void (*metadata_prune)(uint64_t rtptime); }; int outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime); void outputs_device_stop(struct output_session *session); int outputs_device_probe(struct output_device *device, output_status_cb cb); void outputs_device_free(struct output_device *device); int outputs_device_volume_set(struct output_device *device, output_status_cb cb); void outputs_playback_start(uint64_t next_pkt, struct timespec *ts); void outputs_playback_stop(void); void outputs_write(uint8_t *buf, uint64_t rtptime); int outputs_flush(output_status_cb cb, uint64_t rtptime); void outputs_status_cb(struct output_session *session, output_status_cb cb); struct output_metadata * outputs_metadata_prepare(int id); void outputs_metadata_send(struct output_metadata *omd, uint64_t rtptime, uint64_t offset, int startup); void outputs_metadata_purge(void); void outputs_metadata_prune(uint64_t rtptime); void outputs_metadata_free(struct output_metadata *omd); void outputs_authorize(enum output_types type, const char *pin); int outputs_priority(struct output_device *device); const char * outputs_name(enum output_types type); int outputs_init(void); void outputs_deinit(void); #endif /* !__OUTPUTS_H__ */ forked-daapd-25.0/src/outputs/000077500000000000000000000000001313447753700163115ustar00rootroot00000000000000forked-daapd-25.0/src/outputs/alsa.c000066400000000000000000000577711313447753700174160ustar00rootroot00000000000000/* * Copyright (C) 2015-2016 Espen Jürgensen * Copyright (C) 2010 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include "misc.h" #include "conffile.h" #include "logger.h" #include "player.h" #include "outputs.h" #define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES) // The maximum number of samples that the output is allowed to get behind (or // ahead) of the player position, before compensation is attempted #define ALSA_MAX_LATENCY 352 // If latency is jumping up and down we don't do compensation since we probably // wouldn't do a good job. This sets the maximum the latency is allowed to vary // within the 10 seconds where we measure latency each second. #define ALSA_MAX_LATENCY_VARIANCE 352 // TODO Unglobalise these and add support for multiple sound cards static char *card_name; static char *mixer_name; static char *mixer_device_name; static snd_pcm_t *hdl; static snd_mixer_t *mixer_hdl; static snd_mixer_elem_t *vol_elem; static long vol_min; static long vol_max; static int offset; #define ALSA_F_STARTED (1 << 15) enum alsa_state { ALSA_STATE_FAILED = 0, ALSA_STATE_STOPPED = 1, ALSA_STATE_STARTED = ALSA_F_STARTED, ALSA_STATE_STREAMING = ALSA_F_STARTED | 0x01, }; enum alsa_sync_state { ALSA_SYNC_OK, ALSA_SYNC_AHEAD, ALSA_SYNC_BEHIND, }; struct alsa_session { enum alsa_state state; char *devname; uint64_t pos; uint64_t start_pos; int32_t last_latency; int sync_counter; // An array that will hold the packets we prebuffer. The length of the array // is prebuf_len (measured in rtp_packets) uint8_t *prebuf; uint32_t prebuf_len; uint32_t prebuf_head; uint32_t prebuf_tail; int volume; struct event *deferredev; output_status_cb defer_cb; /* Do not dereference - only passed to the status cb */ struct output_device *device; struct output_session *output_session; output_status_cb status_cb; struct alsa_session *next; }; /* From player.c */ extern struct event_base *evbase_player; static struct alsa_session *sessions; /* Forwards */ static void defer_cb(int fd, short what, void *arg); /* ---------------------------- SESSION HANDLING ---------------------------- */ static void prebuf_free(struct alsa_session *as) { if (as->prebuf) free(as->prebuf); as->prebuf = NULL; as->prebuf_len = 0; as->prebuf_head = 0; as->prebuf_tail = 0; } static void alsa_session_free(struct alsa_session *as) { event_free(as->deferredev); prebuf_free(as); free(as->output_session); free(as); as = NULL; } static void alsa_session_cleanup(struct alsa_session *as) { struct alsa_session *s; if (as == sessions) sessions = sessions->next; else { for (s = sessions; s && (s->next != as); s = s->next) ; /* EMPTY */ if (!s) DPRINTF(E_WARN, L_LAUDIO, "WARNING: struct alsa_session not found in list; BUG!\n"); else s->next = as->next; } alsa_session_free(as); } static struct alsa_session * alsa_session_make(struct output_device *device, output_status_cb cb) { struct output_session *os; struct alsa_session *as; os = calloc(1, sizeof(struct output_session)); if (!os) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session (os)\n"); return NULL; } as = calloc(1, sizeof(struct alsa_session)); if (!as) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session (as)\n"); free(os); return NULL; } as->deferredev = evtimer_new(evbase_player, defer_cb, as); if (!as->deferredev) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA deferred event\n"); free(os); free(as); return NULL; } os->session = as; os->type = device->type; as->output_session = os; as->state = ALSA_STATE_STOPPED; as->device = device; as->status_cb = cb; as->volume = device->volume; as->devname = card_name; as->next = sessions; sessions = as; return as; } /* ---------------------------- STATUS HANDLERS ----------------------------- */ // Maps our internal state to the generic output state and then makes a callback // to the player to tell that state static void defer_cb(int fd, short what, void *arg) { struct alsa_session *as = arg; enum output_device_state state; switch (as->state) { case ALSA_STATE_FAILED: state = OUTPUT_STATE_FAILED; break; case ALSA_STATE_STOPPED: state = OUTPUT_STATE_STOPPED; break; case ALSA_STATE_STARTED: state = OUTPUT_STATE_CONNECTED; break; case ALSA_STATE_STREAMING: state = OUTPUT_STATE_STREAMING; break; default: DPRINTF(E_LOG, L_LAUDIO, "Bug! Unhandled state in alsa_status()\n"); state = OUTPUT_STATE_FAILED; } if (as->defer_cb) as->defer_cb(as->device, as->output_session, state); if (!(as->state & ALSA_F_STARTED)) alsa_session_cleanup(as); } // Note: alsa_states also nukes the session if it is not ALSA_F_STARTED static void alsa_status(struct alsa_session *as) { as->defer_cb = as->status_cb; event_active(as->deferredev, 0, 0); as->status_cb = NULL; } /* ------------------------------- MISC HELPERS ----------------------------- */ /*static int start_threshold_set(snd_pcm_uframes_t threshold) { snd_pcm_sw_params_t *sw_params; int ret; ret = snd_pcm_sw_params_malloc(&sw_params); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not allocate sw params: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_sw_params_current(hdl, sw_params); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve current sw params: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_sw_params_set_start_threshold(hdl, sw_params, threshold); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not set start threshold: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_sw_params(hdl, sw_params); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not set sw params: %s\n", snd_strerror(ret)); goto out_fail; } return 0; out_fail: snd_pcm_sw_params_free(sw_params); return -1; } */ static int mixer_open(void) { snd_mixer_elem_t *elem; snd_mixer_elem_t *master; snd_mixer_elem_t *pcm; snd_mixer_elem_t *custom; snd_mixer_selem_id_t *sid; int ret; ret = snd_mixer_open(&mixer_hdl, 0); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret)); mixer_hdl = NULL; return -1; } ret = snd_mixer_attach(mixer_hdl, mixer_device_name); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret)); goto out_close; } ret = snd_mixer_selem_register(mixer_hdl, NULL, NULL); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret)); goto out_detach; } ret = snd_mixer_load(mixer_hdl); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret)); goto out_detach; } // Grab interesting elements snd_mixer_selem_id_alloca(&sid); pcm = NULL; master = NULL; custom = NULL; for (elem = snd_mixer_first_elem(mixer_hdl); elem; elem = snd_mixer_elem_next(elem)) { snd_mixer_selem_get_id(elem, sid); if (mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), mixer_name) == 0)) { custom = elem; break; } else if (strcmp(snd_mixer_selem_id_get_name(sid), "PCM") == 0) pcm = elem; else if (strcmp(snd_mixer_selem_id_get_name(sid), "Master") == 0) master = elem; } if (mixer_name) { if (custom) vol_elem = custom; else { DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", mixer_name); goto out_detach; } } else if (pcm) vol_elem = pcm; else if (master) vol_elem = master; else { DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n"); goto out_detach; } // Get min & max volume snd_mixer_selem_get_playback_volume_range(vol_elem, &vol_min, &vol_max); return 0; out_detach: snd_mixer_detach(mixer_hdl, card_name); out_close: snd_mixer_close(mixer_hdl); mixer_hdl = NULL; vol_elem = NULL; return -1; } static int device_open(struct alsa_session *as) { snd_pcm_hw_params_t *hw_params; snd_pcm_uframes_t bufsize; int ret; hw_params = NULL; ret = snd_pcm_open(&hdl, card_name, SND_PCM_STREAM_PLAYBACK, 0); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device: %s\n", snd_strerror(ret)); return -1; } // HW params ret = snd_pcm_hw_params_malloc(&hw_params); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not allocate hw params: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params_any(hdl, hw_params); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params_set_access(hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params_set_format(hdl, hw_params, SND_PCM_FORMAT_S16_LE); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not set S16LE format: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params_set_channels(hdl, hw_params, 2); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not set stereo output: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params_set_rate(hdl, hw_params, 44100, 0); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support 44.1 kHz: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params_set_buffer_size_max(hdl, hw_params, &bufsize); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not set buffer size to max: %s\n", snd_strerror(ret)); goto out_fail; } ret = snd_pcm_hw_params(hdl, hw_params); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret)); goto out_fail; } snd_pcm_hw_params_free(hw_params); hw_params = NULL; ret = mixer_open(); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer\n"); goto out_fail; } return 0; out_fail: if (hw_params) snd_pcm_hw_params_free(hw_params); snd_pcm_close(hdl); hdl = NULL; return -1; } static void device_close(void) { snd_pcm_close(hdl); hdl = NULL; if (mixer_hdl) { snd_mixer_detach(mixer_hdl, card_name); snd_mixer_close(mixer_hdl); mixer_hdl = NULL; vol_elem = NULL; } } static void playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos) { snd_output_t *output; snd_pcm_state_t state; char *debug_pcm_cfg; int ret; state = snd_pcm_state(hdl); if (state != SND_PCM_STATE_PREPARED) { if (state == SND_PCM_STATE_RUNNING) snd_pcm_drop(hdl); ret = snd_pcm_prepare(hdl); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s' (state %d): %s\n", as->devname, state, snd_strerror(ret)); return; } } // Clear prebuffer in case start somehow got called twice without a stop in between prebuf_free(as); // Adjust the starting position with the configured value start_pos -= offset; // The difference between pos and start_pos should match the 2 second // buffer that AirPlay uses. We will not use alsa's buffer for the initial // buffering, because my sound card's start_threshold is not to be counted on. // Instead we allocate our own buffer, and when it is time to play we write as // much as we can to alsa's buffer. as->prebuf_len = (start_pos - pos) / AIRTUNES_V2_PACKET_SAMPLES + 1; if (as->prebuf_len > (3 * 44100 - offset) / AIRTUNES_V2_PACKET_SAMPLES) { DPRINTF(E_LOG, L_LAUDIO, "Sanity check of prebuf_len (%" PRIu32 " packets) failed\n", as->prebuf_len); return; } DPRINTF(E_DBG, L_LAUDIO, "Will prebuffer %d packets\n", as->prebuf_len); as->prebuf = malloc(as->prebuf_len * PACKET_SIZE); if (!as->prebuf) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for audio buffer (requested %" PRIu32 " packets)\n", as->prebuf_len); return; } as->pos = pos; as->start_pos = start_pos - AIRTUNES_V2_PACKET_SAMPLES; // Dump PCM config data for E_DBG logging ret = snd_output_buffer_open(&output); if (ret == 0) { if (snd_pcm_dump_setup(hdl, output) == 0) { snd_output_buffer_string(output, &debug_pcm_cfg); DPRINTF(E_DBG, L_LAUDIO, "Dump of sound device config:\n%s\n", debug_pcm_cfg); } snd_output_close(output); } as->state = ALSA_STATE_STREAMING; } // This function writes the sample buf into either the prebuffer or directly to // ALSA, depending on how much room there is in ALSA, and whether we are // prebuffering or not. It also transfers from the the prebuffer to ALSA, if // needed. Returns 0 on success, negative on error. static int buffer_write(struct alsa_session *as, uint8_t *buf, snd_pcm_sframes_t *avail, int prebuffering, int prebuf_empty) { uint8_t *pkt; int npackets; snd_pcm_sframes_t nsamp; snd_pcm_sframes_t ret; nsamp = AIRTUNES_V2_PACKET_SAMPLES; if (prebuffering || !prebuf_empty || *avail < AIRTUNES_V2_PACKET_SAMPLES) { pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE]; memcpy(pkt, buf, PACKET_SIZE); as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; if (prebuffering || *avail < AIRTUNES_V2_PACKET_SAMPLES) return 0; // No actual writing // We will now set buf so that we will transfer as much as possible to ALSA buf = &as->prebuf[as->prebuf_tail * PACKET_SIZE]; if (as->prebuf_head > as->prebuf_tail) npackets = as->prebuf_head - as->prebuf_tail; else npackets = as->prebuf_len - as->prebuf_tail; nsamp = npackets * AIRTUNES_V2_PACKET_SAMPLES; while (nsamp > *avail) { npackets -= 1; nsamp -= AIRTUNES_V2_PACKET_SAMPLES; } as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len; } ret = snd_pcm_writei(hdl, buf, nsamp); if (ret < 0) return ret; if (ret != nsamp) DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); *avail -= ret; return 0; } // Checks if ALSA's playback position is ahead or behind the player's enum alsa_sync_state sync_check(struct alsa_session *as, uint64_t rtptime, snd_pcm_sframes_t delay, int prebuf_empty) { enum alsa_sync_state sync; struct timespec now; uint64_t cur_pos; uint64_t pb_pos; int32_t latency; int npackets; sync = ALSA_SYNC_OK; if (player_get_current_pos(&cur_pos, &now, 0) != 0) return sync; if (!prebuf_empty) npackets = (as->prebuf_head - (as->prebuf_tail + 1) + as->prebuf_len) % as->prebuf_len + 1; else npackets = 0; pb_pos = rtptime - delay - AIRTUNES_V2_PACKET_SAMPLES * npackets; latency = cur_pos - (pb_pos - offset); // If the latency is low or very different from our last measurement, we reset the sync_counter if (abs(latency) < ALSA_MAX_LATENCY || abs(as->last_latency - latency) > ALSA_MAX_LATENCY_VARIANCE) { as->sync_counter = 0; sync = ALSA_SYNC_OK; } // If we have measured a consistent latency for 10 seconds, then we take action else if (as->sync_counter >= 10 * 126) { DPRINTF(E_INFO, L_LAUDIO, "Taking action to compensate for ALSA latency of %d samples\n", latency); as->sync_counter = 0; if (latency > 0) sync = ALSA_SYNC_BEHIND; else sync = ALSA_SYNC_AHEAD; } as->last_latency = latency; if (latency) DPRINTF(E_SPAM, L_LAUDIO, "Sync %d cur_pos %" PRIu64 ", pb_pos %" PRIu64 " (diff %d, delay %li), pos %" PRIu64 "\n", sync, cur_pos, pb_pos, latency, delay, as->pos); return sync; } static void playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) { snd_pcm_sframes_t ret; snd_pcm_sframes_t avail; snd_pcm_sframes_t delay; enum alsa_sync_state sync; int prebuffering; int prebuf_empty; prebuffering = (as->pos < as->start_pos); prebuf_empty = (as->prebuf_head == as->prebuf_tail); as->pos += AIRTUNES_V2_PACKET_SAMPLES; if (prebuffering) { buffer_write(as, buf, NULL, prebuffering, prebuf_empty); return; } ret = snd_pcm_avail_delay(hdl, &avail, &delay); if (ret < 0) goto alsa_error; // Every second we do a sync check sync = ALSA_SYNC_OK; as->sync_counter++; if (as->sync_counter % 126 == 0) sync = sync_check(as, rtptime, delay, prebuf_empty); // Skip write -> reduce the delay if (sync == ALSA_SYNC_BEHIND) return; ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); // Double write -> increase the delay if (sync == ALSA_SYNC_AHEAD && (ret == 0)) ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); if (ret < 0) goto alsa_error; return; alsa_error: if (ret == -EPIPE) { DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n"); ret = snd_pcm_prepare(hdl); if (ret < 0) { DPRINTF(E_WARN, L_LAUDIO, "ALSA couldn't recover from underrun: %s\n", snd_strerror(ret)); return; } // Fill the prebuf with audio before restarting, so we don't underrun again as->start_pos = as->pos + AIRTUNES_V2_PACKET_SAMPLES * (as->prebuf_len - 1); return; } DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret)); as->state = ALSA_STATE_FAILED; alsa_status(as); } static void playback_pos_get(uint64_t *pos, uint64_t next_pkt) { uint64_t cur_pos; struct timespec now; int ret; ret = player_get_current_pos(&cur_pos, &now, 0); if (ret < 0) { DPRINTF(E_LOG, L_LAUDIO, "Could not get playback position, setting to next_pkt - 2 seconds\n"); cur_pos = next_pkt - 88200; } // Make pos the rtptime of the packet containing cur_pos *pos = next_pkt; while (*pos > cur_pos) *pos -= AIRTUNES_V2_PACKET_SAMPLES; } /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int alsa_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) { struct alsa_session *as; int ret; as = alsa_session_make(device, cb); if (!as) return -1; ret = device_open(as); if (ret < 0) { alsa_session_cleanup(as); return -1; } as->state = ALSA_STATE_STARTED; alsa_status(as); return 0; } static void alsa_device_stop(struct output_session *session) { struct alsa_session *as = session->session; device_close(); as->state = ALSA_STATE_STOPPED; alsa_status(as); } static int alsa_device_probe(struct output_device *device, output_status_cb cb) { struct alsa_session *as; int ret; as = alsa_session_make(device, cb); if (!as) return -1; ret = device_open(as); if (ret < 0) { alsa_session_cleanup(as); return -1; } device_close(); as->state = ALSA_STATE_STOPPED; alsa_status(as); return 0; } static int alsa_device_volume_set(struct output_device *device, output_status_cb cb) { struct alsa_session *as; int pcm_vol; if (!device->session || !device->session->session) return 0; as = device->session->session; if (!mixer_hdl || !vol_elem) return 0; snd_mixer_handle_events(mixer_hdl); if (!snd_mixer_selem_is_active(vol_elem)) return 0; switch (device->volume) { case 0: pcm_vol = vol_min; break; case 100: pcm_vol = vol_max; break; default: pcm_vol = vol_min + (device->volume * (vol_max - vol_min)) / 100; break; } DPRINTF(E_DBG, L_LAUDIO, "Setting ALSA volume to %d (%d)\n", pcm_vol, device->volume); snd_mixer_selem_set_playback_volume_all(vol_elem, pcm_vol); as->status_cb = cb; alsa_status(as); return 1; } static void alsa_playback_start(uint64_t next_pkt, struct timespec *ts) { struct alsa_session *as; uint64_t pos; if (!sessions) return; playback_pos_get(&pos, next_pkt); DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA audio (pos %" PRIu64 ", next_pkt %" PRIu64 ")\n", pos, next_pkt); for (as = sessions; as; as = as->next) playback_start(as, pos, next_pkt); } static void alsa_playback_stop(void) { struct alsa_session *as; for (as = sessions; as; as = as->next) { snd_pcm_drop(hdl); prebuf_free(as); as->state = ALSA_STATE_STARTED; alsa_status(as); } } static void alsa_write(uint8_t *buf, uint64_t rtptime) { struct alsa_session *as; uint64_t pos; for (as = sessions; as; as = as->next) { if (as->state == ALSA_STATE_STARTED) { playback_pos_get(&pos, rtptime); DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA device '%s' (pos %" PRIu64 ", rtptime %" PRIu64 ")\n", as->devname, pos, rtptime); playback_start(as, pos, rtptime); } playback_write(as, buf, rtptime); } } static int alsa_flush(output_status_cb cb, uint64_t rtptime) { struct alsa_session *as; int i; i = 0; for (as = sessions; as; as = as->next) { i++; snd_pcm_drop(hdl); prebuf_free(as); as->status_cb = cb; as->state = ALSA_STATE_STARTED; alsa_status(as); } return i; } static void alsa_set_status_cb(struct output_session *session, output_status_cb cb) { struct alsa_session *as = session->session; as->status_cb = cb; } static int alsa_init(void) { struct output_device *device; cfg_t *cfg_audio; char *nickname; char *type; cfg_audio = cfg_getsec(cfg, "audio"); type = cfg_getstr(cfg_audio, "type"); if (type && (strcasecmp(type, "alsa") != 0)) return -1; card_name = cfg_getstr(cfg_audio, "card"); mixer_name = cfg_getstr(cfg_audio, "mixer"); mixer_device_name = cfg_getstr(cfg_audio, "mixer_device"); if (mixer_device_name == NULL || strlen(mixer_device_name) == 0) mixer_device_name = card_name; nickname = cfg_getstr(cfg_audio, "nickname"); offset = cfg_getint(cfg_audio, "offset"); if (abs(offset) > 44100) { DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset (%d) set in the configuration is out of bounds\n", offset); offset = 44100 * (offset/abs(offset)); } device = calloc(1, sizeof(struct output_device)); if (!device) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA device\n"); return -1; } device->id = 0; device->name = strdup(nickname); device->type = OUTPUT_TYPE_ALSA; device->type_name = outputs_name(device->type); device->advertised = 1; device->has_video = 0; DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", card_name, nickname); player_device_add(device); snd_lib_error_set_handler(logger_alsa); hdl = NULL; mixer_hdl = NULL; vol_elem = NULL; return 0; } static void alsa_deinit(void) { snd_lib_error_set_handler(NULL); } struct output_definition output_alsa = { .name = "ALSA", .type = OUTPUT_TYPE_ALSA, .priority = 3, .disabled = 0, .init = alsa_init, .deinit = alsa_deinit, .device_start = alsa_device_start, .device_stop = alsa_device_stop, .device_probe = alsa_device_probe, .device_volume_set = alsa_device_volume_set, .playback_start = alsa_playback_start, .playback_stop = alsa_playback_stop, .write = alsa_write, .flush = alsa_flush, .status_cb = alsa_set_status_cb, }; forked-daapd-25.0/src/outputs/cast.c000066400000000000000000001347421313447753700174220ustar00rootroot00000000000000/* * Copyright (C) 2015-2016 Espen Jürgensen * * Credit goes to the authors of pychromecast and those before that who have * discovered how to do this. * * 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 */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_ENDIAN_H # include #elif defined(HAVE_SYS_ENDIAN_H) # include #elif defined(HAVE_LIBKERN_OSBYTEORDER_H) #include #define htobe32(x) OSSwapHostToBigInt32(x) #define be32toh(x) OSSwapBigToHostInt32(x) #endif #include #include #include #include "conffile.h" #include "mdns.h" #include "logger.h" #include "player.h" #include "outputs.h" #ifdef HAVE_PROTOBUF_OLD #include "cast_channel.v0.pb-c.h" #else #include "cast_channel.pb-c.h" #endif // Number of bytes to request from TLS connection #define MAX_BUF 4096 // CA file location (not very portable...?) #define CAFILE "/etc/ssl/certs/ca-certificates.crt" // Seconds without a heartbeat from the Chromecast before we close the session #define HEARTBEAT_TIMEOUT 8 // Seconds after a flush (pause) before we close the session #define FLUSH_TIMEOUT 30 // Seconds to wait for a reply before making the callback requested by caller #define REPLY_TIMEOUT 5 // ID of the default receiver app #define CAST_APP_ID "CC1AD845" // Namespaces #define NS_CONNECTION "urn:x-cast:com.google.cast.tp.connection" #define NS_RECEIVER "urn:x-cast:com.google.cast.receiver" #define NS_HEARTBEAT "urn:x-cast:com.google.cast.tp.heartbeat" #define NS_MEDIA "urn:x-cast:com.google.cast.media" #define USE_TRANSPORT_ID (1 << 1) #define USE_REQUEST_ID (1 << 2) #define USE_REQUEST_ID_ONLY (1 << 3) #define CALLBACK_REGISTER_SIZE 32 //#define DEBUG_CONNECTION 1 union sockaddr_all { struct sockaddr_in sin; struct sockaddr_in6 sin6; struct sockaddr sa; struct sockaddr_storage ss; }; struct cast_session; struct cast_msg_payload; typedef void (*cast_reply_cb)(struct cast_session *cs, struct cast_msg_payload *payload); // Session is starting up #define CAST_STATE_F_STARTUP (1 << 13) // The default receiver app is ready #define CAST_STATE_F_MEDIA_CONNECTED (1 << 14) // Media is loaded in the receiver app #define CAST_STATE_F_MEDIA_LOADED (1 << 15) // Media is playing in the receiver app #define CAST_STATE_F_MEDIA_PLAYING (1 << 16) // Beware, the order of this enum has meaning enum cast_state { // Something bad happened during a session CAST_STATE_FAILED = 0, // No session allocated CAST_STATE_NONE = 1, // Session allocated, but no connection CAST_STATE_DISCONNECTED = CAST_STATE_F_STARTUP | 0x01, // TCP connect, TLS handshake, CONNECT and GET_STATUS request CAST_STATE_CONNECTED = CAST_STATE_F_STARTUP | 0x02, // Default media receiver app is launched CAST_STATE_MEDIA_LAUNCHED = CAST_STATE_F_STARTUP | 0x03, // CONNECT and GET_STATUS made to receiver app CAST_STATE_MEDIA_CONNECTED = CAST_STATE_F_MEDIA_CONNECTED, // Receiver app has loaded our media CAST_STATE_MEDIA_LOADED = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED, // After PAUSE CAST_STATE_MEDIA_PAUSED = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | 0x01, // After LOAD CAST_STATE_MEDIA_BUFFERING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_PLAYING, // After PLAY CAST_STATE_MEDIA_PLAYING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_PLAYING | 0x01, }; struct cast_session { // Current state enum cast_state state; // Used to register a target state if we are transitioning from one to another enum cast_state wanted_state; // Connection fd and session, and listener event int server_fd; gnutls_session_t tls_session; struct event *ev; char *devname; char *address; unsigned short port; // ChromeCast uses a float between 0 - 1 float volume; // IP address URL of forked-daapd's mp3 stream char stream_url[128]; // Outgoing request which have the USE_REQUEST_ID flag get a new id, and a // callback is registered. The callback is called when an incoming message // from the peer with that request id arrives. If nothing arrives within // REPLY_TIMEOUT we make the callback with a NULL payload pointer. int request_id; cast_reply_cb callback_register[CALLBACK_REGISTER_SIZE]; struct event *reply_timeout; // This is used to work around a bug where no response is given by the device. // For certain requests, we will then retry, e.g. by checking status. We // register our retry so that we on only retry once. int retry; // Session info from the ChromeCast char *transport_id; char *session_id; int media_session_id; /* Do not dereference - only passed to the status cb */ struct output_device *device; struct output_session *output_session; output_status_cb status_cb; struct cast_session *next; }; enum cast_msg_types { UNKNOWN, PING, PONG, CONNECT, CLOSE, GET_STATUS, RECEIVER_STATUS, LAUNCH, STOP, MEDIA_CONNECT, MEDIA_CLOSE, MEDIA_GET_STATUS, MEDIA_STATUS, MEDIA_LOAD, MEDIA_PLAY, MEDIA_PAUSE, MEDIA_STOP, MEDIA_LOAD_FAILED, MEDIA_LOAD_CANCELLED, SET_VOLUME, }; struct cast_msg_basic { enum cast_msg_types type; char *tag; // Used for looking up incoming message type char *namespace; char *payload; int flags; }; struct cast_msg_payload { enum cast_msg_types type; int request_id; const char *app_id; const char *session_id; const char *transport_id; const char *player_state; int media_session_id; }; // Array of the cast messages that we use. Must be in sync with cast_msg_types. struct cast_msg_basic cast_msg[] = { { .type = UNKNOWN, .namespace = "", .payload = "", }, { .type = PING, .tag = "PING", .namespace = NS_HEARTBEAT, .payload = "{'type':'PING'}", }, { .type = PONG, .tag = "PONG", .namespace = NS_HEARTBEAT, .payload = "{'type':'PONG'}", }, { .type = CONNECT, .namespace = NS_CONNECTION, .payload = "{'type':'CONNECT'}", // msg.payload_utf8 = "{\"origin\":{},\"userAgent\":\"forked-daapd\",\"type\":\"CONNECT\",\"senderInfo\":{\"browserVersion\":\"44.0.2403.30\",\"version\":\"15.605.1.3\",\"connectionType\":1,\"platform\":4,\"sdkType\":2,\"systemVersion\":\"Macintosh; Intel Mac OS X10_10_3\"}}"; }, { .type = CLOSE, .tag = "CLOSE", .namespace = NS_CONNECTION, .payload = "{'type':'CLOSE'}", }, { .type = GET_STATUS, .namespace = NS_RECEIVER, .payload = "{'type':'GET_STATUS','requestId':%d}", .flags = USE_REQUEST_ID_ONLY, }, { .type = RECEIVER_STATUS, .tag = "RECEIVER_STATUS", }, { .type = LAUNCH, .namespace = NS_RECEIVER, .payload = "{'type':'LAUNCH','requestId':%d,'appId':'" CAST_APP_ID "'}", .flags = USE_REQUEST_ID_ONLY, }, { .type = STOP, .namespace = NS_RECEIVER, .payload = "{'type':'STOP','sessionId':'%s','requestId':%d}", .flags = USE_REQUEST_ID, }, { .type = MEDIA_CONNECT, .namespace = NS_CONNECTION, .payload = "{'type':'CONNECT'}", .flags = USE_TRANSPORT_ID, }, { .type = MEDIA_CLOSE, .namespace = NS_CONNECTION, .payload = "{'type':'CLOSE'}", .flags = USE_TRANSPORT_ID, }, { .type = MEDIA_GET_STATUS, .namespace = NS_MEDIA, .payload = "{'type':'GET_STATUS','requestId':%d}", .flags = USE_TRANSPORT_ID | USE_REQUEST_ID_ONLY, }, { .type = MEDIA_STATUS, .tag = "MEDIA_STATUS", }, { .type = MEDIA_LOAD, .namespace = NS_MEDIA, .payload = "{'currentTime':0,'media':{'contentId':'%s','streamType':'LIVE','contentType':'audio/mp3'},'customData':{},'sessionId':'%s','requestId':%d,'type':'LOAD','autoplay':1}", .flags = USE_TRANSPORT_ID | USE_REQUEST_ID, }, { .type = MEDIA_PLAY, .namespace = NS_MEDIA, .payload = "{'mediaSessionId':%d,'sessionId':'%s','type':'PLAY','requestId':%d}", .flags = USE_TRANSPORT_ID | USE_REQUEST_ID, }, { .type = MEDIA_PAUSE, .namespace = NS_MEDIA, .payload = "{'mediaSessionId':%d,'sessionId':'%s','type':'PAUSE','requestId':%d}", .flags = USE_TRANSPORT_ID | USE_REQUEST_ID, }, { .type = MEDIA_STOP, .namespace = NS_MEDIA, .payload = "{'mediaSessionId':%d,'sessionId':'%s','type':'STOP','requestId':%d}", .flags = USE_TRANSPORT_ID | USE_REQUEST_ID, }, { .type = MEDIA_LOAD_FAILED, .tag = "LOAD_FAILED", }, { .type = MEDIA_LOAD_CANCELLED, .tag = "LOAD_CANCELLED", }, { .type = SET_VOLUME, .namespace = NS_RECEIVER, .payload = "{'type':'SET_VOLUME','volume':{'level':%.2f,'muted':0},'requestId':%d}", .flags = USE_REQUEST_ID, }, { .type = 0, }, }; /* From player.c */ extern struct event_base *evbase_player; /* Globals */ static gnutls_certificate_credentials_t tls_credentials; static struct cast_session *sessions; static struct event *flush_timer; static struct timeval heartbeat_timeout = { HEARTBEAT_TIMEOUT, 0 }; static struct timeval flush_timeout = { FLUSH_TIMEOUT, 0 }; static struct timeval reply_timeout = { REPLY_TIMEOUT, 0 }; /* ------------------------------- MISC HELPERS ----------------------------- */ static int tcp_connect(const char *address, unsigned int port, int family) { union sockaddr_all sa; int fd; int len; int ret; // TODO Open non-block right away so we don't block the player while connecting // and during TLS handshake (we would probably need to introduce a deferredev) #ifdef SOCK_CLOEXEC fd = socket(family, SOCK_STREAM | SOCK_CLOEXEC, 0); #else fd = socket(family, SOCK_STREAM, 0); #endif if (fd < 0) { DPRINTF(E_LOG, L_CAST, "Could not create socket: %s\n", strerror(errno)); return -1; } switch (family) { case AF_INET: sa.sin.sin_port = htons(port); ret = inet_pton(AF_INET, address, &sa.sin.sin_addr); len = sizeof(sa.sin); break; case AF_INET6: sa.sin6.sin6_port = htons(port); ret = inet_pton(AF_INET6, address, &sa.sin6.sin6_addr); len = sizeof(sa.sin6); break; default: DPRINTF(E_WARN, L_CAST, "Unknown family %d\n", family); close(fd); return -1; } if (ret <= 0) { DPRINTF(E_LOG, L_CAST, "Device address not valid (%s)\n", address); close(fd); return -1; } sa.ss.ss_family = family; ret = connect(fd, &sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_CAST, "connect() to [%s]:%u failed: %s\n", address, port, strerror(errno)); close(fd); return -1; } return fd; } static void tcp_close(int fd) { /* no more receptions */ shutdown(fd, SHUT_RDWR); close(fd); } static int stream_url_make(char *out, size_t len, const char *peer_addr, int family) { struct ifaddrs *ifap; struct ifaddrs *ifa; union sockaddr_all haddr; union sockaddr_all hmask; union sockaddr_all paddr; char host_addr[128]; unsigned short port; int found; int ret; if (family == AF_INET) ret = inet_pton(AF_INET, peer_addr, &paddr.sin.sin_addr); else ret = inet_pton(AF_INET6, peer_addr, &paddr.sin6.sin6_addr); if (ret != 1) return -1; found = 0; getifaddrs(&ifap); for (ifa = ifap; !found && ifa; ifa = ifa->ifa_next) { if (ifa->ifa_addr->sa_family != family) continue; if (family == AF_INET) { memcpy(&haddr.sin, ifa->ifa_addr, sizeof(struct sockaddr_in)); memcpy(&hmask.sin, ifa->ifa_netmask, sizeof(struct sockaddr_in)); found = ((haddr.sin.sin_addr.s_addr & hmask.sin.sin_addr.s_addr) == (paddr.sin.sin_addr.s_addr & hmask.sin.sin_addr.s_addr)); if (found) inet_ntop(family, &haddr.sin.sin_addr, host_addr, sizeof(host_addr)); } else if (family == AF_INET6) { memcpy(&haddr.sin6, ifa->ifa_addr, sizeof(struct sockaddr_in6)); found = (memcmp(&haddr.sin6.sin6_addr.s6_addr, &paddr.sin6.sin6_addr.s6_addr, 8) == 0); if (found) inet_ntop(family, &haddr.sin6.sin6_addr, host_addr, sizeof(host_addr)); } } freeifaddrs(ifap); if (!found) return -1; port = cfg_getint(cfg_getsec(cfg, "library"), "port"); if (family == AF_INET) snprintf(out, len, "http://%s:%d/stream.mp3", host_addr, port); else snprintf(out, len, "http://[%s]:%d/stream.mp3", host_addr, port); return 0; } static char * squote_to_dquote(char *buf) { char *ptr; for (ptr = buf; *ptr != '\0'; ptr++) if (*ptr == '\'') *ptr = '"'; return buf; } /* ----------------------------- SESSION CLEANUP ---------------------------- */ static void cast_session_free(struct cast_session *cs) { event_free(cs->reply_timeout); event_free(cs->ev); if (cs->server_fd >= 0) tcp_close(cs->server_fd); gnutls_deinit(cs->tls_session); if (cs->address) free(cs->address); if (cs->devname) free(cs->devname); if (cs->session_id) free(cs->session_id); if (cs->transport_id) free(cs->transport_id); free(cs->output_session); free(cs); } static void cast_session_cleanup(struct cast_session *cs) { struct cast_session *s; if (cs == sessions) sessions = sessions->next; else { for (s = sessions; s && (s->next != cs); s = s->next) ; /* EMPTY */ if (!s) DPRINTF(E_WARN, L_CAST, "WARNING: struct cast_session not found in list; BUG!\n"); else s->next = cs->next; } cast_session_free(cs); } // Forward static void cast_session_shutdown(struct cast_session *cs, enum cast_state wanted_state); /* --------------------------- CAST MESSAGE HANDLING ------------------------ */ static int cast_msg_send(struct cast_session *cs, enum cast_msg_types type, cast_reply_cb reply_cb) { Extensions__CoreApi__CastChannel__CastMessage msg = EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__INIT; char msg_buf[MAX_BUF]; uint8_t buf[MAX_BUF]; uint32_t be; size_t len; int ret; #ifdef DEBUG_CONNECTION DPRINTF(E_DBG, L_CAST, "Preparing to send message type %d to '%s'\n", type, cs->devname); #endif msg.source_id = "sender-0"; msg.namespace_ = cast_msg[type].namespace; if ((cast_msg[type].flags & USE_TRANSPORT_ID) && !cs->transport_id) { DPRINTF(E_LOG, L_CAST, "Error, didn't get transportId for message (type %d) to '%s'\n", type, cs->devname); return -1; } if (cast_msg[type].flags & USE_TRANSPORT_ID) msg.destination_id = cs->transport_id; else msg.destination_id = "receiver-0"; if (cast_msg[type].flags & (USE_REQUEST_ID | USE_REQUEST_ID_ONLY)) { cs->request_id++; if (reply_cb) { cs->callback_register[cs->request_id % CALLBACK_REGISTER_SIZE] = reply_cb; event_add(cs->reply_timeout, &reply_timeout); } } // Special handling of some message types if (cast_msg[type].flags & USE_REQUEST_ID_ONLY) snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->request_id); else if (type == STOP) snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->session_id, cs->request_id); else if (type == MEDIA_LOAD) snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->stream_url, cs->session_id, cs->request_id); else if ((type == MEDIA_PLAY) || (type == MEDIA_PAUSE) || (type == MEDIA_STOP)) snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->media_session_id, cs->session_id, cs->request_id); else if (type == SET_VOLUME) snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->volume, cs->request_id); else snprintf(msg_buf, sizeof(msg_buf), "%s", cast_msg[type].payload); squote_to_dquote(msg_buf); msg.payload_utf8 = msg_buf; len = extensions__core_api__cast_channel__cast_message__get_packed_size(&msg); if (len <= 0) { DPRINTF(E_LOG, L_CAST, "Could not send message (type %d), invalid length: %zu\n", type, len); return -1; } // The message must be prefixed with Big-Endian 32 bit length be = htobe32(len); memcpy(buf, &be, 4); // Now add the packed message and send it extensions__core_api__cast_channel__cast_message__pack(&msg, buf + 4); ret = gnutls_record_send(cs->tls_session, buf, len + 4); if (ret < 0) { DPRINTF(E_LOG, L_CAST, "Could not send message, TLS error\n"); return -1; } else if (ret != len + 4) { DPRINTF(E_LOG, L_CAST, "BUG! Message partially sent, and we are not able to send the rest\n"); return -1; } if (type != PONG) DPRINTF(E_DBG, L_CAST, "TX %zu %s %s %s %s\n", len, msg.source_id, msg.destination_id, msg.namespace_, msg.payload_utf8); return 0; } static void * cast_msg_parse(struct cast_msg_payload *payload, char *s) { json_object *haystack; json_object *somehay; json_object *needle; const char *val; int i; haystack = json_tokener_parse(s); if (!haystack) { DPRINTF(E_LOG, L_CAST, "JSON parser returned an error\n"); return NULL; } payload->type = UNKNOWN; if (json_object_object_get_ex(haystack, "type", &needle)) { val = json_object_get_string(needle); for (i = 1; cast_msg[i].type; i++) { if (cast_msg[i].tag && (strcmp(val, cast_msg[i].tag) == 0)) { payload->type = cast_msg[i].type; break; } } } if (json_object_object_get_ex(haystack, "requestId", &needle)) payload->request_id = json_object_get_int(needle); // Might be done now if ((payload->type != RECEIVER_STATUS) && (payload->type != MEDIA_STATUS)) return haystack; // Isn't this marvelous if ( json_object_object_get_ex(haystack, "status", &needle) && (json_object_get_type(needle) == json_type_array) && (somehay = json_object_array_get_idx(needle, 0)) ) { if ( json_object_object_get_ex(somehay, "mediaSessionId", &needle) && (json_object_get_type(needle) == json_type_int) ) payload->media_session_id = json_object_get_int(needle); if ( json_object_object_get_ex(somehay, "playerState", &needle) && (json_object_get_type(needle) == json_type_string) ) payload->player_state = json_object_get_string(needle); } if ( json_object_object_get_ex(haystack, "status", &somehay) && json_object_object_get_ex(somehay, "applications", &needle) && (json_object_get_type(needle) == json_type_array) && (somehay = json_object_array_get_idx(needle, 0)) ) { if ( json_object_object_get_ex(somehay, "appId", &needle) && (json_object_get_type(needle) == json_type_string) ) payload->app_id = json_object_get_string(needle); if ( json_object_object_get_ex(somehay, "sessionId", &needle) && (json_object_get_type(needle) == json_type_string) ) payload->session_id = json_object_get_string(needle); if ( json_object_object_get_ex(somehay, "transportId", &needle) && (json_object_get_type(needle) == json_type_string) ) payload->transport_id = json_object_get_string(needle); } return haystack; } static void cast_msg_parse_free(void *haystack) { #ifdef HAVE_JSON_C_OLD json_object_put((json_object *)haystack); #else if (json_object_put((json_object *)haystack) != 1) DPRINTF(E_LOG, L_CAST, "Memleak: JSON parser did not free object\n"); #endif } static void cast_msg_process(struct cast_session *cs, const uint8_t *data, size_t len) { Extensions__CoreApi__CastChannel__CastMessage *reply; cast_reply_cb reply_cb; struct cast_msg_payload payload = { 0 }; void *hdl; int unknown_app_id; int unknown_session_id; int i; #ifdef DEBUG_CONNECTION char *b64 = b64_encode(data, len); if (b64) { DPRINTF(E_DBG, L_CAST, "Reply dump (len %zu): %s\n", len, b64); free(b64); } #endif reply = extensions__core_api__cast_channel__cast_message__unpack(NULL, len, data); if (!reply) { DPRINTF(E_LOG, L_CAST, "Could not unpack message!\n"); return; } hdl = cast_msg_parse(&payload, reply->payload_utf8); if (!hdl) { DPRINTF(E_DBG, L_CAST, "Could not parse message: %s\n", reply->payload_utf8); goto out_free_unpacked; } if (payload.type == PING) { cast_msg_send(cs, PONG, NULL); goto out_free_parsed; } DPRINTF(E_DBG, L_CAST, "RX %zu %s %s %s %s\n", len, reply->source_id, reply->destination_id, reply->namespace_, reply->payload_utf8); if (payload.type == UNKNOWN) goto out_free_parsed; i = payload.request_id % CALLBACK_REGISTER_SIZE; if (payload.request_id && cs->callback_register[i]) { reply_cb = cs->callback_register[i]; cs->callback_register[i] = NULL; // Cancel the timeout if no pending callbacks for (i = 0; (i < CALLBACK_REGISTER_SIZE) && (!cs->callback_register[i]); i++); if (i == CALLBACK_REGISTER_SIZE) evtimer_del(cs->reply_timeout); reply_cb(cs, &payload); goto out_free_parsed; } // TODO Should we read volume and playerstate changes from the Chromecast? if (payload.type == RECEIVER_STATUS && (cs->state & CAST_STATE_F_MEDIA_CONNECTED)) { unknown_app_id = payload.app_id && (strcmp(payload.app_id, CAST_APP_ID) != 0); unknown_session_id = payload.session_id && (strcmp(payload.session_id, cs->session_id) != 0); if (unknown_app_id || unknown_session_id) { DPRINTF(E_WARN, L_CAST, "Our session on '%s' was hijacked\n", cs->devname); // Downgrade state, we don't have the receiver app any more cs->state = CAST_STATE_CONNECTED; cast_session_shutdown(cs, CAST_STATE_FAILED); goto out_free_parsed; } } if (payload.type == MEDIA_STATUS && (cs->state & CAST_STATE_F_MEDIA_PLAYING)) { if (payload.player_state && (strcmp(payload.player_state, "PAUSED") == 0)) { DPRINTF(E_WARN, L_CAST, "Something paused our session on '%s'\n", cs->devname); /* cs->state = CAST_STATE_MEDIA_CONNECTED; // Kill the session, the player will need to restart it cast_session_shutdown(cs, CAST_STATE_NONE); goto out_free_parsed; */ } } out_free_parsed: cast_msg_parse_free(hdl); out_free_unpacked: extensions__core_api__cast_channel__cast_message__free_unpacked(reply, NULL); } /* -------------------------------- CALLBACKS ------------------------------- */ /* Maps our internal state to the generic output state and then makes a callback * to the player to tell that state */ static void cast_status(struct cast_session *cs) { output_status_cb status_cb = cs->status_cb; enum output_device_state state; switch (cs->state) { case CAST_STATE_FAILED: state = OUTPUT_STATE_FAILED; break; case CAST_STATE_NONE: state = OUTPUT_STATE_STOPPED; break; case CAST_STATE_DISCONNECTED ... CAST_STATE_MEDIA_LAUNCHED: state = OUTPUT_STATE_STARTUP; break; case CAST_STATE_MEDIA_CONNECTED: state = OUTPUT_STATE_CONNECTED; break; case CAST_STATE_MEDIA_LOADED ... CAST_STATE_MEDIA_PAUSED: state = OUTPUT_STATE_CONNECTED; break; case CAST_STATE_MEDIA_BUFFERING ... CAST_STATE_MEDIA_PLAYING: state = OUTPUT_STATE_STREAMING; break; default: DPRINTF(E_LOG, L_CAST, "Bug! Unhandled state in cast_status()\n"); state = OUTPUT_STATE_FAILED; } cs->status_cb = NULL; if (status_cb) status_cb(cs->device, cs->output_session, state); } /* cast_cb_stop*: Callback chain for shutting down a session */ static void cast_cb_stop(struct cast_session *cs, struct cast_msg_payload *payload) { if (!payload) DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our STOP - will continue anyway\n"); else if (payload->type != RECEIVER_STATUS) DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our STOP (got type: %d) - will continue anyway\n", payload->type); cs->state = CAST_STATE_CONNECTED; if (cs->state == cs->wanted_state) cast_status(cs); else cast_session_shutdown(cs, cs->wanted_state); } static void cast_cb_stop_media(struct cast_session *cs, struct cast_msg_payload *payload) { if (!payload) DPRINTF(E_LOG, L_CAST, "No MEDIA_STATUS reply to our STOP - will continue anyway\n"); else if (payload->type != MEDIA_STATUS) DPRINTF(E_LOG, L_CAST, "No MEDIA_STATUS reply to our STOP (got type: %d) - will continue anyway\n", payload->type); cs->state = CAST_STATE_MEDIA_CONNECTED; if (cs->state == cs->wanted_state) cast_status(cs); else cast_session_shutdown(cs, cs->wanted_state); } /* cast_cb_startup*: Callback chain for starting a session */ static void cast_cb_startup_volume(struct cast_session *cs, struct cast_msg_payload *payload) { /* Session startup and setup is done, tell our user */ DPRINTF(E_DBG, L_CAST, "Session ready\n"); cast_status(cs); } static void cast_cb_startup_media(struct cast_session *cs, struct cast_msg_payload *payload) { int ret; if (!payload) { DPRINTF(E_LOG, L_CAST, "No MEDIA_STATUS reply to our GET_STATUS - aborting\n"); goto error; } else if (payload->type != MEDIA_STATUS) { DPRINTF(E_LOG, L_CAST, "No MEDIA_STATUS reply to our GET_STATUS (got type: %d) - aborting\n", payload->type); goto error; } ret = cast_msg_send(cs, SET_VOLUME, cast_cb_startup_volume); if (ret < 0) goto error; cs->state = CAST_STATE_MEDIA_CONNECTED; return; error: cast_session_shutdown(cs, CAST_STATE_FAILED); } static void cast_cb_startup_launch(struct cast_session *cs, struct cast_msg_payload *payload) { int ret; // Sometimes the response to a LAUNCH is just a broadcast RECEIVER_STATUS // without our requestId. That won't be registered by our response handler, // and we get an empty callback due to timeout. In this case we send a // GET_STATUS to see if we are good to go anyway. if (!payload && !cs->retry) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our LAUNCH - trying GET_STATUS instead\n"); cs->retry++; ret = cast_msg_send(cs, GET_STATUS, cast_cb_startup_launch); if (ret != 0) goto error; return; } if (!payload) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our LAUNCH - aborting\n"); goto error; } if (payload->type != RECEIVER_STATUS) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our LAUNCH (got type: %d) - aborting\n", payload->type); goto error; } if (!payload->transport_id || !payload->session_id) { DPRINTF(E_LOG, L_CAST, "Missing session id or transport id in RECEIVER_STATUS - aborting\n"); goto error; } if (cs->session_id || cs->transport_id) DPRINTF(E_LOG, L_CAST, "Bug! Memleaking...\n"); cs->session_id = strdup(payload->session_id); cs->transport_id = strdup(payload->transport_id); cs->retry = 0; ret = cast_msg_send(cs, MEDIA_CONNECT, NULL); if (ret == 0) ret = cast_msg_send(cs, MEDIA_GET_STATUS, cast_cb_startup_media); if (ret < 0) goto error; cs->state = CAST_STATE_MEDIA_LAUNCHED; return; error: cast_session_shutdown(cs, CAST_STATE_FAILED); } static void cast_cb_startup_connect(struct cast_session *cs, struct cast_msg_payload *payload) { int ret; if (!payload) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our GET_STATUS - aborting\n"); goto error; } else if (payload->type != RECEIVER_STATUS) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our GET_STATUS (got type: %d) - aborting\n", payload->type); goto error; } ret = cast_msg_send(cs, LAUNCH, cast_cb_startup_launch); if (ret < 0) goto error; cs->state = CAST_STATE_CONNECTED; return; error: cast_session_shutdown(cs, CAST_STATE_FAILED); } /* cast_cb_probe: Callback from cast_device_probe */ static void cast_cb_probe(struct cast_session *cs, struct cast_msg_payload *payload) { if (!payload) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our GET_STATUS - aborting\n"); goto error; } else if (payload->type != RECEIVER_STATUS) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our GET_STATUS (got type: %d) - aborting\n", payload->type); goto error; } cs->state = CAST_STATE_CONNECTED; cast_status(cs); cast_session_shutdown(cs, CAST_STATE_NONE); return; error: cast_session_shutdown(cs, CAST_STATE_FAILED); } /* cast_cb_load: Callback from starting playback */ static void cast_cb_load(struct cast_session *cs, struct cast_msg_payload *payload) { if (!payload) { DPRINTF(E_LOG, L_CAST, "No reply from '%s' to our LOAD request\n", cs->devname); goto error; } else if ((payload->type == MEDIA_LOAD_FAILED) || (payload->type == MEDIA_LOAD_CANCELLED)) { DPRINTF(E_LOG, L_CAST, "The device '%s' could not start playback\n", cs->devname); goto error; } else if (!payload->media_session_id) { DPRINTF(E_LOG, L_CAST, "Missing media session id in MEDIA_STATUS - aborting\n"); goto error; } cs->media_session_id = payload->media_session_id; // We autoplay for the time being cs->state = CAST_STATE_MEDIA_PLAYING; cast_status(cs); return; error: cast_session_shutdown(cs, CAST_STATE_FAILED); } static void cast_cb_volume(struct cast_session *cs, struct cast_msg_payload *payload) { cast_status(cs); } static void cast_cb_flush(struct cast_session *cs, struct cast_msg_payload *payload) { if (!payload) DPRINTF(E_LOG, L_CAST, "No reply to PAUSE request from '%s' - will continue\n", cs->devname); else if (payload->type != MEDIA_STATUS) DPRINTF(E_LOG, L_CAST, "Unexpected reply to PAUSE request from '%s' - will continue\n", cs->devname); cs->state = CAST_STATE_MEDIA_PAUSED; cast_status(cs); } /* The core of this module. Libevent makes a callback to this function whenever * there is new data to be read on the fd from the ChromeCast. If everything is * good then the data will be passed to cast_msg_process() that will then * parse and make callbacks, if relevant. */ static void cast_listen_cb(int fd, short what, void *arg) { struct cast_session *cs; uint8_t buffer[MAX_BUF + 1]; // Not sure about the +1, but is copied from gnutls examples uint32_t be; size_t len; int received; int ret; for (cs = sessions; cs; cs = cs->next) { if (cs == (struct cast_session *)arg) break; } if (!cs) { DPRINTF(E_INFO, L_CAST, "Callback on dead session, ignoring\n"); return; } if (what == EV_TIMEOUT) { DPRINTF(E_LOG, L_CAST, "No heartbeat from '%s', shutting down\n", cs->devname); goto fail; } #ifdef DEBUG_CONNECTION DPRINTF(E_DBG, L_CAST, "New data from '%s'\n", cs->devname); #endif // We first read the 4 byte header and then the actual message. The header // will be the length of the message. ret = gnutls_record_recv(cs->tls_session, buffer, 4); if (ret != 4) goto no_read; memcpy(&be, buffer, 4); len = be32toh(be); if ((len == 0) || (len > MAX_BUF)) { DPRINTF(E_LOG, L_CAST, "Bad length of incoming message, aborting (len=%zu, size=%d)\n", len, MAX_BUF); goto fail; } received = 0; while (received < len) { ret = gnutls_record_recv(cs->tls_session, buffer + received, len - received); if (ret <= 0) goto no_read; received += ret; #ifdef DEBUG_CONNECTION DPRINTF(E_DBG, L_CAST, "Received %d bytes out of expected %zu bytes\n", received, len); #endif } ret = gnutls_record_check_pending(cs->tls_session); // Process the message - note that this may result in cs being invalidated cast_msg_process(cs, buffer, len); // In the event there was more data waiting for us we go again if (ret > 0) { DPRINTF(E_INFO, L_CAST, "More data pending from device (%d bytes)\n", ret); cast_listen_cb(fd, what, arg); } return; no_read: if ((ret != GNUTLS_E_INTERRUPTED) && (ret != GNUTLS_E_AGAIN)) { DPRINTF(E_LOG, L_CAST, "Session error: %s\n", gnutls_strerror(ret)); goto fail; } DPRINTF(E_DBG, L_CAST, "Return value from tls is %d (GNUTLS_E_AGAIN is %d)\n", ret, GNUTLS_E_AGAIN); return; fail: // Downgrade state to make cast_session_shutdown perform an exit which is // quick and won't require a reponse from the device cs->state = CAST_STATE_CONNECTED; cast_session_shutdown(cs, CAST_STATE_FAILED); } static void cast_reply_timeout_cb(int fd, short what, void *arg) { struct cast_session *cs; int i; cs = (struct cast_session *)arg; i = cs->request_id % CALLBACK_REGISTER_SIZE; DPRINTF(E_LOG, L_CAST, "Request %d timed out, will run empty callback\n", i); if (cs->callback_register[i]) { cs->callback_register[i](cs, NULL); cs->callback_register[i] = NULL; } } static void cast_device_cb(const char *name, const char *type, const char *domain, const char *hostname, int family, const char *address, int port, struct keyval *txt) { struct output_device *device; const char *friendly_name; uint32_t id; id = djb_hash(name, strlen(name)); if (!id) { DPRINTF(E_LOG, L_CAST, "Could not hash ChromeCast device name (%s)\n", name); return; } friendly_name = keyval_get(txt, "fn"); if (friendly_name) name = friendly_name; DPRINTF(E_DBG, L_CAST, "Event for Chromecast device '%s' (port %d, id %" PRIu32 ")\n", name, port, id); device = calloc(1, sizeof(struct output_device)); if (!device) { DPRINTF(E_LOG, L_CAST, "Out of memory for new Chromecast device\n"); return; } device->id = id; device->name = strdup(name); device->type = OUTPUT_TYPE_CAST; device->type_name = outputs_name(device->type); if (port < 0) { /* Device stopped advertising */ switch (family) { case AF_INET: device->v4_port = 1; break; case AF_INET6: device->v6_port = 1; break; } player_device_remove(device); return; } DPRINTF(E_INFO, L_CAST, "Adding Chromecast device '%s'\n", name); device->advertised = 1; switch (family) { case AF_INET: device->v4_address = strdup(address); device->v4_port = port; break; case AF_INET6: device->v6_address = strdup(address); device->v6_port = port; break; } player_device_add(device); } /* --------------------- SESSION CONSTRUCTION AND SHUTDOWN ------------------ */ // Allocates a session and sets of the startup sequence until the session reaches // the CAST_STATE_MEDIA_CONNECTED status (so it is ready to load media) static struct cast_session * cast_session_make(struct output_device *device, int family, output_status_cb cb) { struct output_session *os; struct cast_session *cs; const char *proto; const char *err; char *address; unsigned short port; int flags; int ret; switch (family) { case AF_INET: /* We always have the v4 services, so no need to check */ if (!device->v4_address) return NULL; address = device->v4_address; port = device->v4_port; break; case AF_INET6: if (!device->v6_address) return NULL; address = device->v6_address; port = device->v6_port; break; default: return NULL; } os = calloc(1, sizeof(struct output_session)); if (!os) { DPRINTF(E_LOG, L_CAST, "Out of memory (os)\n"); return NULL; } cs = calloc(1, sizeof(struct cast_session)); if (!cs) { DPRINTF(E_LOG, L_CAST, "Out of memory (cs)\n"); free(os); return NULL; } os->session = cs; os->type = device->type; cs->output_session = os; cs->state = CAST_STATE_DISCONNECTED; cs->device = device; cs->status_cb = cb; /* Init TLS session, use default priorities and put the x509 credentials to the current session */ if ( ((ret = gnutls_init(&cs->tls_session, GNUTLS_CLIENT)) != GNUTLS_E_SUCCESS) || ((ret = gnutls_priority_set_direct(cs->tls_session, "PERFORMANCE", &err)) != GNUTLS_E_SUCCESS) || ((ret = gnutls_credentials_set(cs->tls_session, GNUTLS_CRD_CERTIFICATE, tls_credentials)) != GNUTLS_E_SUCCESS) ) { DPRINTF(E_LOG, L_CAST, "Could not initialize GNUTLS session: %s\n", gnutls_strerror(ret)); goto out_free_session; } cs->server_fd = tcp_connect(address, port, family); if (cs->server_fd < 0) { DPRINTF(E_LOG, L_CAST, "Could not connect to %s\n", device->name); goto out_deinit_gnutls; } ret = stream_url_make(cs->stream_url, sizeof(cs->stream_url), address, family); if (ret < 0) { DPRINTF(E_LOG, L_CAST, "Bug! Could find a network interface on same subnet as %s\n", device->name); goto out_close_connection; } cs->ev = event_new(evbase_player, cs->server_fd, EV_READ | EV_PERSIST, cast_listen_cb, cs); if (!cs->ev) { DPRINTF(E_LOG, L_CAST, "Out of memory for listener event\n"); goto out_close_connection; } cs->reply_timeout = evtimer_new(evbase_player, cast_reply_timeout_cb, cs); if (!cs->reply_timeout) { DPRINTF(E_LOG, L_CAST, "Out of memory for reply_timeout\n"); goto out_close_connection; } gnutls_transport_set_ptr(cs->tls_session, (gnutls_transport_ptr_t)cs->server_fd); ret = gnutls_handshake(cs->tls_session); if (ret != GNUTLS_E_SUCCESS) { DPRINTF(E_LOG, L_CAST, "Could not attach TLS to TCP connection: %s\n", gnutls_strerror(ret)); goto out_free_ev; } flags = fcntl(cs->server_fd, F_GETFL, 0); fcntl(cs->server_fd, F_SETFL, flags | O_NONBLOCK); event_add(cs->ev, &heartbeat_timeout); cs->devname = strdup(device->name); cs->address = strdup(address); cs->volume = 0.01 * device->volume; cs->next = sessions; sessions = cs; proto = gnutls_protocol_get_name(gnutls_protocol_get_version(cs->tls_session)); DPRINTF(E_INFO, L_CAST, "Connection to '%s' established using %s\n", cs->devname, proto); return cs; out_free_ev: event_free(cs->reply_timeout); event_free(cs->ev); out_close_connection: tcp_close(cs->server_fd); out_deinit_gnutls: gnutls_deinit(cs->tls_session); out_free_session: free(cs); return NULL; } // Attempts to "nicely" bring down a session to wanted_state, and then issues // the callback. If wanted_state is CAST_STATE_NONE/FAILED then the session is purged. static void cast_session_shutdown(struct cast_session *cs, enum cast_state wanted_state) { int pending; int ret; if (cs->state == wanted_state) { cast_status(cs); return; } else if (cs->state < wanted_state) { DPRINTF(E_LOG, L_CAST, "Bug! Shutdown request got wanted_state (%d) that is higher than current state (%d)\n", wanted_state, cs->state); return; } cs->wanted_state = wanted_state; pending = 0; switch (cs->state) { case CAST_STATE_MEDIA_LOADED ... CAST_STATE_MEDIA_PLAYING: ret = cast_msg_send(cs, MEDIA_STOP, cast_cb_stop_media); pending = 1; break; case CAST_STATE_MEDIA_CONNECTED: ret = cast_msg_send(cs, MEDIA_CLOSE, NULL); cs->state = CAST_STATE_MEDIA_LAUNCHED; if ((ret < 0) || (wanted_state >= CAST_STATE_MEDIA_LAUNCHED)) break; /* FALLTHROUGH */ case CAST_STATE_MEDIA_LAUNCHED: ret = cast_msg_send(cs, STOP, cast_cb_stop); pending = 1; break; case CAST_STATE_CONNECTED: ret = cast_msg_send(cs, CLOSE, NULL); if (ret == 0) gnutls_bye(cs->tls_session, GNUTLS_SHUT_RDWR); tcp_close(cs->server_fd); cs->server_fd = -1; cs->state = CAST_STATE_DISCONNECTED; break; case CAST_STATE_DISCONNECTED: ret = 0; break; default: DPRINTF(E_LOG, L_CAST, "Bug! Shutdown doesn't know how to handle current state\n"); ret = -1; } // We couldn't talk to the device, tell the user and clean up if (ret < 0) { cs->state = CAST_STATE_FAILED; cast_status(cs); cast_session_cleanup(cs); return; } // If pending callbacks then we let them take care of the rest if (pending) return; // Asked to destroy the session if (wanted_state == CAST_STATE_NONE || wanted_state == CAST_STATE_FAILED) { cs->state = wanted_state; cast_status(cs); cast_session_cleanup(cs); return; } cast_status(cs); } /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int cast_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) { struct cast_session *cs; int ret; cs = cast_session_make(device, AF_INET6, cb); if (cs) { ret = cast_msg_send(cs, CONNECT, NULL); if (ret == 0) ret = cast_msg_send(cs, GET_STATUS, cast_cb_startup_connect); if (ret < 0) { DPRINTF(E_WARN, L_CAST, "Could not send CONNECT or GET_STATUS request on IPv6 (start)\n"); cast_session_cleanup(cs); } else return 0; } cs = cast_session_make(device, AF_INET, cb); if (!cs) return -1; ret = cast_msg_send(cs, CONNECT, NULL); if (ret == 0) ret = cast_msg_send(cs, GET_STATUS, cast_cb_startup_connect); if (ret < 0) { DPRINTF(E_LOG, L_CAST, "Could not send CONNECT or GET_STATUS request on IPv4 (start)\n"); cast_session_cleanup(cs); return -1; } return 0; } static void cast_device_stop(struct output_session *session) { struct cast_session *cs = session->session; cast_session_shutdown(cs, CAST_STATE_NONE); } static int cast_device_probe(struct output_device *device, output_status_cb cb) { struct cast_session *cs; int ret; cs = cast_session_make(device, AF_INET6, cb); if (cs) { ret = cast_msg_send(cs, CONNECT, NULL); if (ret == 0) ret = cast_msg_send(cs, GET_STATUS, cast_cb_probe); if (ret < 0) { DPRINTF(E_WARN, L_CAST, "Could not send CONNECT or GET_STATUS request on IPv6 (start)\n"); cast_session_cleanup(cs); } else return 0; } cs = cast_session_make(device, AF_INET, cb); if (!cs) return -1; ret = cast_msg_send(cs, CONNECT, NULL); if (ret == 0) ret = cast_msg_send(cs, GET_STATUS, cast_cb_probe); if (ret < 0) { DPRINTF(E_LOG, L_CAST, "Could not send CONNECT or GET_STATUS request on IPv4 (start)\n"); cast_session_cleanup(cs); return -1; } return 0; } static int cast_volume_set(struct output_device *device, output_status_cb cb) { struct cast_session *cs; int ret; if (!device->session || !device->session->session) return 0; cs = device->session->session; if (!(cs->state & CAST_STATE_F_MEDIA_CONNECTED)) return 0; cs->volume = 0.01 * device->volume; ret = cast_msg_send(cs, SET_VOLUME, cast_cb_volume); if (ret < 0) { cast_session_shutdown(cs, CAST_STATE_FAILED); return 0; } // Setting it here means it will not be used for the above cast_session_shutdown cs->status_cb = cb; return 1; } static void cast_playback_start(uint64_t next_pkt, struct timespec *ts) { struct cast_session *cs; if (evtimer_pending(flush_timer, NULL)) event_del(flush_timer); // TODO Maybe we could avoid reloading and instead support play->pause->play for (cs = sessions; cs; cs = cs->next) { if (cs->state & CAST_STATE_F_MEDIA_CONNECTED) cast_msg_send(cs, MEDIA_LOAD, cast_cb_load); } } static void cast_playback_stop(void) { struct cast_session *cs; struct cast_session *next; for (cs = sessions; cs; cs = next) { next = cs->next; if (cs->state & CAST_STATE_F_MEDIA_CONNECTED) cast_session_shutdown(cs, CAST_STATE_NONE); } } static void cast_flush_timer_cb(int fd, short what, void *arg) { DPRINTF(E_DBG, L_CAST, "Flush timer expired; tearing down all sessions\n"); cast_playback_stop(); } static int cast_flush(output_status_cb cb, uint64_t rtptime) { struct cast_session *cs; struct cast_session *next; int pending; int ret; pending = 0; for (cs = sessions; cs; cs = next) { next = cs->next; if (!(cs->state & CAST_STATE_F_MEDIA_PLAYING)) continue; ret = cast_msg_send(cs, MEDIA_PAUSE, cast_cb_flush); if (ret < 0) { cast_session_shutdown(cs, CAST_STATE_FAILED); continue; } cs->status_cb = cb; pending++; } if (pending > 0) evtimer_add(flush_timer, &flush_timeout); return pending; } static void cast_set_status_cb(struct output_session *session, output_status_cb cb) { struct cast_session *cs = session->session; cs->status_cb = cb; } static int cast_init(void) { int family; int i; int ret; // Sanity check for (i = 1; cast_msg[i].type; i++) { if (cast_msg[i].type != i) { DPRINTF(E_LOG, L_CAST, "BUG! Cast messages and types are misaligned (type %d!=%d). Could not initialize.\n", cast_msg[i].type, i); return -1; } } // Setting the cert file seems not to be required if ( ((ret = gnutls_global_init()) != GNUTLS_E_SUCCESS) || ((ret = gnutls_certificate_allocate_credentials(&tls_credentials)) != GNUTLS_E_SUCCESS) // || ((ret = gnutls_certificate_set_x509_trust_file(tls_credentials, CAFILE, GNUTLS_X509_FMT_PEM)) < 0) ) { DPRINTF(E_LOG, L_CAST, "Could not initialize GNUTLS: %s\n", gnutls_strerror(ret)); return -1; } flush_timer = evtimer_new(evbase_player, cast_flush_timer_cb, NULL); if (!flush_timer) { DPRINTF(E_LOG, L_CAST, "Out of memory for flush timer\n"); goto out_tls_deinit; } if (cfg_getbool(cfg_getsec(cfg, "general"), "ipv6")) family = AF_UNSPEC; else family = AF_INET; ret = mdns_browse("_googlecast._tcp", family, cast_device_cb); if (ret < 0) { DPRINTF(E_LOG, L_CAST, "Could not add mDNS browser for Chromecast devices\n"); goto out_free_flush_timer; } return 0; out_free_flush_timer: event_free(flush_timer); out_tls_deinit: gnutls_certificate_free_credentials(tls_credentials); gnutls_global_deinit(); return -1; } static void cast_deinit(void) { struct cast_session *cs; for (cs = sessions; sessions; cs = sessions) { sessions = cs->next; cast_session_free(cs); } event_free(flush_timer); gnutls_certificate_free_credentials(tls_credentials); gnutls_global_deinit(); } struct output_definition output_cast = { .name = "Chromecast", .type = OUTPUT_TYPE_CAST, .priority = 2, .disabled = 0, .init = cast_init, .deinit = cast_deinit, .device_start = cast_device_start, .device_stop = cast_device_stop, .device_probe = cast_device_probe, // .device_free_extra is unset - nothing to free .device_volume_set = cast_volume_set, .playback_start = cast_playback_start, .playback_stop = cast_playback_stop, // .write is unset - we don't write, the Chromecast will read our mp3 stream .flush = cast_flush, .status_cb = cast_set_status_cb, /* TODO metadata support .metadata_prepare = cast_metadata_prepare, .metadata_send = cast_metadata_send, .metadata_purge = cast_metadata_purge, .metadata_prune = cast_metadata_prune, */ }; forked-daapd-25.0/src/outputs/cast_channel.pb-c.c000066400000000000000000000707171313447753700217330ustar00rootroot00000000000000/* Generated by the protocol buffer compiler. DO NOT EDIT! */ /* Generated from: cast_channel.proto */ /* Do not generate deprecated warnings for self */ #ifndef PROTOBUF_C__NO_DEPRECATED #define PROTOBUF_C__NO_DEPRECATED #endif #include "cast_channel.pb-c.h" void extensions__core_api__cast_channel__cast_message__init (Extensions__CoreApi__CastChannel__CastMessage *message) { static Extensions__CoreApi__CastChannel__CastMessage init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__cast_message__get_packed_size (const Extensions__CoreApi__CastChannel__CastMessage *message) { assert(message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__cast_message__pack (const Extensions__CoreApi__CastChannel__CastMessage *message, uint8_t *out) { assert(message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__cast_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCBuffer *buffer) { assert(message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__CastMessage * extensions__core_api__cast_channel__cast_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__CastMessage *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__cast_message__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__cast_message__free_unpacked (Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCAllocator *allocator) { assert(message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__auth_challenge__init (Extensions__CoreApi__CastChannel__AuthChallenge *message) { static Extensions__CoreApi__CastChannel__AuthChallenge init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_CHALLENGE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__auth_challenge__get_packed_size (const Extensions__CoreApi__CastChannel__AuthChallenge *message) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__auth_challenge__pack (const Extensions__CoreApi__CastChannel__AuthChallenge *message, uint8_t *out) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__auth_challenge__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCBuffer *buffer) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__AuthChallenge * extensions__core_api__cast_channel__auth_challenge__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__AuthChallenge *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__auth_challenge__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__auth_challenge__free_unpacked (Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCAllocator *allocator) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__auth_response__init (Extensions__CoreApi__CastChannel__AuthResponse *message) { static Extensions__CoreApi__CastChannel__AuthResponse init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_RESPONSE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__auth_response__get_packed_size (const Extensions__CoreApi__CastChannel__AuthResponse *message) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__auth_response__pack (const Extensions__CoreApi__CastChannel__AuthResponse *message, uint8_t *out) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__auth_response__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCBuffer *buffer) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__AuthResponse * extensions__core_api__cast_channel__auth_response__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__AuthResponse *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__auth_response__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__auth_response__free_unpacked (Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCAllocator *allocator) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__auth_error__init (Extensions__CoreApi__CastChannel__AuthError *message) { static Extensions__CoreApi__CastChannel__AuthError init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__auth_error__get_packed_size (const Extensions__CoreApi__CastChannel__AuthError *message) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__auth_error__pack (const Extensions__CoreApi__CastChannel__AuthError *message, uint8_t *out) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__auth_error__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCBuffer *buffer) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__AuthError * extensions__core_api__cast_channel__auth_error__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__AuthError *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__auth_error__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__auth_error__free_unpacked (Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCAllocator *allocator) { assert(message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__device_auth_message__init (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message) { static Extensions__CoreApi__CastChannel__DeviceAuthMessage init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__DEVICE_AUTH_MESSAGE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__device_auth_message__get_packed_size (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message) { assert(message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__device_auth_message__pack (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, uint8_t *out) { assert(message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__device_auth_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCBuffer *buffer) { assert(message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__DeviceAuthMessage * extensions__core_api__cast_channel__device_auth_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__DeviceAuthMessage *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__device_auth_message__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__device_auth_message__free_unpacked (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCAllocator *allocator) { assert(message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } const ProtobufCEnumValue extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_number[1] = { { "CASTV2_1_0", "EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PROTOCOL_VERSION__CASTV2_1_0", 0 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__cast_message__protocol_version__value_ranges[] = { {0, 0},{0, 1} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_name[1] = { { "CASTV2_1_0", 0 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__protocol_version__descriptor = { PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.CastMessage.ProtocolVersion", "ProtocolVersion", "Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion", "extensions.core_api.cast_channel", 1, extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_number, 1, extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_name, 1, extensions__core_api__cast_channel__cast_message__protocol_version__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; const ProtobufCEnumValue extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_number[2] = { { "STRING", "EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__STRING", 0 }, { "BINARY", "EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__BINARY", 1 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__cast_message__payload_type__value_ranges[] = { {0, 0},{0, 2} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_name[2] = { { "BINARY", 1 }, { "STRING", 0 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__payload_type__descriptor = { PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.CastMessage.PayloadType", "PayloadType", "Extensions__CoreApi__CastChannel__CastMessage__PayloadType", "extensions.core_api.cast_channel", 2, extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_number, 2, extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_name, 1, extensions__core_api__cast_channel__cast_message__payload_type__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__cast_message__field_descriptors[7] = { { "protocol_version", 1, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__CastMessage, protocol_version), &extensions__core_api__cast_channel__cast_message__protocol_version__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "source_id", 2, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__CastMessage, source_id), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "destination_id", 3, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__CastMessage, destination_id), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "namespace", 4, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__CastMessage, namespace_), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "payload_type", 5, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__CastMessage, payload_type), &extensions__core_api__cast_channel__cast_message__payload_type__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "payload_utf8", 6, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__CastMessage, payload_utf8), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "payload_binary", 7, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_BYTES, offsetof(Extensions__CoreApi__CastChannel__CastMessage, has_payload_binary), offsetof(Extensions__CoreApi__CastChannel__CastMessage, payload_binary), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__cast_message__field_indices_by_name[] = { 2, /* field[2] = destination_id */ 3, /* field[3] = namespace */ 6, /* field[6] = payload_binary */ 4, /* field[4] = payload_type */ 5, /* field[5] = payload_utf8 */ 0, /* field[0] = protocol_version */ 1, /* field[1] = source_id */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__cast_message__number_ranges[1 + 1] = { { 1, 0 }, { 0, 7 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__cast_message__descriptor = { PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.CastMessage", "CastMessage", "Extensions__CoreApi__CastChannel__CastMessage", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__CastMessage), 7, extensions__core_api__cast_channel__cast_message__field_descriptors, extensions__core_api__cast_channel__cast_message__field_indices_by_name, 1, extensions__core_api__cast_channel__cast_message__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__cast_message__init, NULL,NULL,NULL /* reserved[123] */ }; static const Extensions__CoreApi__CastChannel__SignatureAlgorithm extensions__core_api__cast_channel__auth_challenge__signature_algorithm__default_value = EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__auth_challenge__field_descriptors[1] = { { "signature_algorithm", 1, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_ENUM, offsetof(Extensions__CoreApi__CastChannel__AuthChallenge, has_signature_algorithm), offsetof(Extensions__CoreApi__CastChannel__AuthChallenge, signature_algorithm), &extensions__core_api__cast_channel__signature_algorithm__descriptor, &extensions__core_api__cast_channel__auth_challenge__signature_algorithm__default_value, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__auth_challenge__field_indices_by_name[] = { 0, /* field[0] = signature_algorithm */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_challenge__number_ranges[1 + 1] = { { 1, 0 }, { 0, 1 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_challenge__descriptor = { PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthChallenge", "AuthChallenge", "Extensions__CoreApi__CastChannel__AuthChallenge", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__AuthChallenge), 1, extensions__core_api__cast_channel__auth_challenge__field_descriptors, extensions__core_api__cast_channel__auth_challenge__field_indices_by_name, 1, extensions__core_api__cast_channel__auth_challenge__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__auth_challenge__init, NULL,NULL,NULL /* reserved[123] */ }; static const Extensions__CoreApi__CastChannel__SignatureAlgorithm extensions__core_api__cast_channel__auth_response__signature_algorithm__default_value = EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__auth_response__field_descriptors[4] = { { "signature", 1, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_BYTES, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__AuthResponse, signature), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "client_auth_certificate", 2, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_BYTES, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__AuthResponse, client_auth_certificate), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "intermediate_certificate", 3, PROTOBUF_C_LABEL_REPEATED, PROTOBUF_C_TYPE_BYTES, offsetof(Extensions__CoreApi__CastChannel__AuthResponse, n_intermediate_certificate), offsetof(Extensions__CoreApi__CastChannel__AuthResponse, intermediate_certificate), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "signature_algorithm", 4, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_ENUM, offsetof(Extensions__CoreApi__CastChannel__AuthResponse, has_signature_algorithm), offsetof(Extensions__CoreApi__CastChannel__AuthResponse, signature_algorithm), &extensions__core_api__cast_channel__signature_algorithm__descriptor, &extensions__core_api__cast_channel__auth_response__signature_algorithm__default_value, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__auth_response__field_indices_by_name[] = { 1, /* field[1] = client_auth_certificate */ 2, /* field[2] = intermediate_certificate */ 0, /* field[0] = signature */ 3, /* field[3] = signature_algorithm */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_response__number_ranges[1 + 1] = { { 1, 0 }, { 0, 4 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_response__descriptor = { PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthResponse", "AuthResponse", "Extensions__CoreApi__CastChannel__AuthResponse", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__AuthResponse), 4, extensions__core_api__cast_channel__auth_response__field_descriptors, extensions__core_api__cast_channel__auth_response__field_indices_by_name, 1, extensions__core_api__cast_channel__auth_response__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__auth_response__init, NULL,NULL,NULL /* reserved[123] */ }; const ProtobufCEnumValue extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_number[3] = { { "INTERNAL_ERROR", "EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__INTERNAL_ERROR", 0 }, { "NO_TLS", "EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__NO_TLS", 1 }, { "SIGNATURE_ALGORITHM_UNAVAILABLE", "EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__SIGNATURE_ALGORITHM_UNAVAILABLE", 2 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_error__error_type__value_ranges[] = { {0, 0},{0, 3} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_name[3] = { { "INTERNAL_ERROR", 0 }, { "NO_TLS", 1 }, { "SIGNATURE_ALGORITHM_UNAVAILABLE", 2 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__auth_error__error_type__descriptor = { PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthError.ErrorType", "ErrorType", "Extensions__CoreApi__CastChannel__AuthError__ErrorType", "extensions.core_api.cast_channel", 3, extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_number, 3, extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_name, 1, extensions__core_api__cast_channel__auth_error__error_type__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__auth_error__field_descriptors[1] = { { "error_type", 1, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__AuthError, error_type), &extensions__core_api__cast_channel__auth_error__error_type__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__auth_error__field_indices_by_name[] = { 0, /* field[0] = error_type */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_error__number_ranges[1 + 1] = { { 1, 0 }, { 0, 1 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_error__descriptor = { PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthError", "AuthError", "Extensions__CoreApi__CastChannel__AuthError", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__AuthError), 1, extensions__core_api__cast_channel__auth_error__field_descriptors, extensions__core_api__cast_channel__auth_error__field_indices_by_name, 1, extensions__core_api__cast_channel__auth_error__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__auth_error__init, NULL,NULL,NULL /* reserved[123] */ }; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__device_auth_message__field_descriptors[3] = { { "challenge", 1, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_MESSAGE, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__DeviceAuthMessage, challenge), &extensions__core_api__cast_channel__auth_challenge__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "response", 2, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_MESSAGE, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__DeviceAuthMessage, response), &extensions__core_api__cast_channel__auth_response__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "error", 3, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_MESSAGE, 0, /* quantifier_offset */ offsetof(Extensions__CoreApi__CastChannel__DeviceAuthMessage, error), &extensions__core_api__cast_channel__auth_error__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__device_auth_message__field_indices_by_name[] = { 0, /* field[0] = challenge */ 2, /* field[2] = error */ 1, /* field[1] = response */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__device_auth_message__number_ranges[1 + 1] = { { 1, 0 }, { 0, 3 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__device_auth_message__descriptor = { PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.DeviceAuthMessage", "DeviceAuthMessage", "Extensions__CoreApi__CastChannel__DeviceAuthMessage", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__DeviceAuthMessage), 3, extensions__core_api__cast_channel__device_auth_message__field_descriptors, extensions__core_api__cast_channel__device_auth_message__field_indices_by_name, 1, extensions__core_api__cast_channel__device_auth_message__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__device_auth_message__init, NULL,NULL,NULL /* reserved[123] */ }; const ProtobufCEnumValue extensions__core_api__cast_channel__signature_algorithm__enum_values_by_number[3] = { { "UNSPECIFIED", "EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__UNSPECIFIED", 0 }, { "RSASSA_PKCS1V15", "EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15", 1 }, { "RSASSA_PSS", "EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PSS", 2 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__signature_algorithm__value_ranges[] = { {0, 0},{0, 3} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__signature_algorithm__enum_values_by_name[3] = { { "RSASSA_PKCS1V15", 1 }, { "RSASSA_PSS", 2 }, { "UNSPECIFIED", 0 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__signature_algorithm__descriptor = { PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.SignatureAlgorithm", "SignatureAlgorithm", "Extensions__CoreApi__CastChannel__SignatureAlgorithm", "extensions.core_api.cast_channel", 3, extensions__core_api__cast_channel__signature_algorithm__enum_values_by_number, 3, extensions__core_api__cast_channel__signature_algorithm__enum_values_by_name, 1, extensions__core_api__cast_channel__signature_algorithm__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; forked-daapd-25.0/src/outputs/cast_channel.pb-c.h000066400000000000000000000331141313447753700217260ustar00rootroot00000000000000/* Generated by the protocol buffer compiler. DO NOT EDIT! */ /* Generated from: cast_channel.proto */ #ifndef PROTOBUF_C_cast_5fchannel_2eproto__INCLUDED #define PROTOBUF_C_cast_5fchannel_2eproto__INCLUDED #include PROTOBUF_C__BEGIN_DECLS #if PROTOBUF_C_VERSION_NUMBER < 1000000 # error This file was generated by a newer version of protoc-c which is incompatible with your libprotobuf-c headers. Please update your headers. #elif 1000002 < PROTOBUF_C_MIN_COMPILER_VERSION # error This file was generated by an older version of protoc-c which is incompatible with your libprotobuf-c headers. Please regenerate this file with a newer version of protoc-c. #endif typedef struct _Extensions__CoreApi__CastChannel__CastMessage Extensions__CoreApi__CastChannel__CastMessage; typedef struct _Extensions__CoreApi__CastChannel__AuthChallenge Extensions__CoreApi__CastChannel__AuthChallenge; typedef struct _Extensions__CoreApi__CastChannel__AuthResponse Extensions__CoreApi__CastChannel__AuthResponse; typedef struct _Extensions__CoreApi__CastChannel__AuthError Extensions__CoreApi__CastChannel__AuthError; typedef struct _Extensions__CoreApi__CastChannel__DeviceAuthMessage Extensions__CoreApi__CastChannel__DeviceAuthMessage; /* --- enums --- */ typedef enum _Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion { EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PROTOCOL_VERSION__CASTV2_1_0 = 0 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PROTOCOL_VERSION) } Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion; typedef enum _Extensions__CoreApi__CastChannel__CastMessage__PayloadType { EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__STRING = 0, EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__BINARY = 1 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE) } Extensions__CoreApi__CastChannel__CastMessage__PayloadType; typedef enum _Extensions__CoreApi__CastChannel__AuthError__ErrorType { EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__INTERNAL_ERROR = 0, EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__NO_TLS = 1, EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__SIGNATURE_ALGORITHM_UNAVAILABLE = 2 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE) } Extensions__CoreApi__CastChannel__AuthError__ErrorType; typedef enum _Extensions__CoreApi__CastChannel__SignatureAlgorithm { EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__UNSPECIFIED = 0, EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15 = 1, EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PSS = 2 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM) } Extensions__CoreApi__CastChannel__SignatureAlgorithm; /* --- messages --- */ struct _Extensions__CoreApi__CastChannel__CastMessage { ProtobufCMessage base; Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion protocol_version; char *source_id; char *destination_id; char *namespace_; Extensions__CoreApi__CastChannel__CastMessage__PayloadType payload_type; char *payload_utf8; protobuf_c_boolean has_payload_binary; ProtobufCBinaryData payload_binary; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__cast_message__descriptor) \ , 0, NULL, NULL, NULL, 0, NULL, 0,{0,NULL} } struct _Extensions__CoreApi__CastChannel__AuthChallenge { ProtobufCMessage base; protobuf_c_boolean has_signature_algorithm; Extensions__CoreApi__CastChannel__SignatureAlgorithm signature_algorithm; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_CHALLENGE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__auth_challenge__descriptor) \ , 0,EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15 } struct _Extensions__CoreApi__CastChannel__AuthResponse { ProtobufCMessage base; ProtobufCBinaryData signature; ProtobufCBinaryData client_auth_certificate; size_t n_intermediate_certificate; ProtobufCBinaryData *intermediate_certificate; protobuf_c_boolean has_signature_algorithm; Extensions__CoreApi__CastChannel__SignatureAlgorithm signature_algorithm; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_RESPONSE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__auth_response__descriptor) \ , {0,NULL}, {0,NULL}, 0,NULL, 0,EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15 } struct _Extensions__CoreApi__CastChannel__AuthError { ProtobufCMessage base; Extensions__CoreApi__CastChannel__AuthError__ErrorType error_type; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__auth_error__descriptor) \ , 0 } struct _Extensions__CoreApi__CastChannel__DeviceAuthMessage { ProtobufCMessage base; Extensions__CoreApi__CastChannel__AuthChallenge *challenge; Extensions__CoreApi__CastChannel__AuthResponse *response; Extensions__CoreApi__CastChannel__AuthError *error; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__DEVICE_AUTH_MESSAGE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__device_auth_message__descriptor) \ , NULL, NULL, NULL } /* Extensions__CoreApi__CastChannel__CastMessage methods */ void extensions__core_api__cast_channel__cast_message__init (Extensions__CoreApi__CastChannel__CastMessage *message); size_t extensions__core_api__cast_channel__cast_message__get_packed_size (const Extensions__CoreApi__CastChannel__CastMessage *message); size_t extensions__core_api__cast_channel__cast_message__pack (const Extensions__CoreApi__CastChannel__CastMessage *message, uint8_t *out); size_t extensions__core_api__cast_channel__cast_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__CastMessage * extensions__core_api__cast_channel__cast_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__cast_message__free_unpacked (Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__AuthChallenge methods */ void extensions__core_api__cast_channel__auth_challenge__init (Extensions__CoreApi__CastChannel__AuthChallenge *message); size_t extensions__core_api__cast_channel__auth_challenge__get_packed_size (const Extensions__CoreApi__CastChannel__AuthChallenge *message); size_t extensions__core_api__cast_channel__auth_challenge__pack (const Extensions__CoreApi__CastChannel__AuthChallenge *message, uint8_t *out); size_t extensions__core_api__cast_channel__auth_challenge__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__AuthChallenge * extensions__core_api__cast_channel__auth_challenge__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__auth_challenge__free_unpacked (Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__AuthResponse methods */ void extensions__core_api__cast_channel__auth_response__init (Extensions__CoreApi__CastChannel__AuthResponse *message); size_t extensions__core_api__cast_channel__auth_response__get_packed_size (const Extensions__CoreApi__CastChannel__AuthResponse *message); size_t extensions__core_api__cast_channel__auth_response__pack (const Extensions__CoreApi__CastChannel__AuthResponse *message, uint8_t *out); size_t extensions__core_api__cast_channel__auth_response__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__AuthResponse * extensions__core_api__cast_channel__auth_response__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__auth_response__free_unpacked (Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__AuthError methods */ void extensions__core_api__cast_channel__auth_error__init (Extensions__CoreApi__CastChannel__AuthError *message); size_t extensions__core_api__cast_channel__auth_error__get_packed_size (const Extensions__CoreApi__CastChannel__AuthError *message); size_t extensions__core_api__cast_channel__auth_error__pack (const Extensions__CoreApi__CastChannel__AuthError *message, uint8_t *out); size_t extensions__core_api__cast_channel__auth_error__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__AuthError * extensions__core_api__cast_channel__auth_error__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__auth_error__free_unpacked (Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__DeviceAuthMessage methods */ void extensions__core_api__cast_channel__device_auth_message__init (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message); size_t extensions__core_api__cast_channel__device_auth_message__get_packed_size (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message); size_t extensions__core_api__cast_channel__device_auth_message__pack (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, uint8_t *out); size_t extensions__core_api__cast_channel__device_auth_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__DeviceAuthMessage * extensions__core_api__cast_channel__device_auth_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__device_auth_message__free_unpacked (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCAllocator *allocator); /* --- per-message closures --- */ typedef void (*Extensions__CoreApi__CastChannel__CastMessage_Closure) (const Extensions__CoreApi__CastChannel__CastMessage *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__AuthChallenge_Closure) (const Extensions__CoreApi__CastChannel__AuthChallenge *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__AuthResponse_Closure) (const Extensions__CoreApi__CastChannel__AuthResponse *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__AuthError_Closure) (const Extensions__CoreApi__CastChannel__AuthError *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__DeviceAuthMessage_Closure) (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, void *closure_data); /* --- services --- */ /* --- descriptors --- */ extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__signature_algorithm__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__cast_message__descriptor; extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__protocol_version__descriptor; extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__payload_type__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_challenge__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_response__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_error__descriptor; extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__auth_error__error_type__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__device_auth_message__descriptor; PROTOBUF_C__END_DECLS #endif /* PROTOBUF_C_cast_5fchannel_2eproto__INCLUDED */ forked-daapd-25.0/src/outputs/cast_channel.v0.pb-c.c000066400000000000000000000715521313447753700222550ustar00rootroot00000000000000/* Generated by the protocol buffer compiler. DO NOT EDIT! */ /* Do not generate deprecated warnings for self */ #ifndef PROTOBUF_C_NO_DEPRECATED #define PROTOBUF_C_NO_DEPRECATED #endif #include "cast_channel.v0.pb-c.h" void extensions__core_api__cast_channel__cast_message__init (Extensions__CoreApi__CastChannel__CastMessage *message) { static Extensions__CoreApi__CastChannel__CastMessage init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__cast_message__get_packed_size (const Extensions__CoreApi__CastChannel__CastMessage *message) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__cast_message__pack (const Extensions__CoreApi__CastChannel__CastMessage *message, uint8_t *out) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__cast_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCBuffer *buffer) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__CastMessage * extensions__core_api__cast_channel__cast_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__CastMessage *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__cast_message__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__cast_message__free_unpacked (Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCAllocator *allocator) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__cast_message__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__auth_challenge__init (Extensions__CoreApi__CastChannel__AuthChallenge *message) { static Extensions__CoreApi__CastChannel__AuthChallenge init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_CHALLENGE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__auth_challenge__get_packed_size (const Extensions__CoreApi__CastChannel__AuthChallenge *message) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__auth_challenge__pack (const Extensions__CoreApi__CastChannel__AuthChallenge *message, uint8_t *out) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__auth_challenge__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCBuffer *buffer) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__AuthChallenge * extensions__core_api__cast_channel__auth_challenge__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__AuthChallenge *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__auth_challenge__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__auth_challenge__free_unpacked (Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCAllocator *allocator) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_challenge__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__auth_response__init (Extensions__CoreApi__CastChannel__AuthResponse *message) { static Extensions__CoreApi__CastChannel__AuthResponse init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_RESPONSE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__auth_response__get_packed_size (const Extensions__CoreApi__CastChannel__AuthResponse *message) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__auth_response__pack (const Extensions__CoreApi__CastChannel__AuthResponse *message, uint8_t *out) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__auth_response__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCBuffer *buffer) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__AuthResponse * extensions__core_api__cast_channel__auth_response__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__AuthResponse *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__auth_response__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__auth_response__free_unpacked (Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCAllocator *allocator) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_response__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__auth_error__init (Extensions__CoreApi__CastChannel__AuthError *message) { static Extensions__CoreApi__CastChannel__AuthError init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__auth_error__get_packed_size (const Extensions__CoreApi__CastChannel__AuthError *message) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__auth_error__pack (const Extensions__CoreApi__CastChannel__AuthError *message, uint8_t *out) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__auth_error__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCBuffer *buffer) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__AuthError * extensions__core_api__cast_channel__auth_error__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__AuthError *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__auth_error__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__auth_error__free_unpacked (Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCAllocator *allocator) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__auth_error__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } void extensions__core_api__cast_channel__device_auth_message__init (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message) { static Extensions__CoreApi__CastChannel__DeviceAuthMessage init_value = EXTENSIONS__CORE_API__CAST_CHANNEL__DEVICE_AUTH_MESSAGE__INIT; *message = init_value; } size_t extensions__core_api__cast_channel__device_auth_message__get_packed_size (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t extensions__core_api__cast_channel__device_auth_message__pack (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, uint8_t *out) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t extensions__core_api__cast_channel__device_auth_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCBuffer *buffer) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } Extensions__CoreApi__CastChannel__DeviceAuthMessage * extensions__core_api__cast_channel__device_auth_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Extensions__CoreApi__CastChannel__DeviceAuthMessage *) protobuf_c_message_unpack (&extensions__core_api__cast_channel__device_auth_message__descriptor, allocator, len, data); } void extensions__core_api__cast_channel__device_auth_message__free_unpacked (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCAllocator *allocator) { PROTOBUF_C_ASSERT (message->base.descriptor == &extensions__core_api__cast_channel__device_auth_message__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } const ProtobufCEnumValue extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_number[1] = { { "CASTV2_1_0", "EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PROTOCOL_VERSION__CASTV2_1_0", 0 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__cast_message__protocol_version__value_ranges[] = { {0, 0},{0, 1} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_name[1] = { { "CASTV2_1_0", 0 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__protocol_version__descriptor = { PROTOBUF_C_ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.CastMessage.ProtocolVersion", "ProtocolVersion", "Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion", "extensions.core_api.cast_channel", 1, extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_number, 1, extensions__core_api__cast_channel__cast_message__protocol_version__enum_values_by_name, 1, extensions__core_api__cast_channel__cast_message__protocol_version__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; const ProtobufCEnumValue extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_number[2] = { { "STRING", "EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__STRING", 0 }, { "BINARY", "EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__BINARY", 1 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__cast_message__payload_type__value_ranges[] = { {0, 0},{0, 2} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_name[2] = { { "BINARY", 1 }, { "STRING", 0 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__payload_type__descriptor = { PROTOBUF_C_ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.CastMessage.PayloadType", "PayloadType", "Extensions__CoreApi__CastChannel__CastMessage__PayloadType", "extensions.core_api.cast_channel", 2, extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_number, 2, extensions__core_api__cast_channel__cast_message__payload_type__enum_values_by_name, 1, extensions__core_api__cast_channel__cast_message__payload_type__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__cast_message__field_descriptors[7] = { { "protocol_version", 1, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, protocol_version), &extensions__core_api__cast_channel__cast_message__protocol_version__descriptor, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "source_id", 2, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, source_id), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "destination_id", 3, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, destination_id), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "namespace", 4, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, namespace_), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "payload_type", 5, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, payload_type), &extensions__core_api__cast_channel__cast_message__payload_type__descriptor, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "payload_utf8", 6, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, payload_utf8), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "payload_binary", 7, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_BYTES, PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, has_payload_binary), PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__CastMessage, payload_binary), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__cast_message__field_indices_by_name[] = { 2, /* field[2] = destination_id */ 3, /* field[3] = namespace */ 6, /* field[6] = payload_binary */ 4, /* field[4] = payload_type */ 5, /* field[5] = payload_utf8 */ 0, /* field[0] = protocol_version */ 1, /* field[1] = source_id */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__cast_message__number_ranges[1 + 1] = { { 1, 0 }, { 0, 7 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__cast_message__descriptor = { PROTOBUF_C_MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.CastMessage", "CastMessage", "Extensions__CoreApi__CastChannel__CastMessage", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__CastMessage), 7, extensions__core_api__cast_channel__cast_message__field_descriptors, extensions__core_api__cast_channel__cast_message__field_indices_by_name, 1, extensions__core_api__cast_channel__cast_message__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__cast_message__init, NULL,NULL,NULL /* reserved[123] */ }; static const Extensions__CoreApi__CastChannel__SignatureAlgorithm extensions__core_api__cast_channel__auth_challenge__signature_algorithm__default_value = EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__auth_challenge__field_descriptors[1] = { { "signature_algorithm", 1, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_ENUM, PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthChallenge, has_signature_algorithm), PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthChallenge, signature_algorithm), &extensions__core_api__cast_channel__signature_algorithm__descriptor, &extensions__core_api__cast_channel__auth_challenge__signature_algorithm__default_value, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__auth_challenge__field_indices_by_name[] = { 0, /* field[0] = signature_algorithm */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_challenge__number_ranges[1 + 1] = { { 1, 0 }, { 0, 1 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_challenge__descriptor = { PROTOBUF_C_MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthChallenge", "AuthChallenge", "Extensions__CoreApi__CastChannel__AuthChallenge", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__AuthChallenge), 1, extensions__core_api__cast_channel__auth_challenge__field_descriptors, extensions__core_api__cast_channel__auth_challenge__field_indices_by_name, 1, extensions__core_api__cast_channel__auth_challenge__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__auth_challenge__init, NULL,NULL,NULL /* reserved[123] */ }; static const Extensions__CoreApi__CastChannel__SignatureAlgorithm extensions__core_api__cast_channel__auth_response__signature_algorithm__default_value = EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__auth_response__field_descriptors[4] = { { "signature", 1, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_BYTES, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthResponse, signature), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "client_auth_certificate", 2, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_BYTES, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthResponse, client_auth_certificate), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "intermediate_certificate", 3, PROTOBUF_C_LABEL_REPEATED, PROTOBUF_C_TYPE_BYTES, PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthResponse, n_intermediate_certificate), PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthResponse, intermediate_certificate), NULL, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "signature_algorithm", 4, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_ENUM, PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthResponse, has_signature_algorithm), PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthResponse, signature_algorithm), &extensions__core_api__cast_channel__signature_algorithm__descriptor, &extensions__core_api__cast_channel__auth_response__signature_algorithm__default_value, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__auth_response__field_indices_by_name[] = { 1, /* field[1] = client_auth_certificate */ 2, /* field[2] = intermediate_certificate */ 0, /* field[0] = signature */ 3, /* field[3] = signature_algorithm */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_response__number_ranges[1 + 1] = { { 1, 0 }, { 0, 4 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_response__descriptor = { PROTOBUF_C_MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthResponse", "AuthResponse", "Extensions__CoreApi__CastChannel__AuthResponse", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__AuthResponse), 4, extensions__core_api__cast_channel__auth_response__field_descriptors, extensions__core_api__cast_channel__auth_response__field_indices_by_name, 1, extensions__core_api__cast_channel__auth_response__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__auth_response__init, NULL,NULL,NULL /* reserved[123] */ }; const ProtobufCEnumValue extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_number[3] = { { "INTERNAL_ERROR", "EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__INTERNAL_ERROR", 0 }, { "NO_TLS", "EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__NO_TLS", 1 }, { "SIGNATURE_ALGORITHM_UNAVAILABLE", "EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__SIGNATURE_ALGORITHM_UNAVAILABLE", 2 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_error__error_type__value_ranges[] = { {0, 0},{0, 3} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_name[3] = { { "INTERNAL_ERROR", 0 }, { "NO_TLS", 1 }, { "SIGNATURE_ALGORITHM_UNAVAILABLE", 2 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__auth_error__error_type__descriptor = { PROTOBUF_C_ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthError.ErrorType", "ErrorType", "Extensions__CoreApi__CastChannel__AuthError__ErrorType", "extensions.core_api.cast_channel", 3, extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_number, 3, extensions__core_api__cast_channel__auth_error__error_type__enum_values_by_name, 1, extensions__core_api__cast_channel__auth_error__error_type__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__auth_error__field_descriptors[1] = { { "error_type", 1, PROTOBUF_C_LABEL_REQUIRED, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__AuthError, error_type), &extensions__core_api__cast_channel__auth_error__error_type__descriptor, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__auth_error__field_indices_by_name[] = { 0, /* field[0] = error_type */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__auth_error__number_ranges[1 + 1] = { { 1, 0 }, { 0, 1 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_error__descriptor = { PROTOBUF_C_MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.AuthError", "AuthError", "Extensions__CoreApi__CastChannel__AuthError", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__AuthError), 1, extensions__core_api__cast_channel__auth_error__field_descriptors, extensions__core_api__cast_channel__auth_error__field_indices_by_name, 1, extensions__core_api__cast_channel__auth_error__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__auth_error__init, NULL,NULL,NULL /* reserved[123] */ }; static const ProtobufCFieldDescriptor extensions__core_api__cast_channel__device_auth_message__field_descriptors[3] = { { "challenge", 1, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_MESSAGE, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__DeviceAuthMessage, challenge), &extensions__core_api__cast_channel__auth_challenge__descriptor, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "response", 2, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_MESSAGE, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__DeviceAuthMessage, response), &extensions__core_api__cast_channel__auth_response__descriptor, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "error", 3, PROTOBUF_C_LABEL_OPTIONAL, PROTOBUF_C_TYPE_MESSAGE, 0, /* quantifier_offset */ PROTOBUF_C_OFFSETOF(Extensions__CoreApi__CastChannel__DeviceAuthMessage, error), &extensions__core_api__cast_channel__auth_error__descriptor, NULL, 0, /* packed */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned extensions__core_api__cast_channel__device_auth_message__field_indices_by_name[] = { 0, /* field[0] = challenge */ 2, /* field[2] = error */ 1, /* field[1] = response */ }; static const ProtobufCIntRange extensions__core_api__cast_channel__device_auth_message__number_ranges[1 + 1] = { { 1, 0 }, { 0, 3 } }; const ProtobufCMessageDescriptor extensions__core_api__cast_channel__device_auth_message__descriptor = { PROTOBUF_C_MESSAGE_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.DeviceAuthMessage", "DeviceAuthMessage", "Extensions__CoreApi__CastChannel__DeviceAuthMessage", "extensions.core_api.cast_channel", sizeof(Extensions__CoreApi__CastChannel__DeviceAuthMessage), 3, extensions__core_api__cast_channel__device_auth_message__field_descriptors, extensions__core_api__cast_channel__device_auth_message__field_indices_by_name, 1, extensions__core_api__cast_channel__device_auth_message__number_ranges, (ProtobufCMessageInit) extensions__core_api__cast_channel__device_auth_message__init, NULL,NULL,NULL /* reserved[123] */ }; const ProtobufCEnumValue extensions__core_api__cast_channel__signature_algorithm__enum_values_by_number[3] = { { "UNSPECIFIED", "EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__UNSPECIFIED", 0 }, { "RSASSA_PKCS1v15", "EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15", 1 }, { "RSASSA_PSS", "EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PSS", 2 }, }; static const ProtobufCIntRange extensions__core_api__cast_channel__signature_algorithm__value_ranges[] = { {0, 0},{0, 3} }; const ProtobufCEnumValueIndex extensions__core_api__cast_channel__signature_algorithm__enum_values_by_name[3] = { { "RSASSA_PKCS1v15", 1 }, { "RSASSA_PSS", 2 }, { "UNSPECIFIED", 0 }, }; const ProtobufCEnumDescriptor extensions__core_api__cast_channel__signature_algorithm__descriptor = { PROTOBUF_C_ENUM_DESCRIPTOR_MAGIC, "extensions.core_api.cast_channel.SignatureAlgorithm", "SignatureAlgorithm", "Extensions__CoreApi__CastChannel__SignatureAlgorithm", "extensions.core_api.cast_channel", 3, extensions__core_api__cast_channel__signature_algorithm__enum_values_by_number, 3, extensions__core_api__cast_channel__signature_algorithm__enum_values_by_name, 1, extensions__core_api__cast_channel__signature_algorithm__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; forked-daapd-25.0/src/outputs/cast_channel.v0.pb-c.h000066400000000000000000000313361313447753700222560ustar00rootroot00000000000000/* Generated by the protocol buffer compiler. DO NOT EDIT! */ #ifndef PROTOBUF_C_cast_5fchannel_2eproto__INCLUDED #define PROTOBUF_C_cast_5fchannel_2eproto__INCLUDED #include PROTOBUF_C_BEGIN_DECLS typedef struct _Extensions__CoreApi__CastChannel__CastMessage Extensions__CoreApi__CastChannel__CastMessage; typedef struct _Extensions__CoreApi__CastChannel__AuthChallenge Extensions__CoreApi__CastChannel__AuthChallenge; typedef struct _Extensions__CoreApi__CastChannel__AuthResponse Extensions__CoreApi__CastChannel__AuthResponse; typedef struct _Extensions__CoreApi__CastChannel__AuthError Extensions__CoreApi__CastChannel__AuthError; typedef struct _Extensions__CoreApi__CastChannel__DeviceAuthMessage Extensions__CoreApi__CastChannel__DeviceAuthMessage; /* --- enums --- */ typedef enum _Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion { EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PROTOCOL_VERSION__CASTV2_1_0 = 0 } Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion; typedef enum _Extensions__CoreApi__CastChannel__CastMessage__PayloadType { EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__STRING = 0, EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__PAYLOAD_TYPE__BINARY = 1 } Extensions__CoreApi__CastChannel__CastMessage__PayloadType; typedef enum _Extensions__CoreApi__CastChannel__AuthError__ErrorType { EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__INTERNAL_ERROR = 0, EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__NO_TLS = 1, EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__ERROR_TYPE__SIGNATURE_ALGORITHM_UNAVAILABLE = 2 } Extensions__CoreApi__CastChannel__AuthError__ErrorType; typedef enum _Extensions__CoreApi__CastChannel__SignatureAlgorithm { EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__UNSPECIFIED = 0, EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15 = 1, EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PSS = 2 } Extensions__CoreApi__CastChannel__SignatureAlgorithm; /* --- messages --- */ struct _Extensions__CoreApi__CastChannel__CastMessage { ProtobufCMessage base; Extensions__CoreApi__CastChannel__CastMessage__ProtocolVersion protocol_version; char *source_id; char *destination_id; char *namespace_; Extensions__CoreApi__CastChannel__CastMessage__PayloadType payload_type; char *payload_utf8; protobuf_c_boolean has_payload_binary; ProtobufCBinaryData payload_binary; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__CAST_MESSAGE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__cast_message__descriptor) \ , 0, NULL, NULL, NULL, 0, NULL, 0,{0,NULL} } struct _Extensions__CoreApi__CastChannel__AuthChallenge { ProtobufCMessage base; protobuf_c_boolean has_signature_algorithm; Extensions__CoreApi__CastChannel__SignatureAlgorithm signature_algorithm; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_CHALLENGE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__auth_challenge__descriptor) \ , 0,EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15 } struct _Extensions__CoreApi__CastChannel__AuthResponse { ProtobufCMessage base; ProtobufCBinaryData signature; ProtobufCBinaryData client_auth_certificate; size_t n_intermediate_certificate; ProtobufCBinaryData *intermediate_certificate; protobuf_c_boolean has_signature_algorithm; Extensions__CoreApi__CastChannel__SignatureAlgorithm signature_algorithm; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_RESPONSE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__auth_response__descriptor) \ , {0,NULL}, {0,NULL}, 0,NULL, 0,EXTENSIONS__CORE_API__CAST_CHANNEL__SIGNATURE_ALGORITHM__RSASSA_PKCS1V15 } struct _Extensions__CoreApi__CastChannel__AuthError { ProtobufCMessage base; Extensions__CoreApi__CastChannel__AuthError__ErrorType error_type; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__AUTH_ERROR__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__auth_error__descriptor) \ , 0 } struct _Extensions__CoreApi__CastChannel__DeviceAuthMessage { ProtobufCMessage base; Extensions__CoreApi__CastChannel__AuthChallenge *challenge; Extensions__CoreApi__CastChannel__AuthResponse *response; Extensions__CoreApi__CastChannel__AuthError *error; }; #define EXTENSIONS__CORE_API__CAST_CHANNEL__DEVICE_AUTH_MESSAGE__INIT \ { PROTOBUF_C_MESSAGE_INIT (&extensions__core_api__cast_channel__device_auth_message__descriptor) \ , NULL, NULL, NULL } /* Extensions__CoreApi__CastChannel__CastMessage methods */ void extensions__core_api__cast_channel__cast_message__init (Extensions__CoreApi__CastChannel__CastMessage *message); size_t extensions__core_api__cast_channel__cast_message__get_packed_size (const Extensions__CoreApi__CastChannel__CastMessage *message); size_t extensions__core_api__cast_channel__cast_message__pack (const Extensions__CoreApi__CastChannel__CastMessage *message, uint8_t *out); size_t extensions__core_api__cast_channel__cast_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__CastMessage * extensions__core_api__cast_channel__cast_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__cast_message__free_unpacked (Extensions__CoreApi__CastChannel__CastMessage *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__AuthChallenge methods */ void extensions__core_api__cast_channel__auth_challenge__init (Extensions__CoreApi__CastChannel__AuthChallenge *message); size_t extensions__core_api__cast_channel__auth_challenge__get_packed_size (const Extensions__CoreApi__CastChannel__AuthChallenge *message); size_t extensions__core_api__cast_channel__auth_challenge__pack (const Extensions__CoreApi__CastChannel__AuthChallenge *message, uint8_t *out); size_t extensions__core_api__cast_channel__auth_challenge__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__AuthChallenge * extensions__core_api__cast_channel__auth_challenge__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__auth_challenge__free_unpacked (Extensions__CoreApi__CastChannel__AuthChallenge *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__AuthResponse methods */ void extensions__core_api__cast_channel__auth_response__init (Extensions__CoreApi__CastChannel__AuthResponse *message); size_t extensions__core_api__cast_channel__auth_response__get_packed_size (const Extensions__CoreApi__CastChannel__AuthResponse *message); size_t extensions__core_api__cast_channel__auth_response__pack (const Extensions__CoreApi__CastChannel__AuthResponse *message, uint8_t *out); size_t extensions__core_api__cast_channel__auth_response__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__AuthResponse * extensions__core_api__cast_channel__auth_response__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__auth_response__free_unpacked (Extensions__CoreApi__CastChannel__AuthResponse *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__AuthError methods */ void extensions__core_api__cast_channel__auth_error__init (Extensions__CoreApi__CastChannel__AuthError *message); size_t extensions__core_api__cast_channel__auth_error__get_packed_size (const Extensions__CoreApi__CastChannel__AuthError *message); size_t extensions__core_api__cast_channel__auth_error__pack (const Extensions__CoreApi__CastChannel__AuthError *message, uint8_t *out); size_t extensions__core_api__cast_channel__auth_error__pack_to_buffer (const Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__AuthError * extensions__core_api__cast_channel__auth_error__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__auth_error__free_unpacked (Extensions__CoreApi__CastChannel__AuthError *message, ProtobufCAllocator *allocator); /* Extensions__CoreApi__CastChannel__DeviceAuthMessage methods */ void extensions__core_api__cast_channel__device_auth_message__init (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message); size_t extensions__core_api__cast_channel__device_auth_message__get_packed_size (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message); size_t extensions__core_api__cast_channel__device_auth_message__pack (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, uint8_t *out); size_t extensions__core_api__cast_channel__device_auth_message__pack_to_buffer (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCBuffer *buffer); Extensions__CoreApi__CastChannel__DeviceAuthMessage * extensions__core_api__cast_channel__device_auth_message__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void extensions__core_api__cast_channel__device_auth_message__free_unpacked (Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, ProtobufCAllocator *allocator); /* --- per-message closures --- */ typedef void (*Extensions__CoreApi__CastChannel__CastMessage_Closure) (const Extensions__CoreApi__CastChannel__CastMessage *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__AuthChallenge_Closure) (const Extensions__CoreApi__CastChannel__AuthChallenge *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__AuthResponse_Closure) (const Extensions__CoreApi__CastChannel__AuthResponse *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__AuthError_Closure) (const Extensions__CoreApi__CastChannel__AuthError *message, void *closure_data); typedef void (*Extensions__CoreApi__CastChannel__DeviceAuthMessage_Closure) (const Extensions__CoreApi__CastChannel__DeviceAuthMessage *message, void *closure_data); /* --- services --- */ /* --- descriptors --- */ extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__signature_algorithm__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__cast_message__descriptor; extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__protocol_version__descriptor; extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__cast_message__payload_type__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_challenge__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_response__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__auth_error__descriptor; extern const ProtobufCEnumDescriptor extensions__core_api__cast_channel__auth_error__error_type__descriptor; extern const ProtobufCMessageDescriptor extensions__core_api__cast_channel__device_auth_message__descriptor; PROTOBUF_C_END_DECLS #endif /* PROTOBUF_cast_5fchannel_2eproto__INCLUDED */ forked-daapd-25.0/src/outputs/dummy.c000066400000000000000000000144601313447753700176150ustar00rootroot00000000000000/* * Copyright (C) 2016 Espen Jürgensen * * 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 */ /* This file includes much of the boilerplate code required for making an * audio output for forked-daapd. */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include "conffile.h" #include "logger.h" #include "player.h" #include "outputs.h" struct dummy_session { enum output_device_state state; struct event *deferredev; output_status_cb defer_cb; /* Do not dereference - only passed to the status cb */ struct output_device *device; struct output_session *output_session; output_status_cb status_cb; }; /* From player.c */ extern struct event_base *evbase_player; struct dummy_session *sessions; /* Forwards */ static void defer_cb(int fd, short what, void *arg); /* ---------------------------- SESSION HANDLING ---------------------------- */ static void dummy_session_free(struct dummy_session *ds) { event_free(ds->deferredev); free(ds->output_session); free(ds); ds = NULL; } static void dummy_session_cleanup(struct dummy_session *ds) { // Normally some here code to remove from linked list - here we just say: sessions = NULL; dummy_session_free(ds); } static struct dummy_session * dummy_session_make(struct output_device *device, output_status_cb cb) { struct output_session *os; struct dummy_session *ds; os = calloc(1, sizeof(struct output_session)); if (!os) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (os)\n"); return NULL; } ds = calloc(1, sizeof(struct dummy_session)); if (!ds) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (as)\n"); free(os); return NULL; } ds->deferredev = evtimer_new(evbase_player, defer_cb, ds); if (!ds->deferredev) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy deferred event\n"); free(os); free(ds); return NULL; } os->session = ds; os->type = device->type; ds->output_session = os; ds->state = OUTPUT_STATE_CONNECTED; ds->device = device; ds->status_cb = cb; sessions = ds; return ds; } /* ---------------------------- STATUS HANDLERS ----------------------------- */ // Maps our internal state to the generic output state and then makes a callback // to the player to tell that state static void defer_cb(int fd, short what, void *arg) { struct dummy_session *ds = arg; if (ds->defer_cb) ds->defer_cb(ds->device, ds->output_session, ds->state); if (ds->state == OUTPUT_STATE_STOPPED) dummy_session_cleanup(ds); } static void dummy_status(struct dummy_session *ds) { ds->defer_cb = ds->status_cb; event_active(ds->deferredev, 0, 0); ds->status_cb = NULL; } /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int dummy_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) { struct dummy_session *ds; ds = dummy_session_make(device, cb); if (!ds) return -1; dummy_status(ds); return 0; } static void dummy_device_stop(struct output_session *session) { struct dummy_session *ds = session->session; ds->state = OUTPUT_STATE_STOPPED; dummy_status(ds); } static int dummy_device_probe(struct output_device *device, output_status_cb cb) { struct dummy_session *ds; ds = dummy_session_make(device, cb); if (!ds) return -1; ds->status_cb = cb; ds->state = OUTPUT_STATE_STOPPED; dummy_status(ds); return 0; } static int dummy_device_volume_set(struct output_device *device, output_status_cb cb) { struct dummy_session *ds; if (!device->session || !device->session->session) return 0; ds = device->session->session; ds->status_cb = cb; dummy_status(ds); return 1; } static void dummy_playback_start(uint64_t next_pkt, struct timespec *ts) { struct dummy_session *ds = sessions; if (!sessions) return; ds->state = OUTPUT_STATE_STREAMING; dummy_status(ds); } static void dummy_playback_stop(void) { struct dummy_session *ds = sessions; if (!sessions) return; ds->state = OUTPUT_STATE_CONNECTED; dummy_status(ds); } static void dummy_set_status_cb(struct output_session *session, output_status_cb cb) { struct dummy_session *ds = session->session; ds->status_cb = cb; } static int dummy_init(void) { struct output_device *device; cfg_t *cfg_audio; char *nickname; char *type; cfg_audio = cfg_getsec(cfg, "audio"); type = cfg_getstr(cfg_audio, "type"); if (!type || (strcasecmp(type, "dummy") != 0)) return -1; nickname = cfg_getstr(cfg_audio, "nickname"); device = calloc(1, sizeof(struct output_device)); if (!device) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy device\n"); return -1; } device->id = 0; device->name = strdup(nickname); device->type = OUTPUT_TYPE_DUMMY; device->type_name = outputs_name(device->type); device->advertised = 1; device->has_video = 0; DPRINTF(E_INFO, L_LAUDIO, "Adding dummy output device '%s'\n", nickname); player_device_add(device); return 0; } static void dummy_deinit(void) { return; } struct output_definition output_dummy = { .name = "dummy", .type = OUTPUT_TYPE_DUMMY, .priority = 99, .disabled = 0, .init = dummy_init, .deinit = dummy_deinit, .device_start = dummy_device_start, .device_stop = dummy_device_stop, .device_probe = dummy_device_probe, .device_volume_set = dummy_device_volume_set, .playback_start = dummy_playback_start, .playback_stop = dummy_playback_stop, .status_cb = dummy_set_status_cb, }; forked-daapd-25.0/src/outputs/fifo.c000066400000000000000000000306431313447753700174060ustar00rootroot00000000000000/* * Copyright (C) 2016 Christian Meffert * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include "misc.h" #include "conffile.h" #include "logger.h" #include "player.h" #include "outputs.h" #define FIFO_BUFFER_SIZE 65536 /* pipe capacity on Linux >= 2.6.11 */ struct fifo_packet { /* pcm data */ uint8_t samples[1408]; // STOB(AIRTUNES_V2_PACKET_SAMPLES) /* RTP-time of the first sample*/ uint64_t rtptime; struct fifo_packet *next; struct fifo_packet *prev; }; struct fifo_buffer { struct fifo_packet *head; struct fifo_packet *tail; }; static struct fifo_buffer buffer; static void free_buffer() { struct fifo_packet *packet; struct fifo_packet *tmp; packet = buffer.tail; while (packet) { tmp = packet; packet = packet->next; free(tmp); } buffer.tail = NULL; buffer.head = NULL; } struct fifo_session { enum output_device_state state; char *path; int input_fd; int output_fd; int created; struct event *deferredev; output_status_cb defer_cb; /* Do not dereference - only passed to the status cb */ struct output_device *device; struct output_session *output_session; output_status_cb status_cb; }; /* From player.c */ extern struct event_base *evbase_player; static struct fifo_session *sessions; /* Forwards */ static void defer_cb(int fd, short what, void *arg); /* ---------------------------- FIFO HANDLING ---------------------------- */ static void fifo_delete(struct fifo_session *fifo_session) { DPRINTF(E_DBG, L_FIFO, "Removing FIFO \"%s\"\n", fifo_session->path); if (unlink(fifo_session->path) < 0) { DPRINTF(E_WARN, L_FIFO, "Could not remove FIFO \"%s\": %d\n", fifo_session->path, errno); return; } fifo_session->created = 0; } static void fifo_close(struct fifo_session *fifo_session) { struct stat st; if (fifo_session->input_fd > 0) { close(fifo_session->input_fd); fifo_session->input_fd = -1; } if (fifo_session->output_fd > 0) { close(fifo_session->output_fd); fifo_session->output_fd = -1; } if (fifo_session->created && (stat(fifo_session->path, &st) == 0)) fifo_delete(fifo_session); } static int fifo_make(struct fifo_session *fifo_session) { DPRINTF(E_DBG, L_FIFO, "Creating FIFO \"%s\"\n", fifo_session->path); if (mkfifo(fifo_session->path, 0666) < 0) { DPRINTF(E_LOG, L_FIFO, "Could not create FIFO \"%s\": %d\n", fifo_session->path, errno); return -1; } fifo_session->created = 1; return 0; } static int fifo_check(struct fifo_session *fifo_session) { struct stat st; if (stat(fifo_session->path, &st) < 0) { if (errno == ENOENT) { /* Path doesn't exist */ return fifo_make(fifo_session); } DPRINTF(E_LOG, L_FIFO, "Failed to stat FIFO \"%s\": %d\n", fifo_session->path, errno); return -1; } if (!S_ISFIFO(st.st_mode)) { DPRINTF(E_LOG, L_FIFO, "\"%s\" already exists, but is not a FIFO\n", fifo_session->path); return -1; } return 0; } static int fifo_open(struct fifo_session *fifo_session) { int ret; ret = fifo_check(fifo_session); if (ret < 0) return -1; fifo_session->input_fd = open(fifo_session->path, O_RDONLY | O_NONBLOCK, 0); if (fifo_session->input_fd < 0) { DPRINTF(E_LOG, L_FIFO, "Could not open FIFO \"%s\" for reading: %d\n", fifo_session->path, errno); fifo_close(fifo_session); return -1; } fifo_session->output_fd = open(fifo_session->path, O_WRONLY | O_NONBLOCK, 0); if (fifo_session->output_fd < 0) { DPRINTF(E_LOG, L_FIFO, "Could not open FIFO \"%s\" for writing: %d\n", fifo_session->path, errno); fifo_close(fifo_session); return -1; } return 0; } static void fifo_empty(struct fifo_session *fifo_session) { char buf[FIFO_BUFFER_SIZE]; int bytes = 1; while (bytes > 0 && errno != EINTR) bytes = read(fifo_session->input_fd, buf, FIFO_BUFFER_SIZE); if (bytes < 0 && errno != EAGAIN) { DPRINTF(E_LOG, L_FIFO, "Flush of FIFO \"%s\" failed: %d\n", fifo_session->path, errno); } } /* ---------------------------- SESSION HANDLING ---------------------------- */ static void fifo_session_free(struct fifo_session *fifo_session) { event_free(fifo_session->deferredev); free(fifo_session->output_session); free(fifo_session); free_buffer(); fifo_session = NULL; } static void fifo_session_cleanup(struct fifo_session *fifo_session) { // Normally some here code to remove from linked list - here we just say: sessions = NULL; fifo_session_free(fifo_session); } static struct fifo_session * fifo_session_make(struct output_device *device, output_status_cb cb) { struct output_session *output_session; struct fifo_session *fifo_session; output_session = calloc(1, sizeof(struct output_session)); if (!output_session) { DPRINTF(E_LOG, L_FIFO, "Out of memory (os)\n"); return NULL; } fifo_session = calloc(1, sizeof(struct fifo_session)); if (!fifo_session) { DPRINTF(E_LOG, L_FIFO, "Out of memory (fs)\n"); free(output_session); return NULL; } fifo_session->deferredev = evtimer_new(evbase_player, defer_cb, fifo_session); if (!fifo_session->deferredev) { DPRINTF(E_LOG, L_FIFO, "Out of memory for fifo deferred event\n"); free(output_session); free(fifo_session); return NULL; } output_session->session = fifo_session; output_session->type = device->type; fifo_session->output_session = output_session; fifo_session->state = OUTPUT_STATE_CONNECTED; fifo_session->device = device; fifo_session->status_cb = cb; fifo_session->created = 0; fifo_session->path = device->extra_device_info; fifo_session->input_fd = -1; fifo_session->output_fd = -1; sessions = fifo_session; return fifo_session; } /* ---------------------------- STATUS HANDLERS ----------------------------- */ // Maps our internal state to the generic output state and then makes a callback // to the player to tell that state static void defer_cb(int fd, short what, void *arg) { struct fifo_session *ds = arg; if (ds->defer_cb) ds->defer_cb(ds->device, ds->output_session, ds->state); if (ds->state == OUTPUT_STATE_STOPPED) fifo_session_cleanup(ds); } static void fifo_status(struct fifo_session *fifo_session) { fifo_session->defer_cb = fifo_session->status_cb; event_active(fifo_session->deferredev, 0, 0); fifo_session->status_cb = NULL; } /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int fifo_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) { struct fifo_session *fifo_session; int ret; fifo_session = fifo_session_make(device, cb); if (!fifo_session) return -1; ret = fifo_open(fifo_session); if (ret < 0) return -1; fifo_status(fifo_session); return 0; } static void fifo_device_stop(struct output_session *output_session) { struct fifo_session *fifo_session = output_session->session; fifo_close(fifo_session); free_buffer(); fifo_session->state = OUTPUT_STATE_STOPPED; fifo_status(fifo_session); } static int fifo_device_probe(struct output_device *device, output_status_cb cb) { struct fifo_session *fifo_session; int ret; fifo_session = fifo_session_make(device, cb); if (!fifo_session) return -1; ret = fifo_open(fifo_session); if (ret < 0) { fifo_session_cleanup(fifo_session); return -1; } fifo_close(fifo_session); fifo_session->status_cb = cb; fifo_session->state = OUTPUT_STATE_STOPPED; fifo_status(fifo_session); return 0; } static int fifo_device_volume_set(struct output_device *device, output_status_cb cb) { struct fifo_session *fifo_session; if (!device->session || !device->session->session) return 0; fifo_session = device->session->session; fifo_session->status_cb = cb; fifo_status(fifo_session); return 1; } static void fifo_playback_start(uint64_t next_pkt, struct timespec *ts) { struct fifo_session *fifo_session = sessions; if (!fifo_session) return; fifo_session->state = OUTPUT_STATE_STREAMING; fifo_status(fifo_session); } static void fifo_playback_stop(void) { struct fifo_session *fifo_session = sessions; if (!fifo_session) return; free_buffer(); fifo_session->state = OUTPUT_STATE_CONNECTED; fifo_status(fifo_session); } static int fifo_flush(output_status_cb cb, uint64_t rtptime) { struct fifo_session *fifo_session = sessions; if (!fifo_session) return 0; fifo_empty(fifo_session); free_buffer(); fifo_session->status_cb = cb; fifo_session->state = OUTPUT_STATE_CONNECTED; fifo_status(fifo_session); return 1; } static void fifo_write(uint8_t *buf, uint64_t rtptime) { struct fifo_session *fifo_session = sessions; size_t length = STOB(AIRTUNES_V2_PACKET_SAMPLES); ssize_t bytes; struct fifo_packet *packet; uint64_t cur_pos; struct timespec now; int ret; if (!fifo_session || !fifo_session->device->selected) return; packet = (struct fifo_packet *) calloc(1, sizeof(struct fifo_packet)); memcpy(packet->samples, buf, sizeof(packet->samples)); packet->rtptime = rtptime; if (buffer.head) { buffer.head->next = packet; packet->prev = buffer.head; } buffer.head = packet; if (!buffer.tail) buffer.tail = packet; ret = player_get_current_pos(&cur_pos, &now, 0); if (ret < 0) { DPRINTF(E_LOG, L_FIFO, "Could not get playback position\n"); return; } while (buffer.tail && buffer.tail->rtptime <= cur_pos) { bytes = write(fifo_session->output_fd, buffer.tail->samples, length); if (bytes > 0) { packet = buffer.tail; buffer.tail = buffer.tail->next; free(packet); return; } if (bytes < 0) { switch (errno) { case EAGAIN: /* The pipe is full, so empty it */ fifo_empty(fifo_session); continue; case EINTR: continue; } DPRINTF(E_LOG, L_FIFO, "Failed to write to FIFO %s: %d\n", fifo_session->path, errno); return; } } } static void fifo_set_status_cb(struct output_session *session, output_status_cb cb) { struct fifo_session *fifo_session = session->session; fifo_session->status_cb = cb; } static int fifo_init(void) { struct output_device *device; cfg_t *cfg_fifo; char *nickname; char *path; cfg_fifo = cfg_getsec(cfg, "fifo"); if (!cfg_fifo) return -1; path = cfg_getstr(cfg_fifo, "path"); if (!path) return -1; nickname = cfg_getstr(cfg_fifo, "nickname"); memset(&buffer, 0, sizeof(struct fifo_buffer)); device = calloc(1, sizeof(struct output_device)); if (!device) { DPRINTF(E_LOG, L_FIFO, "Out of memory for fifo device\n"); return -1; } device->id = 100; device->name = strdup(nickname); device->type = OUTPUT_TYPE_FIFO; device->type_name = outputs_name(device->type); device->advertised = 1; device->has_video = 0; device->extra_device_info = path; DPRINTF(E_INFO, L_FIFO, "Adding fifo output device '%s' with path '%s'\n", nickname, path); player_device_add(device); return 0; } static void fifo_deinit(void) { return; } struct output_definition output_fifo = { .name = "fifo", .type = OUTPUT_TYPE_FIFO, .priority = 98, .disabled = 0, .init = fifo_init, .deinit = fifo_deinit, .device_start = fifo_device_start, .device_stop = fifo_device_stop, .device_probe = fifo_device_probe, .device_volume_set = fifo_device_volume_set, .playback_start = fifo_playback_start, .playback_stop = fifo_playback_stop, .write = fifo_write, .flush = fifo_flush, .status_cb = fifo_set_status_cb, }; forked-daapd-25.0/src/outputs/pulse.c000066400000000000000000000562451313447753700176210ustar00rootroot00000000000000/* * Copyright (C) 2016- Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include "misc.h" #include "conffile.h" #include "logger.h" #include "player.h" #include "outputs.h" #include "commands.h" #define PULSE_MAX_DEVICES 64 #define PULSE_LOG_MAX 10 /* TODO for Pulseaudio - Add real sync with AirPlay - Allow per-sink latency config */ struct pulse { pa_threaded_mainloop *mainloop; pa_context *context; struct commands_base *cmdbase; int operation_success; } pulse; struct pulse_session { pa_stream_state_t state; pa_stream *stream; pa_buffer_attr attr; pa_volume_t volume; int logcount; char *devname; /* Do not dereference - only passed to the status cb */ struct output_device *device; struct output_session *output_session; output_status_cb status_cb; struct pulse_session *next; }; // From player.c extern struct event_base *evbase_player; // Globals static struct pulse_session *sessions; // Internal list with indeces of the Pulseaudio devices (sinks) we have registered static uint32_t pulse_known_devices[PULSE_MAX_DEVICES]; // Converts from 0 - 100 to Pulseaudio's scale static inline pa_volume_t pulse_from_device_volume(int device_volume) { return (PA_VOLUME_MUTED + (device_volume * (PA_VOLUME_NORM - PA_VOLUME_MUTED)) / 100); } /* ---------------------------- SESSION HANDLING ---------------------------- */ static void pulse_session_free(struct pulse_session *ps) { if (ps->stream) { pa_threaded_mainloop_lock(pulse.mainloop); pa_stream_set_underflow_callback(ps->stream, NULL, NULL); pa_stream_set_overflow_callback(ps->stream, NULL, NULL); pa_stream_set_state_callback(ps->stream, NULL, NULL); pa_stream_disconnect(ps->stream); pa_stream_unref(ps->stream); pa_threaded_mainloop_unlock(pulse.mainloop); } if (ps->devname) free(ps->devname); free(ps->output_session); free(ps); } static void pulse_session_cleanup(struct pulse_session *ps) { struct pulse_session *p; if (ps == sessions) sessions = sessions->next; else { for (p = sessions; p && (p->next != ps); p = p->next) ; /* EMPTY */ if (!p) DPRINTF(E_WARN, L_LAUDIO, "WARNING: struct pulse_session not found in list; BUG!\n"); else p->next = ps->next; } pulse_session_free(ps); } static struct pulse_session * pulse_session_make(struct output_device *device, output_status_cb cb) { struct output_session *os; struct pulse_session *ps; os = calloc(1, sizeof(struct output_session)); if (!os) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory (os)\n"); return NULL; } ps = calloc(1, sizeof(struct pulse_session)); if (!ps) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory (ps)\n"); free(os); return NULL; } os->session = ps; os->type = device->type; ps->output_session = os; ps->state = PA_STREAM_UNCONNECTED; ps->device = device; ps->status_cb = cb; ps->volume = pulse_from_device_volume(device->volume); ps->devname = strdup(device->extra_device_info); ps->next = sessions; sessions = ps; return ps; } /* ---------------------------- COMMAND HANDLERS ---------------------------- */ // Maps our internal state to the generic output state and then makes a callback // to the player to tell that state. Should always be called deferred. static enum command_state send_status(void *arg, int *ptr) { struct pulse_session *ps = arg; output_status_cb status_cb; enum output_device_state state; switch (ps->state) { case PA_STREAM_FAILED: state = OUTPUT_STATE_FAILED; break; case PA_STREAM_UNCONNECTED: case PA_STREAM_TERMINATED: state = OUTPUT_STATE_STOPPED; break; case PA_STREAM_READY: state = OUTPUT_STATE_CONNECTED; break; case PA_STREAM_CREATING: state = OUTPUT_STATE_STARTUP; break; default: DPRINTF(E_LOG, L_LAUDIO, "Bug! Unhandled state in send_status()\n"); state = OUTPUT_STATE_FAILED; } status_cb = ps->status_cb; ps->status_cb = NULL; if (status_cb) status_cb(ps->device, ps->output_session, state); return COMMAND_PENDING; // Don't want the command module to clean up ps } static enum command_state session_shutdown(void *arg, int *ptr) { struct pulse_session *ps = arg; send_status(ps, ptr); pulse_session_cleanup(ps); return COMMAND_PENDING; // Don't want the command module to clean up ps } /* ---------------------- EXECUTED IN PULSEAUDIO THREAD --------------------- */ static void pulse_status(struct pulse_session *ps) { // async to avoid risk of deadlock if the player should make calls back to Pulseaudio commands_exec_async(pulse.cmdbase, send_status, ps); } static void pulse_session_shutdown(struct pulse_session *ps) { // async to avoid risk of deadlock if the player should make calls back to Pulseaudio commands_exec_async(pulse.cmdbase, session_shutdown, ps); } static void pulse_session_shutdown_all(pa_stream_state_t state) { struct pulse_session *ps; struct pulse_session *next; for (ps = sessions; ps; ps = next) { next = ps->next; ps->state = state; pulse_session_shutdown(ps); } } /* --------------------- CALLBACKS FROM PULSEAUDIO THREAD ------------------- */ // This will be called if something happens to the stream after it was opened static void stream_state_cb(pa_stream *s, void *userdata) { struct pulse_session *ps = userdata; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stream to '%s' changed state (%d)\n", ps->devname, ps->state); ps->state = pa_stream_get_state(s); if (!PA_STREAM_IS_GOOD(ps->state)) { if (ps->state == PA_STREAM_FAILED) { errno = pa_context_errno(pulse.context); DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio stream to '%s' failed with error: %s\n", ps->devname, pa_strerror(errno)); } else DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio stream to '%s' aborted (%d)\n", ps->devname, ps->state); pulse_session_shutdown(ps); return; } } static void underrun_cb(pa_stream *s, void *userdata) { struct pulse_session *ps = userdata; if (ps->logcount > PULSE_LOG_MAX) return; ps->logcount++; if (ps->logcount < PULSE_LOG_MAX) DPRINTF(E_WARN, L_LAUDIO, "Pulseaudio reports buffer underrun on '%s'\n", ps->devname); else if (ps->logcount == PULSE_LOG_MAX) DPRINTF(E_WARN, L_LAUDIO, "Pulseaudio reports buffer underrun on '%s' (no further logging)\n", ps->devname); } static void overrun_cb(pa_stream *s, void *userdata) { struct pulse_session *ps = userdata; if (ps->logcount > PULSE_LOG_MAX) return; ps->logcount++; if (ps->logcount < PULSE_LOG_MAX) DPRINTF(E_WARN, L_LAUDIO, "Pulseaudio reports buffer overrun on '%s'\n", ps->devname); else if (ps->logcount == PULSE_LOG_MAX) DPRINTF(E_WARN, L_LAUDIO, "Pulseaudio reports buffer overrun on '%s' (no further logging)\n", ps->devname); } // This will be called our request to open the stream has completed static void start_cb(pa_stream *s, void *userdata) { struct pulse_session *ps = userdata; ps->state = pa_stream_get_state(s); if (ps->state == PA_STREAM_CREATING) return; if (ps->state != PA_STREAM_READY) { DPRINTF(E_LOG, L_LAUDIO, "Error starting Pulseaudio stream to '%s' (%d)\n", ps->devname, ps->state); pulse_session_shutdown(ps); return; } pa_stream_set_underflow_callback(ps->stream, underrun_cb, ps); pa_stream_set_overflow_callback(ps->stream, overrun_cb, ps); pa_stream_set_state_callback(ps->stream, stream_state_cb, ps); pulse_status(ps); } static void close_cb(pa_stream *s, void *userdata) { struct pulse_session *ps = userdata; pulse_session_shutdown(ps); } // This will be called our request to probe the stream has completed static void probe_cb(pa_stream *s, void *userdata) { struct pulse_session *ps = userdata; ps->state = pa_stream_get_state(s); if (ps->state == PA_STREAM_CREATING) return; if (ps->state != PA_STREAM_READY) { DPRINTF(E_LOG, L_LAUDIO, "Error probing Pulseaudio stream to '%s' (%d)\n", ps->devname, ps->state); pulse_session_shutdown(ps); return; } // This will callback to the player with succes and then remove the session pulse_session_shutdown(ps); } static void flush_cb(pa_stream *s, int success, void *userdata) { struct pulse_session *ps = userdata; pulse_status(ps); } static void volume_cb(pa_context *c, int success, void *userdata) { struct pulse_session *ps = userdata; pulse_status(ps); } static void sinklist_cb(pa_context *ctx, const pa_sink_info *info, int eol, void *userdata) { struct output_device *device; const char *name; int i; int pos; if (eol > 0) return; DPRINTF(E_DBG, L_LAUDIO, "Callback for Pulseaudio sink '%s' (id %" PRIu32 ")\n", info->name, info->index); pos = -1; for (i = 0; i < PULSE_MAX_DEVICES; i++) { if (pulse_known_devices[i] == (info->index + 1)) return; if (pulse_known_devices[i] == 0) pos = i; } if (pos == -1) { DPRINTF(E_LOG, L_LAUDIO, "Maximum number of Pulseaudio devices reached (%d), cannot add '%s'\n", PULSE_MAX_DEVICES, info->name); return; } device = calloc(1, sizeof(struct output_device)); if (!device) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for new Pulseaudio sink\n"); return; } if (info->index == 0) { name = cfg_getstr(cfg_getsec(cfg, "audio"), "nickname"); DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s) with name '%s'\n", info->description, info->name, name); } else { name = info->description; DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s)\n", info->description, info->name); } pulse_known_devices[pos] = info->index + 1; // Array values of 0 mean no device, so we add 1 to make sure the value is > 0 device->id = info->index; device->name = strdup(name); device->type = OUTPUT_TYPE_PULSE; device->type_name = outputs_name(device->type); device->advertised = 1; device->extra_device_info = strdup(info->name); player_device_add(device); } static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) { struct output_device *device; pa_operation *o; int i; DPRINTF(E_DBG, L_LAUDIO, "Callback for Pulseaudio subscribe (id %" PRIu32 ", event %d)\n", index, t); if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) != PA_SUBSCRIPTION_EVENT_SINK) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio subscribe called back with unknown event\n"); return; } if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { device = calloc(1, sizeof(struct output_device)); if (!device) { DPRINTF(E_LOG, L_LAUDIO, "Out of memory for temp Pulseaudio device\n"); return; } device->id = index; DPRINTF(E_LOG, L_LAUDIO, "Removing Pulseaudio sink with id %" PRIu32 "\n", index); for (i = 0; i < PULSE_MAX_DEVICES; i++) { if (pulse_known_devices[i] == index) pulse_known_devices[i] = 0; } player_device_remove(device); return; } o = pa_context_get_sink_info_by_index(c, index, sinklist_cb, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio error getting sink info for id %" PRIu32 "\n", index); return; } pa_operation_unref(o); } static void context_state_cb(pa_context *c, void *userdata) { pa_context_state_t state; pa_operation *o; state = pa_context_get_state(c); switch (state) { case PA_CONTEXT_READY: DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio context state changed to ready\n"); o = pa_context_get_sink_info_list(c, sinklist_cb, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Could not list Pulseaudio sink info\n"); return; } pa_operation_unref(o); pa_context_set_subscribe_callback(c, subscribe_cb, NULL); o = pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SINK, NULL, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to Pulseaudio sink info\n"); return; } pa_operation_unref(o); pa_threaded_mainloop_signal(pulse.mainloop, 0); break; case PA_CONTEXT_FAILED: DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio failed with error: %s\n", pa_strerror(pa_context_errno(c))); pulse_session_shutdown_all(PA_STREAM_FAILED); pa_threaded_mainloop_signal(pulse.mainloop, 0); break; case PA_CONTEXT_TERMINATED: DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio terminated\n"); pulse_session_shutdown_all(PA_STREAM_UNCONNECTED); pa_threaded_mainloop_signal(pulse.mainloop, 0); break; case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: break; } } /* ------------------------------- MISC HELPERS ----------------------------- */ // Used by init and deinit to stop main thread static void pulse_free(void) { if (pulse.mainloop) pa_threaded_mainloop_stop(pulse.mainloop); if (pulse.context) { pa_context_disconnect(pulse.context); pa_context_unref(pulse.context); } if (pulse.cmdbase) commands_base_free(pulse.cmdbase); if (pulse.mainloop) pa_threaded_mainloop_free(pulse.mainloop); } static int stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb) { pa_stream_flags_t flags; pa_sample_spec ss; pa_cvolume cvol; int offset; int ret; DPRINTF(E_DBG, L_LAUDIO, "Opening Pulseaudio stream to '%s'\n", ps->devname); ss.format = PA_SAMPLE_S16LE; ss.channels = 2; ss.rate = 44100; offset = cfg_getint(cfg_getsec(cfg, "audio"), "offset"); if (abs(offset) > 44100) { DPRINTF(E_LOG, L_LAUDIO, "The audio offset (%d) set in the configuration is out of bounds\n", offset); offset = 44100 * (offset/abs(offset)); } pa_threaded_mainloop_lock(pulse.mainloop); if (!(ps->stream = pa_stream_new(pulse.context, "forked-daapd audio", &ss, NULL))) goto unlock_and_fail; pa_stream_set_state_callback(ps->stream, cb, ps); flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE; ps->attr.tlength = STOB(2 * ss.rate + AIRTUNES_V2_PACKET_SAMPLES - offset); // 2 second latency ps->attr.maxlength = 2 * ps->attr.tlength; ps->attr.prebuf = (uint32_t)-1; ps->attr.minreq = (uint32_t)-1; ps->attr.fragsize = (uint32_t)-1; pa_cvolume_set(&cvol, 2, ps->volume); ret = pa_stream_connect_playback(ps->stream, ps->devname, &ps->attr, flags, &cvol, NULL); if (ret < 0) goto unlock_and_fail; ps->state = pa_stream_get_state(ps->stream); if (!PA_STREAM_IS_GOOD(ps->state)) goto unlock_and_fail; pa_threaded_mainloop_unlock(pulse.mainloop); return 0; unlock_and_fail: ret = pa_context_errno(pulse.context); DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not start '%s': %s\n", ps->devname, pa_strerror(ret)); pa_threaded_mainloop_unlock(pulse.mainloop); return -1; } static void stream_close(struct pulse_session *ps, pa_stream_notify_cb_t cb) { pa_threaded_mainloop_lock(pulse.mainloop); pa_stream_set_underflow_callback(ps->stream, NULL, NULL); pa_stream_set_overflow_callback(ps->stream, NULL, NULL); pa_stream_set_state_callback(ps->stream, cb, ps); pa_stream_disconnect(ps->stream); pa_stream_unref(ps->stream); ps->state = PA_STREAM_TERMINATED; ps->stream = NULL; pa_threaded_mainloop_unlock(pulse.mainloop); } /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) { struct pulse_session *ps; int ret; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio starting '%s'\n", device->name); ps = pulse_session_make(device, cb); if (!ps) return -1; ret = stream_open(ps, start_cb); if (ret < 0) { pulse_session_cleanup(ps); return -1; } return 0; } static void pulse_device_stop(struct output_session *session) { struct pulse_session *ps = session->session; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stopping '%s'\n", ps->devname); stream_close(ps, close_cb); } static int pulse_device_probe(struct output_device *device, output_status_cb cb) { struct pulse_session *ps; int ret; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio probing '%s'\n", device->name); ps = pulse_session_make(device, cb); if (!ps) return -1; ret = stream_open(ps, probe_cb); if (ret < 0) { pulse_session_cleanup(ps); return -1; } return 0; } static void pulse_device_free_extra(struct output_device *device) { free(device->extra_device_info); } static int pulse_device_volume_set(struct output_device *device, output_status_cb cb) { struct pulse_session *ps; uint32_t idx; pa_operation* o; pa_cvolume cvol; if (!sessions || !device->session || !device->session->session) return 0; ps = device->session->session; idx = pa_stream_get_index(ps->stream); ps->volume = pulse_from_device_volume(device->volume); pa_cvolume_set(&cvol, 2, ps->volume); DPRINTF(E_DBG, L_LAUDIO, "Setting Pulseaudio volume for stream %" PRIu32 " to %d (%d)\n", idx, (int)ps->volume, device->volume); pa_threaded_mainloop_lock(pulse.mainloop); ps->status_cb = cb; o = pa_context_set_sink_input_volume(pulse.context, idx, &cvol, volume_cb, ps); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not set volume: %s\n", pa_strerror(pa_context_errno(pulse.context))); pa_threaded_mainloop_unlock(pulse.mainloop); return 0; } pa_operation_unref(o); pa_threaded_mainloop_unlock(pulse.mainloop); return 1; } static void pulse_write(uint8_t *buf, uint64_t rtptime) { struct pulse_session *ps; struct pulse_session *next; size_t length; int ret; if (!sessions) return; length = STOB(AIRTUNES_V2_PACKET_SAMPLES); pa_threaded_mainloop_lock(pulse.mainloop); for (ps = sessions; ps; ps = next) { next = ps->next; if (ps->state != PA_STREAM_READY) continue; ret = pa_stream_write(ps->stream, buf, length, NULL, 0LL, PA_SEEK_RELATIVE); if (ret < 0) { ret = pa_context_errno(pulse.context); DPRINTF(E_LOG, L_LAUDIO, "Error writing Pulseaudio stream data to '%s': %s\n", ps->devname, pa_strerror(ret)); ps->state = PA_STREAM_FAILED; pulse_session_shutdown(ps); continue; } } pa_threaded_mainloop_unlock(pulse.mainloop); } static void pulse_playback_start(uint64_t next_pkt, struct timespec *ts) { struct pulse_session *ps; pa_operation* o; pa_threaded_mainloop_lock(pulse.mainloop); for (ps = sessions; ps; ps = ps->next) { o = pa_stream_cork(ps->stream, 0, NULL, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not resume '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); continue; } pa_operation_unref(o); } pa_threaded_mainloop_unlock(pulse.mainloop); } static void pulse_playback_stop(void) { struct pulse_session *ps; pa_operation* o; pa_threaded_mainloop_lock(pulse.mainloop); for (ps = sessions; ps; ps = ps->next) { o = pa_stream_cork(ps->stream, 1, NULL, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); continue; } pa_operation_unref(o); o = pa_stream_flush(ps->stream, NULL, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); continue; } pa_operation_unref(o); } pa_threaded_mainloop_unlock(pulse.mainloop); } static int pulse_flush(output_status_cb cb, uint64_t rtptime) { struct pulse_session *ps; pa_operation* o; int i; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush\n"); pa_threaded_mainloop_lock(pulse.mainloop); i = 0; for (ps = sessions; ps; ps = ps->next) { i++; ps->status_cb = cb; o = pa_stream_cork(ps->stream, 1, NULL, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); continue; } pa_operation_unref(o); o = pa_stream_flush(ps->stream, flush_cb, ps); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); continue; } pa_operation_unref(o); } pa_threaded_mainloop_unlock(pulse.mainloop); return i; } static void pulse_set_status_cb(struct output_session *session, output_status_cb cb) { struct pulse_session *ps = session->session; ps->status_cb = cb; } static int pulse_init(void) { char *type; int state; int ret; type = cfg_getstr(cfg_getsec(cfg, "audio"), "type"); if (type && (strcasecmp(type, "pulseaudio") != 0)) return -1; ret = 0; if (!(pulse.mainloop = pa_threaded_mainloop_new())) goto fail; if (!(pulse.cmdbase = commands_base_new(evbase_player, NULL))) goto fail; #ifdef HAVE_PA_THREADED_MAINLOOP_SET_NAME pa_threaded_mainloop_set_name(pulse.mainloop, "pulseaudio"); #endif if (!(pulse.context = pa_context_new(pa_threaded_mainloop_get_api(pulse.mainloop), "forked-daapd"))) goto fail; pa_context_set_state_callback(pulse.context, context_state_cb, NULL); if (pa_context_connect(pulse.context, NULL, 0, NULL) < 0) { ret = pa_context_errno(pulse.context); goto fail; } pa_threaded_mainloop_lock(pulse.mainloop); if (pa_threaded_mainloop_start(pulse.mainloop) < 0) goto unlock_and_fail; for (;;) { state = pa_context_get_state(pulse.context); if (state == PA_CONTEXT_READY) break; if (!PA_CONTEXT_IS_GOOD(state)) { ret = pa_context_errno(pulse.context); goto unlock_and_fail; } /* Wait until the context is ready */ pa_threaded_mainloop_wait(pulse.mainloop); } pa_threaded_mainloop_unlock(pulse.mainloop); return 0; unlock_and_fail: pa_threaded_mainloop_unlock(pulse.mainloop); fail: if (ret) DPRINTF(E_LOG, L_LAUDIO, "Error initializing Pulseaudio: %s\n", pa_strerror(ret)); pulse_free(); return -1; } static void pulse_deinit(void) { pulse_free(); } struct output_definition output_pulse = { .name = "Pulseaudio", .type = OUTPUT_TYPE_PULSE, .priority = 3, .disabled = 0, .init = pulse_init, .deinit = pulse_deinit, .device_start = pulse_device_start, .device_stop = pulse_device_stop, .device_probe = pulse_device_probe, .device_free_extra = pulse_device_free_extra, .device_volume_set = pulse_device_volume_set, .playback_start = pulse_playback_start, .playback_stop = pulse_playback_stop, .write = pulse_write, .flush = pulse_flush, .status_cb = pulse_set_status_cb, }; forked-daapd-25.0/src/outputs/raop.c000066400000000000000000003346111313447753700174260ustar00rootroot00000000000000/* * Copyright (C) 2012-2017 Espen Jürgensen * Copyright (C) 2010-2011 Julien BLACHE * * RAOP AirTunes v2 * * Crypto code adapted from VideoLAN * Copyright (C) 2008 the VideoLAN team * Author: Michael Hanselmann * GPLv2+ * * ALAC encoding adapted from raop_play * Copyright (C) 2005 Shiro Ninomiya * GPLv2+ * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_ENDIAN_H # include #elif defined(HAVE_SYS_ENDIAN_H) # include #elif defined(HAVE_LIBKERN_OSBYTEORDER_H) #include #define htobe16(x) OSSwapHostToBigInt16(x) #define be16toh(x) OSSwapBigToHostInt16(x) #define htobe32(x) OSSwapHostToBigInt32(x) #endif #include #include #include #include #include #include #include "evrtsp/evrtsp.h" #include "conffile.h" #include "logger.h" #include "mdns.h" #include "misc.h" #include "player.h" #include "db.h" #include "artwork.h" #include "dmap_common.h" #include "outputs.h" #ifdef RAOP_VERIFICATION #include "raop_verification.h" #endif #ifndef MIN # define MIN(a, b) ((a < b) ? a : b) #endif #define AIRTUNES_V2_HDR_LEN 12 #define ALAC_HDR_LEN 3 #define AIRTUNES_V2_PKT_LEN (AIRTUNES_V2_HDR_LEN + ALAC_HDR_LEN + STOB(AIRTUNES_V2_PACKET_SAMPLES)) #define AIRTUNES_V2_PKT_TAIL_LEN (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN - ((AIRTUNES_V2_PKT_LEN / 16) * 16)) #define AIRTUNES_V2_PKT_TAIL_OFF (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_PKT_TAIL_LEN) #define RETRANSMIT_BUFFER_SIZE 1000 #define RAOP_MD_DELAY_STARTUP 15360 #define RAOP_MD_DELAY_SWITCH (RAOP_MD_DELAY_STARTUP * 2) /* This is an arbitrary value which just needs to be kept in sync with the config */ #define RAOP_CONFIG_MAX_VOLUME 11 union sockaddr_all { struct sockaddr_in sin; struct sockaddr_in6 sin6; struct sockaddr sa; struct sockaddr_storage ss; }; struct raop_v2_packet { uint8_t clear[AIRTUNES_V2_PKT_LEN]; uint8_t encrypted[AIRTUNES_V2_PKT_LEN]; uint16_t seqnum; struct raop_v2_packet *prev; struct raop_v2_packet *next; }; enum raop_devtype { RAOP_DEV_APEX1_80211G, RAOP_DEV_APEX2_80211N, RAOP_DEV_APEX3_80211N, RAOP_DEV_APPLETV, RAOP_DEV_APPLETV4, RAOP_DEV_OTHER, }; // Session is starting up #define RAOP_STATE_F_STARTUP (1 << 13) // Streaming is up (connection established) #define RAOP_STATE_F_CONNECTED (1 << 14) // Couldn't start device #define RAOP_STATE_F_FAILED (1 << 15) enum raop_state { // Device is stopped (no session) RAOP_STATE_STOPPED = 0, // Session startup RAOP_STATE_STARTUP = RAOP_STATE_F_STARTUP | 0x01, RAOP_STATE_OPTIONS = RAOP_STATE_F_STARTUP | 0x02, RAOP_STATE_ANNOUNCE = RAOP_STATE_F_STARTUP | 0x03, RAOP_STATE_SETUP = RAOP_STATE_F_STARTUP | 0x04, RAOP_STATE_RECORD = RAOP_STATE_F_STARTUP | 0x05, // Session established // - streaming ready (RECORD sent and acked, connection established) // - commands (SET_PARAMETER) are possible RAOP_STATE_CONNECTED = RAOP_STATE_F_CONNECTED | 0x01, // Media data is being sent RAOP_STATE_STREAMING = RAOP_STATE_F_CONNECTED | 0x02, // Session is failed, couldn't startup or error occurred RAOP_STATE_FAILED = RAOP_STATE_F_FAILED | 0x01, // Password issue: unknown password or bad password RAOP_STATE_PASSWORD = RAOP_STATE_F_FAILED | 0x02, // Device requires verification, pending PIN from user RAOP_STATE_UNVERIFIED= RAOP_STATE_F_FAILED | 0x03, }; // Info about the device, which is not required by the player, only internally struct raop_extra { enum raop_devtype devtype; bool encrypt; bool wants_metadata; }; struct raop_session { struct evrtsp_connection *ctrl; enum raop_state state; bool req_has_auth; bool encrypt; bool auth_quirk_itunes; bool wants_metadata; bool keep_alive; bool only_probe; struct event *deferredev; int reqs_in_flight; int cseq; char *session; char session_url[128]; char *realm; char *nonce; const char *password; char *devname; char *address; int family; int volume; uint64_t start_rtptime; /* Do not dereference - only passed to the status cb */ struct output_device *device; struct output_session *output_session; output_status_cb status_cb; /* AirTunes v2 */ unsigned short server_port; unsigned short control_port; unsigned short timing_port; #ifdef RAOP_VERIFICATION /* Device verification, see raop_verification.h */ struct verification_verify_context *verification_verify_ctx; struct verification_setup_context *verification_setup_ctx; #endif int server_fd; union sockaddr_all sa; struct raop_service *timing_svc; struct raop_service *control_svc; struct raop_session *next; }; struct raop_metadata { struct evbuffer *metadata; struct evbuffer *artwork; int artwork_fmt; /* Progress data */ uint64_t start; uint64_t end; struct raop_metadata *next; }; struct raop_service { int fd; unsigned short port; struct event *ev; }; typedef void (*evrtsp_req_cb)(struct evrtsp_request *req, void *arg); /* Truncate RTP time to lower 32bits for RAOP */ #define RAOP_RTPTIME(x) ((uint32_t)((x) & (uint64_t)0xffffffff)) /* NTP timestamp definitions */ #define FRAC 4294967296. /* 2^32 as a double */ #define NTP_EPOCH_DELTA 0x83aa7e80 /* 2208988800 - that's 1970 - 1900 in seconds */ struct ntp_stamp { uint32_t sec; uint32_t frac; }; static const uint8_t raop_rsa_pubkey[] = "\xe7\xd7\x44\xf2\xa2\xe2\x78\x8b\x6c\x1f\x55\xa0\x8e\xb7\x05\x44" "\xa8\xfa\x79\x45\xaa\x8b\xe6\xc6\x2c\xe5\xf5\x1c\xbd\xd4\xdc\x68" "\x42\xfe\x3d\x10\x83\xdd\x2e\xde\xc1\xbf\xd4\x25\x2d\xc0\x2e\x6f" "\x39\x8b\xdf\x0e\x61\x48\xea\x84\x85\x5e\x2e\x44\x2d\xa6\xd6\x26" "\x64\xf6\x74\xa1\xf3\x04\x92\x9a\xde\x4f\x68\x93\xef\x2d\xf6\xe7" "\x11\xa8\xc7\x7a\x0d\x91\xc9\xd9\x80\x82\x2e\x50\xd1\x29\x22\xaf" "\xea\x40\xea\x9f\x0e\x14\xc0\xf7\x69\x38\xc5\xf3\x88\x2f\xc0\x32" "\x3d\xd9\xfe\x55\x15\x5f\x51\xbb\x59\x21\xc2\x01\x62\x9f\xd7\x33" "\x52\xd5\xe2\xef\xaa\xbf\x9b\xa0\x48\xd7\xb8\x13\xa2\xb6\x76\x7f" "\x6c\x3c\xcf\x1e\xb4\xce\x67\x3d\x03\x7b\x0d\x2e\xa3\x0c\x5f\xff" "\xeb\x06\xf8\xd0\x8a\xdd\xe4\x09\x57\x1a\x9c\x68\x9f\xef\x10\x72" "\x88\x55\xdd\x8c\xfb\x9a\x8b\xef\x5c\x89\x43\xef\x3b\x5f\xaa\x15" "\xdd\xe6\x98\xbe\xdd\xf3\x59\x96\x03\xeb\x3e\x6f\x61\x37\x2b\xb6" "\x28\xf6\x55\x9f\x59\x9a\x78\xbf\x50\x06\x87\xaa\x7f\x49\x76\xc0" "\x56\x2d\x41\x29\x56\xf8\x98\x9e\x18\xa6\x35\x5b\xd8\x15\x97\x82" "\x5e\x0f\xc8\x75\x34\x3e\xc7\x82\x11\x76\x25\xcd\xbf\x98\x44\x7b"; static const uint8_t raop_rsa_exp[] = "\x01\x00\x01"; /* Keep in sync with enum raop_devtype */ static const char *raop_devtype[] = { "AirPort Express 1 - 802.11g", "AirPort Express 2 - 802.11n", "AirPort Express 3 - 802.11n", "AppleTV", "AppleTV4", "Other", }; /* From player.c */ extern struct event_base *evbase_player; /* RAOP AES stream key */ static uint8_t raop_aes_key[16]; static uint8_t raop_aes_iv[16]; static gcry_cipher_hd_t raop_aes_ctx; /* Base64-encoded AES key and IV for SDP */ static char *raop_aes_key_b64; static char *raop_aes_iv_b64; /* AirTunes v2 time synchronization */ static struct raop_service timing_4svc; static struct raop_service timing_6svc; /* AirTunes v2 playback synchronization / control */ static struct raop_service control_4svc; static struct raop_service control_6svc; static int sync_counter; /* AirTunes v2 audio stream */ static uint32_t ssrc_id; static uint16_t stream_seq; /* Retransmit packet buffer */ static int pktbuf_size; static struct raop_v2_packet *pktbuf_head; static struct raop_v2_packet *pktbuf_tail; /* Metadata */ static struct raop_metadata *metadata_head; static struct raop_metadata *metadata_tail; /* FLUSH timer */ static struct event *flush_timer; /* Keep-alive timer - hack for ATV's with tvOS 10 */ static struct event *keep_alive_timer; static struct timeval keep_alive_tv = { 60, 0 }; /* Sessions */ static struct raop_session *sessions; /* ALAC bits writer - big endian * p outgoing buffer pointer * val bitfield value * blen bitfield length, max 8 bits * bpos bit position in the current byte (pointed by *p) */ static inline void alac_write_bits(uint8_t **p, uint8_t val, int blen, int *bpos) { int lb; int rb; int bd; /* Remaining bits in the current byte */ lb = 7 - *bpos + 1; /* Number of bits overflowing */ rb = lb - blen; if (rb >= 0) { bd = val << rb; if (*bpos == 0) **p = bd; else **p |= bd; /* No over- nor underflow, we're done with this byte */ if (rb == 0) { *p += 1; *bpos = 0; } else *bpos += blen; } else { /* Fill current byte */ bd = val >> -rb; **p |= bd; /* Overflow goes to the next byte */ *p += 1; **p = val << (8 + rb); *bpos = -rb; } } /* Raw data must be little endian */ static void alac_encode(uint8_t *raw, uint8_t *buf, int buflen) { uint8_t *maxraw; int bpos; bpos = 0; maxraw = raw + buflen; alac_write_bits(&buf, 1, 3, &bpos); /* channel=1, stereo */ alac_write_bits(&buf, 0, 4, &bpos); /* unknown */ alac_write_bits(&buf, 0, 8, &bpos); /* unknown */ alac_write_bits(&buf, 0, 4, &bpos); /* unknown */ alac_write_bits(&buf, 0, 1, &bpos); /* hassize */ alac_write_bits(&buf, 0, 2, &bpos); /* unused */ alac_write_bits(&buf, 1, 1, &bpos); /* is-not-compressed */ for (; raw < maxraw; raw += 4) { /* Byteswap to big endian */ alac_write_bits(&buf, *(raw + 1), 8, &bpos); alac_write_bits(&buf, *raw, 8, &bpos); alac_write_bits(&buf, *(raw + 3), 8, &bpos); alac_write_bits(&buf, *(raw + 2), 8, &bpos); } } /* AirTunes v2 time synchronization helpers */ static inline void timespec_to_ntp(struct timespec *ts, struct ntp_stamp *ns) { /* Seconds since NTP Epoch (1900-01-01) */ ns->sec = ts->tv_sec + NTP_EPOCH_DELTA; ns->frac = (uint32_t)((double)ts->tv_nsec * 1e-9 * FRAC); } static inline void ntp_to_timespec(struct ntp_stamp *ns, struct timespec *ts) { /* Seconds since Unix Epoch (1970-01-01) */ ts->tv_sec = ns->sec - NTP_EPOCH_DELTA; ts->tv_nsec = (long)((double)ns->frac / (1e-9 * FRAC)); } static inline int raop_v2_timing_get_clock_ntp(struct ntp_stamp *ns) { struct timespec ts; int ret; ret = clock_gettime(CLOCK_MONOTONIC, &ts); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get clock: %s\n", strerror(errno)); return -1; } timespec_to_ntp(&ts, ns); return 0; } /* RAOP crypto stuff - from VLC */ /* MGF1 is specified in RFC2437, section 10.2.1. Variables are named after the * specification. */ static int raop_crypt_mgf1(uint8_t *mask, size_t l, const uint8_t *z, const size_t zlen, const int hash) { char ebuf[64]; gcry_md_hd_t md_hdl; gpg_error_t gc_err; uint8_t *md; uint32_t counter; uint8_t c[4]; size_t copylen; int len; gc_err = gcry_md_open(&md_hdl, hash, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not open hash: %s\n", ebuf); return -1; } len = gcry_md_get_algo_dlen(hash); counter = 0; while (l > 0) { /* 3. For counter from 0 to \lceil{l / len}\rceil-1, do the following: * a. Convert counter to an octet string C of length 4 with the * primitive I2OSP: C = I2OSP (counter, 4) */ c[0] = (counter >> 24) & 0xff; c[1] = (counter >> 16) & 0xff; c[2] = (counter >> 8) & 0xff; c[3] = counter & 0xff; ++counter; /* b. Concatenate the hash of the seed z and c to the octet string T: * T = T || Hash (Z || C) */ gcry_md_reset(md_hdl); gcry_md_write(md_hdl, z, zlen); gcry_md_write(md_hdl, c, 4); md = gcry_md_read(md_hdl, hash); /* 4. Output the leading l octets of T as the octet string mask. */ copylen = MIN(l, len); memcpy(mask, md, copylen); mask += copylen; l -= copylen; } gcry_md_close(md_hdl); return 0; } /* EME-OAEP-ENCODE is specified in RFC2437, section 9.1.1.1. Variables are * named after the specification. */ static int raop_crypt_add_oaep_padding(uint8_t *em, const size_t emlen, const uint8_t *m, const size_t mlen, const uint8_t *p, const size_t plen) { uint8_t *seed; uint8_t *db; uint8_t *db_mask; uint8_t *seed_mask; size_t emlen_max; size_t pslen; size_t i; int hlen; int ret; /* Space for 0x00 prefix in EM. */ emlen_max = emlen - 1; hlen = gcry_md_get_algo_dlen(GCRY_MD_SHA1); /* Step 2: * If ||M|| > emLen-2hLen-1 then output "message too long" and stop. */ if (mlen > (emlen_max - (2 * hlen) - 1)) { DPRINTF(E_LOG, L_RAOP, "Could not add OAEP padding: message too long\n"); return -1; } /* Step 3: * Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero * octets. The length of PS may be 0. */ pslen = emlen_max - mlen - (2 * hlen) - 1; /* * Step 5: * Concatenate pHash, PS, the message M, and other padding to form a data * block DB as: DB = pHash || PS || 01 || M */ db = calloc(1, hlen + pslen + 1 + mlen); db_mask = calloc(1, emlen_max - hlen); seed_mask = calloc(1, hlen); if (!db || !db_mask || !seed_mask) { DPRINTF(E_LOG, L_RAOP, "Could not allocate memory for OAEP padding\n"); if (db) free(db); if (db_mask) free(db_mask); if (seed_mask) free(seed_mask); return -1; } /* Step 4: * Let pHash = Hash(P), an octet string of length hLen. */ gcry_md_hash_buffer(GCRY_MD_SHA1, db, p, plen); /* Step 3: * Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero * octets. The length of PS may be 0. */ memset(db + hlen, 0, pslen); /* Step 5: * Concatenate pHash, PS, the message M, and other padding to form a data * block DB as: DB = pHash || PS || 01 || M */ db[hlen + pslen] = 0x01; memcpy(db + hlen + pslen + 1, m, mlen); /* Step 6: * Generate a random octet string seed of length hLen */ seed = gcry_random_bytes(hlen, GCRY_STRONG_RANDOM); if (!seed) { DPRINTF(E_LOG, L_RAOP, "Could not allocate memory for OAEP seed\n"); ret = -1; goto out_free_alloced; } /* Step 7: * Let dbMask = MGF(seed, emLen-hLen). */ ret = raop_crypt_mgf1(db_mask, emlen_max - hlen, seed, hlen, GCRY_MD_SHA1); if (ret < 0) goto out_free_all; /* Step 8: * Let maskedDB = DB \xor dbMask. */ for (i = 0; i < (emlen_max - hlen); i++) db[i] ^= db_mask[i]; /* Step 9: * Let seedMask = MGF(maskedDB, hLen). */ ret = raop_crypt_mgf1(seed_mask, hlen, db, emlen_max - hlen, GCRY_MD_SHA1); if (ret < 0) goto out_free_all; /* Step 10: * Let maskedSeed = seed \xor seedMask. */ for (i = 0; i < hlen; i++) seed[i] ^= seed_mask[i]; /* Step 11: * Let EM = maskedSeed || maskedDB. */ em[0] = 0x00; memcpy(em + 1, seed, hlen); memcpy(em + 1 + hlen, db, hlen + pslen + 1 + mlen); /* Step 12: * Output EM. */ ret = 0; out_free_all: free(seed); out_free_alloced: free(db); free(db_mask); free(seed_mask); return ret; } static char * raop_crypt_encrypt_aes_key_base64(void) { char ebuf[64]; uint8_t padded_key[256]; gpg_error_t gc_err; gcry_sexp_t sexp_rsa_params; gcry_sexp_t sexp_input; gcry_sexp_t sexp_encrypted; gcry_sexp_t sexp_token_a; gcry_mpi_t mpi_pubkey; gcry_mpi_t mpi_exp; gcry_mpi_t mpi_input; gcry_mpi_t mpi_output; char *result; uint8_t *value; size_t value_size; int ret; result = NULL; /* Add RSA-OAES-SHA1 padding */ ret = raop_crypt_add_oaep_padding(padded_key, sizeof(padded_key), raop_aes_key, sizeof(raop_aes_key), NULL, 0); if (ret < 0) return NULL; /* Read public key */ gc_err = gcry_mpi_scan(&mpi_pubkey, GCRYMPI_FMT_USG, raop_rsa_pubkey, sizeof(raop_rsa_pubkey) - 1, NULL); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not read RAOP RSA pubkey: %s\n", ebuf); return NULL; } /* Read exponent */ gc_err = gcry_mpi_scan(&mpi_exp, GCRYMPI_FMT_USG, raop_rsa_exp, sizeof(raop_rsa_exp) - 1, NULL); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not read RAOP RSA exponent: %s\n", ebuf); goto out_free_mpi_pubkey; } /* If the input data starts with a set bit (0x80), gcrypt thinks it's a * signed integer and complains. Prefixing it with a zero byte (\0) * works, but involves more work. Converting it to an MPI in our code is * cleaner. */ gc_err = gcry_mpi_scan(&mpi_input, GCRYMPI_FMT_USG, padded_key, sizeof(padded_key), NULL); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not convert input data: %s\n", ebuf); goto out_free_mpi_exp; } /* Build S-expression with RSA parameters */ gc_err = gcry_sexp_build(&sexp_rsa_params, NULL, "(public-key(rsa(n %m)(e %m)))", mpi_pubkey, mpi_exp); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not build RSA params S-exp: %s\n", ebuf); goto out_free_mpi_input; } /* Build S-expression for data */ gc_err = gcry_sexp_build(&sexp_input, NULL, "(data(value %m))", mpi_input); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not build data S-exp: %s\n", ebuf); goto out_free_sexp_params; } /* Encrypt data */ gc_err = gcry_pk_encrypt(&sexp_encrypted, sexp_input, sexp_rsa_params); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not encrypt data: %s\n", ebuf); goto out_free_sexp_input; } /* Extract encrypted data */ sexp_token_a = gcry_sexp_find_token(sexp_encrypted, "a", 0); if (!sexp_token_a) { DPRINTF(E_LOG, L_RAOP, "Could not find token 'a' in result S-exp\n"); goto out_free_sexp_encrypted; } mpi_output = gcry_sexp_nth_mpi(sexp_token_a, 1, GCRYMPI_FMT_USG); if (!mpi_output) { DPRINTF(E_LOG, L_RAOP, "Cannot extract MPI from result\n"); goto out_free_sexp_token_a; } /* Copy encrypted data into char array */ gc_err = gcry_mpi_aprint(GCRYMPI_FMT_USG, &value, &value_size, mpi_output); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not copy encrypted data: %s\n", ebuf); goto out_free_mpi_output; } /* Encode in Base64 */ result = b64_encode(value, value_size); free(value); out_free_mpi_output: gcry_mpi_release(mpi_output); out_free_sexp_token_a: gcry_sexp_release(sexp_token_a); out_free_sexp_encrypted: gcry_sexp_release(sexp_encrypted); out_free_sexp_input: gcry_sexp_release(sexp_input); out_free_sexp_params: gcry_sexp_release(sexp_rsa_params); out_free_mpi_input: gcry_mpi_release(mpi_input); out_free_mpi_exp: gcry_mpi_release(mpi_exp); out_free_mpi_pubkey: gcry_mpi_release(mpi_pubkey); return result; } /* RAOP metadata */ static void raop_metadata_free(struct raop_metadata *rmd) { evbuffer_free(rmd->metadata); if (rmd->artwork) evbuffer_free(rmd->artwork); free(rmd); } static void raop_metadata_purge(void) { struct raop_metadata *rmd; for (rmd = metadata_head; rmd; rmd = metadata_head) { metadata_head = rmd->next; raop_metadata_free(rmd); } metadata_tail = NULL; } static void raop_metadata_prune(uint64_t rtptime) { struct raop_metadata *rmd; for (rmd = metadata_head; rmd; rmd = metadata_head) { if (rmd->end >= rtptime) break; if (metadata_tail == metadata_head) metadata_tail = rmd->next; metadata_head = rmd->next; raop_metadata_free(rmd); } } /* Thread: worker */ static void * raop_metadata_prepare(int id) { struct db_queue_item *queue_item; struct raop_metadata *rmd; struct evbuffer *tmp; int ret; rmd = (struct raop_metadata *)malloc(sizeof(struct raop_metadata)); if (!rmd) { DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP metadata\n"); return NULL; } memset(rmd, 0, sizeof(struct raop_metadata)); queue_item = db_queue_fetch_byitemid(id); if (!queue_item) { DPRINTF(E_LOG, L_RAOP, "Out of memory for queue item\n"); goto out_rmd; } /* Get artwork */ rmd->artwork = evbuffer_new(); if (!rmd->artwork) { DPRINTF(E_LOG, L_RAOP, "Out of memory for artwork evbuffer; no artwork will be sent\n"); goto skip_artwork; } ret = artwork_get_item(rmd->artwork, queue_item->file_id, 600, 600); if (ret < 0) { DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", id); evbuffer_free(rmd->artwork); rmd->artwork = NULL; } rmd->artwork_fmt = ret; skip_artwork: /* Turn it into DAAP metadata */ tmp = evbuffer_new(); if (!tmp) { DPRINTF(E_LOG, L_RAOP, "Out of memory for temporary metadata evbuffer; metadata will not be sent\n"); goto out_qi; } rmd->metadata = evbuffer_new(); if (!rmd->metadata) { DPRINTF(E_LOG, L_RAOP, "Out of memory for metadata evbuffer; metadata will not be sent\n"); evbuffer_free(tmp); goto out_qi; } ret = dmap_encode_queue_metadata(rmd->metadata, tmp, queue_item); evbuffer_free(tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not encode file metadata; metadata will not be sent\n"); goto out_metadata; } /* Progress - raop_metadata_send() will add rtptime to these */ rmd->start = 0; rmd->end = ((uint64_t)queue_item->song_length * 44100UL) / 1000UL; free_queue_item(queue_item, 0); return rmd; out_metadata: evbuffer_free(rmd->metadata); out_qi: free_queue_item(queue_item, 0); out_rmd: free(rmd); return NULL; } /* Helpers */ static int raop_add_auth(struct raop_session *rs, struct evrtsp_request *req, const char *method, const char *uri) { char ha1[33]; char ha2[33]; char ebuf[64]; char auth[256]; const char *hash_fmt; const char *username; uint8_t *hash_bytes; size_t hashlen; gcry_md_hd_t hd; gpg_error_t gc_err; int i; int ret; rs->req_has_auth = 0; if (!rs->nonce) return 0; if (!rs->password) { DPRINTF(E_LOG, L_RAOP, "Authentication required but no password found for device '%s'\n", rs->devname); return -2; } if (rs->auth_quirk_itunes) { hash_fmt = "%02X"; /* Uppercase hex */ username = "iTunes"; } else { hash_fmt = "%02x"; username = ""; /* No username */ } gc_err = gcry_md_open(&hd, GCRY_MD_MD5, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not open MD5: %s\n", ebuf); return -1; } memset(ha1, 0, sizeof(ha1)); memset(ha2, 0, sizeof(ha2)); hashlen = gcry_md_get_algo_dlen(GCRY_MD_MD5); /* HA 1 */ gcry_md_write(hd, username, strlen(username)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, rs->realm, strlen(rs->realm)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, rs->password, strlen(rs->password)); hash_bytes = gcry_md_read(hd, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n"); return -1; } for (i = 0; i < hashlen; i++) sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]); /* RESET */ gcry_md_reset(hd); /* HA 2 */ gcry_md_write(hd, method, strlen(method)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, uri, strlen(uri)); hash_bytes = gcry_md_read(hd, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n"); return -1; } for (i = 0; i < hashlen; i++) sprintf(ha2 + (2 * i), hash_fmt, hash_bytes[i]); /* RESET */ gcry_md_reset(hd); /* Final value */ gcry_md_write(hd, ha1, 32); gcry_md_write(hd, ":", 1); gcry_md_write(hd, rs->nonce, strlen(rs->nonce)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, ha2, 32); hash_bytes = gcry_md_read(hd, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n"); return -1; } for (i = 0; i < hashlen; i++) sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]); gcry_md_close(hd); /* Build header */ ret = snprintf(auth, sizeof(auth), "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"", username, rs->realm, rs->nonce, uri, ha1); if ((ret < 0) || (ret >= sizeof(auth))) { DPRINTF(E_LOG, L_RAOP, "Authorization value header exceeds buffer size\n"); return -1; } evrtsp_add_header(req->output_headers, "Authorization", auth); DPRINTF(E_DBG, L_RAOP, "Authorization header: %s\n", auth); rs->req_has_auth = 1; return 0; } static int raop_parse_auth(struct raop_session *rs, struct evrtsp_request *req) { const char *param; char *auth; char *token; char *ptr; if (rs->realm) { free(rs->realm); rs->realm = NULL; } if (rs->nonce) { free(rs->nonce); rs->nonce = NULL; } param = evrtsp_find_header(req->input_headers, "WWW-Authenticate"); if (!param) { DPRINTF(E_LOG, L_RAOP, "WWW-Authenticate header not found\n"); return -1; } DPRINTF(E_DBG, L_RAOP, "WWW-Authenticate: %s\n", param); if (strncmp(param, "Digest ", strlen("Digest ")) != 0) { DPRINTF(E_LOG, L_RAOP, "Unsupported authentication method: %s\n", param); return -1; } auth = strdup(param); if (!auth) { DPRINTF(E_LOG, L_RAOP, "Out of memory for WWW-Authenticate header copy\n"); return -1; } token = strchr(auth, ' '); token++; token = strtok_r(token, " =", &ptr); while (token) { if (strcmp(token, "realm") == 0) { token = strtok_r(NULL, "=\"", &ptr); if (!token) break; rs->realm = strdup(token); } else if (strcmp(token, "nonce") == 0) { token = strtok_r(NULL, "=\"", &ptr); if (!token) break; rs->nonce = strdup(token); } token = strtok_r(NULL, " =", &ptr); } free(auth); if (!rs->realm || !rs->nonce) { DPRINTF(E_LOG, L_RAOP, "Could not find realm/nonce in WWW-Authenticate header\n"); if (rs->realm) { free(rs->realm); rs->realm = NULL; } if (rs->nonce) { free(rs->nonce); rs->nonce = NULL; } return -1; } DPRINTF(E_DBG, L_RAOP, "Found realm: [%s], nonce: [%s]\n", rs->realm, rs->nonce); return 0; } static int raop_add_headers(struct raop_session *rs, struct evrtsp_request *req, enum evrtsp_cmd_type req_method) { char buf[64]; const char *method; const char *url; int ret; method = evrtsp_method(req_method); DPRINTF(E_DBG, L_RAOP, "Building %s for '%s'\n", method, rs->devname); snprintf(buf, sizeof(buf), "%d", rs->cseq); evrtsp_add_header(req->output_headers, "CSeq", buf); rs->cseq++; evrtsp_add_header(req->output_headers, "User-Agent", "forked-daapd/" VERSION); /* Add Authorization header */ url = (req_method == EVRTSP_REQ_OPTIONS) ? "*" : rs->session_url; ret = raop_add_auth(rs, req, method, url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not add Authorization header\n"); if (ret == -2) rs->state = RAOP_STATE_PASSWORD; return -1; } snprintf(buf, sizeof(buf), "%" PRIX64, libhash); evrtsp_add_header(req->output_headers, "Client-Instance", buf); evrtsp_add_header(req->output_headers, "DACP-ID", buf); if (rs->session) evrtsp_add_header(req->output_headers, "Session", rs->session); /* Content-Length added automatically by evrtsp */ return 0; } /* This check should compare the reply CSeq with the request CSeq, but it has * been removed because RAOP targets like Reflector and AirFoil don't return * the CSeq according to the rtsp spec, and the CSeq is not really important * anyway. */ static int raop_check_cseq(struct raop_session *rs, struct evrtsp_request *req) { return 0; } static int raop_make_sdp(struct raop_session *rs, struct evrtsp_request *req, char *address, int family, uint32_t session_id) { #define SDP_PLD_FMT \ "v=0\r\n" \ "o=iTunes %u 0 IN %s %s\r\n" \ "s=iTunes\r\n" \ "c=IN %s %s\r\n" \ "t=0 0\r\n" \ "m=audio 0 RTP/AVP 96\r\n" \ "a=rtpmap:96 AppleLossless\r\n" \ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n" \ "a=rsaaeskey:%s\r\n" \ "a=aesiv:%s\r\n" #define SDP_PLD_FMT_NO_ENC \ "v=0\r\n" \ "o=iTunes %u 0 IN %s %s\r\n" \ "s=iTunes\r\n" \ "c=IN %s %s\r\n" \ "t=0 0\r\n" \ "m=audio 0 RTP/AVP 96\r\n" \ "a=rtpmap:96 AppleLossless\r\n" \ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n" const char *af; const char *rs_af; char *p; int ret; af = (family == AF_INET) ? "IP4" : "IP6"; rs_af = (rs->family == AF_INET) ? "IP4" : "IP6"; p = strchr(rs->address, '%'); if (p) *p = '\0'; /* Add SDP payload - but don't add RSA/AES key/iv if no encryption - important for ATV3 update 6.0 */ if (rs->encrypt) ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT, session_id, af, address, rs_af, rs->address, AIRTUNES_V2_PACKET_SAMPLES, raop_aes_key_b64, raop_aes_iv_b64); else ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT_NO_ENC, session_id, af, address, rs_af, rs->address, AIRTUNES_V2_PACKET_SAMPLES); if (p) *p = '%'; if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Out of memory for SDP payload\n"); return -1; } return 0; #undef SDP_PLD_FMT #undef SDP_PLD_FMT_NO_ENC } /* RAOP/RTSP requests */ /* * Request queueing HOWTO * * Sending: * - increment rs->reqs_in_flight * - set evrtsp connection closecb to NULL * * Request callback: * - decrement rs->reqs_in_flight first thing, even if the callback is * called for error handling (req == NULL or HTTP error code) * - if rs->reqs_in_flight == 0, setup evrtsp connection closecb * * When a request fails, the whole RAOP session is declared failed and * torn down by calling raop_session_failure(), even if there are requests * queued on the evrtsp connection. There is no reason to think pending * requests would work out better than the one that just failed and recovery * would be tricky to get right. * * evrtsp behaviour with queued requests: * - request callback is called with req == NULL to indicate a connection * error; if there are several requests queued on the connection, this can * happen for each request if the connection isn't destroyed * - the connection is reset, and the closecb is called if the connection was * previously connected. There is no closecb set when there are requests in * flight */ static int raop_send_req_teardown(struct raop_session *rs, evrtsp_req_cb cb) { struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for TEARDOWN\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_TEARDOWN); if (ret < 0) { evrtsp_request_free(req); return -1; } ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_TEARDOWN, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make TEARDOWN request\n"); return -1; } rs->state = RAOP_STATE_CONNECTED; rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static int raop_send_req_flush(struct raop_session *rs, uint64_t rtptime, evrtsp_req_cb cb) { char buf[64]; struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for FLUSH\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_FLUSH); if (ret < 0) { evrtsp_request_free(req); return -1; } /* Restart sequence: last sequence + 1 */ ret = snprintf(buf, sizeof(buf), "seq=%u;rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rtptime)); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in FLUSH request\n"); evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "RTP-Info", buf); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_FLUSH, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make FLUSH request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static int raop_send_req_set_parameter(struct raop_session *rs, struct evbuffer *evbuf, char *ctype, char *rtpinfo, evrtsp_req_cb cb) { struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for SET_PARAMETER\n"); return -1; } ret = evbuffer_add_buffer(req->output_buffer, evbuf); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Out of memory for SET_PARAMETER payload\n"); evrtsp_request_free(req); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_SET_PARAMETER); if (ret < 0) { evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Content-Type", ctype); if (rtpinfo) evrtsp_add_header(req->output_headers, "RTP-Info", rtpinfo); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_SET_PARAMETER, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make SET_PARAMETER request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static int raop_send_req_record(struct raop_session *rs, evrtsp_req_cb cb) { char buf[64]; struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for RECORD\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_RECORD); if (ret < 0) { evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Range", "npt=0-"); /* Start sequence: next sequence */ ret = snprintf(buf, sizeof(buf), "seq=%u;rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rs->start_rtptime)); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in RECORD request\n"); evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "RTP-Info", buf); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_RECORD, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make RECORD request\n"); return -1; } rs->reqs_in_flight++; return 0; } static int raop_send_req_setup(struct raop_session *rs, evrtsp_req_cb cb) { char hdr[128]; struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for SETUP\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_SETUP); if (ret < 0) { evrtsp_request_free(req); return -1; } /* Request UDP transport, AirTunes v2 streaming */ ret = snprintf(hdr, sizeof(hdr), "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;control_port=%u;timing_port=%u", rs->control_svc->port, rs->timing_svc->port); if ((ret < 0) || (ret >= sizeof(hdr))) { DPRINTF(E_LOG, L_RAOP, "Transport header exceeds buffer length\n"); evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Transport", hdr); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_SETUP, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make SETUP request\n"); return -1; } rs->reqs_in_flight++; return 0; } static int raop_send_req_announce(struct raop_session *rs, evrtsp_req_cb cb) { uint8_t challenge[16]; char *challenge_b64; char *ptr; struct evrtsp_request *req; char *address; char *intf; unsigned short port; int family; uint32_t session_id; int ret; /* Determine local address, needed for SDP and session URL */ evrtsp_connection_get_local_address(rs->ctrl, &address, &port, &family); if (!address || (port == 0)) { DPRINTF(E_LOG, L_RAOP, "Could not determine local address\n"); if (address) free(address); return -1; } intf = strchr(address, '%'); if (intf) { *intf = '\0'; intf++; } DPRINTF(E_DBG, L_RAOP, "Local address: %s (LL: %s) port %d\n", address, (intf) ? intf : "no", port); req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for ANNOUNCE\n"); free(address); return -1; } /* Session ID and session URL */ gcry_randomize(&session_id, sizeof(session_id), GCRY_STRONG_RANDOM); ret = snprintf(rs->session_url, sizeof(rs->session_url), "rtsp://%s/%u", address, session_id); if ((ret < 0) || (ret >= sizeof(rs->session_url))) { DPRINTF(E_LOG, L_RAOP, "Session URL length exceeds 127 characters\n"); free(address); goto cleanup_req; } /* SDP payload */ ret = raop_make_sdp(rs, req, address, family, session_id); free(address); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not generate SDP payload for ANNOUNCE\n"); goto cleanup_req; } ret = raop_add_headers(rs, req, EVRTSP_REQ_ANNOUNCE); if (ret < 0) { evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Content-Type", "application/sdp"); /* Challenge - but only if session is encrypted (important for ATV3 after update 6.0) */ if (rs->encrypt) { gcry_randomize(challenge, sizeof(challenge), GCRY_STRONG_RANDOM); challenge_b64 = b64_encode(challenge, sizeof(challenge)); if (!challenge_b64) { DPRINTF(E_LOG, L_RAOP, "Couldn't encode challenge\n"); goto cleanup_req; } /* Remove base64 padding */ ptr = strchr(challenge_b64, '='); if (ptr) *ptr = '\0'; evrtsp_add_header(req->output_headers, "Apple-Challenge", challenge_b64); free(challenge_b64); } ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_ANNOUNCE, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make ANNOUNCE request\n"); return -1; } rs->reqs_in_flight++; return 0; cleanup_req: evrtsp_request_free(req); return -1; } static int raop_send_req_options(struct raop_session *rs, evrtsp_req_cb cb) { struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for OPTIONS\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_OPTIONS); if (ret < 0) { evrtsp_request_free(req); return -1; } ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_OPTIONS, "*"); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make OPTIONS request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } #ifdef RAOP_VERIFICATION static int raop_send_req_pin_start(struct raop_session *rs, evrtsp_req_cb cb) { struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for pair-pin-start\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_POST); if (ret < 0) { evrtsp_request_free(req); return -1; } DPRINTF(E_LOG, L_RAOP, "Starting device verification for '%s', please submit PIN via a *.verification file\n", rs->devname); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_POST, "/pair-pin-start"); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make pair-pin-start request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } #else static int raop_send_req_pin_start(struct raop_session *rs, evrtsp_req_cb cb) { DPRINTF(E_LOG, L_RAOP, "Device '%s' requires verification, but forked-daapd was built with --disable-verification\n", rs->devname); return -1; } #endif /* Maps our internal state to the generic output state and then makes a callback * to the player to tell that state */ static void raop_status(struct raop_session *rs) { output_status_cb status_cb = rs->status_cb; enum output_device_state state; switch (rs->state) { case RAOP_STATE_PASSWORD ... RAOP_STATE_UNVERIFIED: state = OUTPUT_STATE_PASSWORD; break; case RAOP_STATE_FAILED: state = OUTPUT_STATE_FAILED; break; case RAOP_STATE_STOPPED: state = OUTPUT_STATE_STOPPED; break; case RAOP_STATE_STARTUP ... RAOP_STATE_RECORD: state = OUTPUT_STATE_STARTUP; break; case RAOP_STATE_CONNECTED: state = OUTPUT_STATE_CONNECTED; break; case RAOP_STATE_STREAMING: state = OUTPUT_STATE_STREAMING; break; default: DPRINTF(E_LOG, L_RAOP, "Bug! Unhandled state in cast_status()\n"); state = OUTPUT_STATE_FAILED; } rs->status_cb = NULL; if (status_cb) status_cb(rs->device, rs->output_session, state); } static void raop_session_free(struct raop_session *rs) { evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); evrtsp_connection_free(rs->ctrl); event_free(rs->deferredev); close(rs->server_fd); if (rs->realm) free(rs->realm); if (rs->nonce) free(rs->nonce); if (rs->session) free(rs->session); if (rs->address) free(rs->address); if (rs->devname) free(rs->devname); free(rs->output_session); free(rs); rs = NULL; } static void raop_session_cleanup(struct raop_session *rs) { struct raop_session *s; struct raop_v2_packet *pkt; struct raop_v2_packet *next_pkt; if (rs == sessions) sessions = sessions->next; else { for (s = sessions; s && (s->next != rs); s = s->next) ; /* EMPTY */ if (!s) DPRINTF(E_WARN, L_RAOP, "WARNING: struct raop_session not found in list; BUG!\n"); else s->next = rs->next; } raop_session_free(rs); /* No more active sessions, free retransmit buffer */ if (!sessions) { pkt = pktbuf_head; while (pkt) { next_pkt = pkt->next; free(pkt); pkt = next_pkt; } pktbuf_head = NULL; pktbuf_tail = NULL; pktbuf_size = 0; } } static void raop_session_failure(struct raop_session *rs) { /* Session failed, let our user know */ if (rs->state != RAOP_STATE_PASSWORD) rs->state = RAOP_STATE_FAILED; raop_status(rs); raop_session_cleanup(rs); } static void raop_deferredev_cb(int fd, short what, void *arg) { struct raop_session *rs = arg; DPRINTF(E_DBG, L_RAOP, "Cleaning up failed session (deferred) on device '%s'\n", rs->devname); raop_session_failure(rs); } static void raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg) { struct raop_session *rs = arg; struct timeval tv; DPRINTF(E_LOG, L_RAOP, "Device '%s' closed RTSP connection\n", rs->devname); rs->state = RAOP_STATE_FAILED; evutil_timerclear(&tv); evtimer_add(rs->deferredev, &tv); } static struct raop_session * raop_session_make(struct output_device *rd, int family, output_status_cb cb, bool only_probe) { struct output_session *os; struct raop_session *rs; struct raop_extra *re; char *address; char *intf; unsigned short port; int ret; re = rd->extra_device_info; switch (family) { case AF_INET: /* We always have the v4 services, so no need to check */ if (!rd->v4_address) return NULL; address = rd->v4_address; port = rd->v4_port; break; case AF_INET6: if (!rd->v6_address || (timing_6svc.fd < 0) || (control_6svc.fd < 0)) return NULL; address = rd->v6_address; port = rd->v6_port; break; default: return NULL; } os = calloc(1, sizeof(struct output_session)); if (!os) { DPRINTF(E_LOG, L_RAOP, "Out of memory (os)\n"); return NULL; } rs = calloc(1, sizeof(struct raop_session)); if (!rs) { DPRINTF(E_LOG, L_RAOP, "Out of memory (rs)\n"); free(os); return NULL; } os->session = rs; os->type = rd->type; rs->output_session = os; rs->state = RAOP_STATE_STOPPED; rs->only_probe = only_probe; rs->reqs_in_flight = 0; rs->cseq = 1; rs->device = rd; rs->status_cb = cb; rs->server_fd = -1; rs->password = rd->password; rs->wants_metadata = re->wants_metadata; switch (re->devtype) { case RAOP_DEV_APEX1_80211G: rs->encrypt = 1; rs->auth_quirk_itunes = 1; rs->keep_alive = 0; break; case RAOP_DEV_APEX2_80211N: rs->encrypt = 1; rs->auth_quirk_itunes = 0; rs->keep_alive = 0; break; case RAOP_DEV_APEX3_80211N: rs->encrypt = 0; rs->auth_quirk_itunes = 0; rs->keep_alive = 0; break; case RAOP_DEV_APPLETV: rs->encrypt = 0; rs->auth_quirk_itunes = 0; rs->keep_alive = 0; break; case RAOP_DEV_APPLETV4: rs->encrypt = 0; rs->auth_quirk_itunes = 0; rs->keep_alive = 1; break; case RAOP_DEV_OTHER: rs->encrypt = re->encrypt; rs->auth_quirk_itunes = 0; rs->keep_alive = 0; break; } rs->deferredev = evtimer_new(evbase_player, raop_deferredev_cb, rs); if (!rs->deferredev) { DPRINTF(E_LOG, L_RAOP, "Out of memory for deferred error handling!\n"); goto out_free_rs; } rs->ctrl = evrtsp_connection_new(address, port); if (!rs->ctrl) { DPRINTF(E_LOG, L_RAOP, "Could not create control connection to '%s' (%s)\n", rd->name, address); goto out_free_event; } evrtsp_connection_set_base(rs->ctrl, evbase_player); rs->sa.ss.ss_family = family; switch (family) { case AF_INET: rs->timing_svc = &timing_4svc; rs->control_svc = &control_4svc; ret = inet_pton(AF_INET, address, &rs->sa.sin.sin_addr); break; case AF_INET6: rs->timing_svc = &timing_6svc; rs->control_svc = &control_6svc; intf = strchr(address, '%'); if (intf) *intf = '\0'; ret = inet_pton(AF_INET6, address, &rs->sa.sin6.sin6_addr); if (intf) { *intf = '%'; intf++; rs->sa.sin6.sin6_scope_id = if_nametoindex(intf); if (rs->sa.sin6.sin6_scope_id == 0) { DPRINTF(E_LOG, L_RAOP, "Could not find interface %s\n", intf); ret = -1; break; } } break; default: ret = -1; break; } if (ret <= 0) { DPRINTF(E_LOG, L_RAOP, "Device '%s' has invalid address (%s) for %s\n", rd->name, address, (family == AF_INET) ? "ipv4" : "ipv6"); goto out_free_evcon; } rs->devname = strdup(rd->name); rs->address = strdup(address); rs->family = family; rs->volume = rd->volume; rs->next = sessions; sessions = rs; return rs; out_free_evcon: evrtsp_connection_free(rs->ctrl); out_free_event: event_free(rs->deferredev); out_free_rs: free(rs); return NULL; } static void raop_session_failure_cb(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; raop_session_failure(rs); } /* Metadata handling */ static void raop_cb_metadata(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for metadata/artwork/progress: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; /* No status_cb call, user doesn't want/need to know about the status * of metadata requests unless they cause the session to fail. */ if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; error: raop_session_failure(rs); } static int raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint64_t offset, uint32_t delay) { uint32_t display; int ret; /* Here's the deal with progress values: * - first value, called display, is always start minus a delay * -> delay x1 if streaming is starting for this device (joining or not) * -> delay x2 if stream is switching to a new song * - second value, called start, is the RTP time of the first sample for this * song for this device * -> start of song * -> start of song + offset if device is joining in the middle of a song, * or getting out of a pause or seeking * - third value, called end, is the RTP time of the last sample for this song */ display = RAOP_RTPTIME(rmd->start - delay); ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, RAOP_RTPTIME(rmd->start + offset), RAOP_RTPTIME(rmd->end)); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not build progress string for sending\n"); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n"); return ret; } static int raop_metadata_send_artwork(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, char *rtptime) { char *ctype; uint8_t *buf; size_t len; int ret; switch (rmd->artwork_fmt) { case ART_FMT_PNG: ctype = "image/png"; break; case ART_FMT_JPEG: ctype = "image/jpeg"; break; default: DPRINTF(E_LOG, L_RAOP, "Unsupported artwork format %d\n", rmd->artwork_fmt); return -1; } buf = evbuffer_pullup(rmd->artwork, -1); len = evbuffer_get_length(rmd->artwork); ret = evbuffer_add(evbuf, buf, len); if (ret != 0) { DPRINTF(E_LOG, L_RAOP, "Could not copy artwork for sending\n"); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, ctype, rtptime, raop_cb_metadata); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n"); return ret; } static int raop_metadata_send_metadata(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, char *rtptime) { uint8_t *buf; size_t len; int ret; buf = evbuffer_pullup(rmd->metadata, -1); len = evbuffer_get_length(rmd->metadata); ret = evbuffer_add(evbuf, buf, len); if (ret != 0) { DPRINTF(E_LOG, L_RAOP, "Could not copy metadata for sending\n"); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, "application/x-dmap-tagged", rtptime, raop_cb_metadata); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n"); return ret; } static int raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd, uint64_t offset, uint32_t delay) { char rtptime[32]; struct evbuffer *evbuf; int ret; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_RAOP, "Could not allocate temp evbuffer for metadata processing\n"); return -1; } ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", RAOP_RTPTIME(rmd->start)); if ((ret < 0) || (ret >= sizeof(rtptime))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer while sending metadata\n"); ret = -1; goto out; } ret = raop_metadata_send_metadata(rs, evbuf, rmd, rtptime); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send metadata to '%s'\n", rs->devname); ret = -1; goto out; } if (!rmd->artwork) goto skip_artwork; ret = raop_metadata_send_artwork(rs, evbuf, rmd, rtptime); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send artwork to '%s'\n", rs->devname); ret = -1; goto out; } skip_artwork: ret = raop_metadata_send_progress(rs, evbuf, rmd, offset, delay); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send progress to '%s'\n", rs->devname); ret = -1; goto out; } out: evbuffer_free(evbuf); return ret; } static void raop_metadata_startup_send(struct raop_session *rs) { struct raop_metadata *rmd; uint64_t offset; int sent; int ret; if (!rs->wants_metadata) return; sent = 0; for (rmd = metadata_head; rmd; rmd = rmd->next) { /* Current song */ if ((rs->start_rtptime >= rmd->start) && (rs->start_rtptime < rmd->end)) { offset = rs->start_rtptime - rmd->start; ret = raop_metadata_send_internal(rs, rmd, offset, RAOP_MD_DELAY_STARTUP); if (ret < 0) { raop_session_failure(rs); return; } sent = 1; } /* Next song(s) */ else if (sent && (rs->start_rtptime < rmd->start)) { ret = raop_metadata_send_internal(rs, rmd, 0, RAOP_MD_DELAY_SWITCH); if (ret < 0) { raop_session_failure(rs); return; } } } } static void raop_metadata_send(void *metadata, uint64_t rtptime, uint64_t offset, int startup) { struct raop_metadata *rmd; struct raop_session *rs; struct raop_session *next; uint32_t delay; int ret; rmd = metadata; rmd->start += rtptime; rmd->end += rtptime; /* Add the rmd to the metadata list */ if (metadata_tail) metadata_tail->next = rmd; else { metadata_head = rmd; metadata_tail = rmd; } for (rs = sessions; rs; rs = next) { next = rs->next; if (!(rs->state & RAOP_STATE_F_CONNECTED)) continue; if (!rs->wants_metadata) continue; delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; ret = raop_metadata_send_internal(rs, rmd, offset, delay); if (ret < 0) { raop_session_failure(rs); continue; } } } /* Volume handling */ static float raop_volume_convert(int volume, char *name) { float raop_volume; cfg_t *airplay; int max_volume; max_volume = RAOP_CONFIG_MAX_VOLUME; airplay = cfg_gettsec(cfg, "airplay", name); if (airplay) max_volume = cfg_getint(airplay, "max_volume"); if ((max_volume < 1) || (max_volume > RAOP_CONFIG_MAX_VOLUME)) { DPRINTF(E_LOG, L_RAOP, "Config has bad max_volume (%d) for device %s, using default instead\n", max_volume, name); max_volume = RAOP_CONFIG_MAX_VOLUME; } /* RAOP volume * -144.0 is off * 0 - 100 maps to -30.0 - 0 */ if (volume > 0 && volume <= 100) raop_volume = -30.0 + ((float)max_volume * (float)volume * 30.0) / (100.0 * RAOP_CONFIG_MAX_VOLUME); else raop_volume = -144.0; return raop_volume; } static int raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb) { struct evbuffer *evbuf; float raop_volume; int ret; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_RAOP, "Could not allocate evbuffer for volume payload\n"); return -1; } raop_volume = raop_volume_convert(volume, rs->devname); /* Don't let locales get in the way here */ /* We use -%d and -(int)raop_volume so -0.3 won't become 0.3 */ ret = evbuffer_add_printf(evbuf, "volume: -%d.%06d\r\n", -(int)raop_volume, -(int)(1000000.0 * (raop_volume - (int)raop_volume))); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Out of memory for SET_PARAMETER payload (volume)\n"); evbuffer_free(evbuf); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, cb); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for volume\n"); evbuffer_free(evbuf); rs->volume = volume; return ret; } static void raop_cb_set_volume(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for stream volume: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; /* Let our user know */ raop_status(rs); if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; error: raop_session_failure(rs); } /* Volume in [0 - 100] */ static int raop_set_volume_one(struct output_device *rd, output_status_cb cb) { struct raop_session *rs; int ret; if (!rd->session || !rd->session->session) return 0; rs = rd->session->session; if (!(rs->state & RAOP_STATE_F_CONNECTED)) return 0; ret = raop_set_volume_internal(rs, rd->volume, raop_cb_set_volume); if (ret < 0) { raop_session_failure(rs); return 0; } rs->status_cb = cb; return 1; } static void raop_cb_flush(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "FLUSH request failed: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; rs->state = RAOP_STATE_CONNECTED; /* Let our user know */ raop_status(rs); if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; error: raop_session_failure(rs); } static void raop_cb_keep_alive(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for keep alive: %d %s\n", req->response_code, req->response_code_line); goto error; } if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; error: raop_session_failure(rs); } static void raop_cb_pin_start(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "Request for starting PIN verification failed: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; rs->state = RAOP_STATE_UNVERIFIED; raop_status(rs); // TODO If the user never verifies the session will remain stale return; error: raop_session_failure(rs); } // Forward static void raop_device_stop(struct output_session *session); static void raop_flush_timer_cb(int fd, short what, void *arg) { struct raop_session *rs; DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP sessions\n"); for (rs = sessions; rs; rs = rs->next) { if (!(rs->state & RAOP_STATE_F_CONNECTED)) continue; raop_device_stop(rs->output_session); } } static void raop_keep_alive_timer_cb(int fd, short what, void *arg) { struct raop_session *rs; for (rs = sessions; rs; rs = rs->next) { if (!rs->keep_alive) continue; if (!(rs->state & RAOP_STATE_F_CONNECTED)) continue; raop_set_volume_internal(rs, rs->volume, raop_cb_keep_alive); } evtimer_add(keep_alive_timer, &keep_alive_tv); } static int raop_flush(output_status_cb cb, uint64_t rtptime) { struct timeval tv; struct raop_session *rs; struct raop_session *next; int pending; int ret; pending = 0; for (rs = sessions; rs; rs = next) { next = rs->next; if (rs->state != RAOP_STATE_STREAMING) continue; ret = raop_send_req_flush(rs, rtptime, raop_cb_flush); if (ret < 0) { raop_session_failure(rs); continue; } rs->status_cb = cb; pending++; } if (pending > 0) { evutil_timerclear(&tv); tv.tv_sec = 10; evtimer_add(flush_timer, &tv); } return pending; } /* AirTunes v2 time synchronization */ static void raop_v2_timing_cb(int fd, short what, void *arg) { union sockaddr_all sa; uint8_t req[32]; uint8_t res[32]; struct ntp_stamp recv_stamp; struct ntp_stamp xmit_stamp; struct raop_service *svc; int len; int ret; svc = (struct raop_service *)arg; ret = raop_v2_timing_get_clock_ntp(&recv_stamp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get receive timestamp\n"); goto readd; } len = sizeof(sa.ss); ret = recvfrom(svc->fd, req, sizeof(req), 0, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Error reading timing request: %s\n", strerror(errno)); goto readd; } if (ret != 32) { DPRINTF(E_DBG, L_RAOP, "Got timing request with size %d\n", ret); goto readd; } if ((req[0] != 0x80) || (req[1] != 0xd2)) { DPRINTF(E_LOG, L_RAOP, "Packet header doesn't match timing request\n"); goto readd; } memset(res, 0, sizeof(res)); /* Header */ res[0] = 0x80; res[1] = 0xd3; res[2] = req[2]; /* Copy client timestamp */ memcpy(res + 8, req + 24, 8); /* Receive timestamp */ recv_stamp.sec = htobe32(recv_stamp.sec); recv_stamp.frac = htobe32(recv_stamp.frac); memcpy(res + 16, &recv_stamp.sec, 4); memcpy(res + 20, &recv_stamp.frac, 4); /* Transmit timestamp */ ret = raop_v2_timing_get_clock_ntp(&xmit_stamp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get transmit timestamp, falling back to receive timestamp\n"); /* Still better than failing altogether * recv/xmit are close enough that it shouldn't matter much */ memcpy(res + 24, &recv_stamp.sec, 4); memcpy(res + 28, &recv_stamp.frac, 4); } else { xmit_stamp.sec = htobe32(xmit_stamp.sec); xmit_stamp.frac = htobe32(xmit_stamp.frac); memcpy(res + 24, &xmit_stamp.sec, 4); memcpy(res + 28, &xmit_stamp.frac, 4); } ret = sendto(svc->fd, res, sizeof(res), 0, &sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send timing reply: %s\n", strerror(errno)); goto readd; } readd: ret = event_add(svc->ev, NULL); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't re-add event for timing requests\n"); return; } } static int raop_v2_timing_start_one(struct raop_service *svc, int family) { union sockaddr_all sa; int on; int len; int ret; #ifdef SOCK_CLOEXEC svc->fd = socket(family, SOCK_DGRAM | SOCK_CLOEXEC, 0); #else svc->fd = socket(family, SOCK_DGRAM, 0); #endif if (svc->fd < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't make timing socket: %s\n", strerror(errno)); return -1; } if (family == AF_INET6) { on = 1; ret = setsockopt(svc->fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not set IPV6_V6ONLY on timing socket: %s\n", strerror(errno)); goto out_fail; } } memset(&sa, 0, sizeof(union sockaddr_all)); sa.ss.ss_family = family; switch (family) { case AF_INET: sa.sin.sin_addr.s_addr = INADDR_ANY; sa.sin.sin_port = 0; len = sizeof(sa.sin); break; case AF_INET6: sa.sin6.sin6_addr = in6addr_any; sa.sin6.sin6_port = 0; len = sizeof(sa.sin6); break; } ret = bind(svc->fd, &sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't bind timing socket: %s\n", strerror(errno)); goto out_fail; } len = sizeof(sa.ss); ret = getsockname(svc->fd, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get timing socket name: %s\n", strerror(errno)); goto out_fail; } switch (family) { case AF_INET: svc->port = ntohs(sa.sin.sin_port); DPRINTF(E_DBG, L_RAOP, "Timing IPv4 port: %d\n", svc->port); break; case AF_INET6: svc->port = ntohs(sa.sin6.sin6_port); DPRINTF(E_DBG, L_RAOP, "Timing IPv6 port: %d\n", svc->port); break; } svc->ev = event_new(evbase_player, svc->fd, EV_READ, raop_v2_timing_cb, svc); if (!svc->ev) { DPRINTF(E_LOG, L_RAOP, "Out of memory for raop_service event\n"); goto out_fail; } event_add(svc->ev, NULL); return 0; out_fail: close(svc->fd); svc->fd = -1; svc->port = 0; return -1; } static void raop_v2_timing_stop(void) { if (timing_4svc.ev) event_free(timing_4svc.ev); if (timing_6svc.ev) event_free(timing_6svc.ev); close(timing_4svc.fd); timing_4svc.fd = -1; timing_4svc.port = 0; close(timing_6svc.fd); timing_6svc.fd = -1; timing_6svc.port = 0; } static int raop_v2_timing_start(int v6enabled) { int ret; if (v6enabled) { ret = raop_v2_timing_start_one(&timing_6svc, AF_INET6); if (ret < 0) DPRINTF(E_WARN, L_RAOP, "Could not start timing service on IPv6\n"); } ret = raop_v2_timing_start_one(&timing_4svc, AF_INET); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not start timing service on IPv4\n"); raop_v2_timing_stop(); return -1; } return 0; } /* AirTunes v2 playback synchronization */ static void raop_v2_control_send_sync(uint64_t next_pkt, struct timespec *init) { uint8_t msg[20]; struct timespec ts; struct ntp_stamp cur_stamp; struct raop_session *rs; uint64_t cur_pos; uint32_t cur_pos32; uint32_t next_pkt32; int len; int ret; memset(msg, 0, sizeof(msg)); msg[0] = (sync_counter == 0) ? 0x90 : 0x80; msg[1] = 0xd4; msg[3] = 0x07; next_pkt32 = htobe32(RAOP_RTPTIME(next_pkt)); memcpy(msg + 16, &next_pkt32, 4); if (!init) { ret = player_get_current_pos(&cur_pos, &ts, 1); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not get current playback position and clock\n"); return; } timespec_to_ntp(&ts, &cur_stamp); } else { cur_pos = next_pkt - 88200; timespec_to_ntp(init, &cur_stamp); } cur_pos32 = htobe32(RAOP_RTPTIME(cur_pos)); cur_stamp.sec = htobe32(cur_stamp.sec); cur_stamp.frac = htobe32(cur_stamp.frac); memcpy(msg + 4, &cur_pos32, 4); memcpy(msg + 8, &cur_stamp.sec, 4); memcpy(msg + 12, &cur_stamp.frac, 4); for (rs = sessions; rs; rs = rs->next) { if (rs->state != RAOP_STATE_STREAMING) continue; switch (rs->sa.ss.ss_family) { case AF_INET: rs->sa.sin.sin_port = htons(rs->control_port); len = sizeof(rs->sa.sin); break; case AF_INET6: rs->sa.sin6.sin6_port = htons(rs->control_port); len = sizeof(rs->sa.sin6); break; default: DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family); continue; } ret = sendto(rs->control_svc->fd, msg, sizeof(msg), 0, &rs->sa.sa, len); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send playback sync to device '%s': %s\n", rs->devname, strerror(errno)); } } /* Forward */ static void raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len); static void raop_v2_control_cb(int fd, short what, void *arg) { char address[INET6_ADDRSTRLEN]; union sockaddr_all sa; uint8_t req[8]; struct raop_session *rs; struct raop_service *svc; uint16_t seq_start; uint16_t seq_len; int len; int ret; svc = (struct raop_service *)arg; len = sizeof(sa.ss); ret = recvfrom(svc->fd, req, sizeof(req), 0, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Error reading control request: %s\n", strerror(errno)); goto readd; } if (ret != 8) { DPRINTF(E_DBG, L_RAOP, "Got control request with size %d\n", ret); goto readd; } switch (sa.ss.ss_family) { case AF_INET: if (svc != &control_4svc) goto readd; for (rs = sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET) && (sa.sin.sin_addr.s_addr == rs->sa.sin.sin_addr.s_addr)) break; } if (!rs) ret = (inet_ntop(AF_INET, &sa.sin.sin_addr.s_addr, address, sizeof(address)) != NULL); break; case AF_INET6: if (svc != &control_6svc) goto readd; for (rs = sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET6) && IN6_ARE_ADDR_EQUAL(&sa.sin6.sin6_addr, &rs->sa.sin6.sin6_addr)) break; } if (!rs) ret = (inet_ntop(AF_INET6, &sa.sin6.sin6_addr.s6_addr, address, sizeof(address)) != NULL); break; default: DPRINTF(E_LOG, L_RAOP, "Control svc: Unknown address family %d\n", sa.ss.ss_family); goto readd; } if (!rs) { if (!ret) DPRINTF(E_LOG, L_RAOP, "Control request from [error: %s]; not a RAOP client\n", strerror(errno)); else DPRINTF(E_LOG, L_RAOP, "Control request from %s; not a RAOP client\n", address); goto readd; } if ((req[0] != 0x80) || (req[1] != 0xd5)) { DPRINTF(E_LOG, L_RAOP, "Packet header doesn't match retransmit request\n"); goto readd; } memcpy(&seq_start, req + 4, 2); memcpy(&seq_len, req + 6, 2); seq_start = be16toh(seq_start); seq_len = be16toh(seq_len); DPRINTF(E_DBG, L_RAOP, "Got retransmit request from '%s', seq_start %u len %u\n", rs->devname, seq_start, seq_len); raop_v2_resend_range(rs, seq_start, seq_len); readd: ret = event_add(svc->ev, NULL); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't re-add event for control requests\n"); return; } } static int raop_v2_control_start_one(struct raop_service *svc, int family) { union sockaddr_all sa; int on; int len; int ret; #ifdef SOCK_CLOEXEC svc->fd = socket(family, SOCK_DGRAM | SOCK_CLOEXEC, 0); #else svc->fd = socket(family, SOCK_DGRAM, 0); #endif if (svc->fd < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't make control socket: %s\n", strerror(errno)); return -1; } if (family == AF_INET6) { on = 1; ret = setsockopt(svc->fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not set IPV6_V6ONLY on control socket: %s\n", strerror(errno)); goto out_fail; } } memset(&sa, 0, sizeof(union sockaddr_all)); sa.ss.ss_family = family; switch (family) { case AF_INET: sa.sin.sin_addr.s_addr = INADDR_ANY; sa.sin.sin_port = 0; len = sizeof(sa.sin); break; case AF_INET6: sa.sin6.sin6_addr = in6addr_any; sa.sin6.sin6_port = 0; len = sizeof(sa.sin6); break; } ret = bind(svc->fd, &sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't bind control socket: %s\n", strerror(errno)); goto out_fail; } len = sizeof(sa.ss); ret = getsockname(svc->fd, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get control socket name: %s\n", strerror(errno)); goto out_fail; } switch (family) { case AF_INET: svc->port = ntohs(sa.sin.sin_port); DPRINTF(E_DBG, L_RAOP, "Control IPv4 port: %d\n", svc->port); break; case AF_INET6: svc->port = ntohs(sa.sin6.sin6_port); DPRINTF(E_DBG, L_RAOP, "Control IPv6 port: %d\n", svc->port); break; } svc->ev = event_new(evbase_player, svc->fd, EV_READ, raop_v2_control_cb, svc); if (!svc->ev) { DPRINTF(E_LOG, L_RAOP, "Out of memory for control event\n"); goto out_fail; } event_add(svc->ev, NULL); return 0; out_fail: close(svc->fd); svc->fd = -1; svc->port = 0; return -1; } static void raop_v2_control_stop(void) { if (control_4svc.ev) event_free(control_4svc.ev); if (control_6svc.ev) event_free(control_6svc.ev); close(control_4svc.fd); control_4svc.fd = -1; control_4svc.port = 0; close(control_6svc.fd); control_6svc.fd = -1; control_6svc.port = 0; } static int raop_v2_control_start(int v6enabled) { int ret; if (v6enabled) { ret = raop_v2_control_start_one(&control_6svc, AF_INET6); if (ret < 0) DPRINTF(E_WARN, L_RAOP, "Could not start control service on IPv6\n"); } ret = raop_v2_control_start_one(&control_4svc, AF_INET); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not start control service on IPv4\n"); raop_v2_control_stop(); return -1; } return 0; } /* AirTunes v2 streaming */ static struct raop_v2_packet * raop_v2_new_packet(void) { struct raop_v2_packet *pkt; if (pktbuf_size >= RETRANSMIT_BUFFER_SIZE) { pktbuf_size--; pkt = pktbuf_tail; pktbuf_tail = pktbuf_tail->prev; pktbuf_tail->next = NULL; } else { pkt = (struct raop_v2_packet *)malloc(sizeof(struct raop_v2_packet)); if (!pkt) { DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP packet\n"); return NULL; } } return pkt; } static struct raop_v2_packet * raop_v2_make_packet(uint8_t *rawbuf, uint64_t rtptime) { char ebuf[64]; struct raop_v2_packet *pkt; gpg_error_t gc_err; uint32_t rtptime32; uint16_t seq; pkt = raop_v2_new_packet(); if (!pkt) return NULL; memset(pkt, 0, sizeof(struct raop_v2_packet)); alac_encode(rawbuf, pkt->clear + AIRTUNES_V2_HDR_LEN, STOB(AIRTUNES_V2_PACKET_SAMPLES)); stream_seq++; pkt->seqnum = stream_seq; seq = htobe16(pkt->seqnum); rtptime32 = htobe32(RAOP_RTPTIME(rtptime)); pkt->clear[0] = 0x80; pkt->clear[1] = (sync_counter == 0) ? 0xe0 : 0x60; memcpy(pkt->clear + 2, &seq, 2); memcpy(pkt->clear + 4, &rtptime32, 4); /* RTP SSRC ID * Note: should htobe32() that value, but it's just a * random/unique ID so it's no big deal */ memcpy(pkt->clear + 8, &ssrc_id, 4); /* Copy AirTunes v2 header to encrypted packet */ memcpy(pkt->encrypted, pkt->clear, AIRTUNES_V2_HDR_LEN); /* Copy the tail of the audio packet that is left unencrypted */ memcpy(pkt->encrypted + AIRTUNES_V2_PKT_TAIL_OFF, pkt->clear + AIRTUNES_V2_PKT_TAIL_OFF, AIRTUNES_V2_PKT_TAIL_LEN); /* Reset cipher */ gc_err = gcry_cipher_reset(raop_aes_ctx); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not reset AES cipher: %s\n", ebuf); free(pkt); return NULL; } /* Set IV */ gc_err = gcry_cipher_setiv(raop_aes_ctx, raop_aes_iv, sizeof(raop_aes_iv)); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not set AES IV: %s\n", ebuf); free(pkt); return NULL; } /* Encrypt in blocks of 16 bytes */ gc_err = gcry_cipher_encrypt(raop_aes_ctx, pkt->encrypted + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16, pkt->clear + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not encrypt payload: %s\n", ebuf); free(pkt); return NULL; } pkt->prev = NULL; pkt->next = pktbuf_head; if (pktbuf_head) pktbuf_head->prev = pkt; if (!pktbuf_tail) pktbuf_tail = pkt; pktbuf_head = pkt; pktbuf_size++; return pkt; } static int raop_v2_send_packet(struct raop_session *rs, struct raop_v2_packet *pkt) { uint8_t *data; int ret; if (!rs) return -1; data = (rs->encrypt) ? pkt->encrypted : pkt->clear; ret = send(rs->server_fd, data, AIRTUNES_V2_PKT_LEN, 0); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno)); raop_session_failure(rs); return -1; } else if (ret != AIRTUNES_V2_PKT_LEN) { DPRINTF(E_WARN, L_RAOP, "Partial send (%d) for '%s'\n", ret, rs->devname); return -1; } return 0; } // Forward static void raop_playback_stop(void); static void raop_v2_write(uint8_t *buf, uint64_t rtptime) { struct raop_v2_packet *pkt; struct raop_session *rs; struct raop_session *next; pkt = raop_v2_make_packet(buf, rtptime); if (!pkt) { raop_playback_stop(); return; } if (sync_counter == 126) { raop_v2_control_send_sync(rtptime, NULL); sync_counter = 1; } else sync_counter++; for (rs = sessions; rs; rs = next) { // raop_v2_send_packet may free rs on failure, so save rs->next now next = rs->next; if (rs->state != RAOP_STATE_STREAMING) continue; raop_v2_send_packet(rs, pkt); } return; } static void raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len) { struct raop_v2_packet *pktbuf; int ret; uint16_t distance; /* Check that seqnum is in the retransmit buffer */ if ((seqnum > pktbuf_head->seqnum) || (seqnum < pktbuf_tail->seqnum)) { DPRINTF(E_WARN, L_RAOP, "Device '%s' asking for seqnum %u; not in buffer (h %u t %u)\n", rs->devname, seqnum, pktbuf_head->seqnum, pktbuf_tail->seqnum); return; } if (seqnum > pktbuf_head->seqnum) { distance = seqnum - pktbuf_tail->seqnum; if (distance > (RETRANSMIT_BUFFER_SIZE / 2)) pktbuf = pktbuf_head; else pktbuf = pktbuf_tail; } else { distance = pktbuf_head->seqnum - seqnum; if (distance > (RETRANSMIT_BUFFER_SIZE / 2)) pktbuf = pktbuf_tail; else pktbuf = pktbuf_head; } if (pktbuf == pktbuf_head) { while (pktbuf && seqnum != pktbuf->seqnum) pktbuf = pktbuf->next; } else { while (pktbuf && seqnum != pktbuf->seqnum) pktbuf = pktbuf->prev; } while (len && pktbuf) { ret = raop_v2_send_packet(rs, pktbuf); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Error retransmit packet, aborting retransmission\n"); return; } pktbuf = pktbuf->prev; len--; } if (len != 0) DPRINTF(E_LOG, L_RAOP, "WARNING: len non-zero at end of retransmission\n"); } static int raop_v2_stream_open(struct raop_session *rs) { int len; int ret; #ifdef SOCK_CLOEXEC rs->server_fd = socket(rs->sa.ss.ss_family, SOCK_DGRAM | SOCK_CLOEXEC, 0); #else rs->server_fd = socket(rs->sa.ss.ss_family, SOCK_DGRAM, 0); #endif if (rs->server_fd < 0) { DPRINTF(E_LOG, L_RAOP, "Could not create socket for streaming: %s\n", strerror(errno)); return -1; } switch (rs->sa.ss.ss_family) { case AF_INET: rs->sa.sin.sin_port = htons(rs->server_port); len = sizeof(rs->sa.sin); break; case AF_INET6: rs->sa.sin6.sin6_port = htons(rs->server_port); len = sizeof(rs->sa.sin6); break; default: DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family); goto out_fail; } ret = connect(rs->server_fd, &rs->sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "connect() to [%s]:%u failed: %s\n", rs->address, rs->server_port, strerror(errno)); goto out_fail; } /* Include the device into the set of active devices if * playback is in progress. */ if (sync_counter != 0) rs->state = RAOP_STATE_STREAMING; else rs->state = RAOP_STATE_CONNECTED; return 0; out_fail: close(rs->server_fd); rs->server_fd = -1; return -1; } /* Session startup */ static void raop_startup_cancel(struct raop_session *rs) { /* Try being nice to our peer */ if (rs->session) raop_send_req_teardown(rs, raop_session_failure_cb); else raop_session_failure(rs); } static void raop_cb_startup_volume(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for startup volume: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; raop_metadata_startup_send(rs); ret = raop_v2_stream_open(rs); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not open streaming socket\n"); goto cleanup; } /* Session startup and setup is done, tell our user */ raop_status(rs); if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_record(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; const char *param; int ret; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "RECORD request failed in session startup: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; /* Audio latency */ param = evrtsp_find_header(req->input_headers, "Audio-Latency"); if (!param) DPRINTF(E_INFO, L_RAOP, "RECORD reply from '%s' did not have an Audio-Latency header\n", rs->devname); else DPRINTF(E_DBG, L_RAOP, "RAOP audio latency is %s\n", param); rs->state = RAOP_STATE_RECORD; /* Set initial volume */ raop_set_volume_internal(rs, rs->volume, raop_cb_startup_volume); return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_setup(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; const char *param; char *transport; char *token; char *ptr; int tmp; int ret; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SETUP request failed in session startup: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; /* Server-side session ID */ param = evrtsp_find_header(req->input_headers, "Session"); if (!param) { DPRINTF(E_LOG, L_RAOP, "Missing Session header in SETUP reply\n"); goto cleanup; } rs->session = strdup(param); /* Check transport and get remote streaming port */ param = evrtsp_find_header(req->input_headers, "Transport"); if (!param) { DPRINTF(E_LOG, L_RAOP, "Missing Transport header in SETUP reply\n"); goto cleanup; } /* Check transport is really UDP, AirTunes v2 streaming */ if (strncmp(param, "RTP/AVP/UDP;", strlen("RTP/AVP/UDP;")) != 0) { DPRINTF(E_LOG, L_RAOP, "ApEx replied with unsupported Transport: %s\n", param); goto cleanup; } transport = strdup(param); if (!transport) { DPRINTF(E_LOG, L_RAOP, "Out of memory for Transport header copy\n"); goto cleanup; } token = strchr(transport, ';'); token++; token = strtok_r(token, ";=", &ptr); while (token) { DPRINTF(E_DBG, L_RAOP, "token: %s\n", token); if (strcmp(token, "server_port") == 0) { token = strtok_r(NULL, ";=", &ptr); if (!token) break; ret = safe_atoi32(token, &tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not read server_port\n"); break; } rs->server_port = tmp; } else if (strcmp(token, "control_port") == 0) { token = strtok_r(NULL, ";=", &ptr); if (!token) break; ret = safe_atoi32(token, &tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not read control_port\n"); break; } rs->control_port = tmp; } else if (strcmp(token, "timing_port") == 0) { token = strtok_r(NULL, ";=", &ptr); if (!token) break; ret = safe_atoi32(token, &tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not read timing_port\n"); break; } rs->timing_port = tmp; } token = strtok_r(NULL, ";=", &ptr); } free(transport); if ((rs->server_port == 0) || (rs->control_port == 0) || (rs->timing_port == 0)) { DPRINTF(E_LOG, L_RAOP, "Transport header lacked some port numbers in SETUP reply\n"); DPRINTF(E_LOG, L_RAOP, "Transport header was: %s\n", param); goto cleanup; } DPRINTF(E_DBG, L_RAOP, "Negotiated AirTunes v2 UDP streaming session %s; ports s=%u c=%u t=%u\n", rs->session, rs->server_port, rs->control_port, rs->timing_port); rs->state = RAOP_STATE_SETUP; /* Send RECORD */ ret = raop_send_req_record(rs, raop_cb_startup_record); if (ret < 0) goto cleanup; return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_announce(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "ANNOUNCE request failed in session startup: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; rs->state = RAOP_STATE_ANNOUNCE; /* Send SETUP */ ret = raop_send_req_setup(rs, raop_cb_startup_setup); if (ret < 0) goto cleanup; return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_options(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req || !req->response_code) { DPRINTF(E_LOG, L_RAOP, "No response from '%s' (%s) to OPTIONS request\n", rs->devname, rs->address); if (rs->device->v4_address && (rs->sa.ss.ss_family == AF_INET6)) { DPRINTF(E_LOG, L_RAOP, "Falling back to ipv4, the ipv6 address is not responding\n"); free(rs->device->v6_address); rs->device->v6_address = NULL; } goto cleanup; } if ((req->response_code != RTSP_OK) && (req->response_code != RTSP_UNAUTHORIZED) && (req->response_code != RTSP_FORBIDDEN)) { DPRINTF(E_LOG, L_RAOP, "OPTIONS request failed '%s': %d %s\n", rs->devname, req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; if (req->response_code == RTSP_UNAUTHORIZED) { if (rs->req_has_auth) { DPRINTF(E_LOG, L_RAOP, "Bad password for device '%s'\n", rs->devname); rs->state = RAOP_STATE_PASSWORD; goto cleanup; } ret = raop_parse_auth(rs, req); if (ret < 0) goto cleanup; ret = raop_send_req_options(rs, raop_cb_startup_options); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not re-run OPTIONS request with authentication\n"); goto cleanup; } return; } if (req->response_code == RTSP_FORBIDDEN) { ret = raop_send_req_pin_start(rs, raop_cb_pin_start); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not request PIN for device verification\n"); goto cleanup; } return; } rs->state = RAOP_STATE_OPTIONS; if (rs->only_probe) { /* Device probed successfully, tell our user */ raop_status(rs); /* We're not going further with this session */ raop_session_cleanup(rs); } else { /* Send ANNOUNCE */ ret = raop_send_req_announce(rs, raop_cb_startup_announce); if (ret < 0) goto cleanup; } return; cleanup: if (rs->only_probe) raop_session_failure(rs); else raop_startup_cancel(rs); } static void raop_cb_shutdown_teardown(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "TEARDOWN request failed in session shutdown: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; rs->state = RAOP_STATE_STOPPED; /* Session shut down, tell our user */ raop_status(rs); raop_session_cleanup(rs); return; error: raop_session_failure(rs); } /* tvOS device verification - e.g. for the ATV4 (read it from the bottom and up) */ #ifdef RAOP_VERIFICATION static int raop_verification_response_process(int step, struct evrtsp_request *req, struct raop_session *rs) { uint8_t *response; const char *errmsg; size_t len; int ret; rs->reqs_in_flight--; if (!req) { DPRINTF(E_LOG, L_RAOP, "Verification step %d to '%s' failed, empty callback\n", step, rs->devname); return -1; } if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "Verification step %d to '%s' failed with error code %d: %s\n", step, rs->devname, req->response_code, req->response_code_line); return -1; } response = evbuffer_pullup(req->input_buffer, -1); len = evbuffer_get_length(req->input_buffer); switch (step) { case 1: ret = verification_setup_response1(rs->verification_setup_ctx, response, len); errmsg = verification_setup_errmsg(rs->verification_setup_ctx); break; case 2: ret = verification_setup_response2(rs->verification_setup_ctx, response, len); errmsg = verification_setup_errmsg(rs->verification_setup_ctx); break; case 3: ret = verification_setup_response3(rs->verification_setup_ctx, response, len); errmsg = verification_setup_errmsg(rs->verification_setup_ctx); break; case 4: ret = verification_verify_response1(rs->verification_verify_ctx, response, len); errmsg = verification_verify_errmsg(rs->verification_verify_ctx); break; case 5: ret = 0; break; default: ret = -1; errmsg = "Bug! Bad step number"; } if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Verification step %d response from '%s' error: %s\n", step, rs->devname, errmsg); return ret; } static int raop_verification_request_send(int step, struct raop_session *rs, void (*cb)(struct evrtsp_request *, void *)) { struct evrtsp_request *req; uint8_t *body; uint32_t len; const char *errmsg; const char *url; const char *ctype; int ret; switch (step) { case 1: body = verification_setup_request1(&len, rs->verification_setup_ctx); errmsg = verification_setup_errmsg(rs->verification_setup_ctx); url = "/pair-setup-pin"; ctype = "application/x-apple-binary-plist"; break; case 2: body = verification_setup_request2(&len, rs->verification_setup_ctx); errmsg = verification_setup_errmsg(rs->verification_setup_ctx); url = "/pair-setup-pin"; ctype = "application/x-apple-binary-plist"; break; case 3: body = verification_setup_request3(&len, rs->verification_setup_ctx); errmsg = verification_setup_errmsg(rs->verification_setup_ctx); url = "/pair-setup-pin"; ctype = "application/x-apple-binary-plist"; break; case 4: body = verification_verify_request1(&len, rs->verification_verify_ctx); errmsg = verification_verify_errmsg(rs->verification_verify_ctx); url = "/pair-verify"; ctype = "application/octet-stream"; break; case 5: body = verification_verify_request2(&len, rs->verification_verify_ctx); errmsg = verification_verify_errmsg(rs->verification_verify_ctx); url = "/pair-verify"; ctype = "application/octet-stream"; break; default: body = NULL; errmsg = "Bug! Bad step number"; } if (!body) { DPRINTF(E_LOG, L_RAOP, "Verification step %d request error: %s\n", step, errmsg); return -1; } req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for verification step %d\n", step); return -1; } evbuffer_add(req->output_buffer, body, len); free(body); ret = raop_add_headers(rs, req, EVRTSP_REQ_POST); if (ret < 0) { evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Content-Type", ctype); DPRINTF(E_INFO, L_RAOP, "Making verification request step %d to '%s'\n", step, rs->devname); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_POST, url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Verification request step %d to '%s' failed\n", step, rs->devname); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static void raop_cb_verification_verify_step2(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; verification_verify_free(rs->verification_verify_ctx); ret = raop_verification_response_process(5, req, rs); if (ret < 0) { // Clear auth_key, the device did not accept it free(rs->device->auth_key); rs->device->auth_key = NULL; goto error; } DPRINTF(E_INFO, L_RAOP, "Verification of '%s' completed succesfully\n", rs->devname); rs->state = RAOP_STATE_STARTUP; raop_send_req_options(rs, raop_cb_startup_options); return; error: rs->state = RAOP_STATE_UNVERIFIED; raop_status(rs); } static void raop_cb_verification_verify_step1(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; ret = raop_verification_response_process(4, req, rs); if (ret < 0) { // Clear auth_key, the device did not accept it free(rs->device->auth_key); rs->device->auth_key = NULL; goto error; } ret = raop_verification_request_send(5, rs, raop_cb_verification_verify_step2); if (ret < 0) goto error; return; error: rs->state = RAOP_STATE_UNVERIFIED; raop_status(rs); verification_verify_free(rs->verification_verify_ctx); rs->verification_verify_ctx = NULL; } static int raop_verification_verify(struct raop_session *rs) { int ret; rs->verification_verify_ctx = verification_verify_new(rs->device->auth_key); // Naughty boy is dereferencing device if (!rs->verification_verify_ctx) { DPRINTF(E_LOG, L_RAOP, "Out of memory for verification verify context\n"); return -1; } ret = raop_verification_request_send(4, rs, raop_cb_verification_verify_step1); if (ret < 0) goto error; return 0; error: rs->state = RAOP_STATE_UNVERIFIED; raop_status(rs); verification_verify_free(rs->verification_verify_ctx); rs->verification_verify_ctx = NULL; return -1; } static void raop_cb_verification_setup_step3(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; const char *authorization_key; int ret; ret = raop_verification_response_process(3, req, rs); if (ret < 0) goto error; ret = verification_setup_result(&authorization_key, rs->verification_setup_ctx); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Verification setup result error: %s\n", verification_setup_errmsg(rs->verification_setup_ctx)); goto error; } DPRINTF(E_LOG, L_RAOP, "Verification setup stage complete, saving authorization key\n"); // Dereferencing output_device and a blocking db call... :-~ free(rs->device->auth_key); rs->device->auth_key = strdup(authorization_key); db_speaker_save(rs->device); // The player considers this session failed, so we don't need it any more raop_session_cleanup(rs); /* Fallthrough */ error: verification_setup_free(rs->verification_setup_ctx); rs->verification_setup_ctx = NULL; } static void raop_cb_verification_setup_step2(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; ret = raop_verification_response_process(2, req, rs); if (ret < 0) goto error; ret = raop_verification_request_send(3, rs, raop_cb_verification_setup_step3); if (ret < 0) goto error; return; error: verification_setup_free(rs->verification_setup_ctx); rs->verification_setup_ctx = NULL; } static void raop_cb_verification_setup_step1(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; int ret; ret = raop_verification_response_process(1, req, rs); if (ret < 0) goto error; ret = raop_verification_request_send(2, rs, raop_cb_verification_setup_step2); if (ret < 0) goto error; return; error: verification_setup_free(rs->verification_setup_ctx); rs->verification_setup_ctx = NULL; } static void raop_verification_setup(const char *pin) { struct raop_session *rs; int ret; for (rs = sessions; rs; rs = rs->next) { if (rs->state == RAOP_STATE_UNVERIFIED) break; } if (!rs) { DPRINTF(E_LOG, L_RAOP, "Got a PIN for device verification, but no device is awaiting verification\n"); return; } rs->verification_setup_ctx = verification_setup_new(pin); if (!rs->verification_setup_ctx) { DPRINTF(E_LOG, L_RAOP, "Out of memory for verification setup context\n"); return; } ret = raop_verification_request_send(1, rs, raop_cb_verification_setup_step1); if (ret < 0) goto error; return; error: verification_setup_free(rs->verification_setup_ctx); rs->verification_setup_ctx = NULL; } #else static int raop_verification_verify(struct raop_session *rs) { DPRINTF(E_LOG, L_RAOP, "Device '%s' requires verification, but forked-daapd was built with --disable-verification\n", rs->devname); return -1; } #endif /* RAOP_VERIFICATION */ /* RAOP devices discovery - mDNS callback */ /* Thread: main (mdns) */ /* Examples of txt content: * Apple TV 2: ["sf=0x4" "am=AppleTV2,1" "vs=130.14" "vn=65537" "tp=UDP" "ss=16" "sr=4 4100" "sv=false" "pw=false" "md=0,1,2" "et=0,3,5" "da=true" "cn=0,1,2,3" "ch=2"] ["sf=0x4" "am=AppleTV2,1" "vs=105.5" "md=0,1,2" "tp=TCP,UDP" "vn=65537" "pw=false" "ss=16" "sr=44100" "da=true" "sv=false" "et=0,3" "cn=0,1" "ch=2" "txtvers=1"] * Apple TV 3: ["vv=2" "vs=200.54" "vn=65537" "tp=UDP" "sf=0x44" "pk=8...f" "am=AppleTV3,1" "md=0,1,2" "ft=0x5A7FFFF7,0xE" "et=0,3,5" "da=true" "cn=0,1,2,3"] * Apple TV 4: ["vv=2" "vs=301.44.3" "vn=65537" "tp=UDP" "pk=9...f" "am=AppleTV5,3" "md=0,1,2" "sf=0x44" "ft=0x5A7FFFF7,0x4DE" "et=0,3,5" "da=true" "cn=0,1,2,3"] * Sony STR-DN1040: ["fv=s9327.1090.0" "am=STR-DN1040" "vs=141.9" "vn=65537" "tp=UDP" "ss=16" "sr=44100" "sv=false" "pw=false" "md=0,2" "ft=0x44F0A00" "et=0,4" "da=true" "cn=0,1" "ch=2" "txtvers=1"] * AirFoil: ["rastx=iafs" "sm=false" "raver=3.5.3.0" "ek=1" "md=0,1,2" "ramach=Win32NT.6" "et=0,1" "cn=0,1" "sr=44100" "ss=16" "raAudioFormats=ALAC" "raflakyzeroconf=true" "pw=false" "rast=afs" "vn=3" "sv=false" "txtvers=1" "ch=2" "tp=UDP"] * Xbmc 13: ["am=Xbmc,1" "md=0,1,2" "vs=130.14" "da=true" "vn=3" "pw=false" "sr=44100" "ss=16" "sm=false" "tp=UDP" "sv=false" "et=0,1" "ek=1" "ch=2" "cn=0,1" "txtvers=1"] * Shairport (abrasive/1.0): ["pw=false" "txtvers=1" "vn=3" "sr=44100" "ss=16" "ch=2" "cn=0,1" "et=0,1" "ek=1" "sm=false" "tp=UDP"] * JB2: ["fv=95.8947" "am=JB2 Gen" "vs=103.2" "tp=UDP" "vn=65537" "pw=false" "s s=16" "sr=44100" "da=true" "sv=false" "et=0,4" "cn=0,1" "ch=2" "txtvers=1"] * Airport Express 802.11g (Gen 1): ["tp=TCP,UDP" "sm=false" "sv=false" "ek=1" "et=0,1" "cn=0,1" "ch=2" "ss=16" "sr=44100" "pw=false" "vn=3" "txtvers=1"] * Airport Express 802.11n: 802.11n Gen 2 model (firmware 7.6.4): "am=Airport4,107", "et=0,1" 802.11n Gen 3 model (firmware 7.6.4): "am=Airport10,115", "et=0,4" */ static void raop_device_cb(const char *name, const char *type, const char *domain, const char *hostname, int family, const char *address, int port, struct keyval *txt) { struct output_device *rd; struct raop_extra *re; cfg_t *airplay; const char *p; char *at_name; char *password; uint64_t id; uint64_t sf; int ret; ret = safe_hextou64(name, &id); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not extract AirPlay device ID (%s)\n", name); return; } at_name = strchr(name, '@'); if (!at_name) { DPRINTF(E_LOG, L_RAOP, "Could not extract AirPlay device name (%s)\n", name); return; } at_name++; DPRINTF(E_DBG, L_RAOP, "Event for AirPlay device %s (port %d, id %" PRIx64 ")\n", at_name, port, id); airplay = cfg_gettsec(cfg, "airplay", at_name); if (airplay && cfg_getbool(airplay, "exclude")) { DPRINTF(E_LOG, L_RAOP, "Excluding AirPlay device '%s' as set in config\n", at_name); return; } rd = calloc(1, sizeof(struct output_device)); if (!rd) { DPRINTF(E_LOG, L_RAOP, "Out of memory (rd)\n"); return; } re = calloc(1, sizeof(struct raop_extra)); if (!re) { DPRINTF(E_LOG, L_RAOP, "Out of memory (re)\n"); free(rd); return; } rd->id = id; rd->name = strdup(at_name); rd->type = OUTPUT_TYPE_RAOP; rd->type_name = outputs_name(rd->type); rd->extra_device_info = re; if (port < 0) { /* Device stopped advertising */ switch (family) { case AF_INET: rd->v4_port = 1; break; case AF_INET6: rd->v6_port = 1; break; } ret = player_device_remove(rd); if (ret < 0) goto free_rd; return; } /* Protocol */ p = keyval_get(txt, "tp"); if (!p) { DPRINTF(E_LOG, L_RAOP, "AirPlay %s: no tp field in TXT record!\n", name); goto free_rd; } if (*p == '\0') { DPRINTF(E_LOG, L_RAOP, "AirPlay %s: tp has no value\n", name); goto free_rd; } if (!strstr(p, "UDP")) { DPRINTF(E_LOG, L_RAOP, "AirPlay %s: device does not support AirTunes v2 (tp=%s), discarding\n", name, p); goto free_rd; } /* Password protection */ password = NULL; p = keyval_get(txt, "pw"); if (!p) { DPRINTF(E_INFO, L_RAOP, "AirPlay %s: no pw field in TXT record, assuming no password protection\n", name); rd->has_password = 0; } else if (*p == '\0') { DPRINTF(E_LOG, L_RAOP, "AirPlay %s: pw has no value\n", name); goto free_rd; } else { rd->has_password = (strcmp(p, "false") != 0); } if (rd->has_password) { DPRINTF(E_LOG, L_RAOP, "AirPlay device %s is password-protected\n", name); airplay = cfg_gettsec(cfg, "airplay", at_name); if (airplay) password = cfg_getstr(airplay, "password"); if (!password) DPRINTF(E_LOG, L_RAOP, "No password given in config for AirPlay device %s\n", name); } rd->password = password; /* Device verification */ p = keyval_get(txt, "sf"); if (p && (safe_hextou64(p, &sf) == 0)) { if (sf & (1 << 9)) rd->requires_auth = 1; // Note: device_add() in player.c will get the auth key from the db if available } /* Device type */ re->devtype = RAOP_DEV_OTHER; p = keyval_get(txt, "am"); if (!p) re->devtype = RAOP_DEV_APEX1_80211G; // First generation AirPort Express else if (strncmp(p, "AirPort4", strlen("AirPort4")) == 0) re->devtype = RAOP_DEV_APEX2_80211N; // Second generation else if (strncmp(p, "AirPort", strlen("AirPort")) == 0) re->devtype = RAOP_DEV_APEX3_80211N; // Third generation and newer else if (strncmp(p, "AppleTV5,3", strlen("AppleTV5,3")) == 0) re->devtype = RAOP_DEV_APPLETV4; // Stream to ATV with tvOS 10 needs to be kept alive else if (strncmp(p, "AppleTV", strlen("AppleTV")) == 0) re->devtype = RAOP_DEV_APPLETV; else if (*p == '\0') DPRINTF(E_LOG, L_RAOP, "AirPlay %s: am has no value\n", name); /* Encrypt stream */ p = keyval_get(txt, "ek"); if (p && (*p == '1')) re->encrypt = 1; else re->encrypt = 0; /* Metadata support */ p = keyval_get(txt, "md"); if (p && (*p != '\0')) re->wants_metadata = 1; else re->wants_metadata = 0; rd->advertised = 1; switch (family) { case AF_INET: rd->v4_address = strdup(address); rd->v4_port = port; DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device %s: password: %u, verification: %u, encrypt: %u, metadata: %u, type %s, address %s:%d\n", name, rd->has_password, rd->requires_auth, re->encrypt, re->wants_metadata, raop_devtype[re->devtype], address, port); break; case AF_INET6: rd->v6_address = strdup(address); rd->v6_port = port; DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device %s: password: %u, verification: %u, encrypt: %u, metadata: %u, type %s, address [%s]:%d\n", name, rd->has_password, rd->requires_auth, re->encrypt, re->wants_metadata, raop_devtype[re->devtype], address, port); break; default: DPRINTF(E_LOG, L_RAOP, "Error: AirPlay device %s has neither ipv4 og ipv6 address\n", name); goto free_rd; } ret = player_device_add(rd); if (ret < 0) goto free_rd; return; free_rd: outputs_device_free(rd); } static int raop_device_start_generic(struct output_device *rd, output_status_cb cb, uint64_t rtptime, bool only_probe) { struct raop_session *rs; int ret; /* Send an OPTIONS request to establish the connection. If device verification * is required we start with that. After that, we can determine our local * address and build our session URL for all subsequent requests. */ rs = raop_session_make(rd, AF_INET6, cb, only_probe); if (rs) { rs->start_rtptime = rtptime; if (rd->auth_key) ret = raop_verification_verify(rs); else if (rd->requires_auth) ret = raop_send_req_pin_start(rs, raop_cb_pin_start); else ret = raop_send_req_options(rs, raop_cb_startup_options); if (ret == 0) return 0; else { DPRINTF(E_WARN, L_RAOP, "Could not send verification or OPTIONS request on IPv6\n"); raop_session_cleanup(rs); } } rs = raop_session_make(rd, AF_INET, cb, only_probe); if (!rs) return -1; rs->start_rtptime = rtptime; if (rd->auth_key) ret = raop_verification_verify(rs); else if (rd->requires_auth) ret = raop_send_req_pin_start(rs, raop_cb_pin_start); else ret = raop_send_req_options(rs, raop_cb_startup_options); if (ret < 0) { DPRINTF(E_WARN, L_RAOP, "Could not send verification or OPTIONS request on IPv4\n"); raop_session_cleanup(rs); return -1; } return 0; } static int raop_device_probe(struct output_device *rd, output_status_cb cb) { return raop_device_start_generic(rd, cb, 0, 1); } static int raop_device_start(struct output_device *rd, output_status_cb cb, uint64_t rtptime) { return raop_device_start_generic(rd, cb, rtptime, 0); } static void raop_device_stop(struct output_session *session) { struct raop_session *rs = session->session; if (!(rs->state & RAOP_STATE_F_CONNECTED)) raop_session_cleanup(rs); else raop_send_req_teardown(rs, raop_cb_shutdown_teardown); } static void raop_device_free_extra(struct output_device *device) { struct raop_extra *re = device->extra_device_info; free(re); } static void raop_playback_start(uint64_t next_pkt, struct timespec *ts) { struct raop_session *rs; event_del(flush_timer); evtimer_add(keep_alive_timer, &keep_alive_tv); sync_counter = 0; for (rs = sessions; rs; rs = rs->next) { if (rs->state == RAOP_STATE_CONNECTED) rs->state = RAOP_STATE_STREAMING; } /* Send initial playback sync */ raop_v2_control_send_sync(next_pkt, ts); } static void raop_playback_stop(void) { struct raop_session *rs; int ret; evtimer_del(keep_alive_timer); for (rs = sessions; rs; rs = rs->next) { ret = raop_send_req_teardown(rs, raop_cb_shutdown_teardown); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "shutdown: TEARDOWN request failed!\n"); } } static void raop_set_status_cb(struct output_session *session, output_status_cb cb) { struct raop_session *rs = session->session; rs->status_cb = cb; } static int raop_init(void) { char ebuf[64]; char *ptr; char *libname; gpg_error_t gc_err; int v6enabled; int family; int ret; timing_4svc.fd = -1; timing_4svc.port = 0; timing_6svc.fd = -1; timing_6svc.port = 0; control_4svc.fd = -1; control_4svc.port = 0; control_6svc.fd = -1; control_6svc.port = 0; sessions = NULL; pktbuf_size = 0; pktbuf_head = NULL; pktbuf_tail = NULL; metadata_head = NULL; metadata_tail = NULL; /* Generate RTP SSRC ID from library name */ libname = cfg_getstr(cfg_getsec(cfg, "library"), "name"); ssrc_id = djb_hash(libname, strlen(libname)); /* Random RTP sequence start */ gcry_randomize(&stream_seq, sizeof(stream_seq), GCRY_STRONG_RANDOM); /* Generate AES key and IV */ gcry_randomize(raop_aes_key, sizeof(raop_aes_key), GCRY_STRONG_RANDOM); gcry_randomize(raop_aes_iv, sizeof(raop_aes_iv), GCRY_STRONG_RANDOM); /* Setup AES */ gc_err = gcry_cipher_open(&raop_aes_ctx, GCRY_CIPHER_AES, GCRY_CIPHER_MODE_CBC, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not open AES cipher: %s\n", ebuf); return -1; } /* Set key */ gc_err = gcry_cipher_setkey(raop_aes_ctx, raop_aes_key, sizeof(raop_aes_key)); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not set AES key: %s\n", ebuf); goto out_close_cipher; } /* Prepare Base64-encoded key & IV for SDP */ raop_aes_key_b64 = raop_crypt_encrypt_aes_key_base64(); if (!raop_aes_key_b64) { DPRINTF(E_LOG, L_RAOP, "Couldn't encrypt and encode AES session key\n"); goto out_close_cipher; } raop_aes_iv_b64 = b64_encode(raop_aes_iv, sizeof(raop_aes_iv)); if (!raop_aes_iv_b64) { DPRINTF(E_LOG, L_RAOP, "Couldn't encode AES IV\n"); goto out_free_b64_key; } /* Remove base64 padding */ ptr = strchr(raop_aes_key_b64, '='); if (ptr) *ptr = '\0'; ptr = strchr(raop_aes_iv_b64, '='); if (ptr) *ptr = '\0'; flush_timer = evtimer_new(evbase_player, raop_flush_timer_cb, NULL); keep_alive_timer = evtimer_new(evbase_player, raop_keep_alive_timer_cb, NULL); if (!flush_timer || !keep_alive_timer) { DPRINTF(E_LOG, L_RAOP, "Out of memory for flush timer or keep alive timer\n"); goto out_free_b64_iv; } v6enabled = cfg_getbool(cfg_getsec(cfg, "general"), "ipv6"); ret = raop_v2_timing_start(v6enabled); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "AirPlay time synchronization failed to start\n"); goto out_free_timers; } ret = raop_v2_control_start(v6enabled); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "AirPlay playback control failed to start\n"); goto out_stop_timing; } if (v6enabled) v6enabled = !((timing_6svc.fd < 0) || (control_6svc.fd < 0)); if (v6enabled) family = AF_UNSPEC; else family = AF_INET; ret = mdns_browse("_raop._tcp", family, raop_device_cb); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not add mDNS browser for AirPlay devices\n"); goto out_stop_control; } return 0; out_stop_control: raop_v2_control_stop(); out_stop_timing: raop_v2_timing_stop(); out_free_timers: event_free(flush_timer); event_free(keep_alive_timer); out_free_b64_iv: free(raop_aes_iv_b64); out_free_b64_key: free(raop_aes_key_b64); out_close_cipher: gcry_cipher_close(raop_aes_ctx); return -1; } static void raop_deinit(void) { struct raop_session *rs; for (rs = sessions; sessions; rs = sessions) { sessions = rs->next; raop_session_free(rs); } raop_v2_control_stop(); raop_v2_timing_stop(); event_free(flush_timer); event_free(keep_alive_timer); gcry_cipher_close(raop_aes_ctx); free(raop_aes_key_b64); free(raop_aes_iv_b64); } struct output_definition output_raop = { .name = "AirPlay", .type = OUTPUT_TYPE_RAOP, .priority = 1, .disabled = 0, .init = raop_init, .deinit = raop_deinit, .device_start = raop_device_start, .device_stop = raop_device_stop, .device_probe = raop_device_probe, .device_free_extra = raop_device_free_extra, .device_volume_set = raop_set_volume_one, .playback_start = raop_playback_start, .playback_stop = raop_playback_stop, .write = raop_v2_write, .flush = raop_flush, .status_cb = raop_set_status_cb, .metadata_prepare = raop_metadata_prepare, .metadata_send = raop_metadata_send, .metadata_purge = raop_metadata_purge, .metadata_prune = raop_metadata_prune, #ifdef RAOP_VERIFICATION .authorize = raop_verification_setup, #endif }; forked-daapd-25.0/src/outputs/raop_verification.c000066400000000000000000001051021313447753700221570ustar00rootroot00000000000000/* * * The Secure Remote Password 6a implementation included here is by * - Tom Cocagne * * * * The MIT License (MIT) * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ #include #include #include #include #include #include "raop_verification.h" #define CONFIG_GCRYPT 1 /* -------------------- GCRYPT AND OPENSSL COMPABILITY --------------------- */ /* partly borrowed from ffmpeg (rtmpdh.c) */ #if CONFIG_GCRYPT #include #define SHA512_DIGEST_LENGTH 64 #define bnum_new(bn) \ do { \ if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { \ if (!gcry_check_version("1.5.4")) \ abort(); \ gcry_control(GCRYCTL_DISABLE_SECMEM, 0); \ gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); \ } \ bn = gcry_mpi_new(1); \ } while (0) #define bnum_free(bn) gcry_mpi_release(bn) #define bnum_num_bytes(bn) (gcry_mpi_get_nbits(bn) + 7) / 8 #define bnum_is_zero(bn) (gcry_mpi_cmp_ui(bn, (unsigned long)0) == 0) #define bnum_bn2bin(bn, buf, len) gcry_mpi_print(GCRYMPI_FMT_USG, buf, len, NULL, bn) #define bnum_bin2bn(bn, buf, len) gcry_mpi_scan(&bn, GCRYMPI_FMT_USG, buf, len, NULL) #define bnum_hex2bn(bn, buf) gcry_mpi_scan(&bn, GCRYMPI_FMT_HEX, buf, 0, 0) #define bnum_random(bn, num_bits) gcry_mpi_randomize(bn, num_bits, GCRY_WEAK_RANDOM) #define bnum_add(bn, a, b) gcry_mpi_add(bn, a, b) #define bnum_sub(bn, a, b) gcry_mpi_sub(bn, a, b) #define bnum_mul(bn, a, b) gcry_mpi_mul(bn, a, b) typedef gcry_mpi_t bnum; static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p) { gcry_mpi_powm(bn, y, q, p); } #elif CONFIG_OPENSSL #include #include #include #include #include #define bnum_new(bn) bn = BN_new() #define bnum_free(bn) BN_free(bn) #define bnum_num_bytes(bn) BN_num_bytes(bn) #define bnum_is_zero(bn) BN_is_zero(bn) #define bnum_bn2bin(bn, buf, len) BN_bn2bin(bn, buf) #define bnum_bin2bn(bn, buf, len) bn = BN_bin2bn(buf, len, 0) #define bnum_hex2bn(bn, buf) BN_hex2bn(&bn, buf) #define bnum_random(bn, num_bits) BN_rand(bn, num_bits, 0, 0) #define bnum_add(bn, a, b) BN_add(bn, a, b) #define bnum_sub(bn, a, b) BN_sub(bn, a, b) typedef BIGNUM* bnum; static void bnum_mul(bnum bn, bnum a, bnum b) { // No error handling BN_CTX *ctx = BN_CTX_new(); BN_mul(bn, a, b, ctx); BN_CTX_free(ctx); } static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p) { // No error handling BN_CTX *ctx = BN_CTX_new(); BN_mod_exp(bn, y, q, p, ctx); BN_CTX_free(ctx); } #endif /* ----------------------------- DEFINES ETC ------------------------------- */ #define USERNAME "12:34:56:78:90:AB" #define EPK_LENGTH 32 #define AUTHTAG_LENGTH 16 #define AES_SETUP_KEY "Pair-Setup-AES-Key" #define AES_SETUP_IV "Pair-Setup-AES-IV" #define AES_VERIFY_KEY "Pair-Verify-AES-Key" #define AES_VERIFY_IV "Pair-Verify-AES-IV" #ifdef CONFIG_OPENSSL enum hash_alg { HASH_SHA1, HASH_SHA224, HASH_SHA256, HASH_SHA384, HASH_SHA512, }; #elif CONFIG_GCRYPT enum hash_alg { HASH_SHA1 = GCRY_MD_SHA1, HASH_SHA224 = GCRY_MD_SHA224, HASH_SHA256 = GCRY_MD_SHA256, HASH_SHA384 = GCRY_MD_SHA384, HASH_SHA512 = GCRY_MD_SHA512, }; #endif struct verification_setup_context { struct SRPUser *user; char pin[4]; const uint8_t *pkA; int pkA_len; uint8_t *pkB; uint64_t pkB_len; const uint8_t *M1; int M1_len; uint8_t *M2; uint64_t M2_len; uint8_t *salt; uint64_t salt_len; uint8_t public_key[crypto_sign_PUBLICKEYBYTES]; uint8_t private_key[crypto_sign_SECRETKEYBYTES]; // Hex-formatet concatenation of public + private, 0-terminated char auth_key[2 * (crypto_sign_PUBLICKEYBYTES + crypto_sign_SECRETKEYBYTES) + 1]; // We don't actually use the server's epk and authtag for anything uint8_t *epk; uint64_t epk_len; uint8_t *authtag; uint64_t authtag_len; const char *errmsg; }; struct verification_verify_context { uint8_t server_eph_public_key[32]; uint8_t server_public_key[64]; uint8_t client_public_key[crypto_sign_PUBLICKEYBYTES]; uint8_t client_private_key[crypto_sign_SECRETKEYBYTES]; uint8_t client_eph_public_key[32]; uint8_t client_eph_private_key[32]; const char *errmsg; }; /* ---------------------------------- SRP ---------------------------------- */ typedef enum { SRP_NG_2048, SRP_NG_CUSTOM } SRP_NGType; typedef struct { bnum N; bnum g; } NGConstant; #if CONFIG_OPENSSL typedef union { SHA_CTX sha; SHA256_CTX sha256; SHA512_CTX sha512; } HashCTX; #elif CONFIG_GCRYPT typedef gcry_md_hd_t HashCTX; #endif struct SRPUser { enum hash_alg alg; NGConstant *ng; bnum a; bnum A; bnum S; const unsigned char *bytes_A; int authenticated; const char *username; const unsigned char *password; int password_len; unsigned char M [SHA512_DIGEST_LENGTH]; unsigned char H_AMK [SHA512_DIGEST_LENGTH]; unsigned char session_key [2 * SHA512_DIGEST_LENGTH]; // See hash_session_key() int session_key_len; }; struct NGHex { const char *n_hex; const char *g_hex; }; // We only need 2048 right now, but keep the array in case we want to add others later // All constants here were pulled from Appendix A of RFC 5054 static struct NGHex global_Ng_constants[] = { { /* 2048 */ "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4" "A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60" "95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF" "747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907" "8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861" "60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB" "FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73", "2" }, {0,0} /* null sentinel */ }; static NGConstant * new_ng(SRP_NGType ng_type, const char *n_hex, const char *g_hex) { NGConstant *ng = calloc(1, sizeof(NGConstant)); if ( ng_type != SRP_NG_CUSTOM ) { n_hex = global_Ng_constants[ ng_type ].n_hex; g_hex = global_Ng_constants[ ng_type ].g_hex; } bnum_hex2bn(ng->N, n_hex); bnum_hex2bn(ng->g, g_hex); return ng; } static void free_ng(NGConstant * ng) { if (!ng) return; bnum_free(ng->N); bnum_free(ng->g); free(ng); } static int hash_init(enum hash_alg alg, HashCTX *c) { #if CONFIG_OPENSSL switch (alg) { case HASH_SHA1 : return SHA1_Init(&c->sha); case HASH_SHA224: return SHA224_Init(&c->sha256); case HASH_SHA256: return SHA256_Init(&c->sha256); case HASH_SHA384: return SHA384_Init(&c->sha512); case HASH_SHA512: return SHA512_Init(&c->sha512); default: return -1; }; #elif CONFIG_GCRYPT return gcry_md_open(c, alg, 0); #endif } static int hash_update(enum hash_alg alg, HashCTX *c, const void *data, size_t len) { #if CONFIG_OPENSSL switch (alg) { case HASH_SHA1 : return SHA1_Update(&c->sha, data, len); case HASH_SHA224: return SHA224_Update(&c->sha256, data, len); case HASH_SHA256: return SHA256_Update(&c->sha256, data, len); case HASH_SHA384: return SHA384_Update(&c->sha512, data, len); case HASH_SHA512: return SHA512_Update(&c->sha512, data, len); default: return -1; }; #elif CONFIG_GCRYPT gcry_md_write(*c, data, len); return 0; #endif } static int hash_final(enum hash_alg alg, HashCTX *c, unsigned char *md) { #if CONFIG_OPENSSL switch (alg) { case HASH_SHA1 : return SHA1_Final(md, &c->sha); case HASH_SHA224: return SHA224_Final(md, &c->sha256); case HASH_SHA256: return SHA256_Final(md, &c->sha256); case HASH_SHA384: return SHA384_Final(md, &c->sha512); case HASH_SHA512: return SHA512_Final(md, &c->sha512); default: return -1; }; #elif CONFIG_GCRYPT unsigned char *buf = gcry_md_read(*c, alg); if (!buf) return -1; memcpy(md, buf, gcry_md_get_algo_dlen(alg)); gcry_md_close(*c); return 0; #endif } static unsigned char * hash(enum hash_alg alg, const unsigned char *d, size_t n, unsigned char *md) { #if CONFIG_OPENSSL switch (alg) { case HASH_SHA1 : return SHA1(d, n, md); case HASH_SHA224: return SHA224(d, n, md); case HASH_SHA256: return SHA256(d, n, md); case HASH_SHA384: return SHA384(d, n, md); case HASH_SHA512: return SHA512(d, n, md); default: return NULL; }; #elif CONFIG_GCRYPT gcry_md_hash_buffer(alg, md, d, n); return md; #endif } static int hash_length(enum hash_alg alg) { #if CONFIG_OPENSSL switch (alg) { case HASH_SHA1 : return SHA_DIGEST_LENGTH; case HASH_SHA224: return SHA224_DIGEST_LENGTH; case HASH_SHA256: return SHA256_DIGEST_LENGTH; case HASH_SHA384: return SHA384_DIGEST_LENGTH; case HASH_SHA512: return SHA512_DIGEST_LENGTH; default: return -1; }; #elif CONFIG_GCRYPT return gcry_md_get_algo_dlen(alg); #endif } static int hash_ab(enum hash_alg alg, unsigned char *md, const unsigned char *m1, int m1_len, const unsigned char *m2, int m2_len) { HashCTX ctx; hash_init(alg, &ctx); hash_update(alg, &ctx, m1, m1_len); hash_update(alg, &ctx, m2, m2_len); return hash_final(alg, &ctx, md); } static bnum H_nn_pad(enum hash_alg alg, const bnum n1, const bnum n2) { bnum bn; unsigned char *bin; unsigned char buff[SHA512_DIGEST_LENGTH]; int len_n1 = bnum_num_bytes(n1); int len_n2 = bnum_num_bytes(n2); int nbytes = 2 * len_n1; if ((len_n2 < 1) || (len_n2 > len_n1)) return 0; bin = calloc( 1, nbytes ); bnum_bn2bin(n1, bin, len_n1); bnum_bn2bin(n2, bin + nbytes - len_n2, len_n2); hash( alg, bin, nbytes, buff ); free(bin); bnum_bin2bn(bn, buff, hash_length(alg)); return bn; } static bnum H_ns(enum hash_alg alg, const bnum n, const unsigned char *bytes, int len_bytes) { bnum bn; unsigned char buff[SHA512_DIGEST_LENGTH]; int len_n = bnum_num_bytes(n); int nbytes = len_n + len_bytes; unsigned char *bin = malloc(nbytes); bnum_bn2bin(n, bin, len_n); memcpy( bin + len_n, bytes, len_bytes ); hash( alg, bin, nbytes, buff ); free(bin); bnum_bin2bn(bn, buff, hash_length(alg)); return bn; } static bnum calculate_x(enum hash_alg alg, const bnum salt, const char *username, const unsigned char *password, int password_len) { unsigned char ucp_hash[SHA512_DIGEST_LENGTH]; HashCTX ctx; hash_init( alg, &ctx ); hash_update( alg, &ctx, username, strlen(username) ); hash_update( alg, &ctx, ":", 1 ); hash_update( alg, &ctx, password, password_len ); hash_final( alg, &ctx, ucp_hash ); return H_ns( alg, salt, ucp_hash, hash_length(alg) ); } static void update_hash_n(enum hash_alg alg, HashCTX *ctx, const bnum n) { unsigned long len = bnum_num_bytes(n); unsigned char *n_bytes = malloc(len); bnum_bn2bin(n, n_bytes, len); hash_update(alg, ctx, n_bytes, len); free(n_bytes); } static void hash_num(enum hash_alg alg, const bnum n, unsigned char *dest) { int nbytes = bnum_num_bytes(n); unsigned char *bin = malloc(nbytes); bnum_bn2bin(n, bin, nbytes); hash( alg, bin, nbytes, dest ); free(bin); } static int hash_session_key(enum hash_alg alg, const bnum n, unsigned char *dest) { int nbytes = bnum_num_bytes(n); unsigned char *bin = malloc(nbytes); unsigned char fourbytes[4] = { 0 }; // Only God knows the reason for this, and perhaps some poor soul at Apple bnum_bn2bin(n, bin, nbytes); hash_ab(alg, dest, bin, nbytes, fourbytes, sizeof(fourbytes)); fourbytes[3] = 1; // Again, only ... hash_ab(alg, dest + hash_length(alg), bin, nbytes, fourbytes, sizeof(fourbytes)); free(bin); return (2 * hash_length(alg)); } static void calculate_M(enum hash_alg alg, NGConstant *ng, unsigned char *dest, const char *I, const bnum s, const bnum A, const bnum B, const unsigned char *K, int K_len) { unsigned char H_N[ SHA512_DIGEST_LENGTH ]; unsigned char H_g[ SHA512_DIGEST_LENGTH ]; unsigned char H_I[ SHA512_DIGEST_LENGTH ]; unsigned char H_xor[ SHA512_DIGEST_LENGTH ]; HashCTX ctx; int i = 0; int hash_len = hash_length(alg); hash_num( alg, ng->N, H_N ); hash_num( alg, ng->g, H_g ); hash(alg, (const unsigned char *)I, strlen(I), H_I); for (i = 0; i < hash_len; i++) H_xor[i] = H_N[i] ^ H_g[i]; hash_init( alg, &ctx ); hash_update( alg, &ctx, H_xor, hash_len ); hash_update( alg, &ctx, H_I, hash_len ); update_hash_n( alg, &ctx, s ); update_hash_n( alg, &ctx, A ); update_hash_n( alg, &ctx, B ); hash_update( alg, &ctx, K, K_len ); hash_final( alg, &ctx, dest ); } static void calculate_H_AMK(enum hash_alg alg, unsigned char *dest, const bnum A, const unsigned char * M, const unsigned char * K, int K_len) { HashCTX ctx; hash_init( alg, &ctx ); update_hash_n( alg, &ctx, A ); hash_update( alg, &ctx, M, hash_length(alg) ); hash_update( alg, &ctx, K, K_len ); hash_final( alg, &ctx, dest ); } static struct SRPUser * srp_user_new(enum hash_alg alg, SRP_NGType ng_type, const char *username, const unsigned char *bytes_password, int len_password, const char *n_hex, const char *g_hex) { struct SRPUser *usr = calloc(1, sizeof(struct SRPUser)); int ulen = strlen(username) + 1; if (!usr) goto err_exit; usr->alg = alg; usr->ng = new_ng( ng_type, n_hex, g_hex ); bnum_new(usr->a); bnum_new(usr->A); bnum_new(usr->S); if (!usr->ng || !usr->a || !usr->A || !usr->S) goto err_exit; usr->username = (const char *) malloc(ulen); usr->password = (const unsigned char *) malloc(len_password); usr->password_len = len_password; if (!usr->username || !usr->password) goto err_exit; memcpy((char *)usr->username, username, ulen); memcpy((char *)usr->password, bytes_password, len_password); usr->authenticated = 0; usr->bytes_A = 0; return usr; err_exit: if (!usr) return NULL; bnum_free(usr->a); bnum_free(usr->A); bnum_free(usr->S); if (usr->username) free((void*)usr->username); if (usr->password) { memset((void*)usr->password, 0, usr->password_len); free((void*)usr->password); } free(usr); return NULL; } static void srp_user_delete(struct SRPUser *usr) { if(!usr) return; bnum_free(usr->a); bnum_free(usr->A); bnum_free(usr->S); free_ng(usr->ng); memset((void*)usr->password, 0, usr->password_len); free((char *)usr->username); free((char *)usr->password); if (usr->bytes_A) free( (char *)usr->bytes_A ); memset(usr, 0, sizeof(*usr)); free(usr); } static int srp_user_is_authenticated(struct SRPUser *usr) { return usr->authenticated; } static const unsigned char * srp_user_get_session_key(struct SRPUser *usr, int *key_length) { if (key_length) *key_length = usr->session_key_len; return usr->session_key; } /* Output: username, bytes_A, len_A */ static void srp_user_start_authentication(struct SRPUser *usr, const char **username, const unsigned char **bytes_A, int *len_A) { bnum_random(usr->a, 256); bnum_modexp(usr->A, usr->ng->g, usr->a, usr->ng->N); *len_A = bnum_num_bytes(usr->A); *bytes_A = malloc(*len_A); if (!*bytes_A) { *len_A = 0; *bytes_A = 0; *username = 0; return; } bnum_bn2bin(usr->A, (unsigned char *) *bytes_A, *len_A); usr->bytes_A = *bytes_A; *username = usr->username; } /* Output: bytes_M. Buffer length is SHA512_DIGEST_LENGTH */ static void srp_user_process_challenge(struct SRPUser *usr, const unsigned char *bytes_s, int len_s, const unsigned char *bytes_B, int len_B, const unsigned char **bytes_M, int *len_M ) { bnum s, B, k, v; bnum tmp1, tmp2, tmp3; bnum u, x; *len_M = 0; *bytes_M = 0; bnum_bin2bn(s, bytes_s, len_s); bnum_bin2bn(B, bytes_B, len_B); k = H_nn_pad(usr->alg, usr->ng->N, usr->ng->g); bnum_new(v); bnum_new(tmp1); bnum_new(tmp2); bnum_new(tmp3); if (!s || !B || !k || !v || !tmp1 || !tmp2 || !tmp3) goto cleanup1; u = H_nn_pad(usr->alg, usr->A, B); x = calculate_x(usr->alg, s, usr->username, usr->password, usr->password_len); if (!u || !x) goto cleanup2; // SRP-6a safety check if (!bnum_is_zero(B) && !bnum_is_zero(u)) { bnum_modexp(v, usr->ng->g, x, usr->ng->N); // S = (B - k*(g^x)) ^ (a + ux) bnum_mul(tmp1, u, x); bnum_add(tmp2, usr->a, tmp1); // tmp2 = (a + ux) bnum_modexp(tmp1, usr->ng->g, x, usr->ng->N); bnum_mul(tmp3, k, tmp1); // tmp3 = k*(g^x) bnum_sub(tmp1, B, tmp3); // tmp1 = (B - K*(g^x)) bnum_modexp(usr->S, tmp1, tmp2, usr->ng->N); usr->session_key_len = hash_session_key(usr->alg, usr->S, usr->session_key); calculate_M(usr->alg, usr->ng, usr->M, usr->username, s, usr->A, B, usr->session_key, usr->session_key_len); calculate_H_AMK(usr->alg, usr->H_AMK, usr->A, usr->M, usr->session_key, usr->session_key_len); *bytes_M = usr->M; if (len_M) *len_M = hash_length(usr->alg); } else { *bytes_M = NULL; if (len_M) *len_M = 0; } cleanup2: bnum_free(x); bnum_free(u); cleanup1: bnum_free(tmp3); bnum_free(tmp2); bnum_free(tmp1); bnum_free(v); bnum_free(k); bnum_free(B); bnum_free(s); } static void srp_user_verify_session(struct SRPUser *usr, const unsigned char *bytes_HAMK) { if (memcmp(usr->H_AMK, bytes_HAMK, hash_length(usr->alg)) == 0) usr->authenticated = 1; } /* -------------------------------- HELPERS -------------------------------- */ static int encrypt_gcm(unsigned char *ciphertext, int ciphertext_len, unsigned char *tag, unsigned char *plaintext, int plaintext_len, unsigned char *key, unsigned char *iv, const char **errmsg) { #ifdef CONFIG_OPENSSL EVP_CIPHER_CTX *ctx; int len; *errmsg = NULL; if ( !(ctx = EVP_CIPHER_CTX_new()) || (EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) || (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL) != 1) || (EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv) != 1) ) { *errmsg = "Error initialising AES 128 GCM encryption"; goto error; } if (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len) != 1) { *errmsg = "Error GCM encrypting"; goto error; } if (len > ciphertext_len) { *errmsg = "Bug! Buffer overflow"; goto error; } if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1) { *errmsg = "Error finalising GCM encryption"; goto error; } if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, AUTHTAG_LENGTH, tag) != 1) { *errmsg = "Error getting authtag"; goto error; } EVP_CIPHER_CTX_free(ctx); return 0; error: EVP_CIPHER_CTX_free(ctx); return -1; #elif CONFIG_GCRYPT gcry_cipher_hd_t hd; int ret; ret = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_GCM, 0); if (ret < 0) { *errmsg = "Error initialising AES 128 GCM encryption"; return -1; } if ( (gcry_cipher_setkey(hd, key, gcry_cipher_get_algo_keylen(GCRY_CIPHER_AES128)) < 0) || (gcry_cipher_setiv(hd, iv, gcry_cipher_get_algo_blklen(GCRY_CIPHER_AES128)) < 0)) { *errmsg = "Could not set key or iv for AES 128 GCM"; goto error; } ret = gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext, plaintext_len); if (ret < 0) { *errmsg = "Error GCM encrypting"; goto error; } ret = gcry_cipher_gettag(hd, tag, AUTHTAG_LENGTH); if (ret < 0) { *errmsg = "Error getting authtag"; goto error; } gcry_cipher_close(hd); return 0; error: gcry_cipher_close(hd); return -1; #endif } static int encrypt_ctr(unsigned char *ciphertext, int ciphertext_len, unsigned char *plaintext1, int plaintext1_len, unsigned char *plaintext2, int plaintext2_len, unsigned char *key, unsigned char *iv, const char **errmsg) { #ifdef CONFIG_OPENSSL EVP_CIPHER_CTX *ctx; int len; *errmsg = NULL; if ( !(ctx = EVP_CIPHER_CTX_new()) || (EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), NULL, key, iv) != 1) ) { *errmsg = "Error initialising AES 128 CTR encryption"; goto error; } if ( (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext1, plaintext1_len) != 1) || (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext2, plaintext2_len) != 1) ) { *errmsg = "Error CTR encrypting"; goto error; } if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1) { *errmsg = "Error finalising encryption"; goto error; } EVP_CIPHER_CTX_free(ctx); return 0; error: EVP_CIPHER_CTX_free(ctx); return -1; #elif CONFIG_GCRYPT gcry_cipher_hd_t hd; if (gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_CTR, 0) < 0) { *errmsg = "Error initialising AES 128 CTR encryption"; return -1; } if ( (gcry_cipher_setkey(hd, key, gcry_cipher_get_algo_keylen(GCRY_CIPHER_AES128)) < 0) || (gcry_cipher_setctr(hd, iv, gcry_cipher_get_algo_blklen(GCRY_CIPHER_AES128)) < 0) ) { *errmsg = "Could not set key or iv for AES 128 CTR"; goto error; } if ( (gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext1, plaintext1_len) < 0) || (gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, plaintext2, plaintext2_len) < 0) ) { *errmsg = "Error CTR encrypting"; goto error; } gcry_cipher_close(hd); return 0; error: gcry_cipher_close(hd); return -1; #endif } /* ---------------------------------- API ---------------------------------- */ struct verification_setup_context * verification_setup_new(const char *pin) { struct verification_setup_context *sctx; if (sodium_init() == -1) return NULL; sctx = calloc(1, sizeof(struct verification_setup_context)); if (!sctx) return NULL; memcpy(sctx->pin, pin, sizeof(sctx->pin)); return sctx; } void verification_setup_free(struct verification_setup_context *sctx) { if (!sctx) return; srp_user_delete(sctx->user); free(sctx->pkB); free(sctx->M2); free(sctx->salt); free(sctx->epk); free(sctx->authtag); free(sctx); } const char * verification_setup_errmsg(struct verification_setup_context *sctx) { return sctx->errmsg; } uint8_t * verification_setup_request1(uint32_t *len, struct verification_setup_context *sctx) { plist_t dict; plist_t method; plist_t user; char *data = NULL; // Necessary to initialize because plist_to_bin() uses value sctx->user = srp_user_new(HASH_SHA1, SRP_NG_2048, USERNAME, (unsigned char *)sctx->pin, sizeof(sctx->pin), 0, 0); dict = plist_new_dict(); method = plist_new_string("pin"); user = plist_new_string(USERNAME); plist_dict_set_item(dict, "method", method); plist_dict_set_item(dict, "user", user); plist_to_bin(dict, &data, len); plist_free(dict); return (uint8_t *)data; } uint8_t * verification_setup_request2(uint32_t *len, struct verification_setup_context *sctx) { plist_t dict; plist_t pk; plist_t proof; const char *auth_username = NULL; char *data = NULL; // Calculate A srp_user_start_authentication(sctx->user, &auth_username, &sctx->pkA, &sctx->pkA_len); // Calculate M1 (client proof) srp_user_process_challenge(sctx->user, (const unsigned char *)sctx->salt, sctx->salt_len, (const unsigned char *)sctx->pkB, sctx->pkB_len, &sctx->M1, &sctx->M1_len); pk = plist_new_data((char *)sctx->pkA, sctx->pkA_len); proof = plist_new_data((char *)sctx->M1, sctx->M1_len); dict = plist_new_dict(); plist_dict_set_item(dict, "pk", pk); plist_dict_set_item(dict, "proof", proof); plist_to_bin(dict, &data, len); plist_free(dict); return (uint8_t *)data; } uint8_t * verification_setup_request3(uint32_t *len, struct verification_setup_context *sctx) { plist_t dict; plist_t epk; plist_t authtag; char *data = NULL; const unsigned char *session_key; int session_key_len; unsigned char key[SHA512_DIGEST_LENGTH]; unsigned char iv[SHA512_DIGEST_LENGTH]; unsigned char encrypted[128]; // Alloc a bit extra - should only need 2*16 unsigned char tag[16]; const char *errmsg; int ret; session_key = srp_user_get_session_key(sctx->user, &session_key_len); if (!session_key) { sctx->errmsg = "Setup request 3: No valid session key"; return NULL; } ret = hash_ab(HASH_SHA512, key, (unsigned char *)AES_SETUP_KEY, strlen(AES_SETUP_KEY), session_key, session_key_len); if (ret < 0) { sctx->errmsg = "Setup request 3: Hashing of key string and shared secret failed"; return NULL; } ret = hash_ab(HASH_SHA512, iv, (unsigned char *)AES_SETUP_IV, strlen(AES_SETUP_IV), session_key, session_key_len); if (ret < 0) { sctx->errmsg = "Setup request 3: Hashing of iv string and shared secret failed"; return NULL; } iv[15]++; // Magic /* if (iv[15] == 0x00 || iv[15] == 0xff) printf("- note that value of last byte is %d!\n", iv[15]); */ crypto_sign_keypair(sctx->public_key, sctx->private_key); ret = encrypt_gcm(encrypted, sizeof(encrypted), tag, sctx->public_key, sizeof(sctx->public_key), key, iv, &errmsg); if (ret < 0) { sctx->errmsg = errmsg; return NULL; } epk = plist_new_data((char *)encrypted, EPK_LENGTH); authtag = plist_new_data((char *)tag, AUTHTAG_LENGTH); dict = plist_new_dict(); plist_dict_set_item(dict, "epk", epk); plist_dict_set_item(dict, "authTag", authtag); plist_to_bin(dict, &data, len); plist_free(dict); return (uint8_t *)data; } int verification_setup_response1(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len) { plist_t dict; plist_t pk; plist_t salt; plist_from_bin((const char *)data, data_len, &dict); pk = plist_dict_get_item(dict, "pk"); salt = plist_dict_get_item(dict, "salt"); if (!pk || !salt) { sctx->errmsg = "Setup response 1: Missing pk or salt"; plist_free(dict); return -1; } plist_get_data_val(pk, (char **)&sctx->pkB, &sctx->pkB_len); // B plist_get_data_val(salt, (char **)&sctx->salt, &sctx->salt_len); plist_free(dict); return 0; } int verification_setup_response2(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len) { plist_t dict; plist_t proof; plist_from_bin((const char *)data, data_len, &dict); proof = plist_dict_get_item(dict, "proof"); if (!proof) { sctx->errmsg = "Setup response 2: Missing proof"; plist_free(dict); return -1; } plist_get_data_val(proof, (char **)&sctx->M2, &sctx->M2_len); // M2 plist_free(dict); // Check M2 srp_user_verify_session(sctx->user, (const unsigned char *)sctx->M2); if (!srp_user_is_authenticated(sctx->user)) { sctx->errmsg = "Setup response 2: Server authentication failed"; return -1; } return 0; } int verification_setup_response3(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len) { plist_t dict; plist_t epk; plist_t authtag; plist_from_bin((const char *)data, data_len, &dict); epk = plist_dict_get_item(dict, "epk"); if (!epk) { sctx->errmsg = "Setup response 3: Missing epk"; plist_free(dict); return -1; } plist_get_data_val(epk, (char **)&sctx->epk, &sctx->epk_len); authtag = plist_dict_get_item(dict, "authTag"); if (!authtag) { sctx->errmsg = "Setup response 3: Missing authTag"; plist_free(dict); return -1; } plist_get_data_val(authtag, (char **)&sctx->authtag, &sctx->authtag_len); plist_free(dict); return 0; } int verification_setup_result(const char **authorisation_key, struct verification_setup_context *sctx) { struct verification_verify_context *vctx; char *ptr; int i; if (sizeof(vctx->client_public_key) != sizeof(sctx->public_key) || sizeof(vctx->client_private_key) != sizeof(sctx->private_key)) { sctx->errmsg = "Setup result: Bug!"; return -1; } // Fills out the auth_key with public + private in hex. It seems that the private // key actually includes the public key (last 32 bytes), so we could in // principle just export the private key ptr = sctx->auth_key; for (i = 0; i < sizeof(sctx->public_key); i++) ptr += sprintf(ptr, "%02x", sctx->public_key[i]); for (i = 0; i < sizeof(sctx->private_key); i++) ptr += sprintf(ptr, "%02x", sctx->private_key[i]); *ptr = '\0'; *authorisation_key = sctx->auth_key; return 0; } struct verification_verify_context * verification_verify_new(const char *authorisation_key) { struct verification_verify_context *vctx; char hex[] = { 0, 0, 0 }; const char *ptr; int i; if (sodium_init() == -1) return NULL; if (!authorisation_key) return NULL; if (strlen(authorisation_key) != 2 * (sizeof(vctx->client_public_key) + sizeof(vctx->client_private_key))) return NULL; vctx = calloc(1, sizeof(struct verification_verify_context)); if (!vctx) return NULL; ptr = authorisation_key; for (i = 0; i < sizeof(vctx->client_public_key); i++, ptr+=2) { hex[0] = ptr[0]; hex[1] = ptr[1]; vctx->client_public_key[i] = strtol(hex, NULL, 16); } for (i = 0; i < sizeof(vctx->client_private_key); i++, ptr+=2) { hex[0] = ptr[0]; hex[1] = ptr[1]; vctx->client_private_key[i] = strtol(hex, NULL, 16); } return vctx; } void verification_verify_free(struct verification_verify_context *vctx) { if (!vctx) return; free(vctx); } const char * verification_verify_errmsg(struct verification_verify_context *vctx) { return vctx->errmsg; } uint8_t * verification_verify_request1(uint32_t *len, struct verification_verify_context *vctx) { const uint8_t basepoint[32] = {9}; uint8_t *data; int ret; ret = crypto_scalarmult(vctx->client_eph_public_key, vctx->client_eph_private_key, basepoint); if (ret < 0) { vctx->errmsg = "Verify request 1: Curve 25519 returned an error"; return NULL; } *len = 4 + sizeof(vctx->client_eph_public_key) + sizeof(vctx->client_public_key); data = calloc(1, *len); if (!data) { vctx->errmsg = "Verify request 1: Out of memory"; return NULL; } data[0] = 1; // Magic memcpy(data + 4, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); memcpy(data + 4 + sizeof(vctx->client_eph_public_key), vctx->client_public_key, sizeof(vctx->client_public_key)); return data; } uint8_t * verification_verify_request2(uint32_t *len, struct verification_verify_context *vctx) { uint8_t shared_secret[crypto_scalarmult_BYTES]; uint8_t key[SHA512_DIGEST_LENGTH]; uint8_t iv[SHA512_DIGEST_LENGTH]; uint8_t encrypted[128]; // Alloc a bit extra, should only really need size of public key len uint8_t signature[crypto_sign_BYTES]; uint8_t *data; int ret; const char *errmsg; *len = sizeof(vctx->client_eph_public_key) + sizeof(vctx->server_eph_public_key); data = calloc(1, *len); if (!data) { vctx->errmsg = "Verify request 2: Out of memory"; return NULL; } memcpy(data, vctx->client_eph_public_key, sizeof(vctx->client_eph_public_key)); memcpy(data + sizeof(vctx->client_eph_public_key), vctx->server_eph_public_key, sizeof(vctx->server_eph_public_key)); crypto_sign_detached(signature, NULL, data, *len, vctx->client_private_key); free(data); ret = crypto_scalarmult(shared_secret, vctx->client_eph_private_key, vctx->server_eph_public_key); if (ret < 0) { vctx->errmsg = "Verify request 2: Curve 25519 returned an error"; return NULL; } ret = hash_ab(HASH_SHA512, key, (unsigned char *)AES_VERIFY_KEY, strlen(AES_VERIFY_KEY), shared_secret, sizeof(shared_secret)); if (ret < 0) { vctx->errmsg = "Verify request 2: Hashing of key string and shared secret failed"; return NULL; } ret = hash_ab(HASH_SHA512, iv, (unsigned char *)AES_VERIFY_IV, strlen(AES_VERIFY_IV), shared_secret, sizeof(shared_secret)); if (ret < 0) { vctx->errmsg = "Verify request 2: Hashing of iv string and shared secret failed"; return NULL; } ret = encrypt_ctr(encrypted, sizeof(encrypted), vctx->server_public_key, sizeof(vctx->server_public_key), signature, sizeof(signature), key, iv, &errmsg); if (ret < 0) { vctx->errmsg = errmsg; return NULL; } *len = 4 + sizeof(vctx->server_public_key); data = calloc(1, *len); if (!data) { vctx->errmsg = "Verify request 2: Out of memory"; return NULL; } memcpy(data + 4, encrypted, sizeof(vctx->server_public_key)); return data; } int verification_verify_response1(struct verification_verify_context *vctx, const uint8_t *data, uint32_t data_len) { uint32_t wanted; wanted = sizeof(vctx->server_eph_public_key) + sizeof(vctx->server_public_key); if (data_len < wanted) { vctx->errmsg = "Verify response 2: Unexpected response (too short)"; return -1; } memcpy(vctx->server_eph_public_key, data, sizeof(vctx->server_eph_public_key)); memcpy(vctx->server_public_key, data + sizeof(vctx->server_eph_public_key), sizeof(vctx->server_public_key)); return 0; } forked-daapd-25.0/src/outputs/raop_verification.h000066400000000000000000000044721313447753700221740ustar00rootroot00000000000000#ifndef __VERIFICATION_H__ #define __VERIFICATION_H__ #include struct verification_setup_context; struct verification_verify_context; /* When you have the pin-code (must be 4 bytes), create a new context with this * function and then call verification_setup_request1() */ struct verification_setup_context * verification_setup_new(const char *pin); void verification_setup_free(struct verification_setup_context *sctx); /* Returns last error message */ const char * verification_setup_errmsg(struct verification_setup_context *sctx); uint8_t * verification_setup_request1(uint32_t *len, struct verification_setup_context *sctx); uint8_t * verification_setup_request2(uint32_t *len, struct verification_setup_context *sctx); uint8_t * verification_setup_request3(uint32_t *len, struct verification_setup_context *sctx); int verification_setup_response1(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len); int verification_setup_response2(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len); int verification_setup_response3(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len); /* Returns a 0-terminated string that is the authorisation key. The caller * should save it and use it later to initialize verification_verify_new(). * Note that the pointer becomes invalid when you free sctx. */ int verification_setup_result(const char **authorisation_key, struct verification_setup_context *sctx); /* When you have completed the setup you can extract a key with * verification_setup_result(). Give the string as input to this function to * create a verification context and then call verification_verify_request1() */ struct verification_verify_context * verification_verify_new(const char *authorisation_key); void verification_verify_free(struct verification_verify_context *vctx); /* Returns last error message */ const char * verification_verify_errmsg(struct verification_verify_context *vctx); uint8_t * verification_verify_request1(uint32_t *len, struct verification_verify_context *vctx); uint8_t * verification_verify_request2(uint32_t *len, struct verification_verify_context *vctx); int verification_verify_response1(struct verification_verify_context *vctx, const uint8_t *data, uint32_t data_len); #endif /* !__VERIFICATION_H__ */ forked-daapd-25.0/src/outputs/streaming.c000066400000000000000000000021051313447753700204440ustar00rootroot00000000000000/* * Copyright (C) 2016 Espen Jürgensen * * 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 */ #include #include #include #include #include "outputs.h" #include "httpd_streaming.h" struct output_definition output_streaming = { .name = "mp3 streaming", .type = OUTPUT_TYPE_STREAMING, .priority = 0, .disabled = 0, .write = streaming_write, }; forked-daapd-25.0/src/player.c000066400000000000000000002254601313447753700162370ustar00rootroot00000000000000/* * Copyright (C) 2010-2011 Julien BLACHE * Copyright (C) 2016-2017 Espen Jürgensen * * 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 * About player.c * -------------- * The main tasks of the player are the following: * - handle playback commands, status checks and events from other threads * - receive audio from the input thread and to own the playback buffer * - feed the outputs at the appropriate rate (controlled by the playback timer) * - output device handling (partly outsourced to outputs.c) * - notify about playback status changes * - maintain the playback queue * * The player thread should never be making operations that may block, since * that could block callers requesting status (effectively making forked-daapd * unresponsive) and it could also starve the outputs. In practice this rule is * not always obeyed, for instance some outputs do their setup in ways that * could block. * * * About metadata * -------------- * The player gets metadata from library + inputs and passes it to the outputs * and other clients (e.g. Remotes). * * 1. On playback start, metadata from the library is loaded into the queue * items, and these items are then the source of metadata for clients. * 2. During playback, the input may signal new metadata by making a * input_write() with the INPUT_FLAG_METADATA flag. When the player read * reaches that data, the player will request the metadata from the input * with input_metadata_get(). This metadata is then saved to the currently * playing queue item, and the clients are told to update metadata. * 3. Artwork works differently than textual metadata. The artwork module will * look for artwork in the library, and addition also check the artwork_url * of the queue_item. */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #ifdef HAVE_TIMERFD # include #elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) # include #endif #include #include #include #include "db.h" #include "logger.h" #include "conffile.h" #include "misc.h" #include "player.h" #include "worker.h" #include "listener.h" #include "commands.h" // Audio and metadata outputs #include "outputs.h" // Audio and metadata input #include "input.h" // Scrobbling #ifdef LASTFM # include "lastfm.h" #endif #ifndef MIN # define MIN(a, b) ((a < b) ? a : b) #endif #ifndef MAX #define MAX(a, b) ((a > b) ? a : b) #endif // Default volume (must be from 0 - 100) #define PLAYER_DEFAULT_VOLUME 50 // For every tick_interval, we will read a packet from the input buffer and // write it to the outputs. If the input is empty, we will try to catch up next // tick. However, at some point we will owe the outputs so much data that we // have to suspend playback and wait for the input to get its act together. // (value is in milliseconds and should be low enough to avoid output underrun) #define PLAYER_READ_BEHIND_MAX 1500 // Generally, an output must not block (for long) when outputs_write() is // called. If an output does that anyway, the next tick event will be late, and // by extension playback_cb(). We will try to catch up, but if the delay // gets above this value, we will suspend playback and reset the output. // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 struct volume_param { int volume; uint64_t spk_id; }; struct spk_enum { spk_enum_cb cb; void *arg; }; struct speaker_set_param { uint64_t *device_ids; int intval; }; struct metadata_param { struct input_metadata *input; struct output_metadata *output; }; struct speaker_auth_param { enum output_types type; char pin[5]; }; union player_arg { struct volume_param vol_param; void *noarg; struct spk_enum *spk_enum; struct output_device *device; struct player_status *status; struct player_source *ps; struct metadata_param metadata_param; uint32_t *id_ptr; struct speaker_set_param speaker_set_param; enum repeat_mode mode; struct speaker_auth_param auth; uint32_t id; int intval; }; struct event_base *evbase_player; static int player_exit; static pthread_t tid_player; static struct commands_base *cmdbase; // Keep track of how many outputs need to call back when flushing internally // from the player thread (where we can't use player_playback_pause) static int player_flush_pending; // Config values static int speaker_autoselect; static int clear_queue_on_stop_disabled; // Player status static enum play_status player_state; static enum repeat_mode repeat; static char shuffle; static char consume; // Playback timer #ifdef HAVE_TIMERFD static int pb_timer_fd; #else timer_t pb_timer; #endif static struct event *pb_timer_ev; static struct timespec pb_timer_last; static struct timespec packet_timer_last; // How often the playback timer triggers playback_cb() static struct timespec tick_interval; // Timer resolution static struct timespec timer_res; // Time between two packets static struct timespec packet_time = { 0, AIRTUNES_V2_STREAM_PERIOD }; // How many writes we owe the output (when the input is underrunning) static int pb_read_deficit; // PLAYER_READ_BEHIND_MAX and PLAYER_WRITE_BEHIND_MAX converted to clock ticks static int pb_read_deficit_max; static int pb_write_deficit_max; // True if we are trying to recover from a major playback timer overrun (write problems) static bool pb_write_recovery; // Sync values static struct timespec pb_pos_stamp; static uint64_t pb_pos; // Stream position (packets) static uint64_t last_rtptime; // Output devices static struct output_device *dev_list; // Output status static int output_sessions; // Last commanded volume static int master_volume; // Audio source static struct player_source *cur_playing; static struct player_source *cur_streaming; static uint32_t cur_plid; static uint32_t cur_plversion; // Player buffer (holds one packet) static uint8_t pb_buffer[STOB(AIRTUNES_V2_PACKET_SAMPLES)]; static size_t pb_buffer_offset; // Play history static struct player_history *history; /* -------------------------------- Forwards -------------------------------- */ static void playback_abort(void); static void playback_suspend(void); /* ----------------------------- Volume helpers ----------------------------- */ static int rel_to_vol(int relvol) { float vol; if (relvol == 100) return master_volume; vol = ((float)relvol * (float)master_volume) / 100.0; return (int)vol; } static int vol_to_rel(int volume) { float rel; if (volume == master_volume) return 100; rel = ((float)volume / (float)master_volume) * 100.0; return (int)rel; } // Master volume helpers static void volume_master_update(int newvol) { struct output_device *device; master_volume = newvol; for (device = dev_list; device; device = device->next) { if (device->selected) device->relvol = vol_to_rel(device->volume); } } static void volume_master_find(void) { struct output_device *device; int newmaster; newmaster = -1; for (device = dev_list; device; device = device->next) { if (device->selected && (device->volume > newmaster)) newmaster = device->volume; } volume_master_update(newmaster); } /* ---------------------- Device select/deselect hooks ---------------------- */ static void speaker_select_output(struct output_device *device) { device->selected = 1; if (device->volume > master_volume) { if (player_state == PLAY_STOPPED || master_volume == -1) volume_master_update(device->volume); else device->volume = master_volume; } device->relvol = vol_to_rel(device->volume); } static void speaker_deselect_output(struct output_device *device) { device->selected = 0; if (device->volume == master_volume) volume_master_find(); } /* ----------------------- Misc helpers and callbacks ----------------------- */ // Callback from the worker thread (async operation as it may block) static void playcount_inc_cb(void *arg) { int *id = arg; db_file_inc_playcount(*id); } #ifdef LASTFM // Callback from the worker thread (async operation as it may block) static void scrobble_cb(void *arg) { int *id = arg; lastfm_scrobble(*id); } #endif // Callback from the worker thread. Here the heavy lifting is done: updating the // db_queue_item, retrieving artwork (through outputs_metadata_prepare) and // when done, telling the player to send the metadata to the clients static void metadata_update_cb(void *arg) { struct input_metadata *metadata = arg; struct output_metadata *o_metadata; struct db_queue_item *queue_item; int ret; queue_item = db_queue_fetch_byitemid(metadata->item_id); if (!queue_item) { DPRINTF(E_LOG, L_PLAYER, "Bug! Input metadata item_id does not match anything in queue\n"); goto out_free_metadata; } // Since we won't be using the metadata struct values for anything else than // this we just swap pointers if (metadata->artist) swap_pointers(&queue_item->artist, &metadata->artist); if (metadata->title) swap_pointers(&queue_item->title, &metadata->title); if (metadata->album) swap_pointers(&queue_item->album, &metadata->album); if (metadata->genre) swap_pointers(&queue_item->genre, &metadata->genre); if (metadata->artwork_url) swap_pointers(&queue_item->artwork_url, &metadata->artwork_url); if (metadata->song_length) queue_item->song_length = metadata->song_length; ret = db_queue_update_item(queue_item); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Database error while updating queue with new metadata\n"); goto out_free_queueitem; } o_metadata = outputs_metadata_prepare(metadata->item_id); // Actual sending must be done by player, since the worker does not own the outputs player_metadata_send(metadata, o_metadata); outputs_metadata_free(o_metadata); out_free_queueitem: free_queue_item(queue_item, 0); out_free_metadata: input_metadata_free(metadata, 1); } // Gets the metadata, but since the actual update requires db writes and // possibly retrieving artwork we let the worker do the next step static void metadata_trigger(int startup) { struct input_metadata metadata; int ret; ret = input_metadata_get(&metadata, cur_streaming, startup, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); if (ret < 0) return; worker_execute(metadata_update_cb, &metadata, sizeof(metadata), 0); } /* * Add the song with the given id to the list of previously played songs */ static void history_add(uint32_t id, uint32_t item_id) { unsigned int cur_index; unsigned int next_index; // Check if the current song is already the last in the history to avoid duplicates cur_index = (history->start_index + history->count - 1) % MAX_HISTORY_COUNT; if (id == history->id[cur_index]) { DPRINTF(E_DBG, L_PLAYER, "Current playing/streaming song already in history\n"); return; } // Calculate the next index and update the start-index and count for the id-buffer next_index = (history->start_index + history->count) % MAX_HISTORY_COUNT; if (next_index == history->start_index && history->count > 0) history->start_index = (history->start_index + 1) % MAX_HISTORY_COUNT; history->id[next_index] = id; history->item_id[next_index] = item_id; if (history->count < MAX_HISTORY_COUNT) history->count++; } static void seek_save(void) { int seek; if (!cur_streaming) return; if (cur_streaming->media_kind & (MEDIA_KIND_MOVIE | MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK | MEDIA_KIND_TVSHOW)) { seek = (cur_streaming->output_start - cur_streaming->stream_start) / 44100 * 1000; db_file_seek_update(cur_streaming->id, seek); } } static void status_update(enum play_status status) { player_state = status; listener_notify(LISTENER_PLAYER); } /* ----------- Audio source handling (interfaces with input module) --------- */ static struct player_source * source_now_playing() { if (cur_playing) return cur_playing; return cur_streaming; } /* * Creates a new player source for the given queue item */ static struct player_source * source_new(struct db_queue_item *queue_item) { struct player_source *ps; ps = calloc(1, sizeof(struct player_source)); if (!ps) { DPRINTF(E_LOG, L_PLAYER, "Out of memory (ps)\n"); return NULL; } ps->id = queue_item->file_id; ps->item_id = queue_item->id; ps->data_kind = queue_item->data_kind; ps->media_kind = queue_item->media_kind; ps->len_ms = queue_item->song_length; ps->play_next = NULL; ps->path = strdup(queue_item->path); return ps; } static void source_free(struct player_source *ps) { if (ps->path) free(ps->path); free(ps); } /* * Stops playback for the current streaming source and frees all * player sources (starting from the playing source). Sets current streaming * and playing sources to NULL. */ static void source_stop() { struct player_source *ps_playing; struct player_source *ps_temp; if (cur_streaming) input_stop(cur_streaming); ps_playing = source_now_playing(); while (ps_playing) { ps_temp = ps_playing; ps_playing = ps_playing->play_next; ps_temp->play_next = NULL; source_free(ps_temp); } cur_playing = NULL; cur_streaming = NULL; } /* * Pauses playback * * Resets the streaming source to the playing source and adjusts stream-start * and output-start values to the playing time. Sets the current streaming * source to NULL. */ static int source_pause(uint64_t pos) { struct player_source *ps_playing; struct player_source *ps_playnext; struct player_source *ps_temp; uint64_t seek_frames; int seek_ms; int ret; ps_playing = source_now_playing(); if (!ps_playing) return -1; if (cur_streaming && (cur_streaming == ps_playing)) { if (ps_playing != cur_streaming) { DPRINTF(E_DBG, L_PLAYER, "Pause called on playing source (id=%d) and streaming source already " "switched to the next item (id=%d)\n", ps_playing->id, cur_streaming->id); ret = input_stop(cur_streaming); if (ret < 0) return -1; } else { ret = input_pause(cur_streaming); if (ret < 0) return -1; } } ps_playnext = ps_playing->play_next; while (ps_playnext) { ps_temp = ps_playnext; ps_playnext = ps_playnext->play_next; ps_temp->play_next = NULL; source_free(ps_temp); } ps_playing->play_next = NULL; cur_playing = NULL; cur_streaming = ps_playing; if (!cur_streaming->setup_done) { DPRINTF(E_INFO, L_PLAYER, "Opening '%s'\n", cur_streaming->path); ret = input_setup(cur_streaming); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s'\n", cur_streaming->path); return -1; } } // Seek back to the pause position seek_frames = (pos - cur_streaming->stream_start); seek_ms = (int)((seek_frames * 1000) / 44100); ret = input_seek(cur_streaming, seek_ms); // TODO what if ret < 0? // Adjust start_pos to take into account the pause and seek back cur_streaming->stream_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - ((uint64_t)ret * 44100) / 1000; cur_streaming->output_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; cur_streaming->end = 0; return 0; } /* * Seeks the current streaming source to the given postion in milliseconds * and adjusts stream-start and output-start values. * * @param seek_ms Position in milliseconds to seek * @return The new position in milliseconds or -1 on error */ static int source_seek(int seek_ms) { int ret; ret = input_seek(cur_streaming, seek_ms); if (ret < 0) return -1; // Adjust start_pos to take into account the pause and seek back cur_streaming->stream_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - ((uint64_t)ret * 44100) / 1000; cur_streaming->output_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; return ret; } /* * Starts or resumes playback */ static int source_play() { int ret; ret = input_start(cur_streaming); return ret; } /* * Opens the given player source for playback (but does not start playback) * * The given source is appended to the current streaming source (if one exists) and * becomes the new current streaming source. * * Stream-start and output-start values are set to the given start position. */ static int source_open(struct player_source *ps, uint64_t start_pos, int seek_ms) { int ret; DPRINTF(E_INFO, L_PLAYER, "Opening '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id); if (cur_streaming && cur_streaming->end == 0) { DPRINTF(E_LOG, L_PLAYER, "Current streaming source not at eof '%s' (id=%d, item-id=%d)\n", cur_streaming->path, cur_streaming->id, cur_streaming->item_id); return -1; } ret = input_setup(ps); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id); return -1; } // If a streaming source exists, append the new source as play-next and set it // as the new streaming source if (cur_streaming) cur_streaming->play_next = ps; cur_streaming = ps; cur_streaming->stream_start = start_pos; cur_streaming->output_start = cur_streaming->stream_start; cur_streaming->end = 0; // Seek to the given seek position if (seek_ms) { DPRINTF(E_INFO, L_PLAYER, "Seek to %d ms for '%s' (id=%d, item-id=%d)\n", seek_ms, ps->path, ps->id, ps->item_id); source_seek(seek_ms); } return ret; } /* * Closes the current streaming source and sets its end-time to the given * position */ static int source_close(uint64_t end_pos) { input_stop(cur_streaming); cur_streaming->end = end_pos; return 0; } /* * Updates the now playing item (cur_playing) and notifies remotes and raop devices * about changes. Also takes care of stopping playback after the last item. * * @return Returns the current playback position as rtp-time */ static uint64_t source_check(void) { struct timespec ts; struct player_source *ps; uint64_t pos; int i; int id; int ret; ret = player_get_current_pos(&pos, &ts, 0); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Couldn't get current playback position\n"); return 0; } if (player_state == PLAY_STOPPED) { DPRINTF(E_LOG, L_PLAYER, "Bug! source_check called but playback has already stopped\n"); return pos; } // If cur_playing is NULL, we are still in the first two seconds after starting the stream if (!cur_playing) { if (pos >= cur_streaming->output_start) { cur_playing = cur_streaming; status_update(PLAY_PLAYING); // Start of streaming, no metadata to prune yet } return pos; } // Check if we are still in the middle of the current playing song if ((cur_playing->end == 0) || (pos < cur_playing->end)) return pos; // We have reached the end of the current playing song, update cur_playing to // the next song in the queue and initialize stream_start and output_start values. i = 0; while (cur_playing && (cur_playing->end != 0) && (pos > cur_playing->end)) { i++; id = (int)cur_playing->id; worker_execute(playcount_inc_cb, &id, sizeof(int), 5); #ifdef LASTFM worker_execute(scrobble_cb, &id, sizeof(int), 8); #endif history_add(cur_playing->id, cur_playing->item_id); if (consume) db_queue_delete_byitemid(cur_playing->item_id); if (!cur_playing->play_next) { playback_abort(); return pos; } ps = cur_playing; cur_playing = cur_playing->play_next; source_free(ps); } if (i > 0) { DPRINTF(E_DBG, L_PLAYER, "Playback switched to next song\n"); status_update(PLAY_PLAYING); outputs_metadata_prune(pos); } return pos; } /* * Returns the next player source based on the current streaming source and repeat mode * * If repeat mode is repeat all, shuffle is active and the current streaming source is the * last item in the queue, the queue is reshuffled prior to returning the first item of the * queue. */ static struct player_source * source_next() { struct player_source *ps = NULL; struct db_queue_item *queue_item; if (!cur_streaming) { DPRINTF(E_LOG, L_PLAYER, "source_next() called with no current streaming source available\n"); return NULL; } if (repeat == REPEAT_SONG) { queue_item = db_queue_fetch_byitemid(cur_streaming->item_id); if (!queue_item) { DPRINTF(E_LOG, L_PLAYER, "Error fetching item from queue '%s' (id=%d, item-id=%d)\n", cur_streaming->path, cur_streaming->id, cur_streaming->item_id); return NULL; } } else { queue_item = db_queue_fetch_next(cur_streaming->item_id, shuffle); if (!queue_item && repeat == REPEAT_ALL) { if (shuffle) { db_queue_reshuffle(0); } queue_item = db_queue_fetch_bypos(0, shuffle); if (!queue_item) { DPRINTF(E_LOG, L_PLAYER, "Error fetching item from queue '%s' (id=%d, item-id=%d)\n", cur_streaming->path, cur_streaming->id, cur_streaming->item_id); return NULL; } } } if (!queue_item) { DPRINTF(E_DBG, L_PLAYER, "Reached end of queue\n"); return NULL; } ps = source_new(queue_item); free_queue_item(queue_item, 0); return ps; } /* * Returns the previous player source based on the current streaming source */ static struct player_source * source_prev() { struct player_source *ps = NULL; struct db_queue_item *queue_item; if (!cur_streaming) { DPRINTF(E_LOG, L_PLAYER, "source_prev() called with no current streaming source available\n"); return NULL; } queue_item = db_queue_fetch_prev(cur_streaming->item_id, shuffle); if (!queue_item) return NULL; ps = source_new(queue_item); free_queue_item(queue_item, 0); return ps; } static int source_switch(int nbytes) { struct player_source *ps; int ret; DPRINTF(E_DBG, L_PLAYER, "Switching track\n"); source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); while ((ps = source_next())) { ret = source_open(ps, cur_streaming->end + 1, 0); if (ret < 0) { db_queue_delete_byitemid(ps->item_id); continue; } ret = source_play(); if (ret < 0) { db_queue_delete_byitemid(ps->item_id); source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); continue; } break; } if (!ps) // End of queue { cur_streaming = NULL; return 0; } metadata_trigger(0); return 0; } /* ----------------- Main read, write and playback timer event -------------- */ // Returns -1 on error (caller should abort playback), or bytes read (possibly 0) static int source_read(uint8_t *buf, int len) { int nbytes; uint32_t item_id; int ret; short flags; // Nothing to read, stream silence until source_check() stops playback if (!cur_streaming) { memset(buf, 0, len); return len; } nbytes = input_read(buf, len, &flags); if ((nbytes < 0) || (flags & INPUT_FLAG_ERROR)) { DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id); nbytes = 0; item_id = cur_streaming->item_id; ret = source_switch(0); db_queue_delete_byitemid(item_id); if (ret < 0) return -1; } else if (flags & INPUT_FLAG_EOF) { ret = source_switch(nbytes); if (ret < 0) return -1; } else if (flags & INPUT_FLAG_METADATA) { metadata_trigger(0); } // We pad the output buffer with silence if we don't have enough data for a // full packet and there is no more data coming up (no more tracks in queue) if ((nbytes < len) && (!cur_streaming)) { memset(buf + nbytes, 0, len - nbytes); nbytes = len; } return nbytes; } static void playback_write(void) { int want; int got; source_check(); // Make sure playback is still running after source_check() if (player_state == PLAY_STOPPED) return; pb_read_deficit++; while (pb_read_deficit) { want = sizeof(pb_buffer) - pb_buffer_offset; got = source_read(pb_buffer + pb_buffer_offset, want); if (got == want) { pb_read_deficit--; last_rtptime += AIRTUNES_V2_PACKET_SAMPLES; outputs_write(pb_buffer, last_rtptime); pb_buffer_offset = 0; } else if (got < 0) { DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback\n"); playback_abort(); return; } else if (pb_read_deficit > pb_read_deficit_max) { DPRINTF(E_LOG, L_PLAYER, "Source is not providing sufficient data, temporarily suspending playback (deficit=%d)\n", pb_read_deficit); playback_suspend(); return; } else { DPRINTF(E_SPAM, L_PLAYER, "Partial read (offset=%zu, deficit=%d)\n", pb_buffer_offset, pb_read_deficit); pb_buffer_offset += got; return; } } } static void playback_cb(int fd, short what, void *arg) { struct timespec next_tick; uint64_t overrun; int ret; // Check if we missed any timer expirations overrun = 0; #ifdef HAVE_TIMERFD ret = read(fd, &overrun, sizeof(overrun)); if (ret <= 0) DPRINTF(E_LOG, L_PLAYER, "Error reading timer\n"); else if (overrun > 0) overrun--; #else ret = timer_getoverrun(pb_timer); if (ret < 0) DPRINTF(E_LOG, L_PLAYER, "Error getting timer overrun\n"); else overrun = ret; #endif /* HAVE_TIMERFD */ // We are too delayed, probably some output blocked: reset if first overrun or abort if second overrun if (overrun > pb_write_deficit_max) { if (pb_write_recovery) { DPRINTF(E_LOG, L_PLAYER, "Permanent output delay detected (behind=%" PRIu64 ", max=%d), aborting\n", overrun, pb_write_deficit_max); playback_abort(); return; } DPRINTF(E_LOG, L_PLAYER, "Output delay detected (behind=%" PRIu64 ", max=%d), resetting all outputs\n", overrun, pb_write_deficit_max); pb_write_recovery = true; playback_suspend(); return; } else { if (overrun > 0) DPRINTF(E_WARN, L_PLAYER, "Output delay detected: player is %" PRIu64 " ticks behind, catching up\n", overrun); pb_write_recovery = false; } // If there was an overrun, we will try to read/write a corresponding number // of times so we catch up. The read from the input is non-blocking, so it // should not bring us further behind, even if there is no data. next_tick = timespec_add(pb_timer_last, tick_interval); for (; overrun > 0; overrun--) next_tick = timespec_add(next_tick, tick_interval); do { playback_write(); packet_timer_last = timespec_add(packet_timer_last, packet_time); } while ((timespec_cmp(packet_timer_last, next_tick) < 0) && (player_state == PLAY_PLAYING)); // Make sure playback is still running if (player_state == PLAY_STOPPED) return; pb_timer_last = next_tick; } /* ----------------- Output device handling (add/remove etc) ---------------- */ static void device_list_sort(void) { struct output_device *device; struct output_device *next; struct output_device *prev; int swaps; // Swap sorting since even the most inefficient sorting should do fine here do { swaps = 0; prev = NULL; for (device = dev_list; device && device->next; device = device->next) { next = device->next; if ( (outputs_priority(device) > outputs_priority(next)) || (outputs_priority(device) == outputs_priority(next) && strcasecmp(device->name, next->name) > 0) ) { if (device == dev_list) dev_list = next; if (prev) prev->next = next; device->next = next->next; next->next = device; swaps++; } prev = device; } } while (swaps > 0); } static void device_remove(struct output_device *remove) { struct output_device *device; struct output_device *prev; int ret; prev = NULL; for (device = dev_list; device; device = device->next) { if (device == remove) break; prev = device; } if (!device) return; // Save device volume ret = db_speaker_save(remove); if (ret < 0) DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", remove->type_name, remove->name); DPRINTF(E_INFO, L_PLAYER, "Removing %s device '%s'; stopped advertising\n", remove->type_name, remove->name); // Make sure device isn't selected anymore if (remove->selected) speaker_deselect_output(remove); if (!prev) dev_list = remove->next; else prev->next = remove->next; outputs_device_free(remove); } static int device_check(struct output_device *check) { struct output_device *device; for (device = dev_list; device; device = device->next) { if (device == check) break; } return (device) ? 0 : -1; } static enum command_state device_add(void *arg, int *retval) { union player_arg *cmdarg; struct output_device *add; struct output_device *device; char *keep_name; int ret; cmdarg = arg; add = cmdarg->device; for (device = dev_list; device; device = device->next) { if (device->id == add->id) break; } // New device if (!device) { device = add; keep_name = strdup(device->name); ret = db_speaker_get(device, device->id); if (ret < 0) { device->selected = 0; device->volume = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME; } free(device->name); device->name = keep_name; if (device->selected && (player_state != PLAY_PLAYING)) speaker_select_output(device); else device->selected = 0; device->next = dev_list; dev_list = device; } // Update to a device already in the list else { device->advertised = 1; if (add->v4_address) { if (device->v4_address) free(device->v4_address); device->v4_address = add->v4_address; device->v4_port = add->v4_port; // Address is ours now add->v4_address = NULL; } if (add->v6_address) { if (device->v6_address) free(device->v6_address); device->v6_address = add->v6_address; device->v6_port = add->v6_port; // Address is ours now add->v6_address = NULL; } if (device->name) free(device->name); device->name = add->name; add->name = NULL; device->has_password = add->has_password; device->password = add->password; outputs_device_free(add); } device_list_sort(); *retval = 0; return COMMAND_END; } static enum command_state device_remove_family(void *arg, int *retval) { union player_arg *cmdarg; struct output_device *remove; struct output_device *device; cmdarg = arg; remove = cmdarg->device; for (device = dev_list; device; device = device->next) { if (device->id == remove->id) break; } if (!device) { DPRINTF(E_WARN, L_PLAYER, "The %s device '%s' stopped advertising, but not in our list\n", remove->type_name, remove->name); outputs_device_free(remove); *retval = 0; return COMMAND_END; } // v{4,6}_port non-zero indicates the address family stopped advertising if (remove->v4_port && device->v4_address) { free(device->v4_address); device->v4_address = NULL; device->v4_port = 0; } if (remove->v6_port && device->v6_address) { free(device->v6_address); device->v6_address = NULL; device->v6_port = 0; } if (!device->v4_address && !device->v6_address) { device->advertised = 0; if (!device->session) device_remove(device); } outputs_device_free(remove); *retval = 0; return COMMAND_END; } static enum command_state device_auth_kickoff(void *arg, int *retval) { union player_arg *cmdarg = arg; outputs_authorize(cmdarg->auth.type, cmdarg->auth.pin); *retval = 0; return COMMAND_END; } static enum command_state device_metadata_send(void *arg, int *retval) { union player_arg *cmdarg; struct input_metadata *imd; struct output_metadata *omd; cmdarg = arg; imd = cmdarg->metadata_param.input; omd = cmdarg->metadata_param.output; outputs_metadata_send(omd, imd->rtptime, imd->offset, imd->startup); status_update(player_state); *retval = 0; return COMMAND_END; } /* -------- Output device callbacks executed in the player thread ----------- */ static void device_streaming_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { int ret; DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_streaming_cb\n", outputs_name(device->type)); ret = device_check(device); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Output device disappeared during streaming!\n"); output_sessions--; return; } if (status == OUTPUT_STATE_FAILED) { DPRINTF(E_LOG, L_PLAYER, "The %s device '%s' FAILED\n", device->type_name, device->name); output_sessions--; if (player_state == PLAY_PLAYING) speaker_deselect_output(device); device->session = NULL; if (!device->advertised) device_remove(device); if (output_sessions == 0) playback_abort(); } else if (status == OUTPUT_STATE_STOPPED) { DPRINTF(E_INFO, L_PLAYER, "The %s device '%s' stopped\n", device->type_name, device->name); output_sessions--; device->session = NULL; if (!device->advertised) device_remove(device); } else outputs_status_cb(session, device_streaming_cb); } static void device_command_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb\n", outputs_name(device->type)); outputs_status_cb(session, device_streaming_cb); if (status == OUTPUT_STATE_FAILED) device_streaming_cb(device, session, status); // Used by playback_suspend - is basically the bottom half if (player_flush_pending > 0) { player_flush_pending--; if (player_flush_pending == 0) input_buffer_full_cb(player_playback_start); } commands_exec_end(cmdbase, 0); } static void device_shutdown_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { int retval; int ret; DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_shutdown_cb\n", outputs_name(device->type)); if (output_sessions) output_sessions--; retval = commands_exec_returnvalue(cmdbase); ret = device_check(device); if (ret < 0) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared before shutdown completion!\n"); if (retval != -2) retval = -1; goto out; } device->session = NULL; if (!device->advertised) device_remove(device); out: /* cur_cmd->ret already set * - to 0 (or -2 if password issue) in speaker_set() * - to -1 above on error */ commands_exec_end(cmdbase, retval); } static void device_lost_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_lost_cb\n", outputs_name(device->type)); // We lost that device during startup for some reason, not much we can do here if (status == OUTPUT_STATE_FAILED) DPRINTF(E_WARN, L_PLAYER, "Failed to stop lost device\n"); else DPRINTF(E_INFO, L_PLAYER, "Lost device stopped properly\n"); } static void device_activate_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { struct timespec ts; int retval; int ret; DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb\n", outputs_name(device->type)); retval = commands_exec_returnvalue(cmdbase); ret = device_check(device); if (ret < 0) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during startup!\n"); outputs_status_cb(session, device_lost_cb); outputs_device_stop(session); if (retval != -2) retval = -1; goto out; } if (status == OUTPUT_STATE_PASSWORD) { status = OUTPUT_STATE_FAILED; retval = -2; } if (status == OUTPUT_STATE_FAILED) { speaker_deselect_output(device); if (!device->advertised) device_remove(device); if (retval != -2) retval = -1; goto out; } device->session = session; output_sessions++; if ((player_state == PLAY_PLAYING) && (output_sessions == 1)) { ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &timer_res); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not get current time: %s\n", strerror(errno)); // Fallback to nearest timer expiration time ts.tv_sec = pb_timer_last.tv_sec; ts.tv_nsec = pb_timer_last.tv_nsec; } outputs_playback_start(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, &ts); } outputs_status_cb(session, device_streaming_cb); out: /* cur_cmd->ret already set * - to 0 in speaker_set() (default) * - to -2 above if password issue * - to -1 above on error */ commands_exec_end(cmdbase, retval); } static void device_probe_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { int retval; int ret; DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_probe_cb\n", outputs_name(device->type)); retval = commands_exec_returnvalue(cmdbase); ret = device_check(device); if (ret < 0) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during probe!\n"); if (retval != -2) retval = -1; goto out; } if (status == OUTPUT_STATE_PASSWORD) { status = OUTPUT_STATE_FAILED; retval = -2; } if (status == OUTPUT_STATE_FAILED) { speaker_deselect_output(device); if (!device->advertised) device_remove(device); if (retval != -2) retval = -1; goto out; } out: /* cur_cmd->ret already set * - to 0 in speaker_set() (default) * - to -2 above if password issue * - to -1 above on error */ commands_exec_end(cmdbase, retval); } static void device_restart_cb(struct output_device *device, struct output_session *session, enum output_device_state status) { int retval; int ret; DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_restart_cb\n", outputs_name(device->type)); retval = commands_exec_returnvalue(cmdbase); ret = device_check(device); if (ret < 0) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during restart!\n"); outputs_status_cb(session, device_lost_cb); outputs_device_stop(session); if (retval != -2) retval = -1; goto out; } if (status == OUTPUT_STATE_PASSWORD) { status = OUTPUT_STATE_FAILED; retval = -2; } if (status == OUTPUT_STATE_FAILED) { speaker_deselect_output(device); if (!device->advertised) device_remove(device); if (retval != -2) retval = -1; goto out; } device->session = session; output_sessions++; outputs_status_cb(session, device_streaming_cb); out: commands_exec_end(cmdbase, retval); } /* ------------------------- Internal playback routines --------------------- */ static int playback_timer_start(void) { struct itimerspec tick; int ret; ret = event_add(pb_timer_ev, NULL); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not add playback timer\n"); return -1; } tick.it_interval = tick_interval; tick.it_value = tick_interval; #ifdef HAVE_TIMERFD ret = timerfd_settime(pb_timer_fd, 0, &tick, NULL); #else ret = timer_settime(pb_timer, 0, &tick, NULL); #endif if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not arm playback timer: %s\n", strerror(errno)); return -1; } return 0; } static int playback_timer_stop(void) { struct itimerspec tick; int ret; event_del(pb_timer_ev); memset(&tick, 0, sizeof(struct itimerspec)); #ifdef HAVE_TIMERFD ret = timerfd_settime(pb_timer_fd, 0, &tick, NULL); #else ret = timer_settime(pb_timer, 0, &tick, NULL); #endif if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not disarm playback timer: %s\n", strerror(errno)); return -1; } return 0; } static void playback_abort(void) { outputs_playback_stop(); playback_timer_stop(); source_stop(); if (!clear_queue_on_stop_disabled) db_queue_clear(0); status_update(PLAY_STOPPED); outputs_metadata_purge(); } // Temporarily suspends/resets playback, used when input buffer underruns or in // case of problems writing to the outputs static void playback_suspend(void) { player_flush_pending = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); playback_timer_stop(); status_update(PLAY_PAUSED); seek_save(); // No devices to wait for, just set the restart cb right away if (player_flush_pending == 0) input_buffer_full_cb(player_playback_start); } /* --------------- Actual commands, executed in the player thread ----------- */ static enum command_state get_status(void *arg, int *retval) { union player_arg *cmdarg = arg; struct timespec ts; struct player_source *ps; struct player_status *status; uint64_t pos; int ret; status = cmdarg->status; memset(status, 0, sizeof(struct player_status)); status->shuffle = shuffle; status->consume = consume; status->repeat = repeat; status->volume = master_volume; status->plid = cur_plid; switch (player_state) { case PLAY_STOPPED: DPRINTF(E_DBG, L_PLAYER, "Player status: stopped\n"); status->status = PLAY_STOPPED; break; case PLAY_PAUSED: DPRINTF(E_DBG, L_PLAYER, "Player status: paused\n"); status->status = PLAY_PAUSED; status->id = cur_streaming->id; status->item_id = cur_streaming->item_id; pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - cur_streaming->stream_start; status->pos_ms = (pos * 1000) / 44100; status->len_ms = cur_streaming->len_ms; break; case PLAY_PLAYING: if (!cur_playing) { DPRINTF(E_DBG, L_PLAYER, "Player status: playing (buffering)\n"); status->status = PLAY_PAUSED; ps = cur_streaming; // Avoid a visible 2-second jump backward for the client pos = ps->output_start - ps->stream_start; } else { DPRINTF(E_DBG, L_PLAYER, "Player status: playing\n"); status->status = PLAY_PLAYING; ps = cur_playing; ret = player_get_current_pos(&pos, &ts, 0); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not get current stream position for playstatus\n"); pos = 0; } if (pos < ps->stream_start) pos = 0; else pos -= ps->stream_start; } status->pos_ms = (pos * 1000) / 44100; status->len_ms = ps->len_ms; status->id = ps->id; status->item_id = ps->item_id; break; } *retval = 0; return COMMAND_END; } static enum command_state now_playing(void *arg, int *retval) { union player_arg *cmdarg = arg; uint32_t *id; struct player_source *ps_playing; id = cmdarg->id_ptr; ps_playing = source_now_playing(); if (ps_playing) *id = ps_playing->id; else { *retval = -1; return COMMAND_END; } *retval = 0; return COMMAND_END; } static enum command_state playback_stop(void *arg, int *retval) { struct player_source *ps_playing; // We may be restarting very soon, so we don't bring the devices to a full // stop just yet; this saves time when restarting, which is nicer for the user *retval = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); playback_timer_stop(); ps_playing = source_now_playing(); if (ps_playing) { history_add(ps_playing->id, ps_playing->item_id); } source_stop(); status_update(PLAY_STOPPED); outputs_metadata_purge(); // We're async if we need to flush devices if (*retval > 0) return COMMAND_PENDING; return COMMAND_END; } static enum command_state playback_start_bh(void *arg, int *retval) { int ret; ret = clock_gettime_with_res(CLOCK_MONOTONIC, &pb_pos_stamp, &timer_res); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Couldn't get current clock: %s\n", strerror(errno)); goto out_fail; } playback_timer_stop(); // initialize the packet timer to the same relative time that we have // for the playback timer. packet_timer_last.tv_sec = pb_pos_stamp.tv_sec; packet_timer_last.tv_nsec = pb_pos_stamp.tv_nsec; pb_timer_last.tv_sec = pb_pos_stamp.tv_sec; pb_timer_last.tv_nsec = pb_pos_stamp.tv_nsec; pb_buffer_offset = 0; pb_read_deficit = 0; ret = playback_timer_start(); if (ret < 0) goto out_fail; // Everything OK, start outputs outputs_playback_start(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, &pb_pos_stamp); status_update(PLAY_PLAYING); *retval = 0; return COMMAND_END; out_fail: playback_abort(); *retval = -1; return COMMAND_END; } static enum command_state playback_start_item(void *arg, int *retval) { struct db_queue_item *queue_item = arg; struct media_file_info *mfi; struct output_device *device; struct player_source *ps; int seek_ms; int ret; if (player_state == PLAY_PLAYING) { DPRINTF(E_DBG, L_PLAYER, "Player is already playing, ignoring call to playback start\n"); status_update(player_state); *retval = 1; // Value greater 0 will prevent execution of the bottom half function return COMMAND_END; } // Update global playback position pb_pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - 88200; if (player_state == PLAY_STOPPED && !queue_item) { DPRINTF(E_LOG, L_PLAYER, "Failed to start/resume playback, no queue item given\n"); *retval = -1; return COMMAND_END; } if (!queue_item) { // Resume playback of current source ps = source_now_playing(); DPRINTF(E_DBG, L_PLAYER, "Resume playback of '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id); } else { // Start playback for given queue item DPRINTF(E_DBG, L_PLAYER, "Start playback of '%s' (id=%d, item-id=%d)\n", queue_item->path, queue_item->file_id, queue_item->id); source_stop(); ps = source_new(queue_item); if (!ps) { playback_abort(); *retval = -1; return COMMAND_END; } seek_ms = 0; if (queue_item->file_id > 0) { mfi = db_file_fetch_byid(queue_item->file_id); if (mfi) { seek_ms = mfi->seek; free_mfi(mfi, 0); } } ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, seek_ms); if (ret < 0) { playback_abort(); *retval = -1; return COMMAND_END; } } ret = source_play(); if (ret < 0) { playback_abort(); *retval = -1; return COMMAND_END; } metadata_trigger(1); // Start sessions on selected devices *retval = 0; for (device = dev_list; device; device = device->next) { if (device->selected && !device->session) { ret = outputs_device_start(device, device_restart_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not start selected %s device '%s'\n", device->type_name, device->name); continue; } DPRINTF(E_INFO, L_PLAYER, "Using selected %s device '%s'\n", device->type_name, device->name); (*retval)++; } } // If autoselecting is enabled, try to autoselect a non-selected device if the above failed if (speaker_autoselect && (*retval == 0) && (output_sessions == 0)) for (device = dev_list; device; device = device->next) { if ((outputs_priority(device) == 0) || device->session) continue; speaker_select_output(device); ret = outputs_device_start(device, device_restart_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); if (ret < 0) { DPRINTF(E_DBG, L_PLAYER, "Could not autoselect %s device '%s'\n", device->type_name, device->name); speaker_deselect_output(device); continue; } DPRINTF(E_INFO, L_PLAYER, "Autoselecting %s device '%s'\n", device->type_name, device->name); (*retval)++; break; } // We're async if we need to start devices if (*retval > 0) return COMMAND_PENDING; // async // Otherwise, just run the bottom half *retval = 0; return COMMAND_END; } static enum command_state playback_start_id(void *arg, int *retval) { struct db_queue_item *queue_item = NULL; union player_arg *cmdarg = arg; enum command_state cmd_state; int ret; *retval = -1; if (player_state == PLAY_STOPPED) { db_queue_clear(0); ret = db_queue_add_by_fileid(cmdarg->id, 0, 0); if (ret < 0) return COMMAND_END; queue_item = db_queue_fetch_byfileid(cmdarg->id); if (!queue_item) return COMMAND_END; } cmd_state = playback_start_item(queue_item, retval); free_queue_item(queue_item, 0); return cmd_state; } static enum command_state playback_start(void *arg, int *retval) { struct db_queue_item *queue_item = NULL; enum command_state cmd_state; *retval = -1; if (player_state == PLAY_STOPPED) { // Start playback of first item in queue queue_item = db_queue_fetch_bypos(0, shuffle); if (!queue_item) return COMMAND_END; } cmd_state = playback_start_item(queue_item, retval); free_queue_item(queue_item, 0); return cmd_state; } static enum command_state playback_prev_bh(void *arg, int *retval) { int ret; int pos_sec; struct player_source *ps; // The upper half is playback_pause, therefor the current playing item is // already set as the cur_streaming (cur_playing is NULL). if (!cur_streaming) { DPRINTF(E_LOG, L_PLAYER, "Could not get current stream source\n"); *retval = -1; return COMMAND_END; } // Only add to history if playback started if (cur_streaming->output_start > cur_streaming->stream_start) history_add(cur_streaming->id, cur_streaming->item_id); // Compute the playing time in seconds for the current song if (cur_streaming->output_start > cur_streaming->stream_start) pos_sec = (cur_streaming->output_start - cur_streaming->stream_start) / 44100; else pos_sec = 0; // Only skip to the previous song if the playing time is less than 3 seconds, // otherwise restart the current song. DPRINTF(E_DBG, L_PLAYER, "Skipping song played %d sec\n", pos_sec); if (pos_sec < 3) { ps = source_prev(); if (!ps) { playback_abort(); *retval = -1; return COMMAND_END; } source_stop(); ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, 0); if (ret < 0) { source_free(ps); playback_abort(); *retval = -1; return COMMAND_END; } } else { ret = source_seek(0); if (ret < 0) { playback_abort(); *retval = -1; return COMMAND_END; } } if (player_state == PLAY_STOPPED) { *retval = -1; return COMMAND_END; } // Silent status change - playback_start() sends the real status update player_state = PLAY_PAUSED; *retval = 0; return COMMAND_END; } static enum command_state playback_next_bh(void *arg, int *retval) { struct player_source *ps; int ret; uint32_t item_id; // The upper half is playback_pause, therefor the current playing item is // already set as the cur_streaming (cur_playing is NULL). if (!cur_streaming) { DPRINTF(E_LOG, L_PLAYER, "Could not get current stream source\n"); *retval = -1; return COMMAND_END; } // Only add to history if playback started if (cur_streaming->output_start > cur_streaming->stream_start) history_add(cur_streaming->id, cur_streaming->item_id); item_id = cur_streaming->item_id; ps = source_next(); if (!ps) { playback_abort(); *retval = -1; return COMMAND_END; } source_stop(); ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, 0); if (ret < 0) { source_free(ps); playback_abort(); *retval = -1; return COMMAND_END; } if (player_state == PLAY_STOPPED) { *retval = -1; return COMMAND_END; } if (consume) db_queue_delete_byitemid(item_id); // Silent status change - playback_start() sends the real status update player_state = PLAY_PAUSED; *retval = 0; return COMMAND_END; } static enum command_state playback_seek_bh(void *arg, int *retval) { union player_arg *cmdarg = arg; int ms; int ret; *retval = -1; if (!cur_streaming) return COMMAND_END; ms = cmdarg->intval; ret = source_seek(ms); if (ret < 0) { playback_abort(); return COMMAND_END; } // Silent status change - playback_start() sends the real status update player_state = PLAY_PAUSED; *retval = 0; return COMMAND_END; } static enum command_state playback_pause_bh(void *arg, int *retval) { *retval = -1; // outputs_flush() in playback_pause() may have a caused a failure callback // from the output, which in streaming_cb() can cause playback_abort() -> // cur_streaming is NULL if (!cur_streaming) return COMMAND_END; if (cur_streaming->data_kind == DATA_KIND_HTTP || cur_streaming->data_kind == DATA_KIND_PIPE) { DPRINTF(E_DBG, L_PLAYER, "Source is not pausable, abort playback\n"); playback_abort(); return COMMAND_END; } status_update(PLAY_PAUSED); seek_save(); *retval = 0; return COMMAND_END; } static enum command_state playback_pause(void *arg, int *retval) { uint64_t pos; pos = source_check(); if (pos == 0) { DPRINTF(E_LOG, L_PLAYER, "Could not retrieve current position for pause\n"); playback_abort(); *retval = -1; return COMMAND_END; } // Make sure playback is still running after source_check() if (player_state == PLAY_STOPPED) { *retval = -1; return COMMAND_END; } *retval = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); playback_timer_stop(); source_pause(pos); outputs_metadata_purge(); // We're async if we need to flush devices if (*retval > 0) return COMMAND_PENDING; // async // Otherwise, just run the bottom half return COMMAND_END; } static enum command_state speaker_enumerate(void *arg, int *retval) { union player_arg *cmdarg = arg; struct output_device *device; struct spk_enum *spk_enum; struct spk_flags flags; spk_enum = cmdarg->spk_enum; #ifdef DEBUG_RELVOL DPRINTF(E_DBG, L_PLAYER, "*** master: %d\n", master_volume); #endif for (device = dev_list; device; device = device->next) { if (device->advertised || device->selected) { flags.selected = device->selected; flags.has_password = device->has_password; flags.has_video = device->has_video; spk_enum->cb(device->id, device->name, device->relvol, device->volume, flags, spk_enum->arg); #ifdef DEBUG_RELVOL DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol); #endif } } *retval = 0; return COMMAND_END; } static int speaker_activate(struct output_device *device) { int ret; if (!device) { DPRINTF(E_LOG, L_PLAYER, "Bug! speaker_activate called with device\n"); return -1; } if (player_state == PLAY_PLAYING) { DPRINTF(E_DBG, L_PLAYER, "Activating %s device '%s'\n", device->type_name, device->name); ret = outputs_device_start(device, device_activate_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not start %s device '%s'\n", device->type_name, device->name); return -1; } } else { DPRINTF(E_DBG, L_PLAYER, "Probing %s device '%s'\n", device->type_name, device->name); ret = outputs_device_probe(device, device_probe_cb); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not probe %s device '%s'\n", device->type_name, device->name); return -1; } } return 0; } static int speaker_deactivate(struct output_device *device) { DPRINTF(E_DBG, L_PLAYER, "Deactivating %s device '%s'\n", device->type_name, device->name); outputs_status_cb(device->session, device_shutdown_cb); outputs_device_stop(device->session); return 0; } static enum command_state speaker_set(void *arg, int *retval) { union player_arg *cmdarg = arg; struct output_device *device; uint64_t *ids; int nspk; int i; int ret; *retval = 0; ids = cmdarg->speaker_set_param.device_ids; if (ids) nspk = ids[0]; else nspk = 0; DPRINTF(E_DBG, L_PLAYER, "Speaker set: %d speakers\n", nspk); *retval = 0; for (device = dev_list; device; device = device->next) { for (i = 1; i <= nspk; i++) { DPRINTF(E_DBG, L_PLAYER, "Set %" PRIu64 " device %" PRIu64 "\n", ids[i], device->id); if (ids[i] == device->id) break; } if (i <= nspk) { if (device->has_password && !device->password) { DPRINTF(E_INFO, L_PLAYER, "The %s device '%s' is password-protected, but we don't have it\n", device->type_name, device->name); cmdarg->speaker_set_param.intval = -2; continue; } DPRINTF(E_DBG, L_PLAYER, "The %s device '%s' is selected\n", device->type_name, device->name); if (!device->selected) speaker_select_output(device); if (!device->session) { ret = speaker_activate(device); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not activate %s device '%s'\n", device->type_name, device->name); speaker_deselect_output(device); if (cmdarg->speaker_set_param.intval != -2) cmdarg->speaker_set_param.intval = -1; } else (*retval)++; } } else { DPRINTF(E_DBG, L_PLAYER, "The %s device '%s' is NOT selected\n", device->type_name, device->name); if (device->selected) speaker_deselect_output(device); if (device->session) { ret = speaker_deactivate(device); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not deactivate %s device '%s'\n", device->type_name, device->name); if (cmdarg->speaker_set_param.intval != -2) cmdarg->speaker_set_param.intval = -1; } else (*retval)++; } } } listener_notify(LISTENER_SPEAKER); if (*retval > 0) return COMMAND_PENDING; // async *retval = cmdarg->speaker_set_param.intval; return COMMAND_END; } static enum command_state volume_set(void *arg, int *retval) { union player_arg *cmdarg = arg; struct output_device *device; int volume; *retval = 0; volume = cmdarg->intval; if (master_volume == volume) return COMMAND_END; master_volume = volume; for (device = dev_list; device; device = device->next) { if (!device->selected) continue; device->volume = rel_to_vol(device->relvol); #ifdef DEBUG_RELVOL DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol); #endif if (device->session) *retval += outputs_device_volume_set(device, device_command_cb); } listener_notify(LISTENER_VOLUME); if (*retval > 0) return COMMAND_PENDING; // async return COMMAND_END; } static enum command_state volume_setrel_speaker(void *arg, int *retval) { union player_arg *cmdarg = arg; struct output_device *device; uint64_t id; int relvol; *retval = 0; id = cmdarg->vol_param.spk_id; relvol = cmdarg->vol_param.volume; for (device = dev_list; device; device = device->next) { if (device->id != id) continue; if (!device->selected) { *retval = 0; return COMMAND_END; } device->relvol = relvol; device->volume = rel_to_vol(relvol); #ifdef DEBUG_RELVOL DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol); #endif if (device->session) *retval = outputs_device_volume_set(device, device_command_cb); break; } listener_notify(LISTENER_VOLUME); if (*retval > 0) return COMMAND_PENDING; // async return COMMAND_END; } static enum command_state volume_setabs_speaker(void *arg, int *retval) { union player_arg *cmdarg = arg; struct output_device *device; uint64_t id; int volume; *retval = 0; id = cmdarg->vol_param.spk_id; volume = cmdarg->vol_param.volume; master_volume = volume; for (device = dev_list; device; device = device->next) { if (!device->selected) continue; if (device->id != id) { device->relvol = vol_to_rel(device->volume); #ifdef DEBUG_RELVOL DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol); #endif continue; } else { device->relvol = 100; device->volume = master_volume; #ifdef DEBUG_RELVOL DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol); #endif if (device->session) *retval = outputs_device_volume_set(device, device_command_cb);//FIXME Does this need to be += ? } } listener_notify(LISTENER_VOLUME); if (*retval > 0) return COMMAND_PENDING; // async return COMMAND_END; } static enum command_state repeat_set(void *arg, int *retval) { union player_arg *cmdarg = arg; if (cmdarg->mode == repeat) { *retval = 0; return COMMAND_END; } switch (cmdarg->mode) { case REPEAT_OFF: case REPEAT_SONG: case REPEAT_ALL: repeat = cmdarg->mode; break; default: DPRINTF(E_LOG, L_PLAYER, "Invalid repeat mode: %d\n", cmdarg->mode); *retval = -1; return COMMAND_END; } listener_notify(LISTENER_OPTIONS); *retval = 0; return COMMAND_END; } static enum command_state shuffle_set(void *arg, int *retval) { union player_arg *cmdarg = arg; uint32_t cur_id; switch (cmdarg->intval) { case 1: if (!shuffle) { cur_id = cur_streaming ? cur_streaming->item_id : 0; db_queue_reshuffle(cur_id); } /* FALLTHROUGH */ case 0: shuffle = cmdarg->intval; break; default: DPRINTF(E_LOG, L_PLAYER, "Invalid shuffle mode: %d\n", cmdarg->intval); *retval = -1; return COMMAND_END; } listener_notify(LISTENER_OPTIONS); *retval = 0; return COMMAND_END; } static enum command_state consume_set(void *arg, int *retval) { union player_arg *cmdarg = arg; consume = cmdarg->intval; listener_notify(LISTENER_OPTIONS); *retval = 0; return COMMAND_END; } /* * Removes all items from the history */ static enum command_state playerqueue_clear_history(void *arg, int *retval) { memset(history, 0, sizeof(struct player_history)); cur_plversion++; // TODO [db_queue] need to update db queue version listener_notify(LISTENER_QUEUE); *retval = 0; return COMMAND_END; } static enum command_state playerqueue_plid(void *arg, int *retval) { union player_arg *cmdarg = arg; cur_plid = cmdarg->id; *retval = 0; return COMMAND_END; } /* ------------------------------- Player API ------------------------------- */ int player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit) { uint64_t delta; int ret; ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &timer_res); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno)); return -1; } delta = (ts->tv_sec - pb_pos_stamp.tv_sec) * 1000000 + (ts->tv_nsec - pb_pos_stamp.tv_nsec) / 1000; #ifdef DEBUG_SYNC DPRINTF(E_DBG, L_PLAYER, "Delta is %" PRIu64 " usec\n", delta); #endif delta = (delta * 44100) / 1000000; #ifdef DEBUG_SYNC DPRINTF(E_DBG, L_PLAYER, "Delta is %" PRIu64 " samples\n", delta); #endif *pos = pb_pos + delta; if (commit) { pb_pos = *pos; pb_pos_stamp.tv_sec = ts->tv_sec; pb_pos_stamp.tv_nsec = ts->tv_nsec; #ifdef DEBUG_SYNC DPRINTF(E_DBG, L_PLAYER, "Pos: %" PRIu64 " (clock)\n", *pos); #endif } return 0; } int player_get_status(struct player_status *status) { union player_arg cmdarg; int ret; cmdarg.status = status; ret = commands_exec_sync(cmdbase, get_status, NULL, &cmdarg); return ret; } /* --------------------------- Thread: httpd (DACP) ------------------------- */ /* * Stores the now playing media item dbmfi-id in the given id pointer. * * @param id Pointer will hold the playing item (dbmfi) id if the function returns 0 * @return 0 on success, -1 on failure (e. g. no playing item found) */ int player_now_playing(uint32_t *id) { union player_arg cmdarg; int ret; cmdarg.id_ptr = id; ret = commands_exec_sync(cmdbase, now_playing, NULL, &cmdarg); return ret; } /* * Starts/resumes playback * * Depending on the player state, this will either resume playing the current * item (player is paused) or begin playing the queue from the beginning. * * If shuffle is set, the queue is reshuffled prior to starting playback. * * @return 0 if successful, -1 if an error occurred */ int player_playback_start(void) { int ret; ret = commands_exec_sync(cmdbase, playback_start, playback_start_bh, NULL); return ret; } /* * Starts/resumes playback of the given queue_item * * If shuffle is set, the queue is reshuffled prior to starting playback. * * If a pointer is given as argument "itemid", its value will be set to the playing item id. * * @param queue_item to start playing * @return 0 if successful, -1 if an error occurred */ int player_playback_start_byitem(struct db_queue_item *queue_item) { int ret; ret = commands_exec_sync(cmdbase, playback_start_item, playback_start_bh, queue_item); return ret; } int player_playback_start_byid(uint32_t id) { union player_arg cmdarg; int ret; cmdarg.id = id; ret = commands_exec_sync(cmdbase, playback_start_id, playback_start_bh, &cmdarg); return ret; } int player_playback_stop(void) { int ret; ret = commands_exec_sync(cmdbase, playback_stop, NULL, NULL); return ret; } int player_playback_pause(void) { int ret; ret = commands_exec_sync(cmdbase, playback_pause, playback_pause_bh, NULL); return ret; } int player_playback_seek(int ms) { union player_arg cmdarg; int ret; cmdarg.intval = ms; ret = commands_exec_sync(cmdbase, playback_pause, playback_seek_bh, &cmdarg); return ret; } int player_playback_next(void) { int ret; ret = commands_exec_sync(cmdbase, playback_pause, playback_next_bh, NULL); return ret; } int player_playback_prev(void) { int ret; ret = commands_exec_sync(cmdbase, playback_pause, playback_prev_bh, NULL); return ret; } void player_speaker_enumerate(spk_enum_cb cb, void *arg) { union player_arg cmdarg; struct spk_enum spk_enum; spk_enum.cb = cb; spk_enum.arg = arg; cmdarg.spk_enum = &spk_enum; commands_exec_sync(cmdbase, speaker_enumerate, NULL, &cmdarg); } int player_speaker_set(uint64_t *ids) { union player_arg cmdarg; int ret; cmdarg.speaker_set_param.device_ids = ids; cmdarg.speaker_set_param.intval = 0; ret = commands_exec_sync(cmdbase, speaker_set, NULL, &cmdarg); return ret; } int player_volume_set(int vol) { union player_arg cmdarg; int ret; if (vol < 0 || vol > 100) { DPRINTF(E_LOG, L_PLAYER, "Volume (%d) for player_volume_set is out of range\n", vol); return -1; } cmdarg.intval = vol; ret = commands_exec_sync(cmdbase, volume_set, NULL, &cmdarg); return ret; } int player_volume_setrel_speaker(uint64_t id, int relvol) { union player_arg cmdarg; int ret; if (relvol < 0 || relvol > 100) { DPRINTF(E_LOG, L_PLAYER, "Volume (%d) for player_volume_setrel_speaker is out of range\n", relvol); return -1; } cmdarg.vol_param.spk_id = id; cmdarg.vol_param.volume = relvol; ret = commands_exec_sync(cmdbase, volume_setrel_speaker, NULL, &cmdarg); return ret; } int player_volume_setabs_speaker(uint64_t id, int vol) { union player_arg cmdarg; int ret; if (vol < 0 || vol > 100) { DPRINTF(E_LOG, L_PLAYER, "Volume (%d) for player_volume_setabs_speaker is out of range\n", vol); return -1; } cmdarg.vol_param.spk_id = id; cmdarg.vol_param.volume = vol; ret = commands_exec_sync(cmdbase, volume_setabs_speaker, NULL, &cmdarg); return ret; } int player_repeat_set(enum repeat_mode mode) { union player_arg cmdarg; int ret; cmdarg.mode = mode; ret = commands_exec_sync(cmdbase, repeat_set, NULL, &cmdarg); return ret; } int player_shuffle_set(int enable) { union player_arg cmdarg; int ret; cmdarg.intval = enable; ret = commands_exec_sync(cmdbase, shuffle_set, NULL, &cmdarg); return ret; } int player_consume_set(int enable) { union player_arg cmdarg; int ret; cmdarg.intval = enable; ret = commands_exec_sync(cmdbase, consume_set, NULL, &cmdarg); return ret; } void player_queue_clear_history() { commands_exec_sync(cmdbase, playerqueue_clear_history, NULL, NULL); } void player_queue_plid(uint32_t plid) { union player_arg cmdarg; cmdarg.id = plid; commands_exec_sync(cmdbase, playerqueue_plid, NULL, &cmdarg); } struct player_history * player_history_get(void) { return history; } /* ------------------- Non-blocking commands used by mDNS ------------------- */ int player_device_add(void *device) { union player_arg *cmdarg; int ret; cmdarg = calloc(1, sizeof(union player_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n"); return -1; } cmdarg->device = device; ret = commands_exec_async(cmdbase, device_add, cmdarg); return ret; } int player_device_remove(void *device) { union player_arg *cmdarg; int ret; cmdarg = calloc(1, sizeof(union player_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n"); return -1; } cmdarg->device = device; ret = commands_exec_async(cmdbase, device_remove_family, cmdarg); return ret; } static void player_device_auth_kickoff(enum output_types type, char **arglist) { union player_arg *cmdarg; cmdarg = calloc(1, sizeof(union player_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n"); return; } cmdarg->auth.type = type; memcpy(cmdarg->auth.pin, arglist[0], 4); commands_exec_async(cmdbase, device_auth_kickoff, cmdarg); } /* --------------------------- Thread: filescanner -------------------------- */ void player_raop_verification_kickoff(char **arglist) { player_device_auth_kickoff(OUTPUT_TYPE_RAOP, arglist); } /* ---------------------------- Thread: worker ------------------------------ */ void player_metadata_send(void *imd, void *omd) { union player_arg cmdarg; cmdarg.metadata_param.input = imd; cmdarg.metadata_param.output = omd; commands_exec_sync(cmdbase, device_metadata_send, NULL, &cmdarg); } /* ---------------------------- Thread: player ------------------------------ */ static void * player(void *arg) { struct output_device *device; int ret; ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Error: DB init failed\n"); pthread_exit(NULL); } event_base_dispatch(evbase_player); if (!player_exit) DPRINTF(E_LOG, L_PLAYER, "Player event loop terminated ahead of time!\n"); db_speaker_clear_all(); for (device = dev_list; device; device = device->next) { ret = db_speaker_save(device); if (ret < 0) DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", device->type_name, device->name); } db_perthread_deinit(); pthread_exit(NULL); } /* ----------------------------- Thread: main ------------------------------- */ int player_init(void) { uint64_t interval; uint32_t rnd; int ret; player_exit = 0; speaker_autoselect = cfg_getbool(cfg_getsec(cfg, "general"), "speaker_autoselect"); clear_queue_on_stop_disabled = cfg_getbool(cfg_getsec(cfg, "mpd"), "clear_queue_on_stop_disable"); dev_list = NULL; master_volume = -1; output_sessions = 0; cur_playing = NULL; cur_streaming = NULL; cur_plid = 0; cur_plversion = 0; player_state = PLAY_STOPPED; repeat = REPEAT_OFF; shuffle = 0; consume = 0; history = (struct player_history *)calloc(1, sizeof(struct player_history)); // Determine if the resolution of the system timer is > or < the size // of an audio packet. NOTE: this assumes the system clock resolution // is less than one second. if (clock_getres(CLOCK_MONOTONIC, &timer_res) < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not get the system timer resolution.\n"); return -1; } if (!cfg_getbool(cfg_getsec(cfg, "general"), "high_resolution_clock")) { DPRINTF(E_INFO, L_PLAYER, "High resolution clock not enabled on this system (res is %ld)\n", timer_res.tv_nsec); timer_res.tv_nsec = 2 * AIRTUNES_V2_STREAM_PERIOD; } // Set the tick interval for the playback timer interval = MAX(timer_res.tv_nsec, AIRTUNES_V2_STREAM_PERIOD); tick_interval.tv_nsec = interval; pb_write_deficit_max = (PLAYER_WRITE_BEHIND_MAX * 1000000 / interval); pb_read_deficit_max = (PLAYER_READ_BEHIND_MAX * 1000000 / interval); // Create the playback timer #ifdef HAVE_TIMERFD pb_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); ret = pb_timer_fd; #else ret = timer_create(CLOCK_MONOTONIC, NULL, &pb_timer); #endif if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer: %s\n", strerror(errno)); return -1; } // Random RTP time start gcry_randomize(&rnd, sizeof(rnd), GCRY_STRONG_RANDOM); last_rtptime = ((uint64_t)1 << 32) | rnd; evbase_player = event_base_new(); if (!evbase_player) { DPRINTF(E_LOG, L_PLAYER, "Could not create an event base\n"); goto evbase_fail; } #ifdef HAVE_TIMERFD pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ | EV_PERSIST, playback_cb, NULL); #else pb_timer_ev = event_new(evbase_player, SIGALRM, EV_SIGNAL | EV_PERSIST, playback_cb, NULL); #endif if (!pb_timer_ev) { DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer event\n"); goto evnew_fail; } cmdbase = commands_base_new(evbase_player, NULL); ret = outputs_init(); if (ret < 0) { DPRINTF(E_FATAL, L_PLAYER, "Output initiation failed\n"); goto outputs_fail; } ret = input_init(); if (ret < 0) { DPRINTF(E_FATAL, L_PLAYER, "Input initiation failed\n"); goto input_fail; } ret = pthread_create(&tid_player, NULL, player, NULL); if (ret < 0) { DPRINTF(E_FATAL, L_PLAYER, "Could not spawn player thread: %s\n", strerror(errno)); goto thread_fail; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_player, "player"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_player, "player"); #endif return 0; thread_fail: input_deinit(); input_fail: outputs_deinit(); outputs_fail: commands_base_free(cmdbase); evnew_fail: event_base_free(evbase_player); evbase_fail: #ifdef HAVE_TIMERFD close(pb_timer_fd); #else timer_delete(pb_timer); #endif return -1; } void player_deinit(void) { int ret; player_playback_stop(); #ifdef HAVE_TIMERFD close(pb_timer_fd); #else timer_delete(pb_timer); #endif input_deinit(); outputs_deinit(); player_exit = 1; commands_base_destroy(cmdbase); ret = pthread_join(tid_player, NULL); if (ret != 0) { DPRINTF(E_LOG, L_PLAYER, "Could not join player thread: %s\n", strerror(errno)); return; } free(history); event_base_free(evbase_player); } forked-daapd-25.0/src/player.h000066400000000000000000000053671313447753700162460ustar00rootroot00000000000000 #ifndef __PLAYER_H__ #define __PLAYER_H__ #include #include "db.h" /* AirTunes v2 packet interval in ns */ /* (352 samples/packet * 1e9 ns/s) / 44100 samples/s = 7981859 ns/packet */ # define AIRTUNES_V2_STREAM_PERIOD 7981859 /* AirTunes v2 number of samples per packet */ #define AIRTUNES_V2_PACKET_SAMPLES 352 /* Maximum number of previously played songs that are remembered */ #define MAX_HISTORY_COUNT 20 enum play_status { PLAY_STOPPED = 2, PLAY_PAUSED = 3, PLAY_PLAYING = 4, }; enum repeat_mode { REPEAT_OFF = 0, REPEAT_SONG = 1, REPEAT_ALL = 2, }; struct spk_flags { unsigned selected:1; unsigned has_password:1; unsigned has_video:1; }; struct player_status { enum play_status status; enum repeat_mode repeat; char shuffle; char consume; int volume; /* Playlist id */ uint32_t plid; /* Id of the playing file/item in the files database */ uint32_t id; /* Item-Id of the playing file/item in the queue */ uint32_t item_id; /* Elapsed time in ms of playing item */ uint32_t pos_ms; /* Length in ms of playing item */ uint32_t len_ms; }; typedef void (*spk_enum_cb)(uint64_t id, const char *name, int relvol, int absvol, struct spk_flags flags, void *arg); struct player_history { /* Buffer index of the oldest remembered song */ unsigned int start_index; /* Count of song ids in the buffer */ unsigned int count; /* Circular buffer of song ids previously played by forked-daapd */ uint32_t id[MAX_HISTORY_COUNT]; uint32_t item_id[MAX_HISTORY_COUNT]; }; int player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit); int player_get_status(struct player_status *status); int player_now_playing(uint32_t *id); void player_speaker_enumerate(spk_enum_cb cb, void *arg); int player_speaker_set(uint64_t *ids); int player_playback_start(void); int player_playback_start_byitem(struct db_queue_item *queue_item); int player_playback_start_byid(uint32_t id); int player_playback_stop(void); int player_playback_pause(void); int player_playback_seek(int ms); int player_playback_next(void); int player_playback_prev(void); int player_volume_set(int vol); int player_volume_setrel_speaker(uint64_t id, int relvol); int player_volume_setabs_speaker(uint64_t id, int vol); int player_repeat_set(enum repeat_mode mode); int player_shuffle_set(int enable); int player_consume_set(int enable); void player_queue_clear_history(void); void player_queue_plid(uint32_t plid); struct player_history * player_history_get(void); int player_device_add(void *device); int player_device_remove(void *device); void player_raop_verification_kickoff(char **arglist); void player_metadata_send(void *imd, void *omd); int player_init(void); void player_deinit(void); #endif /* !__PLAYER_H__ */ forked-daapd-25.0/src/remote_pairing.c000066400000000000000000000431271313447753700177450ustar00rootroot00000000000000/* * Copyright (C) 2010 Julien BLACHE * * iTunes - Remote pairing hash function published by Michael Paul Bailey * * Simplified version using standard MD5 published by Jeff Sharkey * * * Pairing process based on the work by * - Michael Croes * * - Jeffrey Sharkey * * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #ifdef HAVE_EVENTFD # include #endif #include #include #include #include #include "logger.h" #include "conffile.h" #include "mdns.h" #include "misc.h" #include "db.h" #include "remote_pairing.h" struct remote_info { struct pairing_info pi; char *paircode; char *pin; unsigned short v4_port; unsigned short v6_port; char *v4_address; char *v6_address; struct evhttp_connection *evcon; struct remote_info *next; }; /* Main event base, from main.c */ extern struct event_base *evbase_main; #ifdef HAVE_EVENTFD static int pairing_efd; #else static int pairing_pipe[2]; #endif static struct event *pairingev; static pthread_mutex_t remote_lck; static struct remote_info *remote_info; /* iTunes - Remote pairing hash */ static char * itunes_pairing_hash(char *paircode, char *pin) { char hash[33]; char ebuf[64]; uint8_t *hash_bytes; size_t hashlen; gcry_md_hd_t hd; gpg_error_t gc_err; int i; if (strlen(paircode) != 16) { DPRINTF(E_LOG, L_REMOTE, "Paircode length != 16, cannot compute pairing hash\n"); return NULL; } if (strlen(pin) != 4) { DPRINTF(E_LOG, L_REMOTE, "Pin length != 4, cannot compute pairing hash\n"); return NULL; } gc_err = gcry_md_open(&hd, GCRY_MD_MD5, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_REMOTE, "Could not open MD5: %s\n", ebuf); return NULL; } gcry_md_write(hd, paircode, 16); /* Add pin code characters on 16 bits - remember Mac OS X is * all UTF-16 (wchar_t). */ for (i = 0; i < 4; i++) { gcry_md_write(hd, pin + i, 1); gcry_md_write(hd, "\0", 1); } hash_bytes = gcry_md_read(hd, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_REMOTE, "Could not read MD5 hash\n"); return NULL; } hashlen = gcry_md_get_algo_dlen(GCRY_MD_MD5); for (i = 0; i < hashlen; i++) sprintf(hash + (2 * i), "%02X", hash_bytes[i]); gcry_md_close(hd); return strdup(hash); } /* Operations on the remote list must happen * with the list lock held by the caller */ static struct remote_info * create_remote(void) { struct remote_info *ri; ri = calloc(1, sizeof(struct remote_info)); if (!ri) { DPRINTF(E_WARN, L_REMOTE, "Out of memory for struct remote_info\n"); return NULL; } return ri; } static void unlink_remote(struct remote_info *ri) { if (ri == remote_info) remote_info = NULL; else DPRINTF(E_LOG, L_REMOTE, "WARNING: struct remote_info not found in list; BUG!\n"); } static void free_remote(struct remote_info *ri) { if (ri->paircode) free(ri->paircode); if (ri->pin) free(ri->pin); if (ri->v4_address) free(ri->v4_address); if (ri->v6_address) free(ri->v6_address); free_pi(&ri->pi, 1); free(ri); } static void remove_remote(struct remote_info *ri) { unlink_remote(ri); free_remote(ri); } static void remove_remote_address_byid(const char *id, int family) { struct remote_info *ri = NULL; if (remote_info && strcmp(remote_info->pi.remote_id, id) == 0) ri = remote_info; if (!ri) { DPRINTF(E_WARN, L_REMOTE, "Remote %s not found in list\n", id); return; } switch (family) { case AF_INET: if (ri->v4_address) { free(ri->v4_address); ri->v4_address = NULL; } break; case AF_INET6: if (ri->v6_address) { free(ri->v6_address); ri->v6_address = NULL; } break; } if (!ri->v4_address && !ri->v6_address) remove_remote(ri); } static int add_remote_mdns_data(const char *id, int family, const char *address, int port, char *name, char *paircode) { char *check_addr; int ret; if (remote_info && strcmp(remote_info->pi.remote_id, id) == 0) { DPRINTF(E_DBG, L_REMOTE, "Remote id %s found\n", id); free_pi(&remote_info->pi, 1); ret = 1; } else { DPRINTF(E_DBG, L_REMOTE, "Remote id %s not known, adding\n", id); if (remote_info) { DPRINTF(E_DBG, L_REMOTE, "Removing existing remote with id %s\n", remote_info->pi.remote_id); remove_remote(remote_info); } remote_info = create_remote(); ret = 0; } free(remote_info->paircode); free(remote_info->pi.remote_id); remote_info->pi.remote_id = strdup(id); switch (family) { case AF_INET: free(remote_info->v4_address); remote_info->v4_address = strdup(address); remote_info->v4_port = port; check_addr = remote_info->v4_address; break; case AF_INET6: free(remote_info->v6_address); remote_info->v6_address = strdup(address); remote_info->v6_port = port; check_addr = remote_info->v6_address; break; default: DPRINTF(E_LOG, L_REMOTE, "Unknown address family %d\n", family); check_addr = NULL; break; } if (!remote_info->pi.remote_id || !check_addr) { DPRINTF(E_LOG, L_REMOTE, "Out of memory for remote pairing data\n"); remove_remote(remote_info); return -1; } remote_info->pi.name = name; remote_info->paircode = paircode; return ret; } static int add_remote_pin_data(const char *pin) { if (!remote_info) { DPRINTF(E_LOG, L_REMOTE, "No remote known from mDNS, ignoring\n"); return -1; } DPRINTF(E_DBG, L_REMOTE, "Adding pin to remote '%s'\n", remote_info->pi.name); free(remote_info->pin); remote_info->pin = strdup(pin); return 0; } static void kickoff_pairing(void) { #ifdef HAVE_EVENTFD int ret; ret = eventfd_write(pairing_efd, 1); if (ret < 0) DPRINTF(E_LOG, L_REMOTE, "Could not send pairing event: %s\n", strerror(errno)); #else int dummy = 42; int ret; ret = write(pairing_pipe[1], &dummy, sizeof(dummy)); if (ret != sizeof(dummy)) DPRINTF(E_LOG, L_REMOTE, "Could not write to pairing fd: %s\n", strerror(errno)); #endif } /* Thread: main (pairing) */ static void pairing_request_cb(struct evhttp_request *req, void *arg) { struct remote_info *ri; struct evbuffer *input_buffer; uint8_t *response; char guid[17]; int buflen; int response_code; int len; int i; int ret; ri = (struct remote_info *)arg; if (!req) { DPRINTF(E_LOG, L_REMOTE, "Empty pairing request callback\n"); goto cleanup; } response_code = evhttp_request_get_response_code(req); if (response_code != HTTP_OK) { DPRINTF(E_LOG, L_REMOTE, "Pairing failed with Remote %s/%s, HTTP response code %d\n", ri->pi.remote_id, ri->pi.name, response_code); goto cleanup; } input_buffer = evhttp_request_get_input_buffer(req); buflen = evbuffer_get_length(input_buffer); if (buflen < 8) { DPRINTF(E_LOG, L_REMOTE, "Remote %s/%s: pairing response too short\n", ri->pi.remote_id, ri->pi.name); goto cleanup; } response = evbuffer_pullup(input_buffer, -1); if ((response[0] != 'c') || (response[1] != 'm') || (response[2] != 'p') || (response[3] != 'a')) { DPRINTF(E_LOG, L_REMOTE, "Remote %s/%s: unknown pairing response, expected cmpa\n", ri->pi.remote_id, ri->pi.name); goto cleanup; } len = (response[4] << 24) | (response[5] << 16) | (response[6] << 8) | (response[7]); if (buflen < 8 + len) { DPRINTF(E_LOG, L_REMOTE, "Remote %s/%s: pairing response truncated (got %d expected %d)\n", ri->pi.remote_id, ri->pi.name, buflen, len + 8); goto cleanup; } response += 8; for (; len > 0; len--, response++) { if ((response[0] != 'c') || (response[1] != 'm') || (response[2] != 'p') || (response[3] != 'g')) continue; else { len -= 8; response += 8; break; } } if (len < 8) { DPRINTF(E_LOG, L_REMOTE, "Remote %s/%s: cmpg truncated in pairing response\n", ri->pi.remote_id, ri->pi.name); goto cleanup; } for (i = 0; i < 8; i++) sprintf(guid + (2 * i), "%02X", response[i]); ri->pi.guid = strdup(guid); DPRINTF(E_LOG, L_REMOTE, "Pairing succeeded with Remote '%s' (id %s), GUID: %s\n", ri->pi.name, ri->pi.remote_id, guid); ret = db_pairing_add(&ri->pi); if (ret < 0) { DPRINTF(E_LOG, L_REMOTE, "Failed to register pairing!\n"); goto cleanup; } cleanup: evhttp_connection_free(ri->evcon); free_remote(ri); } /* Thread: main (pairing) */ static int send_pairing_request(struct remote_info *ri, char *req_uri, int family) { struct evhttp_connection *evcon; struct evhttp_request *req; char *address; unsigned short port; int ret; switch (family) { case AF_INET: if (!ri->v4_address) return -1; address = ri->v4_address; port = ri->v4_port; break; case AF_INET6: if (!ri->v6_address) return -1; address = ri->v6_address; port = ri->v6_port; break; default: return -1; } evcon = evhttp_connection_base_new(evbase_main, NULL, address, port); if (!evcon) { DPRINTF(E_LOG, L_REMOTE, "Could not create connection for pairing with %s\n", ri->pi.name); return -1; } req = evhttp_request_new(pairing_request_cb, ri); if (!req) { DPRINTF(E_WARN, L_REMOTE, "Could not create HTTP request for pairing\n"); goto request_fail; } ret = evhttp_make_request(evcon, req, EVHTTP_REQ_GET, req_uri); if (ret < 0) { DPRINTF(E_WARN, L_REMOTE, "Could not make pairing request\n"); goto request_fail; } DPRINTF(E_DBG, L_REMOTE, "Pairing requested to %s\n", req_uri); ri->evcon = evcon; return 0; request_fail: evhttp_connection_free(evcon); return -1; } /* Thread: main (pairing) */ static void do_pairing(struct remote_info *ri) { char req_uri[128]; char *pairing_hash; int ret; pairing_hash = itunes_pairing_hash(ri->paircode, ri->pin); if (!pairing_hash) { DPRINTF(E_LOG, L_REMOTE, "Could not compute pairing hash!\n"); goto hash_fail; } DPRINTF(E_DBG, L_REMOTE, "Pairing hash for %s/%s: %s\n", ri->pi.remote_id, ri->pi.name, pairing_hash); /* Prepare request URI */ /* The servicename variable is the mDNS service group name; currently it's * a hash of the library name, but in iTunes the service name and the library * ID (DbId) are different (see comment in main.c). * Remote uses the service name to perform mDNS lookups. */ ret = snprintf(req_uri, sizeof(req_uri), "/pair?pairingcode=%s&servicename=%016" PRIX64, pairing_hash, libhash); free(pairing_hash); if ((ret < 0) || (ret >= sizeof(req_uri))) { DPRINTF(E_WARN, L_REMOTE, "Request URI for pairing exceeds buffer size\n"); goto req_uri_fail; } /* Fire up the request */ if (ri->v6_address) { ret = send_pairing_request(ri, req_uri, AF_INET6); if (ret == 0) return; DPRINTF(E_WARN, L_REMOTE, "Could not send pairing request on IPv6\n"); } ret = send_pairing_request(ri, req_uri, AF_INET); if (ret < 0) { DPRINTF(E_WARN, L_REMOTE, "Could not send pairing request on IPv4\n"); goto pairing_fail; } return; pairing_fail: req_uri_fail: hash_fail: free_remote(ri); } /* Thread: main (pairing) */ static void pairing_cb(int fd, short event, void *arg) { struct remote_info *ri; #ifdef HAVE_EVENTFD eventfd_t count; int ret; ret = eventfd_read(pairing_efd, &count); if (ret < 0) { DPRINTF(E_LOG, L_REMOTE, "Could not read event counter: %s\n", strerror(errno)); return; } #else int dummy; /* Drain the pipe */ while (read(pairing_pipe[0], &dummy, sizeof(dummy)) >= 0) ; /* EMPTY */ #endif CHECK_ERR(L_REMOTE, pthread_mutex_lock(&remote_lck)); if (remote_info && remote_info->paircode && remote_info->pin) { ri = remote_info; unlink_remote(ri); do_pairing(ri); } CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck)); event_add(pairingev, NULL); } /* Thread: main (mdns) */ static void touch_remote_cb(const char *name, const char *type, const char *domain, const char *hostname, int family, const char *address, int port, struct keyval *txt) { const char *p; char *devname; char *paircode; int ret; if (port < 0) { /* If Remote stops advertising itself, the pairing either succeeded or * failed; any subsequent attempt will need a new pairing pin, so * we can just forget everything we know about the remote. */ CHECK_ERR(L_REMOTE, pthread_mutex_lock(&remote_lck)); remove_remote_address_byid(name, family); CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck)); } else { /* Get device name (DvNm field in TXT record) */ p = keyval_get(txt, "DvNm"); if (!p) { DPRINTF(E_LOG, L_REMOTE, "Remote %s: no DvNm in TXT record!\n", name); return; } if (*p == '\0') { DPRINTF(E_LOG, L_REMOTE, "Remote %s: DvNm has no value\n", name); return; } devname = strdup(p); if (!devname) { DPRINTF(E_LOG, L_REMOTE, "Out of memory for device name\n"); return; } /* Get pairing code (Pair field in TXT record) */ p = keyval_get(txt, "Pair"); if (!p) { DPRINTF(E_LOG, L_REMOTE, "Remote %s: no Pair in TXT record!\n", name); free(devname); return; } if (*p == '\0') { DPRINTF(E_LOG, L_REMOTE, "Remote %s: Pair has no value\n", name); free(devname); return; } paircode = strdup(p); if (!paircode) { DPRINTF(E_LOG, L_REMOTE, "Out of memory for paircode\n"); free(devname); return; } DPRINTF(E_LOG, L_REMOTE, "Discovered remote '%s' (id %s) at %s:%d, paircode %s\n", devname, name, address, port, paircode); /* Add the data to the list, adding the remote to the list if needed */ CHECK_ERR(L_REMOTE, pthread_mutex_lock(&remote_lck)); ret = add_remote_mdns_data(name, family, address, port, devname, paircode); if (ret < 0) { DPRINTF(E_WARN, L_REMOTE, "Could not add Remote mDNS data, id %s\n", name); free(devname); free(paircode); } else if (ret == 1) kickoff_pairing(); CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck)); } } /* Thread: filescanner, mpd */ void remote_pairing_kickoff(char **arglist) { int ret; ret = strlen(arglist[0]); if (ret != 4) { DPRINTF(E_LOG, L_REMOTE, "Kickoff pairing failed, first line did not contain a 4-digit pin (got %d)\n", ret); return; } DPRINTF(E_LOG, L_REMOTE, "Kickoff pairing with pin '%s'\n", arglist[0]); CHECK_ERR(L_REMOTE, pthread_mutex_lock(&remote_lck)); ret = add_remote_pin_data(arglist[0]); if (ret == 0) kickoff_pairing(); CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck)); } /* Thread: main */ int remote_pairing_init(void) { int ret; remote_info = NULL; CHECK_ERR(L_REMOTE, mutex_init(&remote_lck)); #ifdef HAVE_EVENTFD pairing_efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); if (pairing_efd < 0) { DPRINTF(E_FATAL, L_REMOTE, "Could not create eventfd: %s\n", strerror(errno)); return -1; } #else # ifdef HAVE_PIPE2 ret = pipe2(pairing_pipe, O_CLOEXEC | O_NONBLOCK); # else if ( pipe(pairing_pipe) < 0 || fcntl(pairing_pipe[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 || fcntl(pairing_pipe[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ) ret = -1; else ret = 0; # endif if (ret < 0) { DPRINTF(E_FATAL, L_REMOTE, "Could not create pairing pipe: %s\n", strerror(errno)); return -1; } #endif /* HAVE_EVENTFD */ // No ipv6 for remote at the moment ret = mdns_browse("_touch-remote._tcp", AF_INET, touch_remote_cb); if (ret < 0) { DPRINTF(E_FATAL, L_REMOTE, "Could not browse for Remote services\n"); goto mdns_browse_fail; } #ifdef HAVE_EVENTFD pairingev = event_new(evbase_main, pairing_efd, EV_READ, pairing_cb, NULL); #else pairingev = event_new(evbase_main, pairing_pipe[0], EV_READ, pairing_cb, NULL); #endif if (!pairingev) { DPRINTF(E_FATAL, L_REMOTE, "Out of memory for pairing event\n"); goto pairingev_fail; } event_add(pairingev, NULL); return 0; pairingev_fail: mdns_browse_fail: #ifdef HAVE_EVENTFD close(pairing_efd); #else close(pairing_pipe[0]); close(pairing_pipe[1]); #endif return -1; } /* Thread: main */ void remote_pairing_deinit(void) { if (remote_info) free_remote(remote_info); #ifdef HAVE_EVENTFD close(pairing_efd); #else close(pairing_pipe[0]); close(pairing_pipe[1]); #endif CHECK_ERR(L_REMOTE, pthread_mutex_destroy(&remote_lck)); } forked-daapd-25.0/src/remote_pairing.h000066400000000000000000000003201313447753700177360ustar00rootroot00000000000000 #ifndef __REMOTE_PAIRING_H__ #define __REMOTE_PAIRING_H__ void remote_pairing_kickoff(char **arglist); int remote_pairing_init(void); void remote_pairing_deinit(void); #endif /* !__REMOTE_PAIRING_H__ */ forked-daapd-25.0/src/rng.c000066400000000000000000000060401313447753700155200ustar00rootroot00000000000000/* * Copyright (C) 2010 Julien BLACHE * * 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 */ #include #include #include #include #include #include #include #include #include "rng.h" /* Park & Miller Minimal Standard PRNG * w/ Bays-Durham shuffle * From Numerical Recipes in C, 2nd ed. */ static int32_t rng_rand_internal(int32_t *seed) { int32_t hi; int32_t lo; int32_t res; hi = *seed / 127773; lo = *seed % 127773; res = 16807 * lo - 2836 * hi; if (res < 0) res += 0x7fffffffL; /* 2147483647 */ *seed = res; return res; } void rng_init(struct rng_ctx *ctx) { int32_t val; int i; gcry_randomize(&ctx->seed, sizeof(ctx->seed), GCRY_STRONG_RANDOM); /* Load the shuffle array - first 8 iterations discarded */ for (i = sizeof(ctx->iv) / sizeof(ctx->iv[0]) + 7; i >= 0; i--) { val = rng_rand_internal(&ctx->seed); if (i < sizeof(ctx->iv) / sizeof(ctx->iv[0])) ctx->iv[i] = val; } ctx->iy = ctx->iv[0]; } int32_t rng_rand(struct rng_ctx *ctx) { int i; /* Select return value */ i = ctx->iy / (1 + (0x7fffffffL - 1) / (sizeof(ctx->iv) / sizeof(ctx->iv[0]))); ctx->iy = ctx->iv[i]; /* Refill */ ctx->iv[i] = rng_rand_internal(&ctx->seed); return ctx->iy; } /* Integer in [min, max[ */ /* Taken from GLib 2.0 v2.25.3, g_rand_int_range(), GPLv2+ */ int32_t rng_rand_range(struct rng_ctx *ctx, int32_t min, int32_t max) { int32_t res; int32_t dist; uint32_t maxvalue; uint32_t leftover; dist = max - min; if (dist <= 0) return min; /* maxvalue is set to the predecessor of the greatest * multiple of dist less or equal 2^32. */ if (dist <= 0x80000000u) /* 2^31 */ { /* maxvalue = 2^32 - 1 - (2^32 % dist) */ leftover = (0x80000000u % dist) * 2; if (leftover >= dist) leftover -= dist; maxvalue = 0xffffffffu - leftover; } else maxvalue = dist - 1; do res = rng_rand(ctx); while (res > maxvalue); res %= dist; return min + res; } /* Fisher-Yates shuffling algorithm * Durstenfeld in-place shuffling variant */ void shuffle_int(struct rng_ctx *ctx, int *values, int len) { int i; int32_t j; int tmp; for (i = len - 1; i > 0; i--) { j = rng_rand_range(ctx, 0, i + 1); tmp = values[i]; values[i] = values[j]; values[j] = tmp; } } forked-daapd-25.0/src/rng.h000066400000000000000000000005541313447753700155310ustar00rootroot00000000000000 #ifndef __RNG_H__ #define __RNG_H__ struct rng_ctx { int32_t iy; int32_t iv[32]; /* shuffle array */ int32_t seed; }; void rng_init(struct rng_ctx *ctx); int32_t rng_rand(struct rng_ctx *ctx); int32_t rng_rand_range(struct rng_ctx *ctx, int32_t min, int32_t max); void shuffle_int(struct rng_ctx *ctx, int *values, int len); #endif /* !__RNG_H__ */ forked-daapd-25.0/src/rsp_query.c000066400000000000000000000075531313447753700167750ustar00rootroot00000000000000/* * Copyright (C) 2009-2011 Julien BLACHE * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include "logger.h" #include "misc.h" #include "rsp_query.h" #include "RSPLexer.h" #include "RSPParser.h" #include "RSP2SQL.h" char * rsp_query_parse_sql(const char *rsp_query) { /* Input RSP query, fed to the lexer */ pANTLR3_INPUT_STREAM query; /* Lexer and the resulting token stream, fed to the parser */ pRSPLexer lxr; pANTLR3_COMMON_TOKEN_STREAM tkstream; /* Parser and the resulting AST, fed to the tree parser */ pRSPParser psr; RSPParser_query_return qtree; pANTLR3_COMMON_TREE_NODE_STREAM nodes; /* Tree parser and the resulting SQL query string */ pRSP2SQL sqlconv; pANTLR3_STRING sql; char *ret = NULL; DPRINTF(E_DBG, L_RSP, "Trying RSP query -%s-\n", rsp_query); #if ANTLR3C_NEW_INPUT query = antlr3StringStreamNew ((pANTLR3_UINT8)rsp_query, ANTLR3_ENC_8BIT, (ANTLR3_UINT64)strlen(rsp_query), (pANTLR3_UINT8)"RSP query"); #else query = antlr3NewAsciiStringInPlaceStream ((pANTLR3_UINT8)rsp_query, (ANTLR3_UINT64)strlen(rsp_query), (pANTLR3_UINT8)"RSP query"); #endif if (!query) { DPRINTF(E_DBG, L_RSP, "Could not create input stream\n"); return NULL; } lxr = RSPLexerNew(query); if (!lxr) { DPRINTF(E_DBG, L_RSP, "Could not create RSP lexer\n"); goto lxr_fail; } tkstream = antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lxr)); if (!tkstream) { DPRINTF(E_DBG, L_RSP, "Could not create RSP token stream\n"); goto tkstream_fail; } psr = RSPParserNew(tkstream); if (!psr) { DPRINTF(E_DBG, L_RSP, "Could not create RSP parser\n"); goto psr_fail; } qtree = psr->query(psr); /* Check for parser errors */ if (psr->pParser->rec->state->errorCount > 0) { DPRINTF(E_LOG, L_RSP, "RSP query parser terminated with %d errors\n", psr->pParser->rec->state->errorCount); goto psr_error; } DPRINTF(E_SPAM, L_RSP, "RSP query AST:\n\t%s\n", qtree.tree->toStringTree(qtree.tree)->chars); nodes = antlr3CommonTreeNodeStreamNewTree(qtree.tree, ANTLR3_SIZE_HINT); if (!nodes) { DPRINTF(E_DBG, L_RSP, "Could not create node stream\n"); goto psr_error; } sqlconv = RSP2SQLNew(nodes); if (!sqlconv) { DPRINTF(E_DBG, L_RSP, "Could not create SQL converter\n"); goto sql_fail; } sql = sqlconv->query(sqlconv); /* Check for tree parser errors */ if (sqlconv->pTreeParser->rec->state->errorCount > 0) { DPRINTF(E_LOG, L_RSP, "RSP query tree parser terminated with %d errors\n", sqlconv->pTreeParser->rec->state->errorCount); goto sql_error; } if (sql) { DPRINTF(E_DBG, L_RSP, "RSP SQL query: -%s-\n", sql->chars); ret = strdup((char *)sql->chars); } else { DPRINTF(E_LOG, L_RSP, "Invalid RSP query\n"); ret = NULL; } sql_error: sqlconv->free(sqlconv); sql_fail: nodes->free(nodes); psr_error: psr->free(psr); psr_fail: tkstream->free(tkstream); tkstream_fail: lxr->free(lxr); lxr_fail: query->close(query); return ret; } forked-daapd-25.0/src/rsp_query.gperf000066400000000000000000000034421313447753700176470ustar00rootroot00000000000000%language=ANSI-C %readonly-tables %enum %switch=1 %compare-lengths %define hash-function-name rsp_query_field_hash %define lookup-function-name rsp_query_field_lookup %define slot-name rsp_field %struct-type %omit-struct-type struct rsp_query_field_map; %% "id", RSP_TYPE_INT "path", RSP_TYPE_STRING "fname", RSP_TYPE_STRING "title", RSP_TYPE_STRING "artist", RSP_TYPE_STRING "album", RSP_TYPE_STRING "genre", RSP_TYPE_STRING "comment", RSP_TYPE_STRING "type", RSP_TYPE_STRING "composer", RSP_TYPE_STRING "orchestra", RSP_TYPE_STRING "grouping", RSP_TYPE_STRING "url", RSP_TYPE_STRING "bitrate", RSP_TYPE_INT "samplerate", RSP_TYPE_INT "song_length", RSP_TYPE_INT "file_size", RSP_TYPE_INT "year", RSP_TYPE_INT "track", RSP_TYPE_INT "total_tracks", RSP_TYPE_INT "disc", RSP_TYPE_INT "total_discs", RSP_TYPE_INT "bpm", RSP_TYPE_INT "compilation", RSP_TYPE_INT "rating", RSP_TYPE_INT "play_count", RSP_TYPE_INT "data_kind", RSP_TYPE_INT "item_kind", RSP_TYPE_INT "description", RSP_TYPE_STRING "time_added", RSP_TYPE_DATE "time_modified", RSP_TYPE_DATE "time_played", RSP_TYPE_DATE "db_timestamp", RSP_TYPE_DATE "sample_count", RSP_TYPE_INT "codectype", RSP_TYPE_STRING "idx", RSP_TYPE_INT "has_video", RSP_TYPE_INT "contentrating", RSP_TYPE_INT "bits_per_sample", RSP_TYPE_INT "album_artist", RSP_TYPE_STRING forked-daapd-25.0/src/rsp_query.h000066400000000000000000000002051313447753700167650ustar00rootroot00000000000000 #ifndef __RSP_QUERY_H__ #define __RSP_QUERY_H__ char * rsp_query_parse_sql(const char *rsp_query); #endif /* !__RSP_QUERY_H__ */ forked-daapd-25.0/src/spotify.c000066400000000000000000002164041313447753700164360ustar00rootroot00000000000000/* * Copyright (C) 2016 Espen Jürgensen * * Stiched together from libspotify examples * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include #include #include #include "spotify.h" #include "spotify_webapi.h" #include "logger.h" #include "misc.h" #include "http.h" #include "conffile.h" #include "cache.h" #include "commands.h" #include "library.h" #include "input.h" /* TODO for the web api: * - UI should be prettier * - map "added_at" to time_added * - what to do about the lack of push? * - use the web api more, implement proper init */ /* A few words on our reloading sequence of saved tracks * * 1. libspotify will not tell us about the user's saved tracks when loading * so we keep track of them with the special playlist spotify:savedtracks. * 2. spotify_login will copy all paths in spotify:savedtracks to a temporary * spotify_reload_list before all Spotify items in the database get purged. * 3. when the connection to Spotify is established after login, we register * all the paths with libspotify, and we also add them back to the * spotify:savedtracks playlist - however, that's just for the * playlistsitems table. Adding the items to the files table is done when * libspotify calls back with metadata - see spotify_pending_process(). * 4. if the user reloads saved tracks, we first clear all items in the * playlist, then add those back that are returned from the web api, and * then use our normal cleanup of stray files to tidy db and cache. */ // How long to wait for artwork (in sec) before giving up #define SPOTIFY_ARTWORK_TIMEOUT 3 // An upper limit on sequential requests to Spotify's web api // - each request will return 50 objects (tracks) #define SPOTIFY_WEB_REQUESTS_MAX 20 /* --- Types --- */ enum spotify_state { SPOTIFY_STATE_INACTIVE, SPOTIFY_STATE_WAIT, SPOTIFY_STATE_PLAYING, SPOTIFY_STATE_PAUSED, SPOTIFY_STATE_STOPPING, SPOTIFY_STATE_STOPPED, }; struct artwork_get_param { struct evbuffer *evbuf; char *path; int max_w; int max_h; sp_image *image; pthread_mutex_t mutex; pthread_cond_t cond; int is_loaded; }; /* --- Globals --- */ // Spotify thread static pthread_t tid_spotify; // Used to make sure no login is attempted before the logout cb from Spotify static pthread_mutex_t login_lck; static pthread_cond_t login_cond; // Event base, pipes and events struct event_base *evbase_spotify; static int g_notify_pipe[2]; static struct event *g_notifyev; static struct commands_base *cmdbase; // The session handle static sp_session *g_sess; // The library handle static void *g_libhandle; // The state telling us what the thread is currently doing static enum spotify_state g_state; // The base playlist id for all Spotify playlists in the db static int spotify_base_plid; // Flag telling us if access to the web api was granted static bool spotify_access_token_valid; // The base playlist id for Spotify saved tracks in the db static int spotify_saved_plid; // Flag to avoid triggering playlist change events while the (re)scan is running static bool scanning; // Timeout timespec static struct timespec spotify_artwork_timeout = { SPOTIFY_ARTWORK_TIMEOUT, 0 }; // Audio buffer static struct evbuffer *spotify_audio_buffer; /** * The application key is specific to forked-daapd, and allows Spotify * to produce statistics on how their service is used. */ const uint8_t g_appkey[] = { 0x01, 0xC6, 0x9D, 0x18, 0xA4, 0xF7, 0x79, 0x12, 0x43, 0x55, 0x0F, 0xAD, 0xBF, 0x23, 0x23, 0x10, 0x2E, 0x51, 0x46, 0x8F, 0x06, 0x3D, 0xEE, 0xC3, 0xF0, 0x2A, 0x5D, 0x8E, 0x72, 0x35, 0xD1, 0x21, 0x44, 0xE3, 0x19, 0x80, 0xED, 0xD5, 0xAD, 0xE6, 0xE1, 0xDD, 0xBE, 0xCB, 0xA9, 0x84, 0xBD, 0xC2, 0xAF, 0xB1, 0xF2, 0xD5, 0x87, 0xFC, 0x35, 0xD6, 0x1C, 0x5F, 0x5B, 0x76, 0x38, 0x1D, 0x6E, 0x49, 0x6D, 0x85, 0x15, 0xCD, 0x38, 0x14, 0xD6, 0xB8, 0xFE, 0x05, 0x0A, 0xAC, 0x9B, 0x31, 0xD1, 0xC0, 0xAF, 0x16, 0x78, 0x48, 0x49, 0x27, 0x41, 0xCA, 0xAF, 0x07, 0xEC, 0x10, 0x5D, 0x19, 0x43, 0x2E, 0x84, 0xEB, 0x43, 0x5D, 0x4B, 0xBF, 0xD0, 0x5C, 0xDF, 0x3D, 0x12, 0x6D, 0x1C, 0x76, 0x4E, 0x9F, 0xBF, 0x14, 0xC9, 0x46, 0x95, 0x99, 0x32, 0x6A, 0xC2, 0xF1, 0x89, 0xA4, 0xB3, 0xF3, 0xA0, 0xEB, 0xDA, 0x84, 0x67, 0x27, 0x07, 0x1F, 0xF6, 0x19, 0xAC, 0xF1, 0xB8, 0xB6, 0xCF, 0xAB, 0xF8, 0x0A, 0xEE, 0x4D, 0xAC, 0xC2, 0x39, 0x63, 0x50, 0x13, 0x7B, 0x51, 0x3A, 0x50, 0xE0, 0x03, 0x6E, 0xB7, 0x17, 0xEE, 0x58, 0xCE, 0xF8, 0x15, 0x3C, 0x70, 0xDE, 0xE6, 0xEB, 0xE6, 0xD4, 0x2C, 0x27, 0xB9, 0xCA, 0x15, 0xCE, 0x2E, 0x31, 0x54, 0xF5, 0x0A, 0x98, 0x8D, 0x78, 0xE5, 0xB6, 0xF8, 0xE4, 0x62, 0x43, 0xAA, 0x37, 0x93, 0xFF, 0xE3, 0xAB, 0x17, 0xC5, 0x81, 0x4F, 0xFD, 0xF1, 0x84, 0xE1, 0x8A, 0x99, 0xB0, 0x1D, 0x85, 0x80, 0xA2, 0x49, 0x35, 0x8D, 0xDD, 0xBC, 0x74, 0x0B, 0xBA, 0x33, 0x5B, 0xD5, 0x7A, 0xB9, 0x2F, 0x9B, 0x24, 0xA5, 0xAB, 0xF6, 0x1E, 0xE3, 0xA3, 0xA8, 0x0D, 0x1E, 0x48, 0xF7, 0xDB, 0xE2, 0x54, 0x65, 0x43, 0xA6, 0xD3, 0x3F, 0x2C, 0x9B, 0x13, 0x9A, 0xBE, 0x0F, 0x4D, 0x51, 0xC3, 0x73, 0xA5, 0xFE, 0xFC, 0x93, 0x12, 0xEF, 0x9C, 0x4D, 0x68, 0xE3, 0xDA, 0x52, 0x67, 0x28, 0x41, 0x17, 0x22, 0x3E, 0x33, 0xB0, 0x3A, 0xFB, 0x44, 0xB0, 0x2E, 0xA6, 0xD2, 0x95, 0xC0, 0x9A, 0xBA, 0x32, 0xA3, 0xC5, 0xFE, 0x86, 0x5D, 0xC8, 0xBB, 0xB5, 0xDE, 0x92, 0x8C, 0x7D, 0xE4, 0x03, 0xD4, 0xF9, 0xAE, 0x41, 0xE3, 0xBD, 0x35, 0x4B, 0x94, 0x27, 0xE0, 0x12, 0x21, 0x46, 0xE9, 0x09, }; // This section defines and assigns function pointers to the libspotify functions // The arguments and return values must be in sync with the spotify api // Please scroll through the ugliness which follows typedef const char* (*fptr_sp_error_message_t)(sp_error error); typedef sp_error (*fptr_sp_session_create_t)(const sp_session_config *config, sp_session **sess); typedef sp_error (*fptr_sp_session_release_t)(sp_session *sess); typedef sp_error (*fptr_sp_session_login_t)(sp_session *session, const char *username, const char *password, bool remember_me, const char *blob); typedef sp_error (*fptr_sp_session_relogin_t)(sp_session *session); typedef sp_error (*fptr_sp_session_logout_t)(sp_session *session); typedef sp_error (*fptr_sp_session_process_events_t)(sp_session *session, int *next_timeout); typedef sp_playlist* (*fptr_sp_session_starred_create_t)(sp_session *session); typedef sp_playlistcontainer* (*fptr_sp_session_playlistcontainer_t)(sp_session *session); typedef sp_error (*fptr_sp_session_player_load_t)(sp_session *session, sp_track *track); typedef sp_error (*fptr_sp_session_player_unload_t)(sp_session *session); typedef sp_error (*fptr_sp_session_player_play_t)(sp_session *session, bool play); typedef sp_error (*fptr_sp_session_player_seek_t)(sp_session *session, int offset); typedef sp_connectionstate (*fptr_sp_session_connectionstate_t)(sp_session *session); typedef sp_error (*fptr_sp_session_preferred_bitrate_t)(sp_session *session, sp_bitrate bitrate); typedef sp_error (*fptr_sp_playlistcontainer_add_callbacks_t)(sp_playlistcontainer *pc, sp_playlistcontainer_callbacks *callbacks, void *userdata); typedef int (*fptr_sp_playlistcontainer_num_playlists_t)(sp_playlistcontainer *pc); typedef sp_playlist* (*fptr_sp_playlistcontainer_playlist_t)(sp_playlistcontainer *pc, int index); typedef sp_error (*fptr_sp_playlist_add_callbacks_t)(sp_playlist *playlist, sp_playlist_callbacks *callbacks, void *userdata); typedef const char* (*fptr_sp_playlist_name_t)(sp_playlist *playlist); typedef sp_error (*fptr_sp_playlist_remove_callbacks_t)(sp_playlist *playlist, sp_playlist_callbacks *callbacks, void *userdata); typedef int (*fptr_sp_playlist_num_tracks_t)(sp_playlist *playlist); typedef sp_track* (*fptr_sp_playlist_track_t)(sp_playlist *playlist, int index); typedef bool (*fptr_sp_playlist_is_loaded_t)(sp_playlist *playlist); typedef int (*fptr_sp_playlist_track_create_time_t)(sp_playlist *playlist, int index); typedef sp_user* (*fptr_sp_playlist_owner_t)(sp_playlist *playlist); typedef sp_error (*fptr_sp_track_error_t)(sp_track *track); typedef bool (*fptr_sp_track_is_loaded_t)(sp_track *track); typedef const char* (*fptr_sp_track_name_t)(sp_track *track); typedef int (*fptr_sp_track_duration_t)(sp_track *track); typedef int (*fptr_sp_track_index_t)(sp_track *track); typedef int (*fptr_sp_track_disc_t)(sp_track *track); typedef sp_album* (*fptr_sp_track_album_t)(sp_track *track); typedef sp_track_availability (*fptr_sp_track_get_availability_t)(sp_session *session, sp_track *track); typedef bool (*fptr_sp_track_is_starred_t)(sp_session *session, sp_track *track); typedef sp_link* (*fptr_sp_link_create_from_playlist_t)(sp_playlist *playlist); typedef sp_link* (*fptr_sp_link_create_from_track_t)(sp_track *track, int offset); typedef sp_link* (*fptr_sp_link_create_from_string_t)(const char *link); typedef int (*fptr_sp_link_as_string_t)(sp_link *link, char *buffer, int buffer_size); typedef sp_track* (*fptr_sp_link_as_track_t)(sp_link *link); typedef sp_error (*fptr_sp_link_release_t)(sp_link *link); typedef const char* (*fptr_sp_album_name_t)(sp_album *album); typedef sp_artist* (*fptr_sp_album_artist_t)(sp_album *album); typedef int (*fptr_sp_album_year_t)(sp_album *album); typedef sp_albumtype (*fptr_sp_album_type_t)(sp_album *album); typedef const byte* (*fptr_sp_album_cover_t)(sp_album *album, sp_image_size size); typedef const char* (*fptr_sp_artist_name_t)(sp_artist *artist); typedef sp_image* (*fptr_sp_image_create_t)(sp_session *session, const byte image_id[20]); typedef bool (*fptr_sp_image_is_loaded_t)(sp_image *image); typedef sp_error (*fptr_sp_image_error_t)(sp_image *image); typedef sp_imageformat (*fptr_sp_image_format_t)(sp_image *image); typedef const void* (*fptr_sp_image_data_t)(sp_image *image, size_t *data_size); typedef sp_error (*fptr_sp_image_release_t)(sp_image *image); typedef sp_error (*fptr_sp_image_add_load_callback_t)(sp_image *image, image_loaded_cb *callback, void *userdata); typedef sp_error (*fptr_sp_image_remove_load_callback_t)(sp_image *image, image_loaded_cb *callback, void *userdata); typedef const char* (*fptr_sp_user_display_name_t)(sp_user *user); typedef const char* (*fptr_sp_user_canonical_name_t)(sp_user *user); /* Define actual function pointers */ fptr_sp_error_message_t fptr_sp_error_message; fptr_sp_session_create_t fptr_sp_session_create; fptr_sp_session_release_t fptr_sp_session_release; fptr_sp_session_login_t fptr_sp_session_login; fptr_sp_session_relogin_t fptr_sp_session_relogin; fptr_sp_session_logout_t fptr_sp_session_logout; fptr_sp_session_starred_create_t fptr_sp_session_starred_create; fptr_sp_session_playlistcontainer_t fptr_sp_session_playlistcontainer; fptr_sp_session_process_events_t fptr_sp_session_process_events; fptr_sp_session_player_load_t fptr_sp_session_player_load; fptr_sp_session_player_unload_t fptr_sp_session_player_unload; fptr_sp_session_player_play_t fptr_sp_session_player_play; fptr_sp_session_player_seek_t fptr_sp_session_player_seek; fptr_sp_session_connectionstate_t fptr_sp_session_connectionstate; fptr_sp_session_preferred_bitrate_t fptr_sp_session_preferred_bitrate; fptr_sp_playlistcontainer_add_callbacks_t fptr_sp_playlistcontainer_add_callbacks; fptr_sp_playlistcontainer_num_playlists_t fptr_sp_playlistcontainer_num_playlists; fptr_sp_playlistcontainer_playlist_t fptr_sp_playlistcontainer_playlist; fptr_sp_playlist_add_callbacks_t fptr_sp_playlist_add_callbacks; fptr_sp_playlist_name_t fptr_sp_playlist_name; fptr_sp_playlist_remove_callbacks_t fptr_sp_playlist_remove_callbacks; fptr_sp_playlist_num_tracks_t fptr_sp_playlist_num_tracks; fptr_sp_playlist_track_t fptr_sp_playlist_track; fptr_sp_playlist_is_loaded_t fptr_sp_playlist_is_loaded; fptr_sp_playlist_track_create_time_t fptr_sp_playlist_track_create_time; fptr_sp_playlist_owner_t fptr_sp_playlist_owner; fptr_sp_track_error_t fptr_sp_track_error; fptr_sp_track_is_loaded_t fptr_sp_track_is_loaded; fptr_sp_track_name_t fptr_sp_track_name; fptr_sp_track_duration_t fptr_sp_track_duration; fptr_sp_track_index_t fptr_sp_track_index; fptr_sp_track_disc_t fptr_sp_track_disc; fptr_sp_track_album_t fptr_sp_track_album; fptr_sp_track_get_availability_t fptr_sp_track_get_availability; fptr_sp_track_is_starred_t fptr_sp_track_is_starred; fptr_sp_link_create_from_playlist_t fptr_sp_link_create_from_playlist; fptr_sp_link_create_from_track_t fptr_sp_link_create_from_track; fptr_sp_link_create_from_string_t fptr_sp_link_create_from_string; fptr_sp_link_as_string_t fptr_sp_link_as_string; fptr_sp_link_as_track_t fptr_sp_link_as_track; fptr_sp_link_release_t fptr_sp_link_release; fptr_sp_album_name_t fptr_sp_album_name; fptr_sp_album_artist_t fptr_sp_album_artist; fptr_sp_album_year_t fptr_sp_album_year; fptr_sp_album_type_t fptr_sp_album_type; fptr_sp_album_cover_t fptr_sp_album_cover; fptr_sp_artist_name_t fptr_sp_artist_name; fptr_sp_image_create_t fptr_sp_image_create; fptr_sp_image_is_loaded_t fptr_sp_image_is_loaded; fptr_sp_image_error_t fptr_sp_image_error; fptr_sp_image_format_t fptr_sp_image_format; fptr_sp_image_data_t fptr_sp_image_data; fptr_sp_image_release_t fptr_sp_image_release; fptr_sp_image_add_load_callback_t fptr_sp_image_add_load_callback; fptr_sp_image_remove_load_callback_t fptr_sp_image_remove_load_callback; fptr_sp_user_display_name_t fptr_sp_user_display_name; fptr_sp_user_canonical_name_t fptr_sp_user_canonical_name; /* Assign function pointers to libspotify symbol */ static int fptr_assign_all() { void *h; char *err; int ret; h = g_libhandle; // The following is non-ISO compliant ret = (fptr_sp_error_message = dlsym(h, "sp_error_message")) && (fptr_sp_session_create = dlsym(h, "sp_session_create")) && (fptr_sp_session_release = dlsym(h, "sp_session_release")) && (fptr_sp_session_login = dlsym(h, "sp_session_login")) && (fptr_sp_session_relogin = dlsym(h, "sp_session_relogin")) && (fptr_sp_session_logout = dlsym(h, "sp_session_logout")) && (fptr_sp_session_playlistcontainer = dlsym(h, "sp_session_playlistcontainer")) && (fptr_sp_session_process_events = dlsym(h, "sp_session_process_events")) && (fptr_sp_session_player_load = dlsym(h, "sp_session_player_load")) && (fptr_sp_session_player_unload = dlsym(h, "sp_session_player_unload")) && (fptr_sp_session_player_play = dlsym(h, "sp_session_player_play")) && (fptr_sp_session_player_seek = dlsym(h, "sp_session_player_seek")) && (fptr_sp_session_connectionstate = dlsym(h, "sp_session_connectionstate")) && (fptr_sp_session_preferred_bitrate = dlsym(h, "sp_session_preferred_bitrate")) && (fptr_sp_playlistcontainer_add_callbacks = dlsym(h, "sp_playlistcontainer_add_callbacks")) && (fptr_sp_playlistcontainer_num_playlists = dlsym(h, "sp_playlistcontainer_num_playlists")) && (fptr_sp_session_starred_create = dlsym(h, "sp_session_starred_create")) && (fptr_sp_playlistcontainer_playlist = dlsym(h, "sp_playlistcontainer_playlist")) && (fptr_sp_playlist_add_callbacks = dlsym(h, "sp_playlist_add_callbacks")) && (fptr_sp_playlist_name = dlsym(h, "sp_playlist_name")) && (fptr_sp_playlist_remove_callbacks = dlsym(h, "sp_playlist_remove_callbacks")) && (fptr_sp_playlist_num_tracks = dlsym(h, "sp_playlist_num_tracks")) && (fptr_sp_playlist_track = dlsym(h, "sp_playlist_track")) && (fptr_sp_playlist_is_loaded = dlsym(h, "sp_playlist_is_loaded")) && (fptr_sp_playlist_track_create_time = dlsym(h, "sp_playlist_track_create_time")) && (fptr_sp_playlist_owner = dlsym(h, "sp_playlist_owner")) && (fptr_sp_track_error = dlsym(h, "sp_track_error")) && (fptr_sp_track_is_loaded = dlsym(h, "sp_track_is_loaded")) && (fptr_sp_track_name = dlsym(h, "sp_track_name")) && (fptr_sp_track_duration = dlsym(h, "sp_track_duration")) && (fptr_sp_track_index = dlsym(h, "sp_track_index")) && (fptr_sp_track_disc = dlsym(h, "sp_track_disc")) && (fptr_sp_track_album = dlsym(h, "sp_track_album")) && (fptr_sp_track_get_availability = dlsym(h, "sp_track_get_availability")) && (fptr_sp_track_is_starred = dlsym(h, "sp_track_is_starred")) && (fptr_sp_link_create_from_playlist = dlsym(h, "sp_link_create_from_playlist")) && (fptr_sp_link_create_from_track = dlsym(h, "sp_link_create_from_track")) && (fptr_sp_link_create_from_string = dlsym(h, "sp_link_create_from_string")) && (fptr_sp_link_as_string = dlsym(h, "sp_link_as_string")) && (fptr_sp_link_as_track = dlsym(h, "sp_link_as_track")) && (fptr_sp_link_release = dlsym(h, "sp_link_release")) && (fptr_sp_album_name = dlsym(h, "sp_album_name")) && (fptr_sp_album_artist = dlsym(h, "sp_album_artist")) && (fptr_sp_album_year = dlsym(h, "sp_album_year")) && (fptr_sp_album_type = dlsym(h, "sp_album_type")) && (fptr_sp_album_cover = dlsym(h, "sp_album_cover")) && (fptr_sp_artist_name = dlsym(h, "sp_artist_name")) && (fptr_sp_image_create = dlsym(h, "sp_image_create")) && (fptr_sp_image_is_loaded = dlsym(h, "sp_image_is_loaded")) && (fptr_sp_image_error = dlsym(h, "sp_image_error")) && (fptr_sp_image_format = dlsym(h, "sp_image_format")) && (fptr_sp_image_data = dlsym(h, "sp_image_data")) && (fptr_sp_image_release = dlsym(h, "sp_image_release")) && (fptr_sp_image_add_load_callback = dlsym(h, "sp_image_add_load_callback")) && (fptr_sp_image_remove_load_callback = dlsym(h, "sp_image_remove_load_callback")) && (fptr_sp_user_display_name = dlsym(h, "sp_user_display_name")) && (fptr_sp_user_canonical_name = dlsym(h, "sp_user_canonical_name")) ; err = dlerror(); if (ret && !err) return ret; else if (err) DPRINTF(E_LOG, L_SPOTIFY, "Assignment error (%d): %s\n", ret, err); else DPRINTF(E_LOG, L_SPOTIFY, "Unknown assignment error (%d)\n", ret); return -1; } // End of ugly part static enum command_state webapi_scan(void *arg, int *ret); static enum command_state webapi_pl_save(void *arg, int *ret); static enum command_state webapi_pl_remove(void *arg, int *ret); static void create_base_playlist(); /* -------------------------- PLAYLIST HELPERS ------------------------- */ /* Should only be called from within the spotify thread */ static int spotify_metadata_get(sp_track *track, struct media_file_info *mfi, const char *pltitle, int time_added) { cfg_t *spotify_cfg; bool artist_override; bool album_override; sp_album *album; sp_artist *artist; sp_albumtype albumtype; bool starred; int compilation; char *albumname; spotify_cfg = cfg_getsec(cfg, "spotify"); artist_override = cfg_getbool(spotify_cfg, "artist_override"); album_override = cfg_getbool(spotify_cfg, "album_override"); album = fptr_sp_track_album(track); if (!album) return -1; artist = fptr_sp_album_artist(album); if (!artist) return -1; albumtype = fptr_sp_album_type(album); starred = fptr_sp_track_is_starred(g_sess, track); /* * Treat album as compilation if one of the following conditions is true: * - spotfy album type is compilation * - artist_override in config is set to true and track is not part of the starred playlist * - starred_artist_override in config is set to true and track is part of the starred playlist */ compilation = ((albumtype == SP_ALBUMTYPE_COMPILATION) || artist_override); if (album_override && pltitle) albumname = strdup(pltitle); else albumname = strdup(fptr_sp_album_name(album)); mfi->title = strdup(fptr_sp_track_name(track)); mfi->album = albumname; mfi->artist = strdup(fptr_sp_artist_name(artist)); mfi->year = fptr_sp_album_year(album); mfi->song_length = fptr_sp_track_duration(track); mfi->track = fptr_sp_track_index(track); mfi->disc = fptr_sp_track_disc(track); mfi->compilation = compilation; mfi->artwork = ARTWORK_SPOTIFY; mfi->type = strdup("spotify"); mfi->codectype = strdup("wav"); mfi->description = strdup("Spotify audio"); mfi->time_added = time_added; DPRINTF(E_SPAM, L_SPOTIFY, "Metadata for track:\n" "Title: %s\n" "Album: %s\n" "Artist: %s\n" "Year: %u\n" "Track: %u\n" "Disc: %u\n" "Compilation: %d\n" "Starred: %d\n", mfi->title, mfi->album, mfi->artist, mfi->year, mfi->track, mfi->disc, mfi->compilation, starred); return 0; } /* * Returns the directory id for /spotify://, if the directory (or the parent * directories) does not yet exist, they will be created. * If an error occured the return value is -1. * * @return directory id for the given artist/album directory */ static int prepare_directories(const char *artist, const char *album) { int dir_id; char virtual_path[PATH_MAX]; int ret; ret = snprintf(virtual_path, sizeof(virtual_path), "/spotify:/%s", artist); if ((ret < 0) || (ret >= sizeof(virtual_path))) { DPRINTF(E_LOG, L_SPOTIFY, "Virtual path exceeds PATH_MAX (/spotify:/%s)\n", artist); return -1; } dir_id = db_directory_addorupdate(virtual_path, 0, DIR_SPOTIFY); if (dir_id <= 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory '%s'\n", virtual_path); return -1; } ret = snprintf(virtual_path, sizeof(virtual_path), "/spotify:/%s/%s", artist, album); if ((ret < 0) || (ret >= sizeof(virtual_path))) { DPRINTF(E_LOG, L_SPOTIFY, "Virtual path exceeds PATH_MAX (/spotify:/%s/%s)\n", artist, album); return -1; } dir_id = db_directory_addorupdate(virtual_path, 0, dir_id); if (dir_id <= 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory '%s'\n", virtual_path); return -1; } return dir_id; } static int spotify_track_save(int plid, sp_track *track, const char *pltitle, int time_added) { struct media_file_info mfi; sp_link *link; char url[1024]; int ret; char virtual_path[PATH_MAX]; int dir_id; time_t stamp; int id; memset(&mfi, 0, sizeof(struct media_file_info)); if (!fptr_sp_track_is_loaded(track)) { DPRINTF(E_LOG, L_SPOTIFY, "Track appears to no longer have the proper status\n"); return -1; } if (fptr_sp_track_get_availability(g_sess, track) != SP_TRACK_AVAILABILITY_AVAILABLE) { DPRINTF(E_LOG, L_SPOTIFY, "Track not available for playback: '%s'\n", fptr_sp_track_name(track)); return 0; } link = fptr_sp_link_create_from_track(track, 0); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for track: '%s'\n", fptr_sp_track_name(track)); return -1; } ret = fptr_sp_link_as_string(link, url, sizeof(url)); if (ret == sizeof(url)) { DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: '%s'\n", url); } fptr_sp_link_release(link); /* Add to playlistitems table */ if (plid) { ret = db_pl_add_item_bypath(plid, url); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not save playlist item: '%s'\n", url); goto fail; } } ret = spotify_metadata_get(track, &mfi, pltitle, time_added); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Metadata missing (but track should be loaded?): '%s'\n", fptr_sp_track_name(track)); goto fail; } dir_id = prepare_directories(mfi.artist, mfi.album); if (dir_id <= 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory for item: '%s'\n", url); goto fail; } // DPRINTF(E_DBG, L_SPOTIFY, "Saving track '%s': '%s' by %s (%s)\n", url, mfi.title, mfi.artist, mfi.album); db_file_stamp_bypath(url, &stamp, &id); mfi.id = id; mfi.path = strdup(url); mfi.fname = strdup(url); mfi.time_modified = time(NULL); mfi.data_kind = DATA_KIND_SPOTIFY; snprintf(virtual_path, PATH_MAX, "/spotify:/%s/%s/%s", mfi.album_artist, mfi.album, mfi.title); mfi.virtual_path = strdup(virtual_path); mfi.directory_id = dir_id; library_add_media(&mfi); free_mfi(&mfi, 1); return 0; fail: free_mfi(&mfi, 1); return -1; } static int spotify_cleanup_files(void) { struct query_params qp; char *path; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_BROWSE_PATH; qp.sort = S_NONE; qp.filter = "f.path LIKE 'spotify:%%' AND NOT f.path IN (SELECT filepath FROM playlistitems)"; ret = db_query_start(&qp); if (ret < 0) { db_query_end(&qp); return -1; } while (((ret = db_query_fetch_string(&qp, &path)) == 0) && (path)) { cache_artwork_delete_by_path(path); } db_query_end(&qp); db_spotify_files_delete(); return 0; } static int spotify_playlist_save(sp_playlist *pl) { struct playlist_info *pli; sp_track *track; sp_link *link; sp_user *owner; char url[1024]; const char *name; const char *ownername; int plid; int num_tracks; char virtual_path[PATH_MAX]; int created; int ret; int i; if (!fptr_sp_playlist_is_loaded(pl)) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist still not loaded - will wait for next callback\n"); return 0; } name = fptr_sp_playlist_name(pl); num_tracks = fptr_sp_playlist_num_tracks(pl); // The starred playlist has an empty name, set it manually to "Starred" if (*name == '\0') name = "Starred"; for (i = 0; i < num_tracks; i++) { track = fptr_sp_playlist_track(pl, i); if (track && !fptr_sp_track_is_loaded(track)) { DPRINTF(E_DBG, L_SPOTIFY, "All playlist tracks not loaded (will wait for next callback): %s\n", name); return 0; } } DPRINTF(E_LOG, L_SPOTIFY, "Saving playlist (%d tracks): '%s'\n", num_tracks, name); // Save playlist (playlists table) link = fptr_sp_link_create_from_playlist(pl); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for playlist (wait): '%s'\n", name); return -1; } ret = fptr_sp_link_as_string(link, url, sizeof(url)); if (ret == sizeof(url)) { DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: %s\n", url); } fptr_sp_link_release(link); owner = fptr_sp_playlist_owner(pl); if (owner) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist '%s' owner: '%s' (canonical) / '%s' (display)\n", name, fptr_sp_user_canonical_name(owner), fptr_sp_user_display_name(owner)); ownername = fptr_sp_user_canonical_name(owner); snprintf(virtual_path, PATH_MAX, "/spotify:/%s (%s)", name, ownername); } else { snprintf(virtual_path, PATH_MAX, "/spotify:/%s", name); } pli = db_pl_fetch_bypath(url); if (pli) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist found ('%s', link %s), updating\n", name, url); plid = pli->id; free(pli->title); pli->title = strdup(name); free(pli->virtual_path); pli->virtual_path = strdup(virtual_path); pli->directory_id = DIR_SPOTIFY; ret = db_pl_update(pli); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error updating playlist ('%s', link %s)\n", name, url); free_pli(pli, 0); return -1; } db_pl_clear_items(plid); } else { DPRINTF(E_DBG, L_SPOTIFY, "Adding playlist ('%s', link %s)\n", name, url); pli = (struct playlist_info *)malloc(sizeof(struct playlist_info)); if (!pli) { DPRINTF(E_LOG, L_SCAN, "Out of memory\n"); return -1; } memset(pli, 0, sizeof(struct playlist_info)); pli->type = PL_PLAIN; pli->title = strdup(name); pli->path = strdup(url); pli->virtual_path = strdup(virtual_path); pli->parent_id = spotify_base_plid; pli->directory_id = DIR_SPOTIFY; ret = db_pl_add(pli, &plid); if ((ret < 0) || (plid < 1)) { DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist ('%s', link %s, ret %d, plid %d)\n", name, url, ret, plid); free_pli(pli, 0); return -1; } } free_pli(pli, 0); // Save tracks and playlistitems (files and playlistitems table) db_transaction_begin(); for (i = 0; i < num_tracks; i++) { track = fptr_sp_playlist_track(pl, i); if (!track) { DPRINTF(E_LOG, L_SPOTIFY, "Track %d in playlist '%s' (id %d) is invalid\n", i, name, plid); continue; } created = fptr_sp_playlist_track_create_time(pl, i); ret = spotify_track_save(plid, track, name, created); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error saving track %d to playlist '%s' (id %d)\n", i, name, plid); continue; } } spotify_cleanup_files(); db_transaction_end(); return plid; } // Registers a track with libspotify, which will make it start loading the track // metadata. When that is done metadata_updated() is called (but we won't be // told which track it was...). Note that this function will result in a ref // count on the sp_link, which the caller must decrease with sp_link_release. static enum command_state uri_register(void *arg, int *retval) { sp_link *link; sp_track *track; char *uri = arg; if (SP_CONNECTION_STATE_LOGGED_IN != fptr_sp_session_connectionstate(g_sess)) { DPRINTF(E_LOG, L_SPOTIFY, "Can't register music, not connected and logged in to Spotify\n"); *retval = -1; return COMMAND_END; } link = fptr_sp_link_create_from_string(uri); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify link: '%s'\n", uri); *retval = -1; return COMMAND_END; } track = fptr_sp_link_as_track(link); if (!track) { DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify track: '%s'\n", uri); *retval = -1; return COMMAND_END; } *retval = 0; return COMMAND_END; } static void webapi_playlist_updated(sp_playlist *pl) { sp_link *link; char url[1024]; int ret; if (!scanning) { // Run playlist save in the library thread link = fptr_sp_link_create_from_playlist(pl); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for playlist: '%s'\n", fptr_sp_playlist_name(pl)); return; } ret = fptr_sp_link_as_string(link, url, sizeof(url)); if (ret == sizeof(url)) { DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: %s\n", url); } fptr_sp_link_release(link); library_exec_async(webapi_pl_save, strdup(url)); } } /* -------------------------- PLAYLIST CALLBACKS ------------------------- */ /** * Called when a playlist is updating or is done updating * * This is called before and after a series of changes are applied to the * playlist. It allows e.g. the user interface to defer updating until the * entire operation is complete. * * @param[in] pl Playlist object * @param[in] done True iff the update is completed * @param[in] userdata Userdata passed to sp_playlist_add_callbacks() */ static void playlist_update_in_progress(sp_playlist *pl, bool done, void *userdata) { if (done) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist update (status %d): %s\n", done, fptr_sp_playlist_name(pl)); if (spotify_access_token_valid) { webapi_playlist_updated(pl); } else { spotify_playlist_save(pl); } } } static void playlist_metadata_updated(sp_playlist *pl, void *userdata) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist metadata updated: %s\n", fptr_sp_playlist_name(pl)); if (spotify_access_token_valid) { //TODO Update disabled to prevent multiple triggering of updates e. g. on adding a playlist //webapi_playlist_updated(pl); } else { spotify_playlist_save(pl); } } /** * The callbacks we are interested in for individual playlists. */ static sp_playlist_callbacks pl_callbacks = { .playlist_update_in_progress = &playlist_update_in_progress, .playlist_metadata_updated = &playlist_metadata_updated, }; /* -------------------- PLAYLIST CONTAINER CALLBACKS --------------------- */ /** * Callback from libspotify, telling us a playlist was added to the playlist container. * * We add our playlist callbacks to the newly added playlist. * * @param pc The playlist container handle * @param pl The playlist handle * @param position Index of the added playlist * @param userdata The opaque pointer */ static void playlist_added(sp_playlistcontainer *pc, sp_playlist *pl, int position, void *userdata) { DPRINTF(E_INFO, L_SPOTIFY, "Playlist added: %s (%d tracks)\n", fptr_sp_playlist_name(pl), fptr_sp_playlist_num_tracks(pl)); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); if (spotify_access_token_valid) { webapi_playlist_updated(pl); } else { spotify_playlist_save(pl); } } static int playlist_remove(const char *uri) { struct playlist_info *pli; int plid; pli = db_pl_fetch_bypath(uri); if (!pli) { DPRINTF(E_LOG, L_SPOTIFY, "Playlist '%s' not found, can't delete\n", uri); return -1; } DPRINTF(E_LOG, L_SPOTIFY, "Removing playlist '%s' (%s)\n", pli->title, uri); plid = pli->id; free_pli(pli, 0); db_spotify_pl_delete(plid); spotify_cleanup_files(); return 0; } /** * Callback from libspotify, telling us a playlist was removed from the playlist container. * * This is the place to remove our playlist callbacks. * * @param pc The playlist container handle * @param pl The playlist handle * @param position Index of the removed playlist * @param userdata The opaque pointer */ static void playlist_removed(sp_playlistcontainer *pc, sp_playlist *pl, int position, void *userdata) { sp_link *link; char url[1024]; int ret; DPRINTF(E_INFO, L_SPOTIFY, "Playlist removed: %s\n", fptr_sp_playlist_name(pl)); fptr_sp_playlist_remove_callbacks(pl, &pl_callbacks, NULL); link = fptr_sp_link_create_from_playlist(pl); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Could not find link for deleted playlist\n"); return; } ret = fptr_sp_link_as_string(link, url, sizeof(url)); if (ret == sizeof(url)) { DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: %s\n", url); } fptr_sp_link_release(link); if (spotify_access_token_valid) { // Run playlist remove in the library thread if (!scanning) library_exec_async(webapi_pl_remove, strdup(url)); } else { playlist_remove(url); } } /** * Callback from libspotify, telling us the rootlist is fully synchronized * * @param pc The playlist container handle * @param userdata The opaque pointer */ static void container_loaded(sp_playlistcontainer *pc, void *userdata) { int num; num = fptr_sp_playlistcontainer_num_playlists(pc); DPRINTF(E_INFO, L_SPOTIFY, "Rootlist synchronized (%d playlists)\n", num); } /** * The playlist container callbacks */ static sp_playlistcontainer_callbacks pc_callbacks = { .playlist_added = &playlist_added, .playlist_removed = &playlist_removed, .container_loaded = &container_loaded, }; /* --------------------- INTERNAL PLAYBACK AND AUDIO ----------------------- */ /* Should only be called from within the spotify thread */ static enum command_state playback_setup(void *arg, int *retval) { sp_link *link; sp_track *track; sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Setting up for playback\n"); link = (sp_link *) arg; if (SP_CONNECTION_STATE_LOGGED_IN != fptr_sp_session_connectionstate(g_sess)) { DPRINTF(E_LOG, L_SPOTIFY, "Can't play music, not connected and logged in to Spotify\n"); *retval = -1; return COMMAND_END; } if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, no Spotify link\n"); *retval = -1; return COMMAND_END; } track = fptr_sp_link_as_track(link); if (!track) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, invalid Spotify track\n"); *retval = -1; return COMMAND_END; } err = fptr_sp_session_player_load(g_sess, track); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed: %s\n", fptr_sp_error_message(err)); *retval = -1; return COMMAND_END; } *retval = 0; return COMMAND_END; } static enum command_state playback_play(void *arg, int *retval) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Starting playback\n"); err = fptr_sp_session_player_play(g_sess, 1); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback failed: %s\n", fptr_sp_error_message(err)); *retval = -1; return COMMAND_END; } g_state = SPOTIFY_STATE_PLAYING; *retval = 0; return COMMAND_END; } static enum command_state playback_pause(void *arg, int *retval) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Pausing playback\n"); err = fptr_sp_session_player_play(g_sess, 0); DPRINTF(E_DBG, L_SPOTIFY, "Playback paused\n"); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback pause failed: %s\n", fptr_sp_error_message(err)); *retval = -1; return COMMAND_END; } g_state = SPOTIFY_STATE_PAUSED; *retval = 0; return COMMAND_END; } static enum command_state playback_stop(void *arg, int *retval) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Stopping playback\n"); err = fptr_sp_session_player_unload(g_sess); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback stop failed: %s\n", fptr_sp_error_message(err)); *retval = -1; return COMMAND_END; } g_state = SPOTIFY_STATE_STOPPED; evbuffer_drain(spotify_audio_buffer, evbuffer_get_length(spotify_audio_buffer)); *retval = 0; return COMMAND_END; } static enum command_state playback_seek(void *arg, int *retval) { int seek_ms; sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Playback seek\n"); seek_ms = *((int *) arg); err = fptr_sp_session_player_seek(g_sess, seek_ms); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not seek: %s\n", fptr_sp_error_message(err)); *retval = -1; return COMMAND_END; } *retval = 0; return COMMAND_END; } static enum command_state playback_eot(void *arg, int *retval) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Playback end of track\n"); err = fptr_sp_session_player_unload(g_sess); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback end of track failed: %s\n", fptr_sp_error_message(err)); *retval = -1; return COMMAND_END; } g_state = SPOTIFY_STATE_STOPPING; // TODO 1) This will block for a while, but perhaps ok? input_write(spotify_audio_buffer, INPUT_FLAG_EOF); *retval = 0; return COMMAND_END; } static void artwork_loaded_cb(sp_image *image, void *userdata) { struct artwork_get_param *artwork; artwork = userdata; CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&artwork->mutex)); artwork->is_loaded = 1; CHECK_ERR(L_SPOTIFY, pthread_cond_signal(&artwork->cond)); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&artwork->mutex)); } static enum command_state artwork_get_bh(void *arg, int *retval) { struct artwork_get_param *artwork; sp_imageformat imageformat; sp_error err; const void *data; size_t data_size; int ret; artwork = arg; sp_image *image = artwork->image; char *path = artwork->path; fptr_sp_image_remove_load_callback(image, artwork_loaded_cb, artwork); err = fptr_sp_image_error(image); if (err != SP_ERROR_OK) { DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork (%s) failed, Spotify error: %s\n", path, fptr_sp_error_message(err)); goto fail; } if (!fptr_sp_image_is_loaded(image)) { DPRINTF(E_LOG, L_SPOTIFY, "Load callback returned, but no image? Possible bug: %s\n", path); goto fail; } imageformat = fptr_sp_image_format(image); if (imageformat != SP_IMAGE_FORMAT_JPEG) { DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid image format from Spotify: %s\n", path); goto fail; } data = fptr_sp_image_data(image, &data_size); if (!data || (data_size == 0)) { DPRINTF(E_LOG, L_SPOTIFY, "Getting artwork failed, no image data from Spotify: %s\n", path); goto fail; } ret = evbuffer_expand(artwork->evbuf, data_size); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for artwork\n"); goto fail; } ret = evbuffer_add(artwork->evbuf, data, data_size); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not add Spotify image to event buffer\n"); goto fail; } DPRINTF(E_DBG, L_SPOTIFY, "Spotify artwork loaded ok\n"); fptr_sp_image_release(image); *retval = 0; return COMMAND_END; fail: fptr_sp_image_release(image); *retval = -1; return COMMAND_END; } static enum command_state artwork_get(void *arg, int *retval) { struct artwork_get_param *artwork; char *path; sp_link *link; sp_track *track; sp_album *album; const byte *image_id; sp_image *image; sp_image_size image_size; sp_error err; artwork = arg; path = artwork->path; // Now begins: path -> link -> track -> album -> image_id -> image -> format -> data link = fptr_sp_link_create_from_string(path); if (!link) { DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid Spotify link: %s\n", path); goto level1_exit; } track = fptr_sp_link_as_track(link); if (!track) { DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid Spotify track: %s\n", path); goto level2_exit; } album = fptr_sp_track_album(track); if (!album) { DPRINTF(E_WARN, L_SPOTIFY, "Getting artwork failed, invalid Spotify album: %s\n", path); goto level2_exit; } // Get an image at least the same size as requested image_size = SP_IMAGE_SIZE_SMALL; // 64x64 if ((artwork->max_w > 64) || (artwork->max_h > 64)) image_size = SP_IMAGE_SIZE_NORMAL; // 300x300 if ((artwork->max_w > 300) || (artwork->max_h > 300)) image_size = SP_IMAGE_SIZE_LARGE; // 640x640 image_id = fptr_sp_album_cover(album, image_size); if (!image_id) { DPRINTF(E_DBG, L_SPOTIFY, "Getting artwork failed, no Spotify image id: %s\n", path); goto level2_exit; } image = fptr_sp_image_create(g_sess, image_id); if (!image) { DPRINTF(E_DBG, L_SPOTIFY, "Getting artwork failed, no Spotify image: %s\n", path); goto level2_exit; } fptr_sp_link_release(link); artwork->image = image; artwork->is_loaded = fptr_sp_image_is_loaded(image); /* If the image is ready we can return it straight away, otherwise we will * let the calling thread wait, since the Spotify thread should not wait */ if (artwork->is_loaded) return artwork_get_bh(artwork, retval); DPRINTF(E_SPAM, L_SPOTIFY, "Will wait for Spotify to call artwork_loaded_cb\n"); /* Async - we will return to spotify_artwork_get which will wait for callback */ err = fptr_sp_image_add_load_callback(image, artwork_loaded_cb, artwork); if (err != SP_ERROR_OK) { DPRINTF(E_WARN, L_SPOTIFY, "Adding artwork cb failed, Spotify error: %s\n", fptr_sp_error_message(err)); *retval = -1; return COMMAND_END; } *retval = 0; return COMMAND_END; level2_exit: fptr_sp_link_release(link); level1_exit: *retval = -1; return COMMAND_END; } /* --------------------------- SESSION CALLBACKS ------------------------- */ /** * This callback is called when an attempt to login has succeeded or failed. * * @sa sp_session_callbacks#logged_in */ static void logged_in(sp_session *sess, sp_error error) { sp_playlist *pl; sp_playlistcontainer *pc; int i; if (SP_ERROR_OK != error) { DPRINTF(E_LOG, L_SPOTIFY, "Login failed: %s\n", fptr_sp_error_message(error)); return; } DPRINTF(E_LOG, L_SPOTIFY, "Login to Spotify succeeded, reloading playlists\n"); if (!spotify_access_token_valid) create_base_playlist(); db_directory_enable_bypath("/spotify:"); pl = fptr_sp_session_starred_create(sess); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); pc = fptr_sp_session_playlistcontainer(sess); fptr_sp_playlistcontainer_add_callbacks(pc, &pc_callbacks, NULL); DPRINTF(E_DBG, L_SPOTIFY, "Found %d playlists\n", fptr_sp_playlistcontainer_num_playlists(pc)); for (i = 0; i < fptr_sp_playlistcontainer_num_playlists(pc); i++) { pl = fptr_sp_playlistcontainer_playlist(pc, i); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); } } /** * Called when logout has been processed. * Either called explicitly if you initialize a logout operation, or implicitly * if there is a permanent connection error * * @sa sp_session_callbacks#logged_out */ static void logged_out(sp_session *sess) { DPRINTF(E_INFO, L_SPOTIFY, "Logout complete\n"); CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&login_lck)); CHECK_ERR(L_SPOTIFY, pthread_cond_signal(&login_cond)); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck)); } /** * This callback is used from libspotify whenever there is PCM data available. * * @sa sp_session_callbacks#music_delivery */ static int music_delivery(sp_session *sess, const sp_audioformat *format, const void *frames, int num_frames) { size_t size; int ret; /* No support for resampling right now */ if ((format->sample_rate != 44100) || (format->channels != 2)) { DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported samplerate or channels, stopping playback\n"); spotify_playback_stop_nonblock(); return num_frames; } // Audio discontinuity, e.g. seek if (num_frames == 0) { evbuffer_drain(spotify_audio_buffer, evbuffer_get_length(spotify_audio_buffer)); return 0; } size = num_frames * sizeof(int16_t) * format->channels; ret = evbuffer_add(spotify_audio_buffer, frames, size); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Out of memory adding audio to buffer\n"); return num_frames; } // The input buffer only accepts writing when it is approaching depletion, and // because we use NONBLOCK it will just return if this is not the case. So in // most cases no actual write is made and spotify_audio_buffer will just grow. input_write(spotify_audio_buffer, INPUT_FLAG_NONBLOCK); return num_frames; } /** * This callback is called from an internal libspotify thread to ask us to * reiterate the main loop. This must not block. * * @sa sp_session_callbacks#notify_main_thread */ static void notify_main_thread(sp_session *sess) { int dummy = 42; int ret; ret = write(g_notify_pipe[1], &dummy, sizeof(dummy)); if (ret != sizeof(dummy)) DPRINTF(E_LOG, L_SPOTIFY, "Could not write to notify fd: %s\n", strerror(errno)); } /** * Called whenever metadata has been updated * * If you have metadata cached outside of libspotify, you should purge * your caches and fetch new versions. * * @param[in] session Session */ static void metadata_updated(sp_session *session) { DPRINTF(E_DBG, L_SPOTIFY, "Session metadata updated\n"); } /* Misc connection error callbacks */ static void play_token_lost(sp_session *sess) { DPRINTF(E_LOG, L_SPOTIFY, "Music interrupted - some other session is playing on the account\n"); spotify_playback_stop_nonblock(); } static void connectionstate_updated(sp_session *session) { if (SP_CONNECTION_STATE_LOGGED_IN == fptr_sp_session_connectionstate(session)) { DPRINTF(E_LOG, L_SPOTIFY, "Connection to Spotify (re)established, reloading saved tracks\n"); } else if (g_state == SPOTIFY_STATE_PLAYING) { DPRINTF(E_LOG, L_SPOTIFY, "Music interrupted - connection error or logged out\n"); spotify_playback_stop_nonblock(); } } /** * This callback is used from libspotify when the current track has ended * * @sa sp_session_callbacks#end_of_track */ static void end_of_track(sp_session *sess) { DPRINTF(E_DBG, L_SPOTIFY, "End of track\n"); commands_exec_async(cmdbase, playback_eot, NULL); } /** * The session callbacks */ static sp_session_callbacks session_callbacks = { .logged_in = &logged_in, .logged_out = &logged_out, .connectionstate_updated = &connectionstate_updated, .notify_main_thread = ¬ify_main_thread, .music_delivery = &music_delivery, .metadata_updated = &metadata_updated, .play_token_lost = &play_token_lost, .log_message = NULL, .end_of_track = &end_of_track, }; /** * The session configuration. */ static sp_session_config spconfig = { .api_version = SPOTIFY_API_VERSION, .cache_location = NULL, .settings_location = NULL, .application_key = g_appkey, .application_key_size = sizeof(g_appkey), .user_agent = "forked-daapd", .callbacks = &session_callbacks, NULL, }; /* ------------------------------- MAIN LOOP ------------------------------- */ /* Thread: spotify */ static void * spotify(void *arg) { int ret; DPRINTF(E_DBG, L_SPOTIFY, "Main loop initiating\n"); ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error: DB init failed\n"); pthread_exit(NULL); } g_state = SPOTIFY_STATE_WAIT; event_base_dispatch(evbase_spotify); if (g_state != SPOTIFY_STATE_INACTIVE) { DPRINTF(E_LOG, L_SPOTIFY, "Spotify event loop terminated ahead of time!\n"); g_state = SPOTIFY_STATE_INACTIVE; } db_perthread_deinit(); DPRINTF(E_DBG, L_SPOTIFY, "Main loop terminating\n"); pthread_exit(NULL); } static void exit_cb() { fptr_sp_session_player_unload(g_sess); fptr_sp_session_logout(g_sess); g_state = SPOTIFY_STATE_INACTIVE; } /* Process events when timeout expires or triggered by libspotify's notify_main_thread */ static void notify_cb(int fd, short what, void *arg) { struct timeval tv; int next_timeout; int dummy; int ret; if (what & EV_READ) { ret = read(g_notify_pipe[0], &dummy, sizeof(dummy)); if (ret != sizeof(dummy)) DPRINTF(E_LOG, L_SPOTIFY, "Error reading from notify pipe\n"); } do { fptr_sp_session_process_events(g_sess, &next_timeout); } while (next_timeout == 0); tv.tv_sec = next_timeout / 1000; tv.tv_usec = (next_timeout % 1000) * 1000; event_add(g_notifyev, &tv); } /* ---------------------------- Our Spotify API --------------------------- */ /* Thread: player */ int spotify_playback_setup(const char *path) { sp_link *link; DPRINTF(E_DBG, L_SPOTIFY, "Playback setup request\n"); link = fptr_sp_link_create_from_string(path); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, invalid Spotify link: %s\n", path); return -1; } return commands_exec_sync(cmdbase, playback_setup, NULL, link); } int spotify_playback_play() { DPRINTF(E_DBG, L_SPOTIFY, "Playback request\n"); return commands_exec_sync(cmdbase, playback_play, NULL, NULL); } int spotify_playback_pause() { DPRINTF(E_DBG, L_SPOTIFY, "Pause request\n"); return commands_exec_sync(cmdbase, playback_pause, NULL, NULL); } /* Thread: libspotify */ void spotify_playback_pause_nonblock(void) { DPRINTF(E_DBG, L_SPOTIFY, "Nonblock pause request\n"); commands_exec_async(cmdbase, playback_pause, NULL); } /* Thread: player and libspotify */ int spotify_playback_stop(void) { DPRINTF(E_DBG, L_SPOTIFY, "Stop request\n"); return commands_exec_sync(cmdbase, playback_stop, NULL, NULL); } /* Thread: player and libspotify */ void spotify_playback_stop_nonblock(void) { DPRINTF(E_DBG, L_SPOTIFY, "Nonblock stop request\n"); commands_exec_async(cmdbase, playback_stop, NULL); } /* Thread: player */ int spotify_playback_seek(int ms) { int ret; ret = commands_exec_sync(cmdbase, playback_seek, NULL, &ms); if (ret == 0) return ms; else return -1; } /* Thread: httpd (artwork) and worker */ int spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) { struct artwork_get_param artwork; struct timespec ts; int ret; artwork.evbuf = evbuf; artwork.path = path; artwork.max_w = max_w; artwork.max_h = max_h; CHECK_ERR(L_SPOTIFY, mutex_init(&artwork.mutex)); CHECK_ERR(L_SPOTIFY, pthread_cond_init(&artwork.cond, NULL)); ret = commands_exec_sync(cmdbase, artwork_get, NULL, &artwork); // Artwork was not ready, wait for callback from libspotify if (ret == 0) { CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&artwork.mutex)); ts = timespec_reltoabs(spotify_artwork_timeout); while ((!artwork.is_loaded) && (ret != ETIMEDOUT)) CHECK_ERR_EXCEPT(L_SPOTIFY, pthread_cond_timedwait(&artwork.cond, &artwork.mutex, &ts), ret, ETIMEDOUT); if (ret == ETIMEDOUT) DPRINTF(E_LOG, L_SPOTIFY, "Timeout waiting for artwork from Spotify\n"); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&artwork.mutex)); ret = commands_exec_sync(cmdbase, artwork_get_bh, NULL, &artwork); } return ret; } /* Thread: httpd */ void spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri) { char *uri; uri = spotifywebapi_oauth_uri_get(redirect_uri); if (!uri) { DPRINTF(E_LOG, L_SPOTIFY, "Cannot display Spotify oath interface (http_form_uriencode() failed)\n"); return; } evbuffer_add_printf(evbuf, "Click here to authorize forked-daapd with Spotify\n", uri); free(uri); } /* Thread: httpd */ void spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const char *redirect_uri) { const char *code; const char *err; int ret; code = evhttp_find_header(param, "code"); if (!code) { evbuffer_add_printf(evbuf, "Error: Didn't receive a code from Spotify\n"); return; } DPRINTF(E_DBG, L_SPOTIFY, "Received OAuth code: %s\n", code); evbuffer_add_printf(evbuf, "

Requesting access token from Spotify...\n"); ret = spotifywebapi_token_get(code, redirect_uri, &err); if (ret < 0) { evbuffer_add_printf(evbuf, "failed

\n

Error: %s

\n", err); return; } // Received a valid access token spotify_access_token_valid = true; // Trigger scan after successful access to spotifywebapi library_exec_async(webapi_scan, NULL); evbuffer_add_printf(evbuf, "ok, all done

\n"); return; } static void spotify_uri_register(const char *uri) { char *tmp; tmp = strdup(uri); commands_exec_async(cmdbase, uri_register, tmp); } /* Thread: library */ void spotify_login(char **arglist) { sp_error err; if (!g_sess) { if (!g_libhandle) DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - could not find libspotify\n"); else DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - no valid Spotify session\n"); return; } if (SP_CONNECTION_STATE_LOGGED_IN == fptr_sp_session_connectionstate(g_sess)) { CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&login_lck)); DPRINTF(E_LOG, L_SPOTIFY, "Logging out of Spotify (current state is %d)\n", g_state); fptr_sp_session_player_unload(g_sess); err = fptr_sp_session_logout(g_sess); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not logout of Spotify: %s\n", fptr_sp_error_message(err)); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck)); return; } CHECK_ERR(L_SPOTIFY, pthread_cond_wait(&login_cond, &login_lck)); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck)); } if (arglist) { DPRINTF(E_LOG, L_SPOTIFY, "Spotify credentials file OK, logging in with username %s\n", arglist[0]); err = fptr_sp_session_login(g_sess, arglist[0], arglist[1], 1, NULL); } else { DPRINTF(E_INFO, L_SPOTIFY, "Relogin to Spotify\n"); err = fptr_sp_session_relogin(g_sess); } if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err)); return; } } static void map_track_to_mfi(const struct spotify_track *track, struct media_file_info* mfi) { mfi->title = safe_strdup(track->name); mfi->album = safe_strdup(track->album); mfi->artist = safe_strdup(track->artist); mfi->album_artist = safe_strdup(track->album_artist); mfi->disc = track->disc_number; mfi->song_length = track->duration_ms; mfi->track = track->track_number; mfi->compilation = track->is_compilation; mfi->artwork = ARTWORK_SPOTIFY; mfi->type = strdup("spotify"); mfi->codectype = strdup("wav"); mfi->description = strdup("Spotify audio"); mfi->path = strdup(track->uri); mfi->fname = strdup(track->uri); } static void map_album_to_mfi(const struct spotify_album *album, struct media_file_info* mfi) { mfi->album = safe_strdup(album->name); mfi->album_artist = safe_strdup(album->artist); mfi->genre = safe_strdup(album->genre); mfi->compilation = album->is_compilation; mfi->year = album->release_year; mfi->time_modified = album->mtime; } /* Thread: library */ static int scan_saved_albums() { struct spotify_request request; json_object *jsontracks; int track_count; struct spotify_album album; struct spotify_track track; struct media_file_info mfi; char virtual_path[PATH_MAX]; int dir_id; time_t stamp; int id; int i; int count; int ret; count = 0; memset(&request, 0, sizeof(struct spotify_request)); while (0 == spotifywebapi_request_next(&request, SPOTIFY_WEBAPI_SAVED_ALBUMS, false)) { while (0 == spotifywebapi_saved_albums_fetch(&request, &jsontracks, &track_count, &album)) { DPRINTF(E_DBG, L_SPOTIFY, "Got saved album: '%s' - '%s' (%s) - track-count: %d\n", album.artist, album.name, album.uri, track_count); db_transaction_begin(); dir_id = prepare_directories(album.artist, album.name); ret = 0; for (i = 0; i < track_count && ret == 0; i++) { ret = spotifywebapi_album_track_fetch(jsontracks, i, &track); if (ret == 0 && track.uri) { db_file_stamp_bypath(track.uri, &stamp, &id); if (stamp && (stamp >= track.mtime)) { db_file_ping(id); } else { memset(&mfi, 0, sizeof(struct media_file_info)); mfi.id = id; map_track_to_mfi(&track, &mfi); map_album_to_mfi(&album, &mfi); mfi.data_kind = DATA_KIND_SPOTIFY; snprintf(virtual_path, PATH_MAX, "/spotify:/%s/%s/%s", mfi.album_artist, mfi.album, mfi.title); mfi.virtual_path = strdup(virtual_path); mfi.directory_id = dir_id; library_add_media(&mfi); free_mfi(&mfi, 1); } spotify_uri_register(track.uri); cache_artwork_ping(track.uri, album.mtime, 0); if (spotify_saved_plid) db_pl_add_item_bypath(spotify_saved_plid, track.uri); } } db_transaction_end(); count++; if (count >= request.total || (count % 10 == 0)) DPRINTF(E_LOG, L_SPOTIFY, "Scanned %d of %d saved albums\n", count, request.total); } } spotifywebapi_request_end(&request); return 0; } /* Thread: library */ static int scan_playlisttracks(struct spotify_playlist *playlist, int plid) { cfg_t *spotify_cfg; bool artist_override; bool album_override; struct spotify_request request; struct spotify_track track; struct media_file_info mfi; char virtual_path[PATH_MAX]; int dir_id; time_t stamp; int id; memset(&request, 0, sizeof(struct spotify_request)); spotify_cfg = cfg_getsec(cfg, "spotify"); artist_override = cfg_getbool(spotify_cfg, "artist_override"); album_override = cfg_getbool(spotify_cfg, "album_override"); while (0 == spotifywebapi_request_next(&request, playlist->tracks_href, true)) { db_transaction_begin(); // DPRINTF(E_DBG, L_SPOTIFY, "Playlist tracks\n%s\n", request.response_body); while (0 == spotifywebapi_playlisttracks_fetch(&request, &track)) { DPRINTF(E_DBG, L_SPOTIFY, "Got playlist track: '%s' (%s) \n", track.name, track.uri); if (!track.is_playable) { DPRINTF(E_LOG, L_SPOTIFY, "Track not available for playback: '%s' - '%s' (%s) (restrictions: %s)\n", track.artist, track.name, track.uri, track.restrictions); continue; } if (track.uri) { if (track.linked_from_uri) DPRINTF(E_DBG, L_SPOTIFY, "Track '%s' (%s) linked from %s\n", track.name, track.uri, track.linked_from_uri); db_file_stamp_bypath(track.uri, &stamp, &id); if (stamp) { db_file_ping(id); } else { memset(&mfi, 0, sizeof(struct media_file_info)); mfi.id = id; dir_id = prepare_directories(track.album_artist, track.album); map_track_to_mfi(&track, &mfi); mfi.compilation = (track.is_compilation || artist_override); if (album_override) { free(mfi.album); mfi.album = strdup(playlist->name); } mfi.time_modified = time(NULL); mfi.data_kind = DATA_KIND_SPOTIFY; snprintf(virtual_path, PATH_MAX, "/spotify:/%s/%s/%s", mfi.album_artist, mfi.album, mfi.title); mfi.virtual_path = strdup(virtual_path); mfi.directory_id = dir_id; library_add_media(&mfi); free_mfi(&mfi, 1); } spotify_uri_register(track.uri); cache_artwork_ping(track.uri, 1, 0); db_pl_add_item_bypath(plid, track.uri); } } db_transaction_end(); } spotifywebapi_request_end(&request); return 0; } /* Thread: library */ static int scan_playlists() { struct spotify_request request; struct spotify_playlist playlist; char virtual_path[PATH_MAX]; int plid; int count; int trackcount; count = 0; trackcount = 0; memset(&request, 0, sizeof(struct spotify_request)); while (0 == spotifywebapi_request_next(&request, SPOTIFY_WEBAPI_SAVED_PLAYLISTS, false)) { while (0 == spotifywebapi_playlists_fetch(&request, &playlist)) { DPRINTF(E_DBG, L_SPOTIFY, "Got playlist: '%s' with %d tracks (%s) \n", playlist.name, playlist.tracks_count, playlist.uri); if (!playlist.uri || !playlist.name || playlist.tracks_count == 0) { DPRINTF(E_LOG, L_SPOTIFY, "Ignoring playlist '%s' with %d tracks (%s)\n", playlist.name, playlist.tracks_count, playlist.uri); continue; } if (playlist.owner) { snprintf(virtual_path, PATH_MAX, "/spotify:/%s (%s)", playlist.name, playlist.owner); } else { snprintf(virtual_path, PATH_MAX, "/spotify:/%s", playlist.name); } db_transaction_begin(); plid = library_add_playlist_info(playlist.uri, playlist.name, virtual_path, PL_PLAIN, spotify_base_plid, DIR_SPOTIFY); db_transaction_end(); if (plid > 0) scan_playlisttracks(&playlist, plid); else DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist: '%s' (%s) \n", playlist.name, playlist.uri); count++; trackcount += playlist.tracks_count; DPRINTF(E_LOG, L_SPOTIFY, "Scanned %d of %d saved playlists (%d tracks)\n", count, request.total, trackcount); } } spotifywebapi_request_end(&request); return 0; } /* Thread: library */ static int scan_playlist(const char *uri) { struct spotify_request request; struct spotify_playlist playlist; char virtual_path[PATH_MAX]; int plid; memset(&request, 0, sizeof(struct spotify_request)); if (0 == spotifywebapi_playlist_start(&request, uri, &playlist)) { if (!playlist.uri) { DPRINTF(E_LOG, L_SPOTIFY, "Got playlist with missing uri for path:: '%s'\n", uri); } else { DPRINTF(E_LOG, L_SPOTIFY, "Saving playlist '%s' with %d tracks (%s) \n", playlist.name, playlist.tracks_count, playlist.uri); if (playlist.owner) { snprintf(virtual_path, PATH_MAX, "/spotify:/%s (%s)", playlist.name, playlist.owner); } else { snprintf(virtual_path, PATH_MAX, "/spotify:/%s", playlist.name); } db_transaction_begin(); plid = library_add_playlist_info(playlist.uri, playlist.name, virtual_path, PL_PLAIN, spotify_base_plid, DIR_SPOTIFY); db_transaction_end(); if (plid > 0) scan_playlisttracks(&playlist, plid); else DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist: '%s' (%s) \n", playlist.name, playlist.uri); } } spotifywebapi_request_end(&request); return 0; } static void create_saved_tracks_playlist() { spotify_saved_plid = library_add_playlist_info("spotify:savedtracks", "Spotify Saved", "/spotify:/Spotify Saved", PL_PLAIN, spotify_base_plid, DIR_SPOTIFY); if (spotify_saved_plid <= 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist for saved tracks\n"); spotify_saved_plid = 0; } } /* * Add or update playlist folder for all spotify playlists (if enabled in config) */ static void create_base_playlist() { cfg_t *spotify_cfg; int ret; spotify_base_plid = 0; spotify_cfg = cfg_getsec(cfg, "spotify"); if (!cfg_getbool(spotify_cfg, "base_playlist_disable")) { ret = library_add_playlist_info("spotify:playlistfolder", "Spotify", NULL, PL_FOLDER, 0, 0); if (ret < 0) DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n"); else spotify_base_plid = ret; } } /* Thread: library */ static int initscan() { scanning = true; /* Refresh access token for the spotify webapi */ spotify_access_token_valid = (0 == spotifywebapi_token_refresh()); if (!spotify_access_token_valid) { DPRINTF(E_LOG, L_SPOTIFY, "Spotify webapi token refresh failed. " "In order to use the web api, authorize forked-daapd to access " "your saved tracks by visiting http://forked-daapd.local:3689/oauth\n"); db_spotify_purge(); } spotify_saved_plid = 0; /* * Login to spotify needs to be done before scanning tracks from the web api. * (Scanned tracks need to be registered with libspotify for playback) */ spotify_login(NULL); /* * Scan saved tracks from the web api */ if (spotify_access_token_valid) { create_base_playlist(); create_saved_tracks_playlist(); scan_saved_albums(); scan_playlists(); } scanning = false; return 0; } /* Thread: library */ static int rescan() { scanning = true; create_base_playlist(); /* * Scan saved tracks from the web api */ if (spotify_access_token_valid) { create_saved_tracks_playlist(); scan_saved_albums(); scan_playlists(); } else { db_transaction_begin(); db_file_ping_bymatch("spotify:", 0); db_pl_ping_bymatch("spotify:", 0); db_directory_ping_bymatch("/spotify:"); db_transaction_end(); } scanning = false; return 0; } /* Thread: library */ static int fullrescan() { scanning = true; create_base_playlist(); /* * Scan saved tracks from the web api */ if (spotify_access_token_valid) { create_saved_tracks_playlist(); scan_saved_albums(); scan_playlists(); } else { spotify_login(NULL); } scanning = false; return 0; } /* Thread: library */ static enum command_state webapi_scan(void *arg, int *ret) { db_spotify_purge(); *ret = rescan(); return COMMAND_END; } /* Thread: library */ static enum command_state webapi_pl_save(void *arg, int *ret) { const char *uri = arg; *ret = scan_playlist(uri); return COMMAND_END; } /* Thread: library */ static enum command_state webapi_pl_remove(void *arg, int *ret) { const char *uri = arg; *ret = playlist_remove(uri); return COMMAND_END; } /* Thread: main */ int spotify_init(void) { cfg_t *spotify_cfg; sp_session *sp; sp_error err; int ret; spotify_access_token_valid = false; scanning = false; /* Initialize libspotify */ g_libhandle = dlopen("libspotify.so", RTLD_LAZY); if (!g_libhandle) { DPRINTF(E_INFO, L_SPOTIFY, "libspotify.so not installed or not found\n"); goto libspotify_fail; } ret = fptr_assign_all(); if (ret < 0) goto assign_fail; #ifdef HAVE_PIPE2 ret = pipe2(g_notify_pipe, O_CLOEXEC); #else ret = pipe(g_notify_pipe); #endif if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not notify command pipe: %s\n", strerror(errno)); goto notify_fail; } evbase_spotify = event_base_new(); if (!evbase_spotify) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create an event base\n"); goto evbase_fail; } g_notifyev = event_new(evbase_spotify, g_notify_pipe[0], EV_READ | EV_TIMEOUT, notify_cb, NULL); if (!g_notifyev) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create notify event\n"); goto evnew_fail; } event_add(g_notifyev, NULL); cmdbase = commands_base_new(evbase_spotify, exit_cb); if (!cmdbase) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create command base\n"); goto cmd_fail; } DPRINTF(E_INFO, L_SPOTIFY, "Spotify session init\n"); spotify_cfg = cfg_getsec(cfg, "spotify"); spconfig.settings_location = cfg_getstr(spotify_cfg, "settings_dir"); spconfig.cache_location = cfg_getstr(spotify_cfg, "cache_dir"); DPRINTF(E_DBG, L_SPOTIFY, "Creating Spotify session\n"); err = fptr_sp_session_create(&spconfig, &sp); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create Spotify session: %s\n", fptr_sp_error_message(err)); goto session_fail; } g_sess = sp; g_state = SPOTIFY_STATE_INACTIVE; switch (cfg_getint(spotify_cfg, "bitrate")) { case 1: fptr_sp_session_preferred_bitrate(g_sess, SP_BITRATE_96k); break; case 2: fptr_sp_session_preferred_bitrate(g_sess, SP_BITRATE_160k); break; case 3: fptr_sp_session_preferred_bitrate(g_sess, SP_BITRATE_320k); break; } spotify_audio_buffer = evbuffer_new(); CHECK_ERR(L_SPOTIFY, evbuffer_enable_locking(spotify_audio_buffer, NULL)); CHECK_ERR(L_SPOTIFY, mutex_init(&login_lck)); CHECK_ERR(L_SPOTIFY, pthread_cond_init(&login_cond, NULL)); /* Spawn thread */ ret = pthread_create(&tid_spotify, NULL, spotify, NULL); if (ret < 0) { DPRINTF(E_FATAL, L_SPOTIFY, "Could not spawn Spotify thread: %s\n", strerror(errno)); goto thread_fail; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_spotify, "spotify"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_spotify, "spotify"); #endif DPRINTF(E_DBG, L_SPOTIFY, "Spotify init complete\n"); return 0; thread_fail: CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&login_cond)); CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&login_lck)); evbuffer_free(spotify_audio_buffer); fptr_sp_session_release(g_sess); g_sess = NULL; session_fail: cmd_fail: evnew_fail: commands_base_free(cmdbase); event_base_free(evbase_spotify); evbase_spotify = NULL; evbase_fail: close(g_notify_pipe[0]); close(g_notify_pipe[1]); notify_fail: assign_fail: dlclose(g_libhandle); g_libhandle = NULL; libspotify_fail: return -1; } void spotify_deinit(void) { int ret; if (!g_libhandle) return; /* Send exit signal to thread (if active) */ if (g_state != SPOTIFY_STATE_INACTIVE) { commands_base_destroy(cmdbase); g_state = SPOTIFY_STATE_INACTIVE; ret = pthread_join(tid_spotify, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_SPOTIFY, "Could not join Spotify thread: %s\n", strerror(errno)); return; } } /* Release session */ fptr_sp_session_release(g_sess); /* Free event base (should free events too) */ event_base_free(evbase_spotify); /* Close pipes */ close(g_notify_pipe[0]); close(g_notify_pipe[1]); /* Destroy locks */ CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&login_cond)); CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&login_lck)); /* Free audio buffer */ evbuffer_free(spotify_audio_buffer); /* Release libspotify handle */ dlclose(g_libhandle); } struct library_source spotifyscanner = { .name = "spotifyscanner", .disabled = 0, .init = spotify_init, .deinit = spotify_deinit, .rescan = rescan, .initscan = initscan, .fullrescan = fullrescan, }; forked-daapd-25.0/src/spotify.h000066400000000000000000000014141313447753700164340ustar00rootroot00000000000000 #ifndef __SPOTIFY_H__ #define __SPOTIFY_H__ #include #include #include int spotify_playback_setup(const char *path); int spotify_playback_play(); int spotify_playback_pause(); void spotify_playback_pause_nonblock(void); int spotify_playback_stop(void); void spotify_playback_stop_nonblock(void); int spotify_playback_seek(int ms); int spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h); void spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri); void spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const char *redirect_uri); void spotify_login(char **arglist); int spotify_init(void); void spotify_deinit(void); #endif /* !__SPOTIFY_H__ */ forked-daapd-25.0/src/spotify_webapi.c000066400000000000000000000511011313447753700177540ustar00rootroot00000000000000/* * Copyright (C) 2016 Espen Jürgensen * Copyright (C) 2016 Christian Meffert * * 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 */ #include "spotify_webapi.h" #include #include #include #include #include #include #include "db.h" #include "http.h" #include "library.h" #include "logger.h" // Credentials for the web api static char *spotify_access_token; static char *spotify_refresh_token; static char *spotify_user_country; static int32_t expires_in = 3600; static time_t token_requested = 0; // Endpoints and credentials for the web api static const char *spotify_client_id = "0e684a5422384114a8ae7ac020f01789"; static const char *spotify_client_secret = "232af95f39014c9ba218285a5c11a239"; static const char *spotify_auth_uri = "https://accounts.spotify.com/authorize"; static const char *spotify_token_uri = "https://accounts.spotify.com/api/token"; static const char *spotify_playlist_uri = "https://api.spotify.com/v1/users/%s/playlists/%s"; static const char *spotify_me_uri = "https://api.spotify.com/v1/me"; /*--------------------- HELPERS FOR SPOTIFY WEB API -------------------------*/ /* All the below is in the httpd thread */ static void jparse_free(json_object* haystack) { if (haystack) { #ifdef HAVE_JSON_C_OLD json_object_put(haystack); #else if (json_object_put(haystack) != 1) DPRINTF(E_LOG, L_SPOTIFY, "Memleak: JSON parser did not free object\n"); #endif } } static int jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle) { if (! (json_object_object_get_ex(haystack, key, needle) && json_object_get_type(*needle) == json_type_array) ) return -1; else return 0; } static const char * jparse_str_from_obj(json_object *haystack, const char *key) { json_object *needle; if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_string) return json_object_get_string(needle); else return NULL; } static int jparse_int_from_obj(json_object *haystack, const char *key) { json_object *needle; if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_int) return json_object_get_int(needle); else return 0; } static int jparse_bool_from_obj(json_object *haystack, const char *key) { json_object *needle; if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_boolean) return json_object_get_boolean(needle); else return false; } static time_t jparse_time_from_obj(json_object *haystack, const char *key) { const char *tmp; struct tm tp; time_t parsed_time; memset(&tp, 0, sizeof(struct tm)); tmp = jparse_str_from_obj(haystack, key); if (!tmp) return 0; strptime(tmp, "%Y-%m-%dT%H:%M:%SZ", &tp); parsed_time = mktime(&tp); if (parsed_time < 0) return 0; return parsed_time; } static const char * jparse_str_from_array(json_object *array, int index, const char *key) { json_object *item; int count; if (json_object_get_type(array) != json_type_array) return NULL; count = json_object_array_length(array); if (count <= 0 || count <= index) return NULL; item = json_object_array_get_idx(array, index); return jparse_str_from_obj(item, key); } static void free_http_client_ctx(struct http_client_ctx *ctx) { if (!ctx) return; if (ctx->input_body) evbuffer_free(ctx->input_body); if (ctx->output_headers) { keyval_clear(ctx->output_headers); free(ctx->output_headers); } free(ctx); } static int request_uri(struct spotify_request *request, const char *uri) { char bearer_token[1024]; int ret; memset(request, 0, sizeof(struct spotify_request)); if (0 > spotifywebapi_token_refresh()) { return -1; } request->ctx = calloc(1, sizeof(struct http_client_ctx)); request->ctx->output_headers = calloc(1, sizeof(struct keyval)); request->ctx->input_body = evbuffer_new(); request->ctx->url = uri; snprintf(bearer_token, sizeof(bearer_token), "Bearer %s", spotify_access_token); if (keyval_add(request->ctx->output_headers, "Authorization", bearer_token) < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Add bearer_token to keyval failed\n"); return -1; } ret = http_client_request(request->ctx); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed\n"); return -1; } // 0-terminate for safety evbuffer_add(request->ctx->input_body, "", 1); request->response_body = (char *) evbuffer_pullup(request->ctx->input_body, -1); if (!request->response_body || (strlen(request->response_body) == 0)) { DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed, response was empty\n"); return -1; } // DPRINTF(E_DBG, L_SPOTIFY, "Wep api response for '%s'\n%s\n", uri, request->response_body); request->haystack = json_tokener_parse(request->response_body); if (!request->haystack) { DPRINTF(E_LOG, L_SPOTIFY, "JSON parser returned an error\n"); return -1; } DPRINTF(E_DBG, L_SPOTIFY, "Got response for '%s'\n", uri); return 0; } void spotifywebapi_request_end(struct spotify_request *request) { free_http_client_ctx(request->ctx); jparse_free(request->haystack); } int spotifywebapi_request_next(struct spotify_request *request, const char *uri, bool append_market) { char *next_uri; int ret; if (request->ctx && !request->next_uri) { // Reached end of paging requests, terminate loop return -1; } if (!request->ctx) { // First paging request if (append_market && spotify_user_country) { if (strchr(uri, '?')) next_uri = safe_asprintf("%s&market=%s", uri, spotify_user_country); else next_uri = safe_asprintf("%s?market=%s", uri, spotify_user_country); } else next_uri = strdup(uri); } else { // Next paging request next_uri = strdup(request->next_uri); spotifywebapi_request_end(request); } ret = request_uri(request, next_uri); free(next_uri); if (ret < 0) return ret; request->total = jparse_int_from_obj(request->haystack, "total"); request->next_uri = jparse_str_from_obj(request->haystack, "next"); if (jparse_array_from_obj(request->haystack, "items", &request->items) < 0) { DPRINTF(E_LOG, L_SPOTIFY, "No items in reply from Spotify. See:\n%s\n", request->response_body); return -1; } request->count = json_object_array_length(request->items); DPRINTF(E_DBG, L_SPOTIFY, "Got %d items\n", request->count); return 0; } static void parse_metadata_track(json_object* jsontrack, struct spotify_track* track) { json_object* jsonalbum; json_object* jsonartists; json_object* needle; if (json_object_object_get_ex(jsontrack, "album", &jsonalbum)) { track->album = jparse_str_from_obj(jsonalbum, "name"); if (json_object_object_get_ex(jsonalbum, "artists", &jsonartists)) { track->album_artist = jparse_str_from_array(jsonartists, 0, "name"); } } if (json_object_object_get_ex(jsontrack, "artists", &jsonartists)) { track->artist = jparse_str_from_array(jsonartists, 0, "name"); } track->disc_number = jparse_int_from_obj(jsontrack, "disc_number"); track->album_type = jparse_str_from_obj(jsonalbum, "album_type"); track->is_compilation = (track->album_type && 0 == strcmp(track->album_type, "compilation")); track->duration_ms = jparse_int_from_obj(jsontrack, "duration_ms"); track->name = jparse_str_from_obj(jsontrack, "name"); track->track_number = jparse_int_from_obj(jsontrack, "track_number"); track->uri = jparse_str_from_obj(jsontrack, "uri"); track->id = jparse_str_from_obj(jsontrack, "id"); // "is_playable" is only returned for a request with a market parameter, default to true if it is not in the response if (json_object_object_get_ex(jsontrack, "is_playable", NULL)) { track->is_playable = jparse_bool_from_obj(jsontrack, "is_playable"); if (json_object_object_get_ex(jsontrack, "restrictions", &needle)) track->restrictions = json_object_to_json_string(needle); if (json_object_object_get_ex(jsontrack, "linked_from", &needle)) track->linked_from_uri = jparse_str_from_obj(needle, "uri"); } else track->is_playable = true; } static int get_year_from_date(const char *date) { char tmp[5]; uint32_t year = 0; if (date && strlen(date) >= 4) { strncpy(tmp, date, sizeof(tmp)); tmp[4] = '\0'; safe_atou32(tmp, &year); } return year; } static void parse_metadata_album(json_object *jsonalbum, struct spotify_album *album) { json_object* jsonartists; if (json_object_object_get_ex(jsonalbum, "artists", &jsonartists)) { album->artist = jparse_str_from_array(jsonartists, 0, "name"); } album->name = jparse_str_from_obj(jsonalbum, "name"); album->uri = jparse_str_from_obj(jsonalbum, "uri"); album->id = jparse_str_from_obj(jsonalbum, "id"); album->album_type = jparse_str_from_obj(jsonalbum, "album_type"); album->is_compilation = (album->album_type && 0 == strcmp(album->album_type, "compilation")); album->label = jparse_str_from_obj(jsonalbum, "label"); album->release_date = jparse_str_from_obj(jsonalbum, "release_date"); album->release_date_precision = jparse_str_from_obj(jsonalbum, "release_date_precision"); album->release_year = get_year_from_date(album->release_date); // TODO Genre is an array of strings ('genres'), but it is always empty (https://github.com/spotify/web-api/issues/157) //album->genre = jparse_str_from_obj(jsonalbum, "genre"); } int spotifywebapi_saved_albums_fetch(struct spotify_request *request, json_object **jsontracks, int *track_count, struct spotify_album *album) { json_object *jsonalbum; json_object *item; json_object *needle; memset(album, 0, sizeof(struct spotify_album)); *track_count = 0; if (request->index >= request->count) { return -1; } item = json_object_array_get_idx(request->items, request->index); if (!(item && json_object_object_get_ex(item, "album", &jsonalbum))) { DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: Item %d did not have 'album'->'uri'\n", request->index); request->index++; return -1; } parse_metadata_album(jsonalbum, album); album->added_at = jparse_str_from_obj(item, "added_at"); album->mtime = jparse_time_from_obj(item, "added_at"); if (json_object_object_get_ex(jsonalbum, "tracks", &needle)) { if (jparse_array_from_obj(needle, "items", jsontracks) == 0) { *track_count = json_object_array_length(*jsontracks); } } request->index++; return 0; } int spotifywebapi_album_track_fetch(json_object *jsontracks, int index, struct spotify_track *track) { json_object *jsontrack; memset(track, 0, sizeof(struct spotify_track)); jsontrack = json_object_array_get_idx(jsontracks, index); if (!jsontrack) { return -1; } parse_metadata_track(jsontrack, track); return 0; } static void parse_metadata_playlist(json_object *jsonplaylist, struct spotify_playlist *playlist) { json_object *needle; playlist->name = jparse_str_from_obj(jsonplaylist, "name"); playlist->uri = jparse_str_from_obj(jsonplaylist, "uri"); playlist->id = jparse_str_from_obj(jsonplaylist, "id"); playlist->href = jparse_str_from_obj(jsonplaylist, "href"); if (json_object_object_get_ex(jsonplaylist, "owner", &needle)) { playlist->owner = jparse_str_from_obj(needle, "id"); } if (json_object_object_get_ex(jsonplaylist, "tracks", &needle)) { playlist->tracks_href = jparse_str_from_obj(needle, "href"); playlist->tracks_count = jparse_int_from_obj(needle, "total"); } } int spotifywebapi_playlists_fetch(struct spotify_request *request, struct spotify_playlist *playlist) { json_object *jsonplaylist; memset(playlist, 0, sizeof(struct spotify_playlist)); if (request->index >= request->count) { DPRINTF(E_DBG, L_SPOTIFY, "All playlists processed\n"); return -1; } jsonplaylist = json_object_array_get_idx(request->items, request->index); if (!jsonplaylist) { DPRINTF(E_LOG, L_SPOTIFY, "Error fetching playlist at index '%d'\n", request->index); return -1; } parse_metadata_playlist(jsonplaylist, playlist); request->index++; return 0; } /* * Extracts the owner and the id from a spotify playlist uri * * Playlist-uri has the following format: spotify:user:[owner]:playlist:[id] * Owner and plid must be freed by the caller. */ static int get_owner_plid_from_uri(const char *uri, char **owner, char **plid) { char *ptr1; char *ptr2; char *tmp; size_t len; ptr1 = strchr(uri, ':'); if (!ptr1) return -1; ptr1++; ptr1 = strchr(ptr1, ':'); if (!ptr1) return -1; ptr1++; ptr2 = strchr(ptr1, ':'); len = ptr2 - ptr1; tmp = malloc(sizeof(char) * (len + 1)); strncpy(tmp, ptr1, len); tmp[len] = '\0'; *owner = tmp; ptr2++; ptr1 = strchr(ptr2, ':'); if (!ptr1) { free(tmp); return -1; } ptr1++; *plid = strdup(ptr1); return 0; } int spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track) { json_object *item; json_object *jsontrack; memset(track, 0, sizeof(struct spotify_track)); if (request->index >= request->count) { return -1; } item = json_object_array_get_idx(request->items, request->index); if (!(item && json_object_object_get_ex(item, "track", &jsontrack))) { DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: Item %d did not have 'track'->'uri'\n", request->index); request->index++; return -1; } parse_metadata_track(jsontrack, track); track->added_at = jparse_str_from_obj(item, "added_at"); track->mtime = jparse_time_from_obj(item, "added_at"); request->index++; return 0; } int spotifywebapi_playlist_start(struct spotify_request *request, const char *path, struct spotify_playlist *playlist) { char uri[1024]; char *owner; char *id; int ret; ret = get_owner_plid_from_uri(path, &owner, &id); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error extracting owner and id from playlist uri '%s'\n", path); return -1; } ret = snprintf(uri, sizeof(uri), spotify_playlist_uri, owner, id); if (ret < 0 || ret >= sizeof(uri)) { DPRINTF(E_LOG, L_SPOTIFY, "Error creating playlist endpoint uri for playlist '%s'\n", path); free(owner); free(id); return -1; } ret = request_uri(request, uri); if (ret < 0) { free(owner); free(id); return -1; } request->haystack = json_tokener_parse(request->response_body); parse_metadata_playlist(request->haystack, playlist); free(owner); free(id); return 0; } static int request_user_country() { struct spotify_request request; int ret; free(spotify_user_country); spotify_user_country = NULL; ret = request_uri(&request, spotify_me_uri); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Failed to read user country\n"); } else { spotify_user_country = safe_strdup(jparse_str_from_obj(request.haystack, "country")); DPRINTF(E_DBG, L_SPOTIFY, "User country: '%s'\n", spotify_user_country); } spotifywebapi_request_end(&request); return 0; } char * spotifywebapi_oauth_uri_get(const char *redirect_uri) { struct keyval kv; char *param; char *uri; int uri_len; int ret; uri = NULL; memset(&kv, 0, sizeof(struct keyval)); ret = ( (keyval_add(&kv, "client_id", spotify_client_id) == 0) && (keyval_add(&kv, "response_type", "code") == 0) && (keyval_add(&kv, "redirect_uri", redirect_uri) == 0) && (keyval_add(&kv, "scope", "user-read-private playlist-read-private user-library-read") == 0) && (keyval_add(&kv, "show_dialog", "false") == 0) ); if (!ret) { DPRINTF(E_LOG, L_SPOTIFY, "Cannot display Spotify oath interface (error adding parameters to keyval)\n"); goto out_clear_kv; } param = http_form_urlencode(&kv); if (param) { uri_len = strlen(spotify_auth_uri) + strlen(param) + 3; uri = calloc(uri_len, sizeof(char)); snprintf(uri, uri_len, "%s/?%s", spotify_auth_uri, param); free(param); } out_clear_kv: keyval_clear(&kv); return uri; } static int tokens_get(struct keyval *kv, const char **err) { struct http_client_ctx ctx; char *param; char *body; json_object *haystack; const char *tmp; int ret; param = http_form_urlencode(kv); if (!param) { *err = "http_form_uriencode() failed"; ret = -1; goto out_clear_kv; } memset(&ctx, 0, sizeof(struct http_client_ctx)); ctx.url = (char *)spotify_token_uri; ctx.output_body = param; ctx.input_body = evbuffer_new(); ret = http_client_request(&ctx); if (ret < 0) { *err = "Did not get a reply from Spotify"; goto out_free_input_body; } // 0-terminate for safety evbuffer_add(ctx.input_body, "", 1); body = (char *)evbuffer_pullup(ctx.input_body, -1); if (!body || (strlen(body) == 0)) { *err = "The reply from Spotify is empty or invalid"; ret = -1; goto out_free_input_body; } DPRINTF(E_DBG, L_SPOTIFY, "Token reply: %s\n", body); haystack = json_tokener_parse(body); if (!haystack) { *err = "JSON parser returned an error"; ret = -1; goto out_free_input_body; } free(spotify_access_token); spotify_access_token = NULL; tmp = jparse_str_from_obj(haystack, "access_token"); if (tmp) spotify_access_token = strdup(tmp); tmp = jparse_str_from_obj(haystack, "refresh_token"); if (tmp) { free(spotify_refresh_token); spotify_refresh_token = strdup(tmp); } expires_in = jparse_int_from_obj(haystack, "expires_in"); if (expires_in == 0) expires_in = 3600; jparse_free(haystack); if (!spotify_access_token) { DPRINTF(E_LOG, L_SPOTIFY, "Could not find access token in reply: %s\n", body); *err = "Could not find access token in Spotify reply (see log)"; ret = -1; goto out_free_input_body; } token_requested = time(NULL); if (spotify_refresh_token) db_admin_set("spotify_refresh_token", spotify_refresh_token); request_user_country(); ret = 0; out_free_input_body: evbuffer_free(ctx.input_body); free(param); out_clear_kv: return ret; } int spotifywebapi_token_get(const char *code, const char *redirect_uri, const char **err) { struct keyval kv; int ret; *err = ""; memset(&kv, 0, sizeof(struct keyval)); ret = ( (keyval_add(&kv, "grant_type", "authorization_code") == 0) && (keyval_add(&kv, "code", code) == 0) && (keyval_add(&kv, "client_id", spotify_client_id) == 0) && (keyval_add(&kv, "client_secret", spotify_client_secret) == 0) && (keyval_add(&kv, "redirect_uri", redirect_uri) == 0) ); if (!ret) { *err = "Add parameters to keyval failed"; ret = -1; } else ret = tokens_get(&kv, err); keyval_clear(&kv); return ret; } int spotifywebapi_token_refresh() { struct keyval kv; char *refresh_token; const char *err; int ret; if (token_requested && difftime(time(NULL), token_requested) < expires_in) { DPRINTF(E_DBG, L_SPOTIFY, "Spotify token still valid\n"); return 0; } refresh_token = db_admin_get("spotify_refresh_token"); if (!refresh_token) { DPRINTF(E_LOG, L_SPOTIFY, "No spotify refresh token found\n"); return -1; } DPRINTF(E_DBG, L_SPOTIFY, "Spotify refresh-token: '%s'\n", refresh_token); memset(&kv, 0, sizeof(struct keyval)); ret = ( (keyval_add(&kv, "grant_type", "refresh_token") == 0) && (keyval_add(&kv, "client_id", spotify_client_id) == 0) && (keyval_add(&kv, "client_secret", spotify_client_secret) == 0) && (keyval_add(&kv, "refresh_token", refresh_token) == 0) ); if (!ret) { DPRINTF(E_LOG, L_SPOTIFY, "Add parameters to keyval failed"); ret = -1; } else ret = tokens_get(&kv, &err); free(refresh_token); keyval_clear(&kv); return ret; } forked-daapd-25.0/src/spotify_webapi.h000066400000000000000000000062331313447753700177670ustar00rootroot00000000000000/* * Copyright (C) 2016 Espen Jürgensen * Copyright (C) 2016 Christian Meffert * * 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 */ #ifndef SRC_SPOTIFY_WEBAPI_H_ #define SRC_SPOTIFY_WEBAPI_H_ #include #include #include #include "http.h" #define SPOTIFY_WEBAPI_SAVED_ALBUMS "https://api.spotify.com/v1/me/albums?limit=50" #define SPOTIFY_WEBAPI_SAVED_PLAYLISTS "https://api.spotify.com/v1/me/playlists?limit=50" struct spotify_album { const char *added_at; time_t mtime; const char *album_type; bool is_compilation; const char *artist; const char *genre; const char *id; const char *label; const char *name; const char *release_date; const char *release_date_precision; int release_year; const char *uri; }; struct spotify_track { const char *added_at; time_t mtime; const char *album; const char *album_artist; const char *artist; int disc_number; const char *album_type; bool is_compilation; int duration_ms; const char *id; const char *name; int track_number; const char *uri; bool is_playable; const char *restrictions; const char *linked_from_uri; }; struct spotify_playlist { const char *id; const char *name; const char *owner; const char *uri; const char *href; const char *tracks_href; int tracks_count; }; struct spotify_request { struct http_client_ctx *ctx; char *response_body; json_object *haystack; json_object *items; int count; int total; const char *next_uri; int index; }; char * spotifywebapi_oauth_uri_get(const char *redirect_uri); int spotifywebapi_token_get(const char *code, const char *redirect_uri, const char **err); int spotifywebapi_token_refresh(); void spotifywebapi_request_end(struct spotify_request *request); int spotifywebapi_request_next(struct spotify_request *request, const char *uri, bool append_market); int spotifywebapi_saved_albums_fetch(struct spotify_request *request, json_object **jsontracks, int *track_count, struct spotify_album *album); int spotifywebapi_album_track_fetch(json_object *jsontracks, int index, struct spotify_track *track); int spotifywebapi_playlists_fetch(struct spotify_request *request, struct spotify_playlist* playlist); int spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track); int spotifywebapi_playlist_start(struct spotify_request *request, const char *path, struct spotify_playlist *playlist); #endif /* SRC_SPOTIFY_WEBAPI_H_ */ forked-daapd-25.0/src/transcode.c000066400000000000000000001421641313447753700167240ustar00rootroot00000000000000/* * Copyright (C) 2015 Espen Jurgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include "ffmpeg-compat.h" #include "logger.h" #include "conffile.h" #include "db.h" #include "avio_evbuffer.h" #include "transcode.h" // Interval between ICY metadata checks for streams, in seconds #define METADATA_ICY_INTERVAL 5 // Maximum number of streams in a file that we will accept #define MAX_STREAMS 64 // Maximum number of times we retry when we encounter bad packets #define MAX_BAD_PACKETS 5 // How long to wait (in microsec) before interrupting av_read_frame #define READ_TIMEOUT 15000000 static const char *default_codecs = "mpeg,wav"; static const char *roku_codecs = "mpeg,mp4a,wma,wav"; static const char *itunes_codecs = "mpeg,mp4a,mp4v,alac,wav"; // Used for passing errors to DPRINTF (can't count on av_err2str being present) static char errbuf[64]; struct filter_ctx { AVFilterContext *buffersink_ctx; AVFilterContext *buffersrc_ctx; AVFilterGraph *filter_graph; }; struct decode_ctx { // Input format context AVFormatContext *ifmt_ctx; // Will point to the max 3 streams that we will transcode AVStream *audio_stream; AVStream *video_stream; AVStream *subtitle_stream; // Duration (used to make wav header) uint32_t duration; // Data kind (used to determine if ICY metadata is relevant to look for) enum data_kind data_kind; // Contains the most recent packet from av_read_frame // Used for resuming after seek and for freeing correctly // in transcode_decode() AVPacket packet; int resume; int resume_offset; // Used to measure if av_read_frame is taking too long int64_t timestamp; }; struct encode_ctx { // Output format context AVFormatContext *ofmt_ctx; // We use filters to resample struct filter_ctx *filter_ctx; // The ffmpeg muxer writes to this buffer using the avio_evbuffer interface struct evbuffer *obuf; // Maps input stream number -> output stream number // So if we are decoding audio stream 3 and encoding it to 0, then // out_stream_map[3] is 0. A value of -1 means the stream is ignored. int out_stream_map[MAX_STREAMS]; // Maps output stream number -> input stream number unsigned int in_stream_map[MAX_STREAMS]; // Used for seeking int64_t prev_pts[MAX_STREAMS]; int64_t offset_pts[MAX_STREAMS]; // Settings for encoding and muxing const char *format; int encode_video; // Audio settings enum AVCodecID audio_codec; int sample_rate; uint64_t channel_layout; int channels; enum AVSampleFormat sample_format; int byte_depth; // Video settings enum AVCodecID video_codec; int video_height; int video_width; // How many output bytes we have processed in total off_t total_bytes; // Used to check for ICY metadata changes at certain intervals uint32_t icy_interval; uint32_t icy_hash; // WAV header int wavhdr; uint8_t header[44]; }; struct transcode_ctx { struct decode_ctx *decode_ctx; struct encode_ctx *encode_ctx; }; struct decoded_frame { AVFrame *frame; unsigned int stream_index; }; /* -------------------------- PROFILE CONFIGURATION ------------------------ */ static int init_profile(struct encode_ctx *ctx, enum transcode_profile profile) { switch (profile) { case XCODE_PCM16_NOHEADER: case XCODE_PCM16_HEADER: ctx->encode_video = 0; ctx->format = "s16le"; ctx->audio_codec = AV_CODEC_ID_PCM_S16LE; ctx->sample_rate = 44100; ctx->channel_layout = AV_CH_LAYOUT_STEREO; ctx->channels = 2; ctx->sample_format = AV_SAMPLE_FMT_S16; ctx->byte_depth = 2; // Bytes per sample = 16/8 return 0; case XCODE_MP3: ctx->encode_video = 0; ctx->format = "mp3"; ctx->audio_codec = AV_CODEC_ID_MP3; ctx->sample_rate = 44100; ctx->channel_layout = AV_CH_LAYOUT_STEREO; ctx->channels = 2; ctx->sample_format = AV_SAMPLE_FMT_S16P; ctx->byte_depth = 2; // Bytes per sample = 16/8 return 0; case XCODE_H264_AAC: ctx->encode_video = 1; return 0; default: DPRINTF(E_LOG, L_XCODE, "Bug! Unknown transcoding profile\n"); return -1; } } /* -------------------------------- HELPERS -------------------------------- */ static inline char * err2str(int errnum) { av_strerror(errnum, errbuf, sizeof(errbuf)); return errbuf; } static inline void add_le16(uint8_t *dst, uint16_t val) { dst[0] = val & 0xff; dst[1] = (val >> 8) & 0xff; } static inline void add_le32(uint8_t *dst, uint32_t val) { dst[0] = val & 0xff; dst[1] = (val >> 8) & 0xff; dst[2] = (val >> 16) & 0xff; dst[3] = (val >> 24) & 0xff; } static void make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_size) { uint32_t wav_len; int duration; if (src_ctx->duration) duration = src_ctx->duration; else duration = 3 * 60 * 1000; /* 3 minutes, in ms */ wav_len = ctx->channels * ctx->byte_depth * ctx->sample_rate * (duration / 1000); *est_size = wav_len + sizeof(ctx->header); memcpy(ctx->header, "RIFF", 4); add_le32(ctx->header + 4, 36 + wav_len); memcpy(ctx->header + 8, "WAVEfmt ", 8); add_le32(ctx->header + 16, 16); add_le16(ctx->header + 20, 1); add_le16(ctx->header + 22, ctx->channels); /* channels */ add_le32(ctx->header + 24, ctx->sample_rate); /* samplerate */ add_le32(ctx->header + 28, ctx->sample_rate * ctx->channels * ctx->byte_depth); /* byte rate */ add_le16(ctx->header + 32, ctx->channels * ctx->byte_depth); /* block align */ add_le16(ctx->header + 34, ctx->byte_depth * 8); /* bits per sample */ memcpy(ctx->header + 36, "data", 4); add_le32(ctx->header + 40, wav_len); } /* * Returns true if in_stream is a stream we should decode, otherwise false * * @in ctx Decode context * @in in_stream Pointer to AVStream * @return True if stream should be decoded, otherwise false */ static int decode_stream(struct decode_ctx *ctx, AVStream *in_stream) { return ((in_stream == ctx->audio_stream) || (in_stream == ctx->video_stream) || (in_stream == ctx->subtitle_stream)); } /* * Called by libavformat while demuxing. Used to interrupt/unblock av_read_frame * in case a source (especially a network stream) becomes unavailable. * * @in arg Will point to the decode context * @return Non-zero if av_read_frame should be interrupted */ static int decode_interrupt_cb(void *arg) { struct decode_ctx *ctx; ctx = (struct decode_ctx *)arg; if (av_gettime() - ctx->timestamp > READ_TIMEOUT) { DPRINTF(E_LOG, L_XCODE, "Timeout while reading source (connection problem?)\n"); return 1; } return 0; } /* Will read the next packet from the source, unless we are in resume mode, in * which case the most recent packet will be returned, but with an adjusted data * pointer. Use ctx->resume and ctx->resume_offset to make the function resume * from the most recent packet. * * @out packet Pointer to an already allocated AVPacket. The content of the * packet will be updated, and packet->data is pointed to the data * returned by av_read_frame(). The packet struct is owned by the * caller, but *not* packet->data, so don't free the packet with * av_free_packet()/av_packet_unref() * @out stream Set to the input AVStream corresponding to the packet * @out stream_index * Set to the input stream index corresponding to the packet * @in ctx Decode context * @return 0 if OK, < 0 on error or end of file */ static int read_packet(AVPacket *packet, AVStream **stream, unsigned int *stream_index, struct decode_ctx *ctx) { AVStream *in_stream; int ret; do { if (ctx->resume) { // Copies packet struct, but not actual packet payload, and adjusts // data pointer to somewhere inside the payload if resume_offset is set *packet = ctx->packet; packet->data += ctx->resume_offset; packet->size -= ctx->resume_offset; ctx->resume = 0; } else { // We are going to read a new packet from source, so now it is safe to // discard the previous packet and reset resume_offset av_packet_unref(&ctx->packet); ctx->resume_offset = 0; ctx->timestamp = av_gettime(); ret = av_read_frame(ctx->ifmt_ctx, &ctx->packet); if (ret < 0) { DPRINTF(E_WARN, L_XCODE, "Could not read frame: %s\n", err2str(ret)); return ret; } *packet = ctx->packet; } in_stream = ctx->ifmt_ctx->streams[packet->stream_index]; } while (!decode_stream(ctx, in_stream)); av_packet_rescale_ts(packet, in_stream->time_base, in_stream->codec->time_base); *stream = in_stream; *stream_index = packet->stream_index; return 0; } static int encode_write_frame(struct encode_ctx *ctx, AVFrame *filt_frame, unsigned int stream_index, int *got_frame) { AVStream *out_stream; AVPacket enc_pkt; int ret; int got_frame_local; if (!got_frame) got_frame = &got_frame_local; out_stream = ctx->ofmt_ctx->streams[stream_index]; // Encode filtered frame enc_pkt.data = NULL; enc_pkt.size = 0; av_init_packet(&enc_pkt); if (out_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) ret = avcodec_encode_audio2(out_stream->codec, &enc_pkt, filt_frame, got_frame); else if (out_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO) ret = avcodec_encode_video2(out_stream->codec, &enc_pkt, filt_frame, got_frame); else return -1; if (ret < 0) return -1; if (!(*got_frame)) return 0; // Prepare packet for muxing enc_pkt.stream_index = stream_index; // This "wonderful" peace of code makes sure that the timestamp never decreases, // even if the user seeked backwards. The muxer will not accept decreasing // timestamps enc_pkt.pts += ctx->offset_pts[stream_index]; if (enc_pkt.pts < ctx->prev_pts[stream_index]) { ctx->offset_pts[stream_index] += ctx->prev_pts[stream_index] - enc_pkt.pts; enc_pkt.pts = ctx->prev_pts[stream_index]; } ctx->prev_pts[stream_index] = enc_pkt.pts; enc_pkt.dts = enc_pkt.pts; //FIXME av_packet_rescale_ts(&enc_pkt, out_stream->codec->time_base, out_stream->time_base); // Mux encoded frame ret = av_interleaved_write_frame(ctx->ofmt_ctx, &enc_pkt); return ret; } #if HAVE_DECL_AV_BUFFERSRC_ADD_FRAME_FLAGS && HAVE_DECL_AV_BUFFERSINK_GET_FRAME static int filter_encode_write_frame(struct encode_ctx *ctx, AVFrame *frame, unsigned int stream_index) { AVFrame *filt_frame; int ret; // Push the decoded frame into the filtergraph if (frame) { ret = av_buffersrc_add_frame_flags(ctx->filter_ctx[stream_index].buffersrc_ctx, frame, 0); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Error while feeding the filtergraph: %s\n", err2str(ret)); return -1; } } // Pull filtered frames from the filtergraph while (1) { filt_frame = av_frame_alloc(); if (!filt_frame) { DPRINTF(E_LOG, L_XCODE, "Out of memory for filt_frame\n"); return -1; } ret = av_buffersink_get_frame(ctx->filter_ctx[stream_index].buffersink_ctx, filt_frame); if (ret < 0) { /* if no more frames for output - returns AVERROR(EAGAIN) * if flushed and no more frames for output - returns AVERROR_EOF * rewrite retcode to 0 to show it as normal procedure completion */ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) ret = 0; av_frame_free(&filt_frame); break; } filt_frame->pict_type = AV_PICTURE_TYPE_NONE; ret = encode_write_frame(ctx, filt_frame, stream_index, NULL); av_frame_free(&filt_frame); if (ret < 0) break; } return ret; } #else static int filter_encode_write_frame(struct encode_ctx *ctx, AVFrame *frame, unsigned int stream_index) { AVFilterBufferRef *picref; AVCodecContext *enc_ctx; AVFrame *filt_frame; int ret; enc_ctx = ctx->ofmt_ctx->streams[stream_index]->codec; // Push the decoded frame into the filtergraph if (frame) { ret = av_buffersrc_write_frame(ctx->filter_ctx[stream_index].buffersrc_ctx, frame); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Error while feeding the filtergraph: %s\n", err2str(ret)); return -1; } } // Pull filtered frames from the filtergraph while (1) { filt_frame = av_frame_alloc(); if (!filt_frame) { DPRINTF(E_LOG, L_XCODE, "Out of memory for filt_frame\n"); return -1; } if (enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO && !(enc_ctx->codec->capabilities & CODEC_CAP_VARIABLE_FRAME_SIZE)) ret = av_buffersink_read_samples(ctx->filter_ctx[stream_index].buffersink_ctx, &picref, enc_ctx->frame_size); else ret = av_buffersink_read(ctx->filter_ctx[stream_index].buffersink_ctx, &picref); if (ret < 0) { /* if no more frames for output - returns AVERROR(EAGAIN) * if flushed and no more frames for output - returns AVERROR_EOF * rewrite retcode to 0 to show it as normal procedure completion */ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) ret = 0; av_frame_free(&filt_frame); break; } avfilter_copy_buf_props(filt_frame, picref); ret = encode_write_frame(ctx, filt_frame, stream_index, NULL); av_frame_free(&filt_frame); avfilter_unref_buffer(picref); if (ret < 0) break; } return ret; } #endif /* Will step through each stream and feed the stream decoder with empty packets * to see if the decoder has more frames lined up. Will return non-zero if a * frame is found. Should be called until it stops returning anything. * * @out frame AVFrame if there was anything to flush, otherwise undefined * @out stream Set to the AVStream where a decoder returned a frame * @out stream_index * Set to the stream index of the stream returning a frame * @in ctx Decode context * @return Non-zero (true) if frame found, otherwise 0 (false) */ static int flush_decoder(AVFrame *frame, AVStream **stream, unsigned int *stream_index, struct decode_ctx *ctx) { AVStream *in_stream; AVPacket dummypacket; int got_frame; int i; memset(&dummypacket, 0, sizeof(AVPacket)); for (i = 0; i < ctx->ifmt_ctx->nb_streams; i++) { in_stream = ctx->ifmt_ctx->streams[i]; if (!decode_stream(ctx, in_stream)) continue; if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) avcodec_decode_audio4(in_stream->codec, frame, &got_frame, &dummypacket); else avcodec_decode_video2(in_stream->codec, frame, &got_frame, &dummypacket); if (!got_frame) continue; DPRINTF(E_DBG, L_XCODE, "Flushing decoders produced a frame from stream %d\n", i); *stream = in_stream; *stream_index = i; return got_frame; } return 0; } static void flush_encoder(struct encode_ctx *ctx, unsigned int stream_index) { int ret; int got_frame; DPRINTF(E_DBG, L_XCODE, "Flushing output stream #%u encoder\n", stream_index); if (!(ctx->ofmt_ctx->streams[stream_index]->codec->codec->capabilities & CODEC_CAP_DELAY)) return; do { ret = encode_write_frame(ctx, NULL, stream_index, &got_frame); } while ((ret == 0) && got_frame); } /* --------------------------- INPUT/OUTPUT INIT --------------------------- */ static int open_input(struct decode_ctx *ctx, const char *path, int decode_video) { AVDictionary *options; AVCodec *decoder; int stream_index; int ret; options = NULL; ctx->ifmt_ctx = avformat_alloc_context();; if (!ctx->ifmt_ctx) { DPRINTF(E_LOG, L_XCODE, "Out of memory for input format context\n"); return -1; } # ifndef HAVE_FFMPEG // Without this, libav is slow to probe some internet streams, which leads to RAOP timeouts if (ctx->data_kind == DATA_KIND_HTTP) ctx->ifmt_ctx->probesize = 64000; # endif if (ctx->data_kind == DATA_KIND_HTTP) av_dict_set(&options, "icy", "1", 0); // TODO Newest versions of ffmpeg have timeout and reconnect options we should use ctx->ifmt_ctx->interrupt_callback.callback = decode_interrupt_cb; ctx->ifmt_ctx->interrupt_callback.opaque = ctx; ctx->timestamp = av_gettime(); ret = avformat_open_input(&ctx->ifmt_ctx, path, NULL, &options); if (options) av_dict_free(&options); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot open '%s': %s\n", path, err2str(ret)); return -1; } ret = avformat_find_stream_info(ctx->ifmt_ctx, NULL); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot find stream information: %s\n", err2str(ret)); goto out_fail; } if (ctx->ifmt_ctx->nb_streams > MAX_STREAMS) { DPRINTF(E_LOG, L_XCODE, "File '%s' has too many streams (%u)\n", path, ctx->ifmt_ctx->nb_streams); goto out_fail; } // Find audio stream and open decoder stream_index = av_find_best_stream(ctx->ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0); if ((stream_index < 0) || (!decoder)) { DPRINTF(E_LOG, L_XCODE, "Did not find audio stream or suitable decoder for %s\n", path); goto out_fail; } ctx->ifmt_ctx->streams[stream_index]->codec->request_sample_fmt = AV_SAMPLE_FMT_S16; ctx->ifmt_ctx->streams[stream_index]->codec->request_channel_layout = AV_CH_LAYOUT_STEREO; // Disabled to see if it is still required // if (decoder->capabilities & CODEC_CAP_TRUNCATED) // ctx->ifmt_ctx->streams[stream_index]->codec->flags |= CODEC_FLAG_TRUNCATED; ret = avcodec_open2(ctx->ifmt_ctx->streams[stream_index]->codec, decoder, NULL); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Failed to open decoder for stream #%d: %s\n", stream_index, err2str(ret)); goto out_fail; } ctx->audio_stream = ctx->ifmt_ctx->streams[stream_index]; // If no video then we are all done if (!decode_video) return 0; // Find video stream and open decoder stream_index = av_find_best_stream(ctx->ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0); if ((stream_index < 0) || (!decoder)) { DPRINTF(E_LOG, L_XCODE, "Did not find video stream or suitable decoder for '%s': %s\n", path, err2str(ret)); return 0; } ret = avcodec_open2(ctx->ifmt_ctx->streams[stream_index]->codec, decoder, NULL); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Failed to open decoder for stream #%d: %s\n", stream_index, err2str(ret)); return 0; } ctx->video_stream = ctx->ifmt_ctx->streams[stream_index]; // Find a (random) subtitle stream which will be remuxed stream_index = av_find_best_stream(ctx->ifmt_ctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, NULL, 0); if (stream_index >= 0) { ctx->subtitle_stream = ctx->ifmt_ctx->streams[stream_index]; } return 0; out_fail: avformat_close_input(&ctx->ifmt_ctx); return -1; } static void close_input(struct decode_ctx *ctx) { if (ctx->audio_stream) avcodec_close(ctx->audio_stream->codec); if (ctx->video_stream) avcodec_close(ctx->video_stream->codec); avformat_close_input(&ctx->ifmt_ctx); } static int open_output(struct encode_ctx *ctx, struct decode_ctx *src_ctx) { AVStream *out_stream; AVStream *in_stream; AVCodecContext *dec_ctx; AVCodecContext *enc_ctx; AVCodec *encoder; const AVCodecDescriptor *codec_desc; enum AVCodecID codec_id; int ret; int i; ctx->ofmt_ctx = NULL; avformat_alloc_output_context2(&ctx->ofmt_ctx, NULL, ctx->format, NULL); if (!ctx->ofmt_ctx) { DPRINTF(E_LOG, L_XCODE, "Could not create output context\n"); return -1; } ctx->obuf = evbuffer_new(); if (!ctx->obuf) { DPRINTF(E_LOG, L_XCODE, "Could not create output evbuffer\n"); goto out_fail_evbuf; } ctx->ofmt_ctx->pb = avio_output_evbuffer_open(ctx->obuf); if (!ctx->ofmt_ctx->pb) { DPRINTF(E_LOG, L_XCODE, "Could not create output avio pb\n"); goto out_fail_pb; } for (i = 0; i < src_ctx->ifmt_ctx->nb_streams; i++) { in_stream = src_ctx->ifmt_ctx->streams[i]; if (!decode_stream(src_ctx, in_stream)) { ctx->out_stream_map[i] = -1; continue; } out_stream = avformat_new_stream(ctx->ofmt_ctx, NULL); if (!out_stream) { DPRINTF(E_LOG, L_XCODE, "Failed allocating output stream\n"); goto out_fail_stream; } ctx->out_stream_map[i] = out_stream->index; ctx->in_stream_map[out_stream->index] = i; dec_ctx = in_stream->codec; enc_ctx = out_stream->codec; // TODO Enough to just remux subtitles? if (dec_ctx->codec_type == AVMEDIA_TYPE_SUBTITLE) { avcodec_copy_context(enc_ctx, dec_ctx); continue; } if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) codec_id = ctx->audio_codec; else if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) codec_id = ctx->video_codec; else continue; codec_desc = avcodec_descriptor_get(codec_id); encoder = avcodec_find_encoder(codec_id); if (!encoder) { if (codec_desc) DPRINTF(E_LOG, L_XCODE, "Necessary encoder (%s) for input stream %u not found\n", codec_desc->name, i); else DPRINTF(E_LOG, L_XCODE, "Necessary encoder (unknown) for input stream %u not found\n", i); goto out_fail_stream; } if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { enc_ctx->sample_rate = ctx->sample_rate; enc_ctx->channel_layout = ctx->channel_layout; enc_ctx->channels = ctx->channels; enc_ctx->sample_fmt = ctx->sample_format; enc_ctx->time_base = (AVRational){1, ctx->sample_rate}; } else { enc_ctx->height = ctx->video_height; enc_ctx->width = ctx->video_width; enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio; //FIXME enc_ctx->pix_fmt = avcodec_find_best_pix_fmt_of_list(encoder->pix_fmts, dec_ctx->pix_fmt, 1, NULL); enc_ctx->time_base = dec_ctx->time_base; } ret = avcodec_open2(enc_ctx, encoder, NULL); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot open encoder (%s) for input stream #%u: %s\n", codec_desc->name, i, err2str(ret)); goto out_fail_codec; } if (ctx->ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) enc_ctx->flags |= CODEC_FLAG_GLOBAL_HEADER; } // Notice, this will not write WAV header (so we do that manually) ret = avformat_write_header(ctx->ofmt_ctx, NULL); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Error writing header to output buffer: %s\n", err2str(ret)); goto out_fail_write; } return 0; out_fail_write: out_fail_codec: for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) { enc_ctx = ctx->ofmt_ctx->streams[i]->codec; if (enc_ctx) avcodec_close(enc_ctx); } out_fail_stream: avio_evbuffer_close(ctx->ofmt_ctx->pb); out_fail_pb: evbuffer_free(ctx->obuf); out_fail_evbuf: avformat_free_context(ctx->ofmt_ctx); return -1; } static void close_output(struct encode_ctx *ctx) { int i; for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) { if (ctx->ofmt_ctx->streams[i]->codec) avcodec_close(ctx->ofmt_ctx->streams[i]->codec); } avio_evbuffer_close(ctx->ofmt_ctx->pb); evbuffer_free(ctx->obuf); avformat_free_context(ctx->ofmt_ctx); } #if HAVE_DECL_AVFILTER_GRAPH_PARSE_PTR static int open_filter(struct filter_ctx *filter_ctx, AVCodecContext *dec_ctx, AVCodecContext *enc_ctx, const char *filter_spec) { AVFilter *buffersrc = NULL; AVFilter *buffersink = NULL; AVFilterContext *buffersrc_ctx = NULL; AVFilterContext *buffersink_ctx = NULL; AVFilterInOut *outputs = avfilter_inout_alloc(); AVFilterInOut *inputs = avfilter_inout_alloc(); AVFilterGraph *filter_graph = avfilter_graph_alloc(); char args[512]; int ret; if (!outputs || !inputs || !filter_graph) { DPRINTF(E_LOG, L_XCODE, "Out of memory for filter_graph, input or output\n"); goto out_fail; } if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) { buffersrc = avfilter_get_by_name("buffer"); buffersink = avfilter_get_by_name("buffersink"); if (!buffersrc || !buffersink) { DPRINTF(E_LOG, L_XCODE, "Filtering source or sink element not found\n"); goto out_fail; } snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt, dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_aspect_ratio.num, dec_ctx->sample_aspect_ratio.den); ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create buffer source: %s\n", err2str(ret)); goto out_fail; } ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create buffer sink: %s\n", err2str(ret)); goto out_fail; } ret = av_opt_set_bin(buffersink_ctx, "pix_fmts", (uint8_t*)&enc_ctx->pix_fmt, sizeof(enc_ctx->pix_fmt), AV_OPT_SEARCH_CHILDREN); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot set output pixel format: %s\n", err2str(ret)); goto out_fail; } } else if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { buffersrc = avfilter_get_by_name("abuffer"); buffersink = avfilter_get_by_name("abuffersink"); if (!buffersrc || !buffersink) { DPRINTF(E_LOG, L_XCODE, "Filtering source or sink element not found\n"); goto out_fail; } if (!dec_ctx->channel_layout) dec_ctx->channel_layout = av_get_default_channel_layout(dec_ctx->channels); snprintf(args, sizeof(args), "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64, dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_rate, av_get_sample_fmt_name(dec_ctx->sample_fmt), dec_ctx->channel_layout); ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer source: %s\n", err2str(ret)); goto out_fail; } ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer sink: %s\n", err2str(ret)); goto out_fail; } ret = av_opt_set_bin(buffersink_ctx, "sample_fmts", (uint8_t*)&enc_ctx->sample_fmt, sizeof(enc_ctx->sample_fmt), AV_OPT_SEARCH_CHILDREN); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot set output sample format: %s\n", err2str(ret)); goto out_fail; } ret = av_opt_set_bin(buffersink_ctx, "channel_layouts", (uint8_t*)&enc_ctx->channel_layout, sizeof(enc_ctx->channel_layout), AV_OPT_SEARCH_CHILDREN); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot set output channel layout: %s\n", err2str(ret)); goto out_fail; } ret = av_opt_set_bin(buffersink_ctx, "sample_rates", (uint8_t*)&enc_ctx->sample_rate, sizeof(enc_ctx->sample_rate), AV_OPT_SEARCH_CHILDREN); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot set output sample rate: %s\n", err2str(ret)); goto out_fail; } } else { DPRINTF(E_LOG, L_XCODE, "Bug! Unknown type passed to filter graph init\n"); goto out_fail; } /* Endpoints for the filter graph. */ outputs->name = av_strdup("in"); outputs->filter_ctx = buffersrc_ctx; outputs->pad_idx = 0; outputs->next = NULL; inputs->name = av_strdup("out"); inputs->filter_ctx = buffersink_ctx; inputs->pad_idx = 0; inputs->next = NULL; if (!outputs->name || !inputs->name) { DPRINTF(E_LOG, L_XCODE, "Out of memory for outputs/inputs\n"); goto out_fail; } ret = avfilter_graph_parse_ptr(filter_graph, filter_spec, &inputs, &outputs, NULL); if (ret < 0) goto out_fail; ret = avfilter_graph_config(filter_graph, NULL); if (ret < 0) goto out_fail; /* Fill filtering context */ filter_ctx->buffersrc_ctx = buffersrc_ctx; filter_ctx->buffersink_ctx = buffersink_ctx; filter_ctx->filter_graph = filter_graph; avfilter_inout_free(&inputs); avfilter_inout_free(&outputs); return 0; out_fail: avfilter_graph_free(&filter_graph); avfilter_inout_free(&inputs); avfilter_inout_free(&outputs); return -1; } #else static int open_filter(struct filter_ctx *filter_ctx, AVCodecContext *dec_ctx, AVCodecContext *enc_ctx, const char *filter_spec) { AVFilter *buffersrc = NULL; AVFilter *format = NULL; AVFilter *buffersink = NULL; AVFilterContext *buffersrc_ctx = NULL; AVFilterContext *format_ctx = NULL; AVFilterContext *buffersink_ctx = NULL; AVFilterGraph *filter_graph = avfilter_graph_alloc(); char args[512]; int ret; if (!filter_graph) { DPRINTF(E_LOG, L_XCODE, "Out of memory for filter_graph\n"); goto out_fail; } if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) { buffersrc = avfilter_get_by_name("buffer"); format = avfilter_get_by_name("format"); buffersink = avfilter_get_by_name("buffersink"); if (!buffersrc || !format || !buffersink) { DPRINTF(E_LOG, L_XCODE, "Filtering source, format or sink element not found\n"); goto out_fail; } snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt, dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_aspect_ratio.num, dec_ctx->sample_aspect_ratio.den); ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create buffer source: %s\n", err2str(ret)); goto out_fail; } snprintf(args, sizeof(args), "pix_fmt=%d", enc_ctx->pix_fmt); ret = avfilter_graph_create_filter(&format_ctx, format, "format", args, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create format filter: %s\n", err2str(ret)); goto out_fail; } ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create buffer sink: %s\n", err2str(ret)); goto out_fail; } } else if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { buffersrc = avfilter_get_by_name("abuffer"); format = avfilter_get_by_name("aformat"); buffersink = avfilter_get_by_name("abuffersink"); if (!buffersrc || !format || !buffersink) { DPRINTF(E_LOG, L_XCODE, "Filtering source, format or sink element not found\n"); goto out_fail; } if (!dec_ctx->channel_layout) dec_ctx->channel_layout = av_get_default_channel_layout(dec_ctx->channels); snprintf(args, sizeof(args), "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64, dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_rate, av_get_sample_fmt_name(dec_ctx->sample_fmt), dec_ctx->channel_layout); ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer source: %s\n", err2str(ret)); goto out_fail; } snprintf(args, sizeof(args), "sample_fmts=%s:sample_rates=%d:channel_layouts=0x%"PRIx64, av_get_sample_fmt_name(enc_ctx->sample_fmt), enc_ctx->sample_rate, enc_ctx->channel_layout); ret = avfilter_graph_create_filter(&format_ctx, format, "format", args, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create audio format filter: %s\n", err2str(ret)); goto out_fail; } ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer sink: %s\n", err2str(ret)); goto out_fail; } } else { DPRINTF(E_LOG, L_XCODE, "Bug! Unknown type passed to filter graph init\n"); goto out_fail; } ret = avfilter_link(buffersrc_ctx, 0, format_ctx, 0); if (ret >= 0) ret = avfilter_link(format_ctx, 0, buffersink_ctx, 0); if (ret < 0) DPRINTF(E_LOG, L_XCODE, "Error connecting filters: %s\n", err2str(ret)); ret = avfilter_graph_config(filter_graph, NULL); if (ret < 0) goto out_fail; /* Fill filtering context */ filter_ctx->buffersrc_ctx = buffersrc_ctx; filter_ctx->buffersink_ctx = buffersink_ctx; filter_ctx->filter_graph = filter_graph; return 0; out_fail: avfilter_graph_free(&filter_graph); return -1; } #endif static int open_filters(struct encode_ctx *ctx, struct decode_ctx *src_ctx) { AVCodecContext *enc_ctx; AVCodecContext *dec_ctx; const char *filter_spec; unsigned int stream_index; int i; int ret; ctx->filter_ctx = av_malloc_array(ctx->ofmt_ctx->nb_streams, sizeof(*ctx->filter_ctx)); if (!ctx->filter_ctx) { DPRINTF(E_LOG, L_XCODE, "Out of memory for outputs/inputs\n"); return -1; } for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) { ctx->filter_ctx[i].buffersrc_ctx = NULL; ctx->filter_ctx[i].buffersink_ctx = NULL; ctx->filter_ctx[i].filter_graph = NULL; stream_index = ctx->in_stream_map[i]; enc_ctx = ctx->ofmt_ctx->streams[i]->codec; dec_ctx = src_ctx->ifmt_ctx->streams[stream_index]->codec; if (enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO) filter_spec = "null"; /* passthrough (dummy) filter for video */ else if (enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO) filter_spec = "anull"; /* passthrough (dummy) filter for audio */ else continue; ret = open_filter(&ctx->filter_ctx[i], dec_ctx, enc_ctx, filter_spec); if (ret < 0) goto out_fail; } return 0; out_fail: for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) { if (ctx->filter_ctx && ctx->filter_ctx[i].filter_graph) avfilter_graph_free(&ctx->filter_ctx[i].filter_graph); } av_free(ctx->filter_ctx); return -1; } static void close_filters(struct encode_ctx *ctx) { int i; for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) { if (ctx->filter_ctx && ctx->filter_ctx[i].filter_graph) avfilter_graph_free(&ctx->filter_ctx[i].filter_graph); } av_free(ctx->filter_ctx); } /* ----------------------------- TRANSCODE API ----------------------------- */ /* Setup */ struct decode_ctx * transcode_decode_setup(enum data_kind data_kind, const char *path, uint32_t song_length, int decode_video) { struct decode_ctx *ctx; ctx = calloc(1, sizeof(struct decode_ctx)); if (!ctx) { DPRINTF(E_LOG, L_XCODE, "Out of memory for decode ctx\n"); return NULL; } ctx->duration = song_length; ctx->data_kind = data_kind; if (open_input(ctx, path, decode_video) < 0) { free(ctx); return NULL; } av_init_packet(&ctx->packet); return ctx; } struct encode_ctx * transcode_encode_setup(struct decode_ctx *src_ctx, enum transcode_profile profile, off_t *est_size) { struct encode_ctx *ctx; ctx = calloc(1, sizeof(struct encode_ctx)); if (!ctx) { DPRINTF(E_LOG, L_XCODE, "Out of memory for encode ctx\n"); return NULL; } if ((init_profile(ctx, profile) < 0) || (open_output(ctx, src_ctx) < 0)) { free(ctx); return NULL; } if (open_filters(ctx, src_ctx) < 0) { close_output(ctx); free(ctx); return NULL; } if (src_ctx->data_kind == DATA_KIND_HTTP) ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->channels * ctx->byte_depth * ctx->sample_rate; if (profile == XCODE_PCM16_HEADER) { ctx->wavhdr = 1; make_wav_header(ctx, src_ctx, est_size); } return ctx; } struct transcode_ctx * transcode_setup(enum data_kind data_kind, const char *path, uint32_t song_length, enum transcode_profile profile, off_t *est_size) { struct transcode_ctx *ctx; ctx = malloc(sizeof(struct transcode_ctx)); if (!ctx) { DPRINTF(E_LOG, L_XCODE, "Out of memory for transcode ctx\n"); return NULL; } ctx->decode_ctx = transcode_decode_setup(data_kind, path, song_length, profile & XCODE_HAS_VIDEO); if (!ctx->decode_ctx) { free(ctx); return NULL; } ctx->encode_ctx = transcode_encode_setup(ctx->decode_ctx, profile, est_size); if (!ctx->encode_ctx) { transcode_decode_cleanup(ctx->decode_ctx); free(ctx); return NULL; } return ctx; } struct decode_ctx * transcode_decode_setup_raw(void) { struct decode_ctx *ctx; struct AVCodec *decoder; ctx = calloc(1, sizeof(struct decode_ctx)); if (!ctx) { DPRINTF(E_LOG, L_XCODE, "Out of memory for decode ctx\n"); return NULL; } ctx->ifmt_ctx = avformat_alloc_context(); if (!ctx->ifmt_ctx) { DPRINTF(E_LOG, L_XCODE, "Out of memory for decode format ctx\n"); free(ctx); return NULL; } decoder = avcodec_find_decoder(AV_CODEC_ID_PCM_S16LE); ctx->audio_stream = avformat_new_stream(ctx->ifmt_ctx, decoder); if (!ctx->audio_stream) { DPRINTF(E_LOG, L_XCODE, "Could not create stream with PCM16 decoder\n"); avformat_free_context(ctx->ifmt_ctx); free(ctx); return NULL; } ctx->audio_stream->codec->time_base.num = 1; ctx->audio_stream->codec->time_base.den = 44100; ctx->audio_stream->codec->sample_rate = 44100; ctx->audio_stream->codec->sample_fmt = AV_SAMPLE_FMT_S16; ctx->audio_stream->codec->channel_layout = AV_CH_LAYOUT_STEREO; return ctx; } int transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype) { char *codectype; cfg_t *lib; int size; int i; if (!file_codectype) { DPRINTF(E_LOG, L_XCODE, "Can't determine decode status, codec type is unknown\n"); return -1; } lib = cfg_getsec(cfg, "library"); size = cfg_size(lib, "no_decode"); if (size > 0) { for (i = 0; i < size; i++) { codectype = cfg_getnstr(lib, "no_decode", i); if (strcmp(file_codectype, codectype) == 0) return 0; // Codectype is in no_decode } } size = cfg_size(lib, "force_decode"); if (size > 0) { for (i = 0; i < size; i++) { codectype = cfg_getnstr(lib, "force_decode", i); if (strcmp(file_codectype, codectype) == 0) return 1; // Codectype is in force_decode } } if (!client_codecs) { if (user_agent) { if (strncmp(user_agent, "iTunes", strlen("iTunes")) == 0) client_codecs = itunes_codecs; else if (strncmp(user_agent, "QuickTime", strlen("QuickTime")) == 0) client_codecs = itunes_codecs; // Use iTunes codecs else if (strncmp(user_agent, "Front%20Row", strlen("Front%20Row")) == 0) client_codecs = itunes_codecs; // Use iTunes codecs else if (strncmp(user_agent, "AppleCoreMedia", strlen("AppleCoreMedia")) == 0) client_codecs = itunes_codecs; // Use iTunes codecs else if (strncmp(user_agent, "Roku", strlen("Roku")) == 0) client_codecs = roku_codecs; else if (strncmp(user_agent, "Hifidelio", strlen("Hifidelio")) == 0) /* Allegedly can't transcode for Hifidelio because their * HTTP implementation doesn't honour Connection: close. * At least, that's why mt-daapd didn't do it. */ return 0; } } else DPRINTF(E_DBG, L_XCODE, "Client advertises codecs: %s\n", client_codecs); if (!client_codecs) { DPRINTF(E_DBG, L_XCODE, "Could not identify client, using default codectype set\n"); client_codecs = default_codecs; } if (strstr(client_codecs, file_codectype)) { DPRINTF(E_DBG, L_XCODE, "Codectype supported by client, no decoding needed\n"); return 0; } DPRINTF(E_DBG, L_XCODE, "Will decode\n"); return 1; } /* Cleanup */ void transcode_decode_cleanup(struct decode_ctx *ctx) { av_packet_unref(&ctx->packet); close_input(ctx); free(ctx); } void transcode_encode_cleanup(struct encode_ctx *ctx) { int i; // Flush filters and encoders for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) { if (!ctx->filter_ctx[i].filter_graph) continue; filter_encode_write_frame(ctx, NULL, i); flush_encoder(ctx, i); } av_write_trailer(ctx->ofmt_ctx); close_filters(ctx); close_output(ctx); free(ctx); } void transcode_cleanup(struct transcode_ctx *ctx) { transcode_encode_cleanup(ctx->encode_ctx); transcode_decode_cleanup(ctx->decode_ctx); free(ctx); } void transcode_decoded_free(struct decoded_frame *decoded) { av_frame_free(&decoded->frame); free(decoded); } /* Encoding, decoding and transcoding */ int transcode_decode(struct decoded_frame **decoded, struct decode_ctx *ctx) { AVPacket packet; AVStream *in_stream; AVFrame *frame; unsigned int stream_index; int got_frame; int retry; int ret; int used; // Alloc the frame we will return on success frame = av_frame_alloc(); if (!frame) { DPRINTF(E_LOG, L_XCODE, "Out of memory for decode frame\n"); return -1; } // Loop until we either fail or get a frame retry = 0; do { ret = read_packet(&packet, &in_stream, &stream_index, ctx); if (ret < 0) { // Some decoders need to be flushed, meaning the decoder is to be called // with empty input until no more frames are returned DPRINTF(E_DBG, L_XCODE, "Could not read packet, will flush decoders\n"); got_frame = flush_decoder(frame, &in_stream, &stream_index, ctx); if (got_frame) break; av_frame_free(&frame); if (ret == AVERROR_EOF) return 0; else return -1; } // "used" will tell us how much of the packet was decoded. We may // not get a frame because of insufficient input, in which case we loop to // read another packet. if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) used = avcodec_decode_audio4(in_stream->codec, frame, &got_frame, &packet); else used = avcodec_decode_video2(in_stream->codec, frame, &got_frame, &packet); // decoder returned an error, but maybe the packet was just a bad apple, // so let's try MAX_BAD_PACKETS times before giving up if (used < 0) { DPRINTF(E_DBG, L_XCODE, "Couldn't decode packet\n"); retry += 1; if (retry < MAX_BAD_PACKETS) continue; DPRINTF(E_LOG, L_XCODE, "Couldn't decode packet after %i retries\n", MAX_BAD_PACKETS); av_frame_free(&frame); return -1; } // decoder didn't process the entire packet, so flag a resume, meaning // that the next read_packet() will return this same packet, but where the // data pointer is adjusted with an offset if (used < packet.size) { DPRINTF(E_SPAM, L_XCODE, "Decoder did not finish packet, packet will be resumed\n"); ctx->resume_offset += used; ctx->resume = 1; } } while (!got_frame); if (got_frame > 0) { // Return the decoded frame and stream index *decoded = malloc(sizeof(struct decoded_frame)); if (!(*decoded)) { DPRINTF(E_LOG, L_XCODE, "Out of memory for decoded result\n"); av_frame_free(&frame); return -1; } (*decoded)->frame = frame; (*decoded)->stream_index = stream_index; } return got_frame; } // Filters and encodes int transcode_encode(struct evbuffer *evbuf, struct decoded_frame *decoded, struct encode_ctx *ctx) { int stream_index; int encoded_length; int ret; encoded_length = 0; stream_index = ctx->out_stream_map[decoded->stream_index]; if (stream_index < 0) return -1; if (ctx->wavhdr) { encoded_length += sizeof(ctx->header); evbuffer_add(evbuf, ctx->header, sizeof(ctx->header)); ctx->wavhdr = 0; } ret = filter_encode_write_frame(ctx, decoded->frame, stream_index); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Error occurred: %s\n", err2str(ret)); return ret; } encoded_length += evbuffer_get_length(ctx->obuf); evbuffer_add_buffer(evbuf, ctx->obuf); return encoded_length; } int transcode(struct evbuffer *evbuf, int wanted, struct transcode_ctx *ctx, int *icy_timer) { struct decoded_frame *decoded; int processed; int ret; *icy_timer = 0; processed = 0; while (processed < wanted) { ret = transcode_decode(&decoded, ctx->decode_ctx); if (ret <= 0) return ret; ret = transcode_encode(evbuf, decoded, ctx->encode_ctx); transcode_decoded_free(decoded); if (ret < 0) return -1; processed += ret; } ctx->encode_ctx->total_bytes += processed; if (ctx->encode_ctx->icy_interval) *icy_timer = (ctx->encode_ctx->total_bytes % ctx->encode_ctx->icy_interval < processed); return processed; } struct decoded_frame * transcode_raw2frame(uint8_t *data, size_t size) { struct decoded_frame *decoded; AVFrame *frame; int ret; decoded = malloc(sizeof(struct decoded_frame)); if (!decoded) { DPRINTF(E_LOG, L_XCODE, "Out of memory for decoded struct\n"); return NULL; } frame = av_frame_alloc(); if (!frame) { DPRINTF(E_LOG, L_XCODE, "Out of memory for frame\n"); free(decoded); return NULL; } decoded->stream_index = 0; decoded->frame = frame; frame->nb_samples = size / 4; frame->format = AV_SAMPLE_FMT_S16; frame->channel_layout = AV_CH_LAYOUT_STEREO; #ifdef HAVE_FFMPEG frame->channels = 2; #endif frame->pts = AV_NOPTS_VALUE; frame->sample_rate = 44100; ret = avcodec_fill_audio_frame(frame, 2, frame->format, data, size, 0); if (ret < 0) { DPRINTF(E_LOG, L_XCODE, "Error filling frame with rawbuf: %s\n", err2str(ret)); transcode_decoded_free(decoded); return NULL; } return decoded; } /* TODO remux this frame without reencoding av_packet_rescale_ts(&packet, in_stream->time_base, out_stream->time_base); ret = av_interleaved_write_frame(ctx->ofmt_ctx, &packet); if (ret < 0) goto end;*/ /* Seeking */ int transcode_seek(struct transcode_ctx *ctx, int ms) { struct decode_ctx *decode_ctx; AVStream *in_stream; int64_t start_time; int64_t target_pts; int64_t got_pts; int got_ms; int ret; int i; decode_ctx = ctx->decode_ctx; in_stream = ctx->decode_ctx->audio_stream; start_time = in_stream->start_time; target_pts = ms; target_pts = target_pts * AV_TIME_BASE / 1000; target_pts = av_rescale_q(target_pts, AV_TIME_BASE_Q, in_stream->time_base); if ((start_time != AV_NOPTS_VALUE) && (start_time > 0)) target_pts += start_time; ret = av_seek_frame(decode_ctx->ifmt_ctx, in_stream->index, target_pts, AVSEEK_FLAG_BACKWARD); if (ret < 0) { DPRINTF(E_WARN, L_XCODE, "Could not seek into stream: %s\n", err2str(ret)); return -1; } for (i = 0; i < decode_ctx->ifmt_ctx->nb_streams; i++) { if (decode_stream(decode_ctx, decode_ctx->ifmt_ctx->streams[i])) avcodec_flush_buffers(decode_ctx->ifmt_ctx->streams[i]->codec); // avcodec_flush_buffers(ctx->ofmt_ctx->streams[stream_nb]->codec); } // Fast forward until first packet with a timestamp is found in_stream->codec->skip_frame = AVDISCARD_NONREF; while (1) { av_packet_unref(&decode_ctx->packet); decode_ctx->timestamp = av_gettime(); ret = av_read_frame(decode_ctx->ifmt_ctx, &decode_ctx->packet); if (ret < 0) { DPRINTF(E_WARN, L_XCODE, "Could not read more data while seeking: %s\n", err2str(ret)); in_stream->codec->skip_frame = AVDISCARD_DEFAULT; return -1; } if (decode_ctx->packet.stream_index != in_stream->index) continue; // Need a pts to return the real position if (decode_ctx->packet.pts == AV_NOPTS_VALUE) continue; break; } in_stream->codec->skip_frame = AVDISCARD_DEFAULT; // Tell transcode_decode() to resume with ctx->packet decode_ctx->resume = 1; decode_ctx->resume_offset = 0; // Compute position in ms from pts got_pts = decode_ctx->packet.pts; if ((start_time != AV_NOPTS_VALUE) && (start_time > 0)) got_pts -= start_time; got_pts = av_rescale_q(got_pts, in_stream->time_base, AV_TIME_BASE_Q); got_ms = got_pts / (AV_TIME_BASE / 1000); // Since negative return would mean error, we disallow it here if (got_ms < 0) got_ms = 0; DPRINTF(E_DBG, L_XCODE, "Seek wanted %d ms, got %d ms\n", ms, got_ms); return got_ms; } /* Metadata */ struct http_icy_metadata * transcode_metadata(struct transcode_ctx *ctx, int *changed) { struct http_icy_metadata *m; if (!ctx->decode_ctx->ifmt_ctx) return NULL; m = http_icy_metadata_get(ctx->decode_ctx->ifmt_ctx, 1); if (!m) return NULL; *changed = (m->hash != ctx->encode_ctx->icy_hash); ctx->encode_ctx->icy_hash = m->hash; return m; } forked-daapd-25.0/src/transcode.h000066400000000000000000000060471313447753700167300ustar00rootroot00000000000000 #ifndef __TRANSCODE_H__ #define __TRANSCODE_H__ #include #include "db.h" #include "http.h" #define XCODE_WAVHEADER (1 << 14) #define XCODE_HAS_VIDEO (1 << 15) enum transcode_profile { // Transcodes the best available audio stream into PCM16 (does not add wav header) XCODE_PCM16_NOHEADER = 1, // Transcodes the best available audio stream into PCM16 (with wav header) XCODE_PCM16_HEADER = XCODE_WAVHEADER | 2, // Transcodes the best available audio stream into MP3 XCODE_MP3 = 3, // Transcodes video + audio + subtitle streams (not tested - for future use) XCODE_H264_AAC = XCODE_HAS_VIDEO | 4, }; struct decode_ctx; struct encode_ctx; struct transcode_ctx; struct decoded_frame; // Setting up struct decode_ctx * transcode_decode_setup(enum data_kind data_kind, const char *path, uint32_t song_length, int decode_video); struct encode_ctx * transcode_encode_setup(struct decode_ctx *src_ctx, enum transcode_profile profile, off_t *est_size); struct transcode_ctx * transcode_setup(enum data_kind data_kind, const char *path, uint32_t song_length, enum transcode_profile profile, off_t *est_size); struct decode_ctx * transcode_decode_setup_raw(void); int transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype); // Cleaning up void transcode_decode_cleanup(struct decode_ctx *ctx); void transcode_encode_cleanup(struct encode_ctx *ctx); void transcode_cleanup(struct transcode_ctx *ctx); void transcode_decoded_free(struct decoded_frame *decoded); // Transcoding /* Demuxes and decodes the next packet from the input. * * @out decoded A newly allocated struct with a pointer to the frame and the * stream. Must be freed with transcode_decoded_free(). * @in ctx Decode context * @return Positive if OK, negative if error, 0 if EOF */ int transcode_decode(struct decoded_frame **decoded, struct decode_ctx *ctx); /* Encodes and remuxes a frame. Also resamples if needed. * * @out evbuf An evbuffer filled with remuxed data * @in frame The frame to encode, e.g. from transcode_decode * @in wanted Bytes that the caller wants processed * @in ctx Encode context * @return Length of evbuf if OK, negative if error */ int transcode_encode(struct evbuffer *evbuf, struct decoded_frame *decoded, struct encode_ctx *ctx); /* Demuxes, decodes, encodes and remuxes the next packet from the input. * * @out evbuf An evbuffer filled with remuxed data * @in wanted Bytes that the caller wants processed * @in ctx Transcode context * @out icy_timer True if METADATA_ICY_INTERVAL has elapsed * @return Bytes processed if OK, negative if error, 0 if EOF */ int transcode(struct evbuffer *evbuf, int wanted, struct transcode_ctx *ctx, int *icy_timer); struct decoded_frame * transcode_raw2frame(uint8_t *data, size_t size); // Seeking int transcode_seek(struct transcode_ctx *ctx, int ms); // Metadata struct http_icy_metadata * transcode_metadata(struct transcode_ctx *ctx, int *changed); #endif /* !__TRANSCODE_H__ */ forked-daapd-25.0/src/worker.c000066400000000000000000000113771313447753700162540ustar00rootroot00000000000000/* * Copyright (C) 2014 Espen Jürgensen * * 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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTHREAD_NP_H # include #endif #include #include "db.h" #include "logger.h" #include "worker.h" #include "commands.h" struct worker_arg { void (*cb)(void *); void *cb_arg; int delay; struct event *timer; }; /* --- Globals --- */ // worker thread static pthread_t tid_worker; // Event base, pipes and events struct event_base *evbase_worker; static int g_initialized; static struct commands_base *cmdbase; /* ---------------------------- CALLBACK EXECUTION ------------------------- */ /* Thread: worker */ static void execute_cb(int fd, short what, void *arg) { struct worker_arg *cmdarg = arg; cmdarg->cb(cmdarg->cb_arg); event_free(cmdarg->timer); free(cmdarg->cb_arg); free(cmdarg); } static enum command_state execute(void *arg, int *retval) { struct worker_arg *cmdarg = arg; struct timeval tv = { cmdarg->delay, 0 }; if (cmdarg->delay) { cmdarg->timer = evtimer_new(evbase_worker, execute_cb, cmdarg); evtimer_add(cmdarg->timer, &tv); *retval = 0; return COMMAND_PENDING; // Not done yet, ask caller not to free cmd } cmdarg->cb(cmdarg->cb_arg); free(cmdarg->cb_arg); *retval = 0; return COMMAND_END; } /* --------------------------------- MAIN --------------------------------- */ /* Thread: worker */ static void * worker(void *arg) { int ret; ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_MAIN, "Error: DB init failed (worker thread)\n"); pthread_exit(NULL); } g_initialized = 1; event_base_dispatch(evbase_worker); if (g_initialized) { DPRINTF(E_LOG, L_MAIN, "Worker event loop terminated ahead of time!\n"); g_initialized = 0; } db_perthread_deinit(); pthread_exit(NULL); } /* ---------------------------- Our worker API --------------------------- */ /* Thread: player */ void worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay) { struct worker_arg *cmdarg; void *argcpy; DPRINTF(E_DBG, L_MAIN, "Got worker execute request\n"); cmdarg = calloc(1, sizeof(struct worker_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_MAIN, "Could not allocate worker_arg\n"); return; } if (arg_size > 0) { argcpy = malloc(arg_size); if (!argcpy) { DPRINTF(E_LOG, L_MAIN, "Out of memory\n"); free(cmdarg); return; } memcpy(argcpy, cb_arg, arg_size); } else argcpy = NULL; cmdarg->cb = cb; cmdarg->cb_arg = argcpy; cmdarg->delay = delay; commands_exec_async(cmdbase, execute, cmdarg); } int worker_init(void) { int ret; evbase_worker = event_base_new(); if (!evbase_worker) { DPRINTF(E_LOG, L_MAIN, "Could not create an event base\n"); goto evbase_fail; } cmdbase = commands_base_new(evbase_worker, NULL); ret = pthread_create(&tid_worker, NULL, worker, NULL); if (ret < 0) { DPRINTF(E_LOG, L_MAIN, "Could not spawn worker thread: %s\n", strerror(errno)); goto thread_fail; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_worker, "worker"); #elif defined(HAVE_PTHREAD_SET_NAME_NP) pthread_set_name_np(tid_worker, "worker"); #endif return 0; thread_fail: commands_base_free(cmdbase); event_base_free(evbase_worker); evbase_worker = NULL; evbase_fail: return -1; } void worker_deinit(void) { int ret; g_initialized = 0; commands_base_destroy(cmdbase); ret = pthread_join(tid_worker, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_MAIN, "Could not join worker thread: %s\n", strerror(errno)); return; } // Free event base (should free events too) event_base_free(evbase_worker); } forked-daapd-25.0/src/worker.h000066400000000000000000000016421313447753700162530ustar00rootroot00000000000000 #ifndef __WORKER_H__ #define __WORKER_H__ /* The worker thread is made for running asyncronous tasks from a real time * thread, mainly the player thread. * The worker_execute() function will trigger a callback from the worker thread. * Before returning the function will copy the argument given, so the caller * does not need to preserve them. However, if the argument contains pointers to * data, the caller must either make sure that the data remains valid until the * callback (which can free it), or make sure the callback does not refer to it. * * @param cb the function to call from the worker thread * @param cb_arg arguments for callback * @param arg_size size of the arguments given * @param delay how much in seconds to delay the execution */ void worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay); int worker_init(void); void worker_deinit(void); #endif /* !__WORKER_H__ */