commit 1f592fbbd983a70d77b818c1f2293c3a8200cd07 Author: Christian Muehlhaeuser Date: Sun Oct 17 05:32:01 2010 +0200 Initial Tomahawk import. diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..9d8f8b0b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +qtcreator-build/* +build/* +.directory +*.a +*.o +._* +*.user +Makefile* +moc_* +*~ +/playdar diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..77479e0be --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +PROJECT( tomahawk ) +CMAKE_MINIMUM_REQUIRED( VERSION 2.8 ) + +SET( CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules" ) + +# Check if we need qtgui: +IF( "${gui}" STREQUAL "no" ) + ADD_DEFINITIONS( -DENABLE_HEADLESS ) + MESSAGE( STATUS "Building in HEADLESS mode ***" ) + FIND_PACKAGE( Qt4 4.6.0 COMPONENTS QtCore QtXml QtNetwork REQUIRED ) +ELSE() + MESSAGE( STATUS "Building full GUI version ***" ) + FIND_PACKAGE( Qt4 4.6.0 COMPONENTS QtGui QtCore QtXml QtNetwork REQUIRED ) +ENDIF() + +FIND_PACKAGE( Taglib 1.6.0 REQUIRED ) +FIND_PACKAGE( LibLastFm REQUIRED ) + +IF( "${gui}" STREQUAL "no" ) +ELSE() + IF( UNIX AND NOT APPLE ) + ADD_SUBDIRECTORY( alsa-playback ) + ELSE() + ADD_SUBDIRECTORY( rtaudio ) + ENDIF( UNIX AND NOT APPLE ) +ENDIF() + +ADD_SUBDIRECTORY( libportfwd ) +ADD_SUBDIRECTORY( qxtweb-standalone ) +ADD_SUBDIRECTORY( src ) + diff --git a/CMakeModules/FindLibLastFm.cmake b/CMakeModules/FindLibLastFm.cmake new file mode 100644 index 000000000..1d863b084 --- /dev/null +++ b/CMakeModules/FindLibLastFm.cmake @@ -0,0 +1,42 @@ +# - Find LibLastFM +# Find the liblastfm includes and the liblastfm libraries +# This module defines +# LIBLASTFM_INCLUDE_DIR, root lastfm include dir +# LIBLASTFM_LIBRARY, the path to liblastfm +# LIBLASTFM_FOUND, whether liblastfm was found + + +find_path(LIBLASTFM_INCLUDE_DIR NAMES Audioscrobbler + HINTS + ~/usr/include + /opt/local/include + /usr/include + /usr/local/include + /opt/kde4/include + ${KDE4_INCLUDE_DIR} + PATH_SUFFIXES lastfm +) + +find_library( LIBLASTFM_LIBRARY NAMES lastfm + PATHS + ~/usr/lib + /opt/local/lib + /usr/lib + /usr/lib64 + /usr/local/lib + /opt/kde4/lib + ${KDE4_LIB_DIR} +) + + +if(LIBLASTFM_INCLUDE_DIR AND LIBLASTFM_LIBRARY) + set(LIBLASTFM_FOUND TRUE) + message(STATUS "Found liblastfm: ${LIBLASTFM_INCLUDE_DIR}, ${LIBLASTFM_LIBRARY}") +else(LIBLASTFM_INCLUDE_DIR AND LIBLASTFM_LIBRARY) + set(LIBLASTFM_FOUND FALSE) + if (LIBLASTFM_FIND_REQUIRED) + message(FATAL_ERROR "Could NOT find required package LibLastFm") + endif(LIBLASTFM_FIND_REQUIRED) +endif(LIBLASTFM_INCLUDE_DIR AND LIBLASTFM_LIBRARY) + +mark_as_advanced(LIBLASTFM_INCLUDE_DIR LIBLASTFM_LIBRARY) diff --git a/CMakeModules/FindLibraryWithDebug.cmake b/CMakeModules/FindLibraryWithDebug.cmake new file mode 100644 index 000000000..afa422ddd --- /dev/null +++ b/CMakeModules/FindLibraryWithDebug.cmake @@ -0,0 +1,113 @@ +# +# FIND_LIBRARY_WITH_DEBUG +# -> enhanced FIND_LIBRARY to allow the search for an +# optional debug library with a WIN32_DEBUG_POSTFIX similar +# to CMAKE_DEBUG_POSTFIX when creating a shared lib +# it has to be the second and third argument + +# Copyright (c) 2007, Christian Ehrlicher, +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +MACRO(FIND_LIBRARY_WITH_DEBUG var_name win32_dbg_postfix_name dgb_postfix libname) + + IF(NOT "${win32_dbg_postfix_name}" STREQUAL "WIN32_DEBUG_POSTFIX") + + # no WIN32_DEBUG_POSTFIX -> simply pass all arguments to FIND_LIBRARY + FIND_LIBRARY(${var_name} + ${win32_dbg_postfix_name} + ${dgb_postfix} + ${libname} + ${ARGN} + ) + + ELSE(NOT "${win32_dbg_postfix_name}" STREQUAL "WIN32_DEBUG_POSTFIX") + + IF(NOT WIN32) + # on non-win32 we don't need to take care about WIN32_DEBUG_POSTFIX + + FIND_LIBRARY(${var_name} ${libname} ${ARGN}) + + ELSE(NOT WIN32) + + # 1. get all possible libnames + SET(args ${ARGN}) + SET(newargs "") + SET(libnames_release "") + SET(libnames_debug "") + + LIST(LENGTH args listCount) + + IF("${libname}" STREQUAL "NAMES") + SET(append_rest 0) + LIST(APPEND args " ") + + FOREACH(i RANGE ${listCount}) + LIST(GET args ${i} val) + + IF(append_rest) + LIST(APPEND newargs ${val}) + ELSE(append_rest) + IF("${val}" STREQUAL "PATHS") + LIST(APPEND newargs ${val}) + SET(append_rest 1) + ELSE("${val}" STREQUAL "PATHS") + LIST(APPEND libnames_release "${val}") + LIST(APPEND libnames_debug "${val}${dgb_postfix}") + ENDIF("${val}" STREQUAL "PATHS") + ENDIF(append_rest) + + ENDFOREACH(i) + + ELSE("${libname}" STREQUAL "NAMES") + + # just one name + LIST(APPEND libnames_release "${libname}") + LIST(APPEND libnames_debug "${libname}${dgb_postfix}") + + SET(newargs ${args}) + + ENDIF("${libname}" STREQUAL "NAMES") + + # search the release lib + FIND_LIBRARY(${var_name}_RELEASE + NAMES ${libnames_release} + ${newargs} + ) + + # search the debug lib + FIND_LIBRARY(${var_name}_DEBUG + NAMES ${libnames_debug} + ${newargs} + ) + + IF(${var_name}_RELEASE AND ${var_name}_DEBUG) + + # both libs found + SET(${var_name} optimized ${${var_name}_RELEASE} + debug ${${var_name}_DEBUG}) + + ELSE(${var_name}_RELEASE AND ${var_name}_DEBUG) + + IF(${var_name}_RELEASE) + + # only release found + SET(${var_name} ${${var_name}_RELEASE}) + + ELSE(${var_name}_RELEASE) + + # only debug (or nothing) found + SET(${var_name} ${${var_name}_DEBUG}) + + ENDIF(${var_name}_RELEASE) + + ENDIF(${var_name}_RELEASE AND ${var_name}_DEBUG) + + MARK_AS_ADVANCED(${var_name}_RELEASE) + MARK_AS_ADVANCED(${var_name}_DEBUG) + + ENDIF(NOT WIN32) + + ENDIF(NOT "${win32_dbg_postfix_name}" STREQUAL "WIN32_DEBUG_POSTFIX") + +ENDMACRO(FIND_LIBRARY_WITH_DEBUG) \ No newline at end of file diff --git a/CMakeModules/FindOggVorbis.cmake b/CMakeModules/FindOggVorbis.cmake new file mode 100644 index 000000000..e72d849d2 --- /dev/null +++ b/CMakeModules/FindOggVorbis.cmake @@ -0,0 +1,89 @@ +# - Try to find the OggVorbis libraries +# Once done this will define +# +# OGGVORBIS_FOUND - system has OggVorbis +# OGGVORBIS_VERSION - set either to 1 or 2 +# OGGVORBIS_INCLUDE_DIR - the OggVorbis include directory +# OGGVORBIS_LIBRARIES - The libraries needed to use OggVorbis +# OGG_LIBRARY - The Ogg library +# VORBIS_LIBRARY - The Vorbis library +# VORBISFILE_LIBRARY - The VorbisFile library +# VORBISENC_LIBRARY - The VorbisEnc library + +# Copyright (c) 2006, Richard Laerkaeng, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +include (CheckLibraryExists) + +find_path(VORBIS_INCLUDE_DIR vorbis/vorbisfile.h) +find_path(OGG_INCLUDE_DIR ogg/ogg.h) + +find_library(OGG_LIBRARY NAMES ogg) +find_library(VORBIS_LIBRARY NAMES vorbis) +find_library(VORBISFILE_LIBRARY NAMES vorbisfile) +find_library(VORBISENC_LIBRARY NAMES vorbisenc) + +mark_as_advanced(VORBIS_INCLUDE_DIR OGG_INCLUDE_DIR + OGG_LIBRARY VORBIS_LIBRARY VORBISFILE_LIBRARY VORBISENC_LIBRARY) + + +if (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) + set(OGGVORBIS_FOUND TRUE) + + set(OGGVORBIS_LIBRARIES ${OGG_LIBRARY} ${VORBIS_LIBRARY} ${VORBISFILE_LIBRARY} ${VORBISENC_LIBRARY}) + + set(_CMAKE_REQUIRED_LIBRARIES_TMP ${CMAKE_REQUIRED_LIBRARIES}) + set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} ${OGGVORBIS_LIBRARIES}) + check_library_exists(vorbis vorbis_bitrate_addblock "" HAVE_LIBVORBISENC2) + set(CMAKE_REQUIRED_LIBRARIES ${_CMAKE_REQUIRED_LIBRARIES_TMP}) + + if (HAVE_LIBVORBISENC2) + set (OGGVORBIS_VERSION 2) + else (HAVE_LIBVORBISENC2) + set (OGGVORBIS_VERSION 1) + endif (HAVE_LIBVORBISENC2) + +else (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) + set (OGGVORBIS_VERSION) + set(OGGVORBIS_FOUND FALSE) +endif (VORBIS_INCLUDE_DIR AND VORBIS_LIBRARY AND VORBISFILE_LIBRARY AND VORBISENC_LIBRARY) + + +if (OGGVORBIS_FOUND) + if (NOT OggVorbis_FIND_QUIETLY) + message(STATUS "Found OggVorbis: ${OGGVORBIS_LIBRARIES}") + endif (NOT OggVorbis_FIND_QUIETLY) +else (OGGVORBIS_FOUND) + if (OggVorbis_FIND_REQUIRED) + message(FATAL_ERROR "Could NOT find OggVorbis libraries") + endif (OggVorbis_FIND_REQUIRED) + if (NOT OggVorbis_FIND_QUITELY) + message(STATUS "Could NOT find OggVorbis libraries") + endif (NOT OggVorbis_FIND_QUITELY) +endif (OGGVORBIS_FOUND) + +#check_include_files(vorbis/vorbisfile.h HAVE_VORBISFILE_H) +#check_library_exists(ogg ogg_page_version "" HAVE_LIBOGG) +#check_library_exists(vorbis vorbis_info_init "" HAVE_LIBVORBIS) +#check_library_exists(vorbisfile ov_open "" HAVE_LIBVORBISFILE) +#check_library_exists(vorbisenc vorbis_info_clear "" HAVE_LIBVORBISENC) +#check_library_exists(vorbis vorbis_bitrate_addblock "" HAVE_LIBVORBISENC2) + +#if (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) +# message(STATUS "Ogg/Vorbis found") +# set (VORBIS_LIBS "-lvorbis -logg") +# set (VORBISFILE_LIBS "-lvorbisfile") +# set (VORBISENC_LIBS "-lvorbisenc") +# set (OGGVORBIS_FOUND TRUE) +# if (HAVE_LIBVORBISENC2) +# set (HAVE_VORBIS 2) +# else (HAVE_LIBVORBISENC2) +# set (HAVE_VORBIS 1) +# endif (HAVE_LIBVORBISENC2) +#else (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) +# message(STATUS "Ogg/Vorbis not found") +#endif (HAVE_LIBOGG AND HAVE_VORBISFILE_H AND HAVE_LIBVORBIS AND HAVE_LIBVORBISFILE AND HAVE_LIBVORBISENC) + diff --git a/CMakeModules/FindTaglib.cmake b/CMakeModules/FindTaglib.cmake new file mode 100644 index 000000000..fd4480460 --- /dev/null +++ b/CMakeModules/FindTaglib.cmake @@ -0,0 +1,89 @@ +# - Try to find the Taglib library +# Once done this will define +# +# TAGLIB_FOUND - system has the taglib library +# TAGLIB_CFLAGS - the taglib cflags +# TAGLIB_LIBRARIES - The libraries needed to use taglib + +# Copyright (c) 2006, Laurent Montel, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +IF(TAGLIB_FOUND) + MESSAGE(STATUS "Using manually specified taglib locations") +ELSE() + + if(NOT TAGLIB_MIN_VERSION) + set(TAGLIB_MIN_VERSION "1.6") + endif(NOT TAGLIB_MIN_VERSION) + + if(NOT WIN32) + find_program(TAGLIBCONFIG_EXECUTABLE NAMES taglib-config PATHS + ${BIN_INSTALL_DIR} + ) + endif(NOT WIN32) + + #reset vars + set(TAGLIB_LIBRARIES) + set(TAGLIB_CFLAGS) + +# MESSAGE( STATUS "PATHS: ${PATHS}") + # if taglib-config has been found + if(TAGLIBCONFIG_EXECUTABLE) + + exec_program(${TAGLIBCONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE TAGLIB_VERSION) + + if(TAGLIB_VERSION STRLESS "${TAGLIB_MIN_VERSION}") + message(STATUS "TagLib version not found: version searched :${TAGLIB_MIN_VERSION}, found ${TAGLIB_VERSION}") + set(TAGLIB_FOUND FALSE) + else(TAGLIB_VERSION STRLESS "${TAGLIB_MIN_VERSION}") + + exec_program(${TAGLIBCONFIG_EXECUTABLE} ARGS --libs RETURN_VALUE _return_VALUE OUTPUT_VARIABLE TAGLIB_LIBRARIES) + + exec_program(${TAGLIBCONFIG_EXECUTABLE} ARGS --cflags RETURN_VALUE _return_VALUE OUTPUT_VARIABLE TAGLIB_CFLAGS) + + if(TAGLIB_LIBRARIES AND TAGLIB_CFLAGS) + set(TAGLIB_FOUND TRUE) +# message(STATUS "Found taglib: ${TAGLIB_LIBRARIES}") + endif(TAGLIB_LIBRARIES AND TAGLIB_CFLAGS) + string(REGEX REPLACE " *-I" ";" TAGLIB_INCLUDES "${TAGLIB_CFLAGS}") + endif(TAGLIB_VERSION STRLESS "${TAGLIB_MIN_VERSION}") + mark_as_advanced(TAGLIB_CFLAGS TAGLIB_LIBRARIES TAGLIB_INCLUDES) + + else(TAGLIBCONFIG_EXECUTABLE) + + include(FindLibraryWithDebug) + include(FindPackageHandleStandardArgs) + + find_path(TAGLIB_CFLAGS + NAMES + tag.h + PATH_SUFFIXES taglib + PATHS + ${KDE4_INCLUDE_DIR} + ${INCLUDE_INSTALL_DIR} + ) + + find_library_with_debug(TAGLIB_LIBRARIES + WIN32_DEBUG_POSTFIX d + NAMES tag + PATHS + ${KDE4_LIB_DIR} + ${LIB_INSTALL_DIR} + ) + + find_package_handle_standard_args(Taglib DEFAULT_MSG + TAGLIB_INCLUDES TAGLIB_LIBRARIES) + endif(TAGLIBCONFIG_EXECUTABLE) +ENDIF() + +if(TAGLIB_FOUND) + if(NOT Taglib_FIND_QUIETLY AND TAGLIBCONFIG_EXECUTABLE) + message(STATUS "Found TagLib: ${TAGLIB_LIBRARIES}") + endif(NOT Taglib_FIND_QUIETLY AND TAGLIBCONFIG_EXECUTABLE) +else(TAGLIB_FOUND) + if(Taglib_FIND_REQUIRED) + message(FATAL_ERROR "Could not find Taglib") + endif(Taglib_FIND_REQUIRED) +endif(TAGLIB_FOUND) diff --git a/CMakeModules/README.txt b/CMakeModules/README.txt new file mode 100644 index 000000000..a9428939c --- /dev/null +++ b/CMakeModules/README.txt @@ -0,0 +1 @@ +FindTaglib.cmake taken from KDE4 kdelibs/cmake/Modules diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..94a045322 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/README b/README new file mode 100644 index 000000000..1e5b26c56 --- /dev/null +++ b/README @@ -0,0 +1,87 @@ +Quickstart on Ubuntu +-------------------- + + sudo apt-get install build-essential cmake libtag1c2a libtag1-dev liblastfm-dev \ + libqt4-dev libqt4-sql-sqlite libvorbis-dev libmad0-dev \ + libasound2-dev libboost-dev zlib1g-dev libgnutls-dev pkg-config + + +Gloox 1.0 (XMPP library) +------------------------ + See: http://camaya.net/glooxdownload + You need to build gloox 1.0 from source, Ubuntu 10.04 only packages v0.9. + + $ # Download and unpack tarball + $ CXXFLAGS=-fPIC ./configure --without-openssl --with-gnutls --without-libidn --with-zlib --without-examples --without-tests + $ CXXFLAGS=-fPIC make + $ sudo make install + +QJson (Qt JSON library) +----------------------- + On Ubuntu 10.04: + $ sudo apt-get install libqjson-dev + + Otherwise: + See: http://sourceforge.net/projects/qjson/files/ (developed using 0.7.1) + + $ # Download and unpack tarball + $ ./configure && make + $ sudo make install + +Now compile Tomahawk +------------------- + $ sudo ldconfig -v | grep -Ei 'qjson|gloox' + $ mkdir build + $ cd build + $ cmake .. + $ make + $ cd .. + $ ./tomahawk + + +Dependencies +------------ + + CMake 2.8.0 http://www.cmake.org/ + Qt 4.6.2 http://qt.nokia.com/ + QJson 0.7.1 http://qjson.sourceforge.net/ + Gloox 1.0 (0.9.x will fail) http://camaya.net/gloox/ + SQLite 3.6.22 http://www.sqlite.org/ + TagLib 1.6.2 http://developer.kde.org/~wheeler/taglib.html + Boost 1.3x http://www.boost.org/ + + Unless you enable the headless mode (no GUI), we also require the following libraries: + + libmad 0.15.1b http://www.underbit.com/products/mad/ + libvorbis 1.2.3 http://xiph.org/vorbis/ + libogg 1.1.4 http://xiph.org/ogg/ + liblastfm 0.3.0 http://github.com/mxcl/liblastfm/ + + Third party libraries that we ship with our source: + + RtAudio 4.0.7 http://www.music.mcgill.ca/~gary/rtaudio/ + MiniUPnP http://miniupnp.free.fr/ + + +To build the app: +----------------- + $ mkdir build + $ cd build + + (Pick one of the following two choices. If unsure pick the second one, you probably want a GUI) + $ cmake -Dgui=no .. # enables headless mode, build without GUI + $ cmake .. # normal build including GUI + + $ make + +To run the app: +--------------- + $ cd .. # return to the top-level tomahawk dir + + (Only run the next two commands if you installed any of the dependencies from source on Linux) + $ export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + $ sudo ldconfig -v + + $ ./tomahawk + +Enjoy! diff --git a/admin/mac/Info.plist b/admin/mac/Info.plist new file mode 100644 index 000000000..3187e4438 --- /dev/null +++ b/admin/mac/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + tomahawk + CFBundleIdentifier + org.tomahawk.Tomahawk + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleVersion + 0.0.1.0 + CFBundleShortVersionString + 0.0.1 + CFBundleSignature + tomahawk + CFBundleIconFile + tomahawk.icns + CFBundleName + Tomahawk + LSMinimumSystemVersion + 10.5.0 + + diff --git a/admin/mac/add-Qt-to-bundle.sh b/admin/mac/add-Qt-to-bundle.sh new file mode 100755 index 000000000..74d062dac --- /dev/null +++ b/admin/mac/add-Qt-to-bundle.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# author: max@last.fm +# usage: Run from inside the bundle root directory, eg. Last.fm.app +# The first parameter should be the QtFrameworks to copy. +# Remaining parameters are plugins to copy, directories and files are +# valid. +# eg: add-Qt-to-bundle.sh 'QtCore QtGui QtXml' \ +# imageformats \ +# sqldrivers/libsqlite.dylib +################################################################################ + + +if [[ ! -d "$QTDIR/lib/QtCore.framework" ]] +then + # this dir is the location of install for the official Trolltech dmg + if [[ -d /Library/Frameworks/QtCore.framework ]] + then + QT_FRAMEWORKS_DIR=/Library/Frameworks + QT_PLUGINS_DIR=/Developer/Applications/Qt/plugins + fi +elif [[ $QTDIR ]] +then + QT_FRAMEWORKS_DIR="$QTDIR/lib" + QT_PLUGINS_DIR="$QTDIR/plugins" +fi + +if [ -z $QTDIR ] +then + echo QTDIR must be set, or install the official Qt dmg + exit 1 +fi +################################################################################ + + +#first frameworks +mkdir -p Contents/Frameworks +for x in $1 +do + echo "C $x" + cp -R $QT_FRAMEWORKS_DIR/$x.framework Contents/Frameworks/ +done + +#plugins +shift +mkdir -p Contents/MacOS +while (( "$#" )) +do + echo "C $1" + + if [[ -d $QT_PLUGINS_DIR/$1 ]] + then + cp -R $QT_PLUGINS_DIR/$1 Contents/MacOS + else + dir=$(basename $(dirname $1)) + mkdir Contents/MacOS/$dir + cp $QT_PLUGINS_DIR/$1 Contents/MacOS/$dir + fi + + shift +done + +#cleanup +find Contents/Frameworks -name Headers -o -name \*.prl -o -name \*_debug | xargs rm -rf +find Contents -name \*_debug -o -name \*_debug.dylib | xargs rm diff --git a/admin/mac/build-release-osx.sh b/admin/mac/build-release-osx.sh new file mode 100755 index 000000000..ebf2b780e --- /dev/null +++ b/admin/mac/build-release-osx.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Usage: dist/build-relese-osx.sh [-j] [--no-clean] +# +# Adding the -j parameter results in building a japanese version. +################################################################################ + + +function header { + echo -e "\033[0;34m==>\033[0;0;1m $1 \033[0;0m" +} + +function die { + exit_code=$? + echo $1 + exit $exit_code +} +################################################################################ + + +ROOT=`pwd` + +QTDIR=`which qmake` +QTDIR=`dirname $QTDIR` +QTDIR=`dirname $QTDIR` +test -L "$QTDIR" && QTDIR=`readlink $QTDIR` + +export QMAKESPEC='macx-g++' +export QTDIR +export VERSION +################################################################################ + + +CLEAN='1' +BUILD='1' +NOTQUICK='1' +CREATEDMG='1' + + header addQt + cd tomahawk.app +# $ROOT/admin/mac/add-Qt-to-bundle.sh \ +# 'QtCore QtGui QtXml QtNetwork QtSql' + + header deposx + $ROOT/admin/mac/deposx.sh + + header Done! + diff --git a/admin/mac/deposx.sh b/admin/mac/deposx.sh new file mode 100755 index 000000000..802576a34 --- /dev/null +++ b/admin/mac/deposx.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# author: max@last.fm, chris@last.fm +################################################################################ + + +if [ -z $QTDIR ] +then + echo QTDIR must be set + exit 1 +fi + +cd Contents + +QTLIBS=`ls Frameworks | cut -d. -f1` +LIBS=`cd MacOS && ls -fR1 | grep dylib` +################################################################################ + + +function deposx_change +{ + echo "D \`$1'" + echo $QTDIR + + + for y in $QTLIBS + do + install_name_tool -change $QTDIR/lib/$y.framework/Versions/4/$y \ + @executable_path/../Frameworks/$y.framework/Versions/4/$y \ + "$1" + + install_name_tool -change $QTDIR/Cellar/qt/4.6.2/lib/$y.framework/Versions/4/$y \ + @executable_path/../Frameworks/$y.framework/Versions/4/$y \ + "$1" + done + + for y in $LIBS + do + install_name_tool -change $y \ + @executable_path/$y \ + "$1" + done +} +################################################################################ + + +# first all libraries and executables +find MacOS -type f -a -perm -100 | while read x +do + echo $x + y=$(file "$x" | grep 'Mach-O') + test -n "$y" && deposx_change "$x" + + install_name_tool -change liblastfm.0.dylib @executable_path/liblastfm.0.dylib $x + install_name_tool -change /usr/local/Cellar/gloox/1.0/lib/libgloox.8.dylib @executable_path/libgloox.8.dylib $x + install_name_tool -change /usr/local/lib/libgloox.8.dylib @executable_path/libgloox.8.dylib $x + install_name_tool -change /usr/local/Cellar/taglib/1.6/lib/libtag.1.dylib @executable_path/libtag.1.dylib $x + install_name_tool -change /usr/local/Cellar/libogg/1.2.0/lib/libogg.0.dylib @executable_path/libogg.0.dylib $x + install_name_tool -change /usr/local/Cellar/libvorbis/1.3.1/lib/libvorbisfile.3.dylib @executable_path/libvorbisfile.3.dylib $x + install_name_tool -change /usr/local/Cellar/mad/0.15.1b/lib/libmad.0.dylib @executable_path/libmad.0.dylib $x +done + +deposx_change MacOS/libqjson.0.7.1.dylib +deposx_change MacOS/liblastfm.0.dylib + +# now Qt +for x in $QTLIBS +do + echo `pwd` +# ls -l Frameworks/$x.framework/Versions/4/$x + deposx_change Frameworks/$x.framework/Versions/4/$x + install_name_tool -id @executable_path/../Frameworks/$x.framework/Versions/4/$x \ + Frameworks/$x.framework/Versions/4/$x +done diff --git a/admin/win/README.txt b/admin/win/README.txt new file mode 100755 index 000000000..ceb98bd3d --- /dev/null +++ b/admin/win/README.txt @@ -0,0 +1,3 @@ +# windres.exe tomahawk.rx -O coff -o tomahawk.res + +# SEE: http://stackoverflow.com/questions/708238/how-do-i-add-an-icon-to-a-mingw-gcc-compiled-executable \ No newline at end of file diff --git a/admin/win/tomahawk.nsi b/admin/win/tomahawk.nsi new file mode 100755 index 000000000..0012132a3 --- /dev/null +++ b/admin/win/tomahawk.nsi @@ -0,0 +1,124 @@ +; assuming the script is in ROOT/admin/win/ +!define ROOTDIR "../.." + +!include "MUI2.nsh" + +Name "Tomahawk" +!define MUI_NAME "Tomahawk" +!define MUI_PRODUCT "Tomahawk" +!define MUI_FILE "Tomahawk" +!define MUI_VERSION "Alpha" +!define MUI_BRANDINGTEXT "Tomahawk-Player Alpha Test" +CRCCheck On + + + +OutFile "tomahawk-setup-alpha.exe" +;ShowInstDetails "nevershow" +ShowUninstDetails "nevershow" +;SetCompressor "bzip2" + +!define MUI_ICON "..\..\data\icons\tomahawk.ico" +!define MUI_UNICON "..\..\data\icons\tomahawk.ico" +;!define MUI_SPECIALBITMAP "Bitmap.bmp" + + +InstallDir "$PROGRAMFILES\${MUI_PRODUCT}" + +;-------------------------------- +;Modern UI Configuration +!define MUI_WELCOMEPAGE_TEXT "This is an Alpha release, and is still buggy.$\n$\nPlease join #tomahawk-player on irc.freenode.net" +!insertmacro MUI_PAGE_WELCOME + +!insertmacro MUI_PAGE_LICENSE "${ROOTDIR}\LICENSE.txt" +;;!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES + +!define MUI_FINISHPAGE_RUN "$INSTDIR\tomahawk.exe" +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_LANGUAGE "English" + +;Modern UI System +;!insertmacro MUI_SYSTEM + +LicenseData "${ROOTDIR}\LICENSE.txt" + +Section "install" + +;Add files +SetOutPath "$INSTDIR" + +;Path to our DLL cache +!define DLLS "${ROOTDIR}\admin\win\dlls" + +File "${ROOTDIR}\build\tomahawk.exe" +File "${ROOTDIR}\LICENSE.txt" + +; QT stuff: +File "${DLLS}\QtCore4.dll" +File "${DLLS}\QtGui4.dll" +File "${DLLS}\QtNetwork4.dll" +File "${DLLS}\QtSql4.dll" +File "${DLLS}\QtXml4.dll" +SetOutPath "$INSTDIR\sqldrivers" +File "${DLLS}\sqldrivers\qsqlite4.dll" +SetOutPath "$INSTDIR" + +; Cygwin/c++ stuff +File "${DLLS}\cygmad-0.dll" +File "${DLLS}\libgcc_s_dw2-1.dll" +File "${DLLS}\mingwm10.dll" + +; Audio stuff +File "${DLLS}\libmad.dll" +File "${DLLS}\librtaudio.dll" + +; Other +File "${DLLS}\libqjson.dll" +File "${DLLS}\libqxtweb-standalone.dll" +File "${DLLS}\libtag.dll" + +;create desktop shortcut +CreateShortCut "$DESKTOP\${MUI_PRODUCT}.lnk" "$INSTDIR\${MUI_FILE}.exe" "" + +;create start-menu items +CreateDirectory "$SMPROGRAMS\${MUI_PRODUCT}" +CreateShortCut "$SMPROGRAMS\${MUI_PRODUCT}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 +CreateShortCut "$SMPROGRAMS\${MUI_PRODUCT}\${MUI_PRODUCT}.lnk" "$INSTDIR\${MUI_FILE}.exe" "" "$INSTDIR\${MUI_FILE}.exe" 0 + +;write uninstall information to the registry +WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" "DisplayName" "${MUI_PRODUCT} (remove only)" +WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" "UninstallString" "$INSTDIR\Uninstall.exe" + +WriteUninstaller "$INSTDIR\Uninstall.exe" + +SectionEnd + + +;-------------------------------- +;Uninstaller Section +Section "Uninstall" + +;Delete Files +RMDir /r "$INSTDIR\*.*" + +;Remove the installation directory +RMDir "$INSTDIR" + +;Delete Start Menu Shortcuts +Delete "$DESKTOP\${MUI_PRODUCT}.lnk" +Delete "$SMPROGRAMS\${MUI_PRODUCT}\*.*" +RmDir "$SMPROGRAMS\${MUI_PRODUCT}" + +;Delete Uninstaller And Unistall Registry Entries +DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\${MUI_PRODUCT}" +DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" + +SectionEnd + + +Function un.onUninstSuccess + MessageBox MB_OK "You have successfully uninstalled ${MUI_PRODUCT}." +FunctionEnd diff --git a/admin/win/tomahawk.rc b/admin/win/tomahawk.rc new file mode 100644 index 000000000..1cd4630ac --- /dev/null +++ b/admin/win/tomahawk.rc @@ -0,0 +1 @@ +ID ICON "data/tomahawk_logo_32x32.ico" diff --git a/alsa-playback/CMakeLists.txt b/alsa-playback/CMakeLists.txt new file mode 100644 index 000000000..d080ebaa6 --- /dev/null +++ b/alsa-playback/CMakeLists.txt @@ -0,0 +1,44 @@ +PROJECT(alsaplayback) + +find_package( Qt4 REQUIRED ) + +include( ${QT_USE_FILE} ) + +CMAKE_MINIMUM_REQUIRED(VERSION 2.6 FATAL_ERROR) +SET(CMAKE_VERBOSE_MAKEFILE ON) +#SET(CMAKE_INSTALL_PREFIX ".") + +SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") + +#ADD_DEFINITIONS(-Wall -O2 -DNDEBUG) +#ADD_DEFINITIONS(-fPIC) + +SET(AUDIO_LIBS "") + +if(UNIX AND NOT APPLE) + SET(AUDIO_LIBS "asound") +endif(UNIX AND NOT APPLE) + +set( alsaplaybackSources + alsaplayback.cpp + alsaaudio.cpp + xconvert.c +) + +set( alsaplaybackHeaders + alsaplayback.h +) + +qt4_wrap_cpp( alsaplaybackMoc ${alsaplaybackHeaders} ) +SET(final_src ${alsaplaybackMoc} ${alsaplaybackSources} ${alsaplaybackHeaders}) + +ADD_LIBRARY(alsaplayback STATIC ${final_src}) + +target_link_libraries( alsaplayback + ${QT_LIBRARIES} + ${AUDIO_LIBS} +) + +#INSTALL(TARGETS alsaplayback ARCHIVE DESTINATION lib) diff --git a/alsa-playback/alsaaudio.cpp b/alsa-playback/alsaaudio.cpp new file mode 100644 index 000000000..542fd64d4 --- /dev/null +++ b/alsa-playback/alsaaudio.cpp @@ -0,0 +1,920 @@ +/*************************************************************************** + * Copyright (C) 2007 by John Stamp, * + * Copyright (C) 2007 by Max Howell, Last.fm Ltd. * + * Copyright (C) 2010 by Christian Muehlhaeuser * + * * + * Large portions of this code are shamelessly copied from audio.c: * + * The XMMS ALSA output plugin * + * Copyright (C) 2001-2003 Matthieu Sozeau * + * Copyright (C) 1998-2003 Peter Alm, Mikael Alm, Olle Hallnas, * + * Thomas Nilsson and 4Front Technologies * + * Copyright (C) 1999-2007 Haavard Kvaalen * + * Copyright (C) 2005 Takashi Iwai * + * * + * 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 02110-1301, USA. * + ***************************************************************************/ + +#include "alsaaudio.h" + +#include +#include + +//no debug +#define snd_pcm_hw_params_dump( hwparams, logs ) +#define snd_pcm_sw_params_dump( x, y ) +#define snd_pcm_dump( x, y ) + +pthread_t AlsaAudio::audio_thread; + +char* AlsaAudio::thread_buffer = NULL; +int AlsaAudio::thread_buffer_size = 0; +int AlsaAudio::rd_index = 0; +int AlsaAudio::wr_index = 0; +unsigned int AlsaAudio::pcmCounter = 0; + +snd_output_t* AlsaAudio::logs = NULL; +bool AlsaAudio::going = false; +snd_pcm_t *AlsaAudio::alsa_pcm = NULL; + +ssize_t AlsaAudio::hw_period_size_in = 0; +snd_format* AlsaAudio::inputf = NULL; +snd_format* AlsaAudio::outputf = NULL; +float AlsaAudio::volume = 1.0; +bool AlsaAudio::paused = false; + +convert_func_t AlsaAudio::alsa_convert_func = NULL; +convert_channel_func_t AlsaAudio::alsa_stereo_convert_func = NULL; +convert_freq_func_t AlsaAudio::alsa_frequency_convert_func = NULL; +xmms_convert_buffers* AlsaAudio::convertb = NULL; + + +AlsaAudio::AlsaAudio() +{ +} + + +AlsaAudio::~AlsaAudio() +{ + // Close here just to be sure + // These are safe to call more than once + stopPlayback(); + alsaClose(); +} + + +/****************************************************************************** + * Device Detection + ******************************************************************************/ + +int +AlsaAudio::getCards( void ) +{ + int card = -1; + int err = 0; + m_devices.clear(); + + // First add the default PCM device + AlsaDeviceInfo dev; + dev.name = "Default PCM device (default)"; + dev.device = "default"; + m_devices.push_back( dev ); + + if ( (err = snd_card_next( &card )) != 0 ) + goto getCardsFailed; + + while ( card > -1 ) + { + getDevicesForCard( card ); + if ( (err = snd_card_next( &card )) != 0 ) + goto getCardsFailed; + } + + return m_devices.size(); + +getCardsFailed: + qDebug() << __PRETTY_FUNCTION__ << "failed:" << snd_strerror( -err ); + return -1; +} + + +void +AlsaAudio::getDevicesForCard( int card ) +{ + int pcm_device = -1, err; + snd_pcm_info_t *pcm_info; + snd_ctl_t *ctl; + char *alsa_name; + QString cardName = "Unknown soundcard"; + QString device_name = QString( "hw:%1" ).arg( card ); + + if ((err = snd_ctl_open( &ctl, device_name.toAscii(), 0 )) < 0) { + qDebug() << "Failed:" << snd_strerror( -err ); + return; + } + + if ((err = snd_card_get_name( card, &alsa_name )) != 0) + { + qDebug() << "Failed:" << snd_strerror( -err ); + } + else + cardName = alsa_name; + + snd_pcm_info_alloca( &pcm_info ); + + for (;;) + { + if ((err = snd_ctl_pcm_next_device( ctl, &pcm_device )) < 0) + { + qDebug() << "Failed:" << snd_strerror( -err ); + pcm_device = -1; + } + if (pcm_device < 0) + break; + + snd_pcm_info_set_device( pcm_info, pcm_device ); + snd_pcm_info_set_subdevice( pcm_info, 0 ); + snd_pcm_info_set_stream( pcm_info, SND_PCM_STREAM_PLAYBACK ); + + if ((err = snd_ctl_pcm_info( ctl, pcm_info )) < 0) + { + if ( err != -ENOENT ) + qDebug() << "Failed: snd_ctl_pcm_info() failed" + "(" << card << ":" << pcm_device << "): " + << snd_strerror( -err ); + continue; + } + + AlsaDeviceInfo dev; + dev.device = QString( "hw:%1,%2" ) + .arg( card ) + .arg( pcm_device ); + dev.name = QString( "%1: %2 (%3)" ) + .arg( cardName ) + .arg( snd_pcm_info_get_name( pcm_info ) ) + .arg( dev.device ); + + m_devices.push_back( dev ); + } + + snd_ctl_close( ctl ); +} + + +AlsaDeviceInfo +AlsaAudio::getDeviceInfo( int device ) +{ + return m_devices[device]; +} + + +/****************************************************************************** + Device Setup +******************************************************************************/ + +bool +AlsaAudio::alsaOpen( QString device, AFormat format, unsigned int rate, + unsigned int channels, snd_pcm_uframes_t periodSize, + unsigned int periodCount, int minBufferCapacity ) +{ + int err, hw_buffer_size; + ssize_t hw_period_size; + snd_pcm_hw_params_t *hwparams; + snd_pcm_sw_params_t *swparams; + snd_pcm_uframes_t alsa_buffer_size, alsa_period_size; + + inputf = snd_format_from_xmms( format, rate, channels ); + convertb = xmms_convert_buffers_new(); + snd_output_stdio_attach( &logs, stderr, 0 ); + + alsa_convert_func = NULL; + alsa_stereo_convert_func = NULL; + alsa_frequency_convert_func = NULL; + + free( outputf ); + outputf = snd_format_from_xmms( inputf->xmms_format, inputf->rate, inputf->channels ); + + qDebug() << "Opening device:" << device; + + // FIXME: Can snd_pcm_open() return EAGAIN? + if ((err = snd_pcm_open( &alsa_pcm, + device.toAscii(), + SND_PCM_STREAM_PLAYBACK, + SND_PCM_NONBLOCK )) < 0) + { + qDebug() << "Failed to open pcm device (" << device << "):" << snd_strerror( -err ); + alsa_pcm = NULL; + free( outputf ); + outputf = NULL; + return false; + } + + snd_pcm_info_t *info; + int alsa_card, alsa_device, alsa_subdevice; + + snd_pcm_info_alloca( &info ); + snd_pcm_info( alsa_pcm, info ); + alsa_card = snd_pcm_info_get_card( info ); + alsa_device = snd_pcm_info_get_device( info ); + alsa_subdevice = snd_pcm_info_get_subdevice( info ); + +// qDebug() << "Card:" << alsa_card; +// qDebug() << "Device:" << alsa_device; +// qDebug() << "Subdevice:" << alsa_subdevice; + + snd_pcm_hw_params_alloca( &hwparams ); + + if ( (err = snd_pcm_hw_params_any( alsa_pcm, hwparams ) ) < 0 ) + { + qDebug() << "No configuration available for playback:" + << snd_strerror( -err ); + alsaClose(); + return false; + } + + if ( ( err = snd_pcm_hw_params_set_access( alsa_pcm, hwparams, + SND_PCM_ACCESS_RW_INTERLEAVED ) ) < 0 ) + { + qDebug() << "Cannot set normal write mode:" << snd_strerror( -err ); + alsaClose(); + return false; + } + + if ( ( err = snd_pcm_hw_params_set_format( alsa_pcm, hwparams, outputf->format ) ) < 0 ) + { + // Try if one of these format work (one of them should work + // on almost all soundcards) + + snd_pcm_format_t formats[] = { SND_PCM_FORMAT_S16_LE, + SND_PCM_FORMAT_S16_BE, + SND_PCM_FORMAT_U8 }; + + uint i; + for ( i = 0; i < sizeof( formats ) / sizeof( formats[0] ); i++ ) + { + if ( snd_pcm_hw_params_set_format( alsa_pcm, hwparams, formats[i] ) == 0 ) + { + outputf->format = formats[i]; + break; + } + } + if ( outputf->format != inputf->format ) + { + outputf->xmms_format = (AFormat)format_from_alsa( outputf->format ); + + qDebug() << "Converting format from" << inputf->xmms_format << "to" << outputf->xmms_format; + + if ( outputf->xmms_format < 0 ) + return -1; + alsa_convert_func = xmms_convert_get_func( outputf->xmms_format, inputf->xmms_format ); + if ( alsa_convert_func == NULL ) + { + qDebug() << "Format translation needed, but not available. Input:" << inputf->xmms_format << "; Output:" << outputf->xmms_format ; + alsaClose(); + return false; + } + } + else + { + qDebug() << "Sample format not available for playback:" << snd_strerror( -err ); + alsaClose(); + return false; + } + } + + snd_pcm_hw_params_set_channels_near( alsa_pcm, hwparams, &outputf->channels ); + if ( outputf->channels != inputf->channels ) + { + qDebug() << "Converting channels from" << inputf->channels << "to" << outputf->channels; + + alsa_stereo_convert_func = + xmms_convert_get_channel_func( outputf->xmms_format, + outputf->channels, + inputf->channels ); + if ( alsa_stereo_convert_func == NULL ) + { + qDebug() << "No stereo conversion available. Format:" << outputf->xmms_format << "; Input Channels:" << inputf->channels << "; Output Channels:" << outputf->channels ; + alsaClose(); + return false; + } + } + + snd_pcm_hw_params_set_rate_near( alsa_pcm, hwparams, &outputf->rate, 0 ); + if ( outputf->rate == 0 ) + { + qDebug() << "No usable samplerate available."; + alsaClose(); + return false; + } + if ( outputf->rate != inputf->rate ) + { + qDebug() << "Converting samplerate from" << inputf->rate << "to" << outputf->rate ; + if ( outputf->channels < 1 || outputf->channels > 2 ) + { + qDebug() << "Unsupported number of channels:" << outputf->channels << "- Resample function not available" ; + alsa_frequency_convert_func = NULL; + alsaClose(); + return false; + } + alsa_frequency_convert_func = + xmms_convert_get_frequency_func( outputf->xmms_format, + outputf->channels ); + if ( alsa_frequency_convert_func == NULL ) + { + qDebug() << "Resample function not available. Format" << outputf->xmms_format ; + alsaClose(); + return false; + } + } + + outputf->sample_bits = snd_pcm_format_physical_width( outputf->format ); + outputf->bps = ( outputf->rate * outputf->sample_bits * outputf->channels ) >> 3; + + if ( ( err = snd_pcm_hw_params_set_period_size_near( alsa_pcm, hwparams, + &periodSize, NULL ) ) < 0 ) + { + qDebug() << "Set period size failed:" << snd_strerror( -err ); + alsaClose(); + return false; + } + + if ( ( err = snd_pcm_hw_params_set_periods_near( alsa_pcm, hwparams, + &periodCount, 0 ) ) < 0 ) + { + qDebug() << "Set period count failed:" << snd_strerror( -err ); + alsaClose(); + return false; + } + + if ( snd_pcm_hw_params( alsa_pcm, hwparams ) < 0 ) + { + snd_pcm_hw_params_dump( hwparams, logs ); + qDebug() << "Unable to install hw params"; + alsaClose(); + return false; + } + + if ( ( err = snd_pcm_hw_params_get_buffer_size( hwparams, &alsa_buffer_size ) ) < 0 ) + { + qDebug() << "snd_pcm_hw_params_get_buffer_size() failed:" << snd_strerror( -err ); + alsaClose(); + return false; + } + + if ( ( err = snd_pcm_hw_params_get_period_size( hwparams, &alsa_period_size, 0 ) ) < 0 ) + { + qDebug() << "snd_pcm_hw_params_get_period_size() failed:" << snd_strerror( -err ); + alsaClose(); + return false; + } + snd_pcm_sw_params_alloca( &swparams ); + snd_pcm_sw_params_current( alsa_pcm, swparams ); + + if ( ( err = snd_pcm_sw_params_set_start_threshold( alsa_pcm, + swparams, alsa_buffer_size - alsa_period_size ) < 0 ) ) + qDebug() << "Setting start threshold failed:" << snd_strerror( -err ); + if ( snd_pcm_sw_params( alsa_pcm, swparams ) < 0 ) + { + qDebug() << "Unable to install sw params"; + alsaClose(); + return false; + } + + #ifndef QT_NO_DEBUG + snd_pcm_sw_params_dump( swparams, logs ); + snd_pcm_dump( alsa_pcm, logs ); + #endif + + hw_period_size = snd_pcm_frames_to_bytes( alsa_pcm, alsa_period_size ); + if ( inputf->bps != outputf->bps ) + { + int align = ( inputf->sample_bits * inputf->channels ) / 8; + hw_period_size_in = ( (quint64)hw_period_size * inputf->bps + + outputf->bps/2 ) / outputf->bps; + hw_period_size_in -= hw_period_size_in % align; + } + else + { + hw_period_size_in = hw_period_size; + } + + hw_buffer_size = snd_pcm_frames_to_bytes( alsa_pcm, alsa_buffer_size ); + thread_buffer_size = minBufferCapacity * 4; + if ( thread_buffer_size < hw_buffer_size ) + thread_buffer_size = hw_buffer_size * 2; + if ( thread_buffer_size < 8192 ) + thread_buffer_size = 8192; + thread_buffer_size += hw_buffer_size; + thread_buffer_size -= thread_buffer_size % hw_period_size; + + thread_buffer = (char*)calloc(thread_buffer_size, sizeof(char)); + +// qDebug() << "Device setup: period size:" << hw_period_size; +// qDebug() << "Device setup: hw_period_size_in:" << hw_period_size_in; +// qDebug() << "Device setup: hw_buffer_size:" << hw_buffer_size; +// qDebug() << "Device setup: thread_buffer_size:" << thread_buffer_size; +// qDebug() << "bits per sample:" << snd_pcm_format_physical_width( outputf->format ) +// << "frame size:" << snd_pcm_frames_to_bytes( alsa_pcm, 1 ) +// << "Bps:" << outputf->bps; + + return true; +} + + +int +AlsaAudio::startPlayback() +{ + int pthreadError = 0; + + // We should double check this here. AlsaPlayback::initAudio + // isn't having its emitted error caught. + // So double check here to avoid a potential assert. + if ( !alsa_pcm ) + return 1; + + going = true; + + // qDebug() << "Starting thread"; + AlsaAudio* aaThread = new AlsaAudio(); + pthreadError = pthread_create( &audio_thread, NULL, &alsa_loop, (void*)aaThread ); + + return pthreadError; +} + + +void +AlsaAudio::clearBuffer( void ) +{ + wr_index = rd_index = pcmCounter = 0; + if ( thread_buffer ) + memset( thread_buffer, 0, thread_buffer_size ); +} + + +/****************************************************************************** + Play Interface +******************************************************************************/ + +void +AlsaAudio::alsaWrite( const QByteArray& input ) +{ + int cnt; + const char *src = input.data(); + int length = input.size(); + //qDebug() << "alsaWrite length:" << length; + + while ( length > 0 ) + { + int wr; + cnt = qMin(length, thread_buffer_size - wr_index); + memcpy(thread_buffer + wr_index, src, cnt); + wr = (wr_index + cnt) % thread_buffer_size; + wr_index = wr; + length -= cnt; + src += cnt; + } +} + + +int +AlsaAudio::get_thread_buffer_filled() const +{ + if ( wr_index >= rd_index ) + { + return wr_index - rd_index; + } + return ( thread_buffer_size - ( rd_index - wr_index ) ); +} + + +// HACK: the buffer may have data, but not enough to send to the card. In that +// case we tell alsaplayback that we don't have any. This may chop off some +// data, but only at the natural end of a track. On my machine, this is at +// most 3759 bytes. That's less than 0.022 sec. It beats padding the buffer +// with 0's if the stream fails mid track. No stutter this way. +int +AlsaAudio::hasData() +{ + int tempSize = get_thread_buffer_filled(); + if ( tempSize < hw_period_size_in ) + return 0; + else + return tempSize; +} + + +int +AlsaAudio::alsa_free() const +{ + //qDebug() << "alsa_free:" << thread_buffer_size - get_thread_buffer_filled() - 1; + return thread_buffer_size - get_thread_buffer_filled() - 1; +} + + +void +AlsaAudio::setVolume ( float v ) +{ + volume = v; +} + + +void +AlsaAudio::stopPlayback() +{ + if (going) + { +// Q_DEBUG_BLOCK; + + going = false; + + pthread_join( audio_thread, NULL ); + } +} + + +void +AlsaAudio::alsaClose() +{ +// Q_DEBUG_BLOCK; + + alsa_close_pcm(); + + xmms_convert_buffers_destroy( convertb ); + convertb = NULL; + + if ( thread_buffer ) + { + free(thread_buffer); + thread_buffer = NULL; + } + if ( inputf ) + { + free( inputf ); + inputf = NULL; + } + if (outputf ) + { + free( outputf ); + outputf = NULL; + } + if ( logs ) + { + snd_output_close( logs ); + logs = NULL; + } +} + + +/****************************************************************************** + Play Thread +******************************************************************************/ + +void* +AlsaAudio::alsa_loop( void* pthis ) +{ + AlsaAudio* aaThread = (AlsaAudio*)pthis; + aaThread->run(); + return NULL; +} + + +void +AlsaAudio::run() +{ + int npfds = snd_pcm_poll_descriptors_count( alsa_pcm ); + int wr = 0; + int err; + + if ( npfds <= 0 ) + goto _error; + + err = snd_pcm_prepare( alsa_pcm ); + if ( err < 0 ) + qDebug() << "snd_pcm_prepare error:" << snd_strerror( err ); + + while ( going && alsa_pcm ) + { + if ( !paused && get_thread_buffer_filled() >= hw_period_size_in ) + { + wr = snd_pcm_wait( alsa_pcm, 10 ); + + if ( wr > 0 ) + { + alsa_write_out_thread_data(); + } + else if ( wr < 0 ) + { + alsa_handle_error( wr ); + } + } + else + { + struct timespec req; + req.tv_sec = 0; + req.tv_nsec = 10000000; //0.1 seconds + nanosleep( &req, NULL ); + } + } + + _error: + err = snd_pcm_drop( alsa_pcm ); + if ( err < 0 ) + qDebug() << "snd_pcm_drop error:" << snd_strerror( err ); + wr_index = rd_index = 0; + memset( thread_buffer, 0, thread_buffer_size ); + +// qDebug() << "Exiting thread"; + + pthread_exit( NULL ); +} + + +/* transfer audio data from thread buffer to h/w */ +void +AlsaAudio::alsa_write_out_thread_data( void ) +{ + ssize_t length; + int cnt; + length = qMin( hw_period_size_in, ssize_t(get_thread_buffer_filled()) ); + length = qMin( length, snd_pcm_frames_to_bytes( alsa_pcm, alsa_get_avail() ) ); + + while (length > 0) + { + int rd; + cnt = qMin(int(length), thread_buffer_size - rd_index); + alsa_do_write( thread_buffer + rd_index, cnt); + rd = (rd_index + cnt) % thread_buffer_size; + rd_index = rd; + length -= cnt; + } +} + + +/* update and get the available space on h/w buffer (in frames) */ +snd_pcm_sframes_t +AlsaAudio::alsa_get_avail( void ) +{ + snd_pcm_sframes_t ret; + + if ( alsa_pcm == NULL ) + return 0; + + while ( ( ret = snd_pcm_avail_update( alsa_pcm ) ) < 0 ) + { + ret = alsa_handle_error( ret ); + if ( ret < 0 ) + { + qDebug() << "alsa_get_avail(): snd_pcm_avail_update() failed:" << snd_strerror( -ret ); + return 0; + } + } + return ret; +} + + +/* transfer data to audio h/w; length is given in bytes + * + * data can be modified via rate conversion or + * software volume before passed to audio h/w + */ +void +AlsaAudio::alsa_do_write( void* data, ssize_t length ) +{ + if ( alsa_convert_func != NULL ) + length = alsa_convert_func( convertb, &data, length ); + if ( alsa_stereo_convert_func != NULL ) + length = alsa_stereo_convert_func( convertb, &data, length ); + if ( alsa_frequency_convert_func != NULL ) + { + length = alsa_frequency_convert_func( convertb, &data, length, + inputf->rate, + outputf->rate ); + } + + volume_adjust( data, length, outputf->xmms_format ); + + alsa_write_audio( (char*)data, length ); +} + + +#define VOLUME_ADJUST( type, endian ) \ +do { \ + type *ptr = (type*)data; \ + for ( i = 0; i < length; i += 2 ) \ + { \ + *ptr = qTo##endian( (type)( qFrom##endian( *ptr ) * volume ) ); \ + ptr++; \ + } \ +} while ( 0 ) + +#define VOLUME_ADJUST8( type ) \ +do { \ + type *ptr = (type*)data; \ + for ( i = 0; i < length; i++ ) \ + { \ + *ptr = (type)( *ptr * volume ); \ + ptr++; \ + } \ +} while ( 0 ) + +void +AlsaAudio::volume_adjust( void* data, ssize_t length, AFormat fmt ) +{ + ssize_t i; + if ( volume == 1.0 ) + return; + + switch ( fmt ) + { + case FMT_S16_LE: + VOLUME_ADJUST( qint16, LittleEndian ); + break; + case FMT_U16_LE: + VOLUME_ADJUST( quint16, LittleEndian ); + break; + case FMT_S16_BE: + VOLUME_ADJUST( qint16, BigEndian ); + break; + case FMT_U16_BE: + VOLUME_ADJUST( quint16, BigEndian ); + break; + case FMT_S8: + VOLUME_ADJUST8( qint8 ); + break; + case FMT_U8: + VOLUME_ADJUST8( quint8 ); + break; + default: + qDebug() << __PRETTY_FUNCTION__ << "unhandled format:" << fmt ; + break; + } +} + + +/* transfer data to audio h/w via normal write */ +void +AlsaAudio::alsa_write_audio( char *data, ssize_t length ) +{ + snd_pcm_sframes_t written_frames; + + while ( length > 0 ) + { + snd_pcm_sframes_t frames = snd_pcm_bytes_to_frames( alsa_pcm, length ); + written_frames = snd_pcm_writei( alsa_pcm, data, frames ); + + if ( written_frames > 0 ) + { + ssize_t written = snd_pcm_frames_to_bytes( alsa_pcm, written_frames ); + pcmCounter += written; + + length -= written; + data += written; + } + else + { + int err = alsa_handle_error( (int)written_frames ); + if ( err < 0 ) + { + qDebug() << __PRETTY_FUNCTION__ << "write error:" << snd_strerror( -err ); + break; + } + } + } +} + + +/* handle generic errors */ +int +AlsaAudio::alsa_handle_error( int err ) +{ + switch ( err ) + { + case -EPIPE: + return xrun_recover(); + case -ESTRPIPE: + return suspend_recover(); + } + + return err; +} + + +/* close PCM and release associated resources */ +void +AlsaAudio::alsa_close_pcm( void ) +{ + if ( alsa_pcm ) + { + int err; + snd_pcm_drop( alsa_pcm ); + if ( ( err = snd_pcm_close( alsa_pcm ) ) < 0 ) + qDebug() << "alsa_close_pcm() failed:" << snd_strerror( -err ); + alsa_pcm = NULL; + } +} + + +int +AlsaAudio::format_from_alsa( snd_pcm_format_t fmt ) +{ + uint i; + for ( i = 0; i < sizeof( format_table ) / sizeof( format_table[0] ); i++ ) + if ( format_table[i].alsa == fmt ) + return format_table[i].xmms; + qDebug() << "Unsupported format:" << snd_pcm_format_name( fmt ); + return -1; +} + + +struct snd_format* +AlsaAudio::snd_format_from_xmms( AFormat fmt, unsigned int rate, unsigned int channels ) +{ + struct snd_format *f = (struct snd_format*)malloc( sizeof( struct snd_format ) ); + uint i; + + f->xmms_format = fmt; + f->format = SND_PCM_FORMAT_UNKNOWN; + + for ( i = 0; i < sizeof( format_table ) / sizeof( format_table[0] ); i++ ) + { + if ( format_table[i].xmms == fmt ) + { + f->format = format_table[i].alsa; + break; + } + } + + /* Get rid of _NE */ + for ( i = 0; i < sizeof( format_table ) / sizeof( format_table[0] ); i++ ) + { + if ( format_table[i].alsa == f->format ) + { + f->xmms_format = format_table[i].xmms; + break; + } + } + + f->rate = rate; + f->channels = channels; + f->sample_bits = snd_pcm_format_physical_width( f->format ); + f->bps = ( rate * f->sample_bits * channels ) >> 3; + + return f; +} + + +int +AlsaAudio::xrun_recover( void ) +{ +#ifndef QT_NO_DEBUG + snd_pcm_status_t *alsa_status; + snd_pcm_status_alloca( &alsa_status ); + if ( snd_pcm_status( alsa_pcm, alsa_status ) < 0 ) + { + qDebug() << "AlsaAudio::xrun_recover(): snd_pcm_status() failed"; + } + else + { + snd_pcm_status_dump( alsa_status, logs ); + qDebug() << "Status:\n" << logs; + } +#endif + + return snd_pcm_prepare( alsa_pcm ); +} + + +int +AlsaAudio::suspend_recover( void ) +{ + int err; + + while ( ( err = snd_pcm_resume( alsa_pcm ) ) == -EAGAIN ) + /* wait until suspend flag is released */ + sleep( 1 ); + if ( err < 0 ) + { + qDebug() << "alsa_handle_error(): snd_pcm_resume() failed." ; + return snd_pcm_prepare( alsa_pcm ); + } + return err; +} + + +unsigned int +AlsaAudio::timeElapsed() +{ + return pcmCounter / outputf->bps; +} diff --git a/alsa-playback/alsaaudio.h b/alsa-playback/alsaaudio.h new file mode 100644 index 000000000..7034f05ad --- /dev/null +++ b/alsa-playback/alsaaudio.h @@ -0,0 +1,136 @@ +/*************************************************************************** + * Copyright (C) 2007 by John Stamp, * + * Copyright (C) 2007 by Max Howell, Last.fm Ltd. * + * Copyright (C) 2010 by Christian Muehlhaeuser * + * * + * 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 02110-1301, USA. * + ***************************************************************************/ + +#ifndef ALSA_AUDIO_H +#define ALSA_AUDIO_H + +#include +#include +#include + +#include +#include "xconvert.h" + +struct AlsaDeviceInfo +{ + QString name; + QString device; +}; + +struct snd_format +{ + unsigned int rate; + unsigned int channels; + snd_pcm_format_t format; + AFormat xmms_format; + int sample_bits; + int bps; +}; + +static const struct +{ + AFormat xmms; + snd_pcm_format_t alsa; +} + +format_table[] = { { FMT_S16_LE, SND_PCM_FORMAT_S16_LE }, + { FMT_S16_BE, SND_PCM_FORMAT_S16_BE }, + { FMT_S16_NE, SND_PCM_FORMAT_S16 }, + { FMT_U16_LE, SND_PCM_FORMAT_U16_LE }, + { FMT_U16_BE, SND_PCM_FORMAT_U16_BE }, + { FMT_U16_NE, SND_PCM_FORMAT_U16 }, + { FMT_U8, SND_PCM_FORMAT_U8 }, + { FMT_S8, SND_PCM_FORMAT_S8 }, }; + +class AlsaAudio +{ +public: + AlsaAudio(); + ~AlsaAudio(); + + int getCards(); + AlsaDeviceInfo getDeviceInfo( int device ); + + bool alsaOpen( QString device, AFormat format, unsigned int rate, + unsigned int channels, snd_pcm_uframes_t periodSize, + unsigned int periodCount, int minBufferCapacity ); + + int startPlayback(); + void stopPlayback(); + + void alsaWrite( const QByteArray& inputData ); + void alsaClose(); + + void setVolume( float vol ); + void setPaused( bool enabled ) { paused = enabled; } + + unsigned int timeElapsed(); + + int hasData(); + int get_thread_buffer_filled() const; + int alsa_free() const; + void clearBuffer(); + +private: + QList m_devices; + + // The following static variables are configured in either + // alsaOpen or alsaSetup and used later in the audio thread + static ssize_t hw_period_size_in; + static snd_output_t *logs; + static bool going; + static snd_pcm_t *alsa_pcm; + static snd_format* inputf; + static snd_format* outputf; + static float volume; + static bool paused; + static convert_func_t alsa_convert_func; + static convert_channel_func_t alsa_stereo_convert_func; + static convert_freq_func_t alsa_frequency_convert_func; + static xmms_convert_buffers *convertb; + static pthread_t audio_thread; + static unsigned int pcmCounter; + + void getDevicesForCard( int card ); + + static void* alsa_loop( void* ); + void run(); + void alsa_write_out_thread_data(); + void alsa_do_write( void* data, ssize_t length ); + void volume_adjust( void* data, ssize_t length, AFormat fmt ); + void alsa_write_audio( char *data, ssize_t length ); + //int get_thread_buffer_filled() const; + + static char* thread_buffer; + static int thread_buffer_size; + static int rd_index, wr_index; + + snd_pcm_sframes_t alsa_get_avail( void ); + int alsa_handle_error( int err ); + int xrun_recover(); + int suspend_recover(); + int format_from_alsa( snd_pcm_format_t fmt ); + snd_format* snd_format_from_xmms( AFormat fmt, unsigned int rate, unsigned int channels ); + + void alsa_close_pcm( void ); +}; + +#endif diff --git a/alsa-playback/alsaplayback.cpp b/alsa-playback/alsaplayback.cpp new file mode 100644 index 000000000..1a44046db --- /dev/null +++ b/alsa-playback/alsaplayback.cpp @@ -0,0 +1,217 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2010 by * + * Christian Muehlhaeuser * + * Erik Jaelevik, Last.fm Ltd * + * Max Howell, Last.fm Ltd * + * * + * 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 Steet, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "alsaaudio.h" +#include "alsaplayback.h" + +#include +#include + + +AlsaPlayback::AlsaPlayback() + : m_audio( 0 ) + , m_paused( false ) + , m_playing( false ) + , m_volume( 0.75 ) + , m_deviceNum( 0 ) +{ + setBufferCapacity( 32768 * 4 ); //FIXME: const value +} + + +AlsaPlayback::~AlsaPlayback() +{ + delete m_audio; +} + + +bool +AlsaPlayback::haveData() +{ + return ( m_audio->hasData() > 0 ); +} + + +bool +AlsaPlayback::needData() +{ + return ( m_audio->get_thread_buffer_filled() < m_bufferCapacity ); +} + + +void +AlsaPlayback::setBufferCapacity( int size ) +{ + m_bufferCapacity = size; +} + + +int +AlsaPlayback::bufferSize() +{ + return m_audio->get_thread_buffer_filled(); +} + + +float +AlsaPlayback::volume() +{ + return m_volume; +} + + +void +AlsaPlayback::setVolume( int volume ) +{ + m_volume = (float)volume / 100.0; + m_audio->setVolume( m_volume ); +} + + +void +AlsaPlayback::triggerTimers() +{ + if ( m_audio ) + emit timeElapsed( m_audio->timeElapsed() ); +} + + +QStringList +AlsaPlayback::soundSystems() +{ + return QStringList() << "Alsa"; +} + + +QStringList +AlsaPlayback::devices() +{ +// Q_DEBUG_BLOCK << "Querying audio devices"; + + QStringList devices; + for (int i = 0, n = m_audio->getCards(); i < n; i++) + devices << m_audio->getDeviceInfo( i ).name; + + return devices; +} + + +bool +AlsaPlayback::startPlayback() +{ + if ( !m_audio ) + { + goto _error; + } + + if ( m_audio->startPlayback() ) + { + goto _error; + } + + m_playing = true; + return true; + +_error: + return false; +} + + +void +AlsaPlayback::stopPlayback() +{ + m_audio->stopPlayback(); + m_paused = false; + m_playing = false; +} + + +void +AlsaPlayback::initAudio( long sampleRate, int channels ) +{ + int periodSize = 1024; // According to mplayer, these two are good defaults. + int periodCount = 16; // They create a buffer size of 16384 frames. + QString cardDevice; + + delete m_audio; + m_audio = new AlsaAudio; + m_audio->clearBuffer(); + + cardDevice = internalSoundCardID( m_deviceNum ); + + // We assume host byte order +#ifdef WORDS_BIGENDIAN + if ( !m_audio->alsaOpen( cardDevice, FMT_S16_BE, sampleRate, channels, periodSize, periodCount, m_bufferCapacity ) ) +#else + if ( !m_audio->alsaOpen( cardDevice, FMT_S16_LE, sampleRate, channels, periodSize, periodCount, m_bufferCapacity ) ) +#endif + { + } +} + + +void +AlsaPlayback::processData( const QByteArray &buffer ) +{ + m_audio->alsaWrite( buffer ); +} + + +void +AlsaPlayback::clearBuffers() +{ + m_audio->clearBuffer(); +} + + +QString +AlsaPlayback::internalSoundCardID( int settingsID ) +{ + int cards = m_audio->getCards(); + + if ( settingsID < cards ) + return m_audio->getDeviceInfo( settingsID ).device; + else + return "default"; +} + + +void +AlsaPlayback::pause() +{ + m_paused = true; + + if ( m_audio ) + { + m_audio->setPaused( true ); + } +} + + +void +AlsaPlayback::resume() +{ + m_paused = false; + + if ( m_audio ) + m_audio->setPaused( false ); +} diff --git a/alsa-playback/alsaplayback.h b/alsa-playback/alsaplayback.h new file mode 100644 index 000000000..6b4219e8d --- /dev/null +++ b/alsa-playback/alsaplayback.h @@ -0,0 +1,80 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2010 by * + * Christian Muehlhaeuser * + * Erik Jaelevik, Last.fm Ltd * + * Max Howell, Last.fm Ltd * + * * + * 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 Steet, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef ALSAPLAYBACK_H +#define ALSAPLAYBACK_H + +#include + +class AlsaPlayback : public QObject +{ + Q_OBJECT + + public: + AlsaPlayback(); + ~AlsaPlayback(); + + virtual void initAudio( long sampleRate, int channels ); + + virtual float volume(); + virtual bool isPaused() { return m_paused; } + virtual bool isPlaying() { return m_playing; } + + virtual bool haveData(); + virtual bool needData(); + virtual void processData( const QByteArray& ); + + virtual void setBufferCapacity( int size ); + virtual int bufferSize(); + + virtual QStringList soundSystems(); + virtual QStringList devices(); + + public slots: + virtual void clearBuffers(); + + virtual bool startPlayback(); + virtual void stopPlayback(); + + virtual void pause(); + virtual void resume(); + + virtual void setVolume( int volume ); + + virtual void triggerTimers(); + + signals: + void timeElapsed( unsigned int seconds ); + + private: + class AlsaAudio *m_audio; + int m_bufferCapacity; + + bool m_paused; + bool m_playing; + float m_volume; + int m_deviceNum; + + QString internalSoundCardID( int settingsID ); +}; + +#endif diff --git a/alsa-playback/xconvert.c b/alsa-playback/xconvert.c new file mode 100644 index 000000000..bd2c40c44 --- /dev/null +++ b/alsa-playback/xconvert.c @@ -0,0 +1,771 @@ +/* + * Copyright (C) 2001-2003 Haavard Kvaalen + * + * Licensed under GNU LGPL version 2. + */ + +#include +#include +#include "xconvert.h" + +// These are adapted from defines in gtypes.h and glibconfig.h +#ifndef FALSE +#define FALSE ( 0 ) +#endif + +#ifndef TRUE +#define TRUE ( !FALSE ) +#endif + +# define GUINT16_SWAP_LE_BE( val ) \ + ( ( uint16_t ) \ + ( \ + ( uint16_t ) ( ( uint16_t ) ( val ) >> 8 ) | \ + ( uint16_t ) ( ( uint16_t ) ( val ) << 8 ) \ + ) \ + ) + +# define GINT16_SWAP_LE_BE( val ) ( ( int16_t ) GUINT16_SWAP_LE_BE ( val ) ) + +#ifdef WORDS_BIGENDIAN + +# define IS_BIG_ENDIAN TRUE + +# define GINT16_TO_BE( val ) ( ( int16_t ) ( val ) ) +# define GINT16_FROM_BE( val ) ( ( int16_t ) ( val ) ) +# define GUINT16_TO_BE( val ) ( ( uint16_t ) ( val ) ) +# define GUINT16_FROM_BE( val ) ( ( uint16_t ) ( val ) ) + +# define GUINT16_TO_LE( val ) ( GUINT16_SWAP_LE_BE ( val ) ) +# define GUINT16_FROM_LE( val ) ( GUINT16_SWAP_LE_BE ( val ) ) +# define GINT16_TO_LE( val ) ( ( int16_t ) GUINT16_SWAP_LE_BE ( val ) ) +# define GINT16_FROM_LE( val ) ( ( int16_t ) GUINT16_SWAP_LE_BE ( val ) ) + +#else + +# define IS_BIG_ENDIAN FALSE + +# define GINT16_TO_LE( val ) ( ( int16_t ) ( val ) ) +# define GINT16_FROM_LE( val ) ( ( int16_t ) ( val ) ) +# define GUINT16_TO_LE( val ) ( ( uint16_t ) ( val ) ) +# define GUINT16_FROM_LE( val ) ( ( uint16_t ) ( val ) ) + +# define GUINT16_TO_BE( val ) ( GUINT16_SWAP_LE_BE ( val ) ) +# define GUINT16_FROM_BE( val ) ( GUINT16_SWAP_LE_BE ( val ) ) +# define GINT16_TO_BE( val ) ( ( int16_t ) GUINT16_SWAP_LE_BE ( val ) ) +# define GINT16_FROM_BE( val ) ( ( int16_t ) GUINT16_SWAP_LE_BE ( val ) ) + +#endif + + +struct buffer { + void *buffer; + uint size; +}; + +struct xmms_convert_buffers { + struct buffer format_buffer, stereo_buffer, freq_buffer; +}; + +struct xmms_convert_buffers* xmms_convert_buffers_new( void ) +{ + return calloc( 1, sizeof( struct xmms_convert_buffers ) ); +} + +static void* convert_get_buffer( struct buffer *buffer, size_t size ) +{ + if ( size > 0 && size <= buffer->size ) + return buffer->buffer; + + buffer->size = size; + buffer->buffer = realloc( buffer->buffer, size ); + return buffer->buffer; +} + +void xmms_convert_buffers_free( struct xmms_convert_buffers* buf ) +{ + convert_get_buffer( &buf->format_buffer, 0 ); + convert_get_buffer( &buf->stereo_buffer, 0 ); + convert_get_buffer( &buf->freq_buffer, 0 ); +} + +void xmms_convert_buffers_destroy( struct xmms_convert_buffers* buf ) +{ + if ( !buf ) + return; + xmms_convert_buffers_free( buf ); + free( buf ); +} + +static int convert_swap_endian( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint16_t *ptr = *data; + int i; + for ( i = 0; i < length; i += 2, ptr++ ) + *ptr = GUINT16_SWAP_LE_BE( *ptr ); + + return i; +} + +static int convert_swap_sign_and_endian_to_native( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint16_t *ptr = *data; + int i; + for ( i = 0; i < length; i += 2, ptr++ ) + *ptr = GUINT16_SWAP_LE_BE( *ptr ) ^ 1 << 15; + + return i; +} + +static int convert_swap_sign_and_endian_to_alien( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint16_t *ptr = *data; + int i; + for ( i = 0; i < length; i += 2, ptr++ ) + *ptr = GUINT16_SWAP_LE_BE( *ptr ^ 1 << 15 ); + + return i; +} + +static int convert_swap_sign16( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int16_t *ptr = *data; + int i; + for ( i = 0; i < length; i += 2, ptr++ ) + *ptr ^= 1 << 15; + + return i; +} + +static int convert_swap_sign8( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int8_t *ptr = *data; + int i; + for ( i = 0; i < length; i++ ) + *ptr++ ^= 1 << 7; + + return i; +} + +static int convert_to_8_native_endian( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int8_t *output = *data; + int16_t *input = *data; + int i; + for ( i = 0; i < length / 2; i++ ) + *output++ = *input++ >> 8; + + return i; +} + +static int convert_to_8_native_endian_swap_sign( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int8_t *output = *data; + int16_t *input = *data; + int i; + for ( i = 0; i < length / 2; i++ ) + *output++ = ( *input++ >> 8 ) ^ ( 1 << 7 ); + + return i; +} + + +static int convert_to_8_alien_endian( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int8_t *output = *data; + int16_t *input = *data; + int i; + for ( i = 0; i < length / 2; i++ ) + *output++ = *input++ & 0xff; + + return i; +} + +static int convert_to_8_alien_endian_swap_sign( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int8_t *output = *data; + int16_t *input = *data; + int i; + for ( i = 0; i < length / 2; i++ ) + *output++ = ( *input++ & 0xff ) ^ ( 1 << 7 ); + + return i; +} + +static int convert_to_16_native_endian( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint8_t *input = *data; + uint16_t *output; + int i; + *data = convert_get_buffer( &buf->format_buffer, length * 2 ); + output = *data; + for ( i = 0; i < length; i++ ) + *output++ = *input++ << 8; + + return i * 2; +} + +static int convert_to_16_native_endian_swap_sign( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint8_t *input = *data; + uint16_t *output; + int i; + *data = convert_get_buffer( &buf->format_buffer, length * 2 ); + output = *data; + for ( i = 0; i < length; i++ ) + *output++ = ( *input++ << 8 ) ^ ( 1 << 15 ); + + return i * 2; +} + + +static int convert_to_16_alien_endian( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint8_t *input = *data; + uint16_t *output; + int i; + *data = convert_get_buffer( &buf->format_buffer, length * 2 ); + output = *data; + for ( i = 0; i < length; i++ ) + *output++ = *input++; + + return i * 2; +} + +static int convert_to_16_alien_endian_swap_sign( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint8_t *input = *data; + uint16_t *output; + int i; + *data = convert_get_buffer( &buf->format_buffer, length * 2 ); + output = *data; + for ( i = 0; i < length; i++ ) + *output++ = *input++ ^ ( 1 << 7 ); + + return i * 2; +} + +static AFormat unnativize( AFormat fmt ) +{ + if ( fmt == FMT_S16_NE ) + { + if ( IS_BIG_ENDIAN ) + return FMT_S16_BE; + else + return FMT_S16_LE; + } + if ( fmt == FMT_U16_NE ) + { + if ( IS_BIG_ENDIAN ) + return FMT_U16_BE; + else + return FMT_U16_LE; + } + return fmt; +} + +convert_func_t xmms_convert_get_func( AFormat output, AFormat input ) +{ + output = unnativize( output ); + input = unnativize( input ); + + if ( output == input ) + return NULL; + + if ( ( output == FMT_U16_BE && input == FMT_U16_LE ) || + ( output == FMT_U16_LE && input == FMT_U16_BE ) || + ( output == FMT_S16_BE && input == FMT_S16_LE ) || + ( output == FMT_S16_LE && input == FMT_S16_BE ) ) + return convert_swap_endian; + + if ( ( output == FMT_U16_BE && input == FMT_S16_BE ) || + ( output == FMT_U16_LE && input == FMT_S16_LE ) || + ( output == FMT_S16_BE && input == FMT_U16_BE ) || + ( output == FMT_S16_LE && input == FMT_U16_LE ) ) + return convert_swap_sign16; + + if ( ( IS_BIG_ENDIAN && + ( ( output == FMT_U16_BE && input == FMT_S16_LE ) || + ( output == FMT_S16_BE && input == FMT_U16_LE ) ) ) || + ( !IS_BIG_ENDIAN && + ( ( output == FMT_U16_LE && input == FMT_S16_BE ) || + ( output == FMT_S16_LE && input == FMT_U16_BE ) ) ) ) + return convert_swap_sign_and_endian_to_native; + + if ( ( !IS_BIG_ENDIAN && + ( ( output == FMT_U16_BE && input == FMT_S16_LE ) || + ( output == FMT_S16_BE && input == FMT_U16_LE ) ) ) || + ( IS_BIG_ENDIAN && + ( ( output == FMT_U16_LE && input == FMT_S16_BE ) || + ( output == FMT_S16_LE && input == FMT_U16_BE ) ) ) ) + return convert_swap_sign_and_endian_to_alien; + + if ( ( IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_U16_BE ) || + ( output == FMT_S8 && input == FMT_S16_BE ) ) ) || + ( !IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_U16_LE ) || + ( output == FMT_S8 && input == FMT_S16_LE ) ) ) ) + return convert_to_8_native_endian; + + if ( ( IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_S16_BE ) || + ( output == FMT_S8 && input == FMT_U16_BE ) ) ) || + ( !IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_S16_LE ) || + ( output == FMT_S8 && input == FMT_U16_LE ) ) ) ) + return convert_to_8_native_endian_swap_sign; + + if ( ( !IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_U16_BE ) || + ( output == FMT_S8 && input == FMT_S16_BE ) ) ) || + ( IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_U16_LE ) || + ( output == FMT_S8 && input == FMT_S16_LE ) ) ) ) + return convert_to_8_alien_endian; + + if ( ( !IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_S16_BE ) || + ( output == FMT_S8 && input == FMT_U16_BE ) ) ) || + ( IS_BIG_ENDIAN && + ( ( output == FMT_U8 && input == FMT_S16_LE ) || + ( output == FMT_S8 && input == FMT_U16_LE ) ) ) ) + return convert_to_8_alien_endian_swap_sign; + + if ( ( output == FMT_U8 && input == FMT_S8 ) || + ( output == FMT_S8 && input == FMT_U8 ) ) + return convert_swap_sign8; + + if ( ( IS_BIG_ENDIAN && + ( ( output == FMT_U16_BE && input == FMT_U8 ) || + ( output == FMT_S16_BE && input == FMT_S8 ) ) ) || + ( !IS_BIG_ENDIAN && + ( ( output == FMT_U16_LE && input == FMT_U8 ) || + ( output == FMT_S16_LE && input == FMT_S8 ) ) ) ) + return convert_to_16_native_endian; + + if ( ( IS_BIG_ENDIAN && + ( ( output == FMT_U16_BE && input == FMT_S8 ) || + ( output == FMT_S16_BE && input == FMT_U8 ) ) ) || + ( !IS_BIG_ENDIAN && + ( ( output == FMT_U16_LE && input == FMT_S8 ) || + ( output == FMT_S16_LE && input == FMT_U8 ) ) ) ) + return convert_to_16_native_endian_swap_sign; + + if ( ( !IS_BIG_ENDIAN && + ( ( output == FMT_U16_BE && input == FMT_U8 ) || + ( output == FMT_S16_BE && input == FMT_S8 ) ) ) || + ( IS_BIG_ENDIAN && + ( ( output == FMT_U16_LE && input == FMT_U8 ) || + ( output == FMT_S16_LE && input == FMT_S8 ) ) ) ) + return convert_to_16_alien_endian; + + if ( ( !IS_BIG_ENDIAN && + ( ( output == FMT_U16_BE && input == FMT_S8 ) || + ( output == FMT_S16_BE && input == FMT_U8 ) ) ) || + ( IS_BIG_ENDIAN && + ( ( output == FMT_U16_LE && input == FMT_S8 ) || + ( output == FMT_S16_LE && input == FMT_U8 ) ) ) ) + return convert_to_16_alien_endian_swap_sign; + + //g_warning( "Translation needed, but not available.\n" + // "Input: %d; Output %d.", input, output ); + return NULL; +} + +static int convert_mono_to_stereo( struct xmms_convert_buffers* buf, void **data, int length, int b16 ) +{ + int i; + void *outbuf = convert_get_buffer( &buf->stereo_buffer, length * 2 ); + + if ( b16 ) + { + uint16_t *output = outbuf, *input = *data; + for ( i = 0; i < length / 2; i++ ) + { + *output++ = *input; + *output++ = *input; + input++; + } + } + else + { + uint8_t *output = outbuf, *input = *data; + for ( i = 0; i < length; i++ ) + { + *output++ = *input; + *output++ = *input; + input++; + } + } + *data = outbuf; + + return length * 2; +} + +static int convert_mono_to_stereo_8( struct xmms_convert_buffers* buf, void **data, int length ) +{ + return convert_mono_to_stereo( buf, data, length, FALSE ); +} + +static int convert_mono_to_stereo_16( struct xmms_convert_buffers* buf, void **data, int length ) +{ + return convert_mono_to_stereo( buf, data, length, TRUE ); +} + +static int convert_stereo_to_mono_u8( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint8_t *output = *data, *input = *data; + int i; + for ( i = 0; i < length / 2; i++ ) + { + uint16_t tmp; + tmp = *input++; + tmp += *input++; + *output++ = tmp / 2; + } + return length / 2; +} +static int convert_stereo_to_mono_s8( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int8_t *output = *data, *input = *data; + int i; + for ( i = 0; i < length / 2; i++ ) + { + int16_t tmp; + tmp = *input++; + tmp += *input++; + *output++ = tmp / 2; + } + return length / 2; +} +static int convert_stereo_to_mono_u16le( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint16_t *output = *data, *input = *data; + int i; + for ( i = 0; i < length / 4; i++ ) + { + uint32_t tmp; + uint16_t stmp; + tmp = GUINT16_FROM_LE( *input ); + input++; + tmp += GUINT16_FROM_LE( *input ); + input++; + stmp = tmp / 2; + *output++ = GUINT16_TO_LE( stmp ); + } + return length / 2; +} + +static int convert_stereo_to_mono_u16be( struct xmms_convert_buffers* buf, void **data, int length ) +{ + uint16_t *output = *data, *input = *data; + int i; + for ( i = 0; i < length / 4; i++ ) + { + uint32_t tmp; + uint16_t stmp; + tmp = GUINT16_FROM_BE( *input ); + input++; + tmp += GUINT16_FROM_BE( *input ); + input++; + stmp = tmp / 2; + *output++ = GUINT16_TO_BE( stmp ); + } + return length / 2; +} + +static int convert_stereo_to_mono_s16le( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int16_t *output = *data, *input = *data; + int i; + for ( i = 0; i < length / 4; i++ ) + { + int32_t tmp; + int16_t stmp; + tmp = GINT16_FROM_LE( *input ); + input++; + tmp += GINT16_FROM_LE( *input ); + input++; + stmp = tmp / 2; + *output++ = GINT16_TO_LE( stmp ); + } + return length / 2; +} + +static int convert_stereo_to_mono_s16be( struct xmms_convert_buffers* buf, void **data, int length ) +{ + int16_t *output = *data, *input = *data; + int i; + for ( i = 0; i < length / 4; i++ ) + { + int32_t tmp; + int16_t stmp; + tmp = GINT16_FROM_BE( *input ); + input++; + tmp += GINT16_FROM_BE( *input ); + input++; + stmp = tmp / 2; + *output++ = GINT16_TO_BE( stmp ); + } + return length / 2; +} + +convert_channel_func_t xmms_convert_get_channel_func( AFormat fmt, int output, int input ) +{ + fmt = unnativize( fmt ); + + if ( output == input ) + return NULL; + + if ( input == 1 && output == 2 ) + switch ( fmt ) + { + case FMT_U8: + case FMT_S8: + return convert_mono_to_stereo_8; + case FMT_U16_LE: + case FMT_U16_BE: + case FMT_S16_LE: + case FMT_S16_BE: + return convert_mono_to_stereo_16; + default: + //g_warning( "Unknown format: %d" + // "No conversion available.", fmt ); + return NULL; + } + if ( input == 2 && output == 1 ) + switch ( fmt ) + { + case FMT_U8: + return convert_stereo_to_mono_u8; + case FMT_S8: + return convert_stereo_to_mono_s8; + case FMT_U16_LE: + return convert_stereo_to_mono_u16le; + case FMT_U16_BE: + return convert_stereo_to_mono_u16be; + case FMT_S16_LE: + return convert_stereo_to_mono_s16le; + case FMT_S16_BE: + return convert_stereo_to_mono_s16be; + default: + //g_warning( "Unknown format: %d. " + // "No conversion available.", fmt ); + return NULL; + } + + //g_warning( "Input has %d channels, soundcard uses %d channels\n" + // "No conversion is available", input, output ); + return NULL; +} + + +#define RESAMPLE_STEREO( sample_type, bswap ) \ +do { \ + const int shift = sizeof ( sample_type ); \ + int i, in_samples, out_samples, x, delta; \ + sample_type *inptr = *data, *outptr; \ + uint nlen = ( ( ( length >> shift ) * ofreq ) / ifreq ); \ + void *nbuf; \ + if ( nlen == 0 ) \ + break; \ + nlen <<= shift; \ + if ( bswap ) \ + convert_swap_endian( NULL, data, length ); \ + nbuf = convert_get_buffer( &buf->freq_buffer, nlen ); \ + outptr = nbuf; \ + in_samples = length >> shift; \ + out_samples = nlen >> shift; \ + delta = ( in_samples << 12 ) / out_samples; \ + for ( x = 0, i = 0; i < out_samples; i++ ) \ + { \ + int x1, frac; \ + x1 = ( x >> 12 ) << 12; \ + frac = x - x1; \ + *outptr++ = \ + ( ( inptr[( x1 >> 12 ) << 1] * \ + ( ( 1<<12 ) - frac ) + \ + inptr[( ( x1 >> 12 ) + 1 ) << 1] * \ + frac ) >> 12 ); \ + *outptr++ = \ + ( ( inptr[( ( x1 >> 12 ) << 1 ) + 1] * \ + ( ( 1<<12 ) - frac ) + \ + inptr[( ( ( x1 >> 12 ) + 1 ) << 1 ) + 1] * \ + frac ) >> 12 ); \ + x += delta; \ + } \ + if ( bswap ) \ + convert_swap_endian( NULL, &nbuf, nlen ); \ + *data = nbuf; \ + return nlen; \ +} while ( 0 ) + + +#define RESAMPLE_MONO( sample_type, bswap ) \ +do { \ + const int shift = sizeof ( sample_type ) - 1; \ + int i, x, delta, in_samples, out_samples; \ + sample_type *inptr = *data, *outptr; \ + uint nlen = ( ( ( length >> shift ) * ofreq ) / ifreq ); \ + void *nbuf; \ + if ( nlen == 0 ) \ + break; \ + nlen <<= shift; \ + if ( bswap ) \ + convert_swap_endian( NULL, data, length ); \ + nbuf = convert_get_buffer( &buf->freq_buffer, nlen ); \ + outptr = nbuf; \ + in_samples = length >> shift; \ + out_samples = nlen >> shift; \ + delta = ( ( length >> shift ) << 12 ) / out_samples; \ + for ( x = 0, i = 0; i < out_samples; i++ ) \ + { \ + int x1, frac; \ + x1 = ( x >> 12 ) << 12; \ + frac = x - x1; \ + *outptr++ = \ + ( ( inptr[x1 >> 12] * ( ( 1<<12 ) - frac ) + \ + inptr[( x1 >> 12 ) + 1] * frac ) >> 12 ); \ + x += delta; \ + } \ + if ( bswap ) \ + convert_swap_endian( NULL, &nbuf, nlen ); \ + *data = nbuf; \ + return nlen; \ +} while ( 0 ) + +static int convert_resample_stereo_s16ne( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_STEREO( int16_t, FALSE ); + return 0; +} + +static int convert_resample_stereo_s16ae( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_STEREO( int16_t, TRUE ); + return 0; +} + +static int convert_resample_stereo_u16ne( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_STEREO( uint16_t, FALSE ); + return 0; +} + +static int convert_resample_stereo_u16ae( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_STEREO( uint16_t, TRUE ); + return 0; +} + +static int convert_resample_mono_s16ne( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_MONO( int16_t, FALSE ); + return 0; +} + +static int convert_resample_mono_s16ae( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_MONO( int16_t, TRUE ); + return 0; +} + +static int convert_resample_mono_u16ne( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_MONO( uint16_t, FALSE ); + return 0; +} + +static int convert_resample_mono_u16ae( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_MONO( uint16_t, TRUE ); + return 0; +} + +static int convert_resample_stereo_u8( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_STEREO( uint8_t, FALSE ); + return 0; +} + +static int convert_resample_mono_u8( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_MONO( uint8_t, FALSE ); + return 0; +} + +static int convert_resample_stereo_s8( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_STEREO( int8_t, FALSE ); + return 0; +} + +static int convert_resample_mono_s8( struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq ) +{ + RESAMPLE_MONO( int8_t, FALSE ); + return 0; +} + + +convert_freq_func_t xmms_convert_get_frequency_func( AFormat fmt, int channels ) +{ + fmt = unnativize( fmt ); + //g_message( "fmt %d, channels: %d", fmt, channels ); + + if ( channels < 1 || channels > 2 ) + { + //g_warning( "Unsupported number of channels: %d. " + // "Resample function not available", channels ); + return NULL; + } + if ( ( IS_BIG_ENDIAN && fmt == FMT_U16_BE ) || + ( !IS_BIG_ENDIAN && fmt == FMT_U16_LE ) ) + { + if ( channels == 1 ) + return convert_resample_mono_u16ne; + else + return convert_resample_stereo_u16ne; + } + if ( ( IS_BIG_ENDIAN && fmt == FMT_S16_BE ) || + ( !IS_BIG_ENDIAN && fmt == FMT_S16_LE ) ) + { + if ( channels == 1 ) + return convert_resample_mono_s16ne; + else + return convert_resample_stereo_s16ne; + } + if ( ( !IS_BIG_ENDIAN && fmt == FMT_U16_BE ) || + ( IS_BIG_ENDIAN && fmt == FMT_U16_LE ) ) + { + if ( channels == 1 ) + return convert_resample_mono_u16ae; + else + return convert_resample_stereo_u16ae; + } + if ( ( !IS_BIG_ENDIAN && fmt == FMT_S16_BE ) || + ( IS_BIG_ENDIAN && fmt == FMT_S16_LE ) ) + { + if ( channels == 1 ) + return convert_resample_mono_s16ae; + else + return convert_resample_stereo_s16ae; + } + if ( fmt == FMT_U8 ) + { + if ( channels == 1 ) + return convert_resample_mono_u8; + else + return convert_resample_stereo_u8; + } + if ( fmt == FMT_S8 ) + { + if ( channels == 1 ) + return convert_resample_mono_s8; + else + return convert_resample_stereo_s8; + } + //g_warning( "Resample function not available" + // "Format %d.", fmt ); + return NULL; +} diff --git a/alsa-playback/xconvert.h b/alsa-playback/xconvert.h new file mode 100644 index 000000000..8e831b758 --- /dev/null +++ b/alsa-playback/xconvert.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2003 Haavard Kvaalen + * + * Licensed under GNU LGPL version 2. + */ + +#if BYTE_ORDER == BIG_ENDIAN +#define WORDS_BIGENDIAN 1 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum +{ + FMT_U8, FMT_S8, FMT_U16_LE, FMT_U16_BE, FMT_U16_NE, FMT_S16_LE, FMT_S16_BE, FMT_S16_NE +} +AFormat; + +struct xmms_convert_buffers; + +struct xmms_convert_buffers* xmms_convert_buffers_new(void); +/* + * Free the data assosiated with the buffers, without destroying the + * context. The context can be reused. + */ +void xmms_convert_buffers_free(struct xmms_convert_buffers* buf); +void xmms_convert_buffers_destroy(struct xmms_convert_buffers* buf); + + +typedef int (*convert_func_t)(struct xmms_convert_buffers* buf, void **data, int length); +typedef int (*convert_channel_func_t)(struct xmms_convert_buffers* buf, void **data, int length); +typedef int (*convert_freq_func_t)(struct xmms_convert_buffers* buf, void **data, int length, int ifreq, int ofreq); + + +convert_func_t xmms_convert_get_func(AFormat output, AFormat input); +convert_channel_func_t xmms_convert_get_channel_func(AFormat fmt, int output, int input); +convert_freq_func_t xmms_convert_get_frequency_func(AFormat fmt, int channels); + +#ifdef __cplusplus +} +#endif diff --git a/data/icons/tomahawk-icon-128.png b/data/icons/tomahawk-icon-128.png new file mode 100644 index 000000000..d969e92af Binary files /dev/null and b/data/icons/tomahawk-icon-128.png differ diff --git a/data/icons/tomahawk-icon-16.png b/data/icons/tomahawk-icon-16.png new file mode 100644 index 000000000..290628f0d Binary files /dev/null and b/data/icons/tomahawk-icon-16.png differ diff --git a/data/icons/tomahawk-icon-256.png b/data/icons/tomahawk-icon-256.png new file mode 100644 index 000000000..3e9d66c92 Binary files /dev/null and b/data/icons/tomahawk-icon-256.png differ diff --git a/data/icons/tomahawk-icon-32.png b/data/icons/tomahawk-icon-32.png new file mode 100644 index 000000000..5d4fa7c8d Binary files /dev/null and b/data/icons/tomahawk-icon-32.png differ diff --git a/data/icons/tomahawk-icon-512.png b/data/icons/tomahawk-icon-512.png new file mode 100644 index 000000000..23594c11f Binary files /dev/null and b/data/icons/tomahawk-icon-512.png differ diff --git a/data/icons/tomahawk-icon-64.png b/data/icons/tomahawk-icon-64.png new file mode 100644 index 000000000..0de2863dc Binary files /dev/null and b/data/icons/tomahawk-icon-64.png differ diff --git a/data/icons/tomahawk.icns b/data/icons/tomahawk.icns new file mode 100644 index 000000000..92f4f310d Binary files /dev/null and b/data/icons/tomahawk.icns differ diff --git a/data/icons/tomahawk.ico b/data/icons/tomahawk.ico new file mode 100644 index 000000000..4e6852f2d Binary files /dev/null and b/data/icons/tomahawk.ico differ diff --git a/data/images/add-friend-button-pressed.png b/data/images/add-friend-button-pressed.png new file mode 100644 index 000000000..960a85862 Binary files /dev/null and b/data/images/add-friend-button-pressed.png differ diff --git a/data/images/add-friend-button-rest.png b/data/images/add-friend-button-rest.png new file mode 100644 index 000000000..a3c2a4447 Binary files /dev/null and b/data/images/add-friend-button-rest.png differ diff --git a/data/images/avatar-dude-plus.png b/data/images/avatar-dude-plus.png new file mode 100644 index 000000000..961b05000 Binary files /dev/null and b/data/images/avatar-dude-plus.png differ diff --git a/data/images/avatar-dude.png b/data/images/avatar-dude.png new file mode 100644 index 000000000..f1c2ebca8 Binary files /dev/null and b/data/images/avatar-dude.png differ diff --git a/data/images/back-pressed.png b/data/images/back-pressed.png new file mode 100644 index 000000000..ef4352ec0 Binary files /dev/null and b/data/images/back-pressed.png differ diff --git a/data/images/back-rest.png b/data/images/back-rest.png new file mode 100644 index 000000000..bfdc807d8 Binary files /dev/null and b/data/images/back-rest.png differ diff --git a/data/images/magnifying-glass.png b/data/images/magnifying-glass.png new file mode 100644 index 000000000..04db422cd Binary files /dev/null and b/data/images/magnifying-glass.png differ diff --git a/data/images/no-album-art-placeholder.png b/data/images/no-album-art-placeholder.png new file mode 100644 index 000000000..55e1f20fa Binary files /dev/null and b/data/images/no-album-art-placeholder.png differ diff --git a/data/images/now-playing-panel.png b/data/images/now-playing-panel.png new file mode 100644 index 000000000..780895cec Binary files /dev/null and b/data/images/now-playing-panel.png differ diff --git a/data/images/now-playing-speaker.png b/data/images/now-playing-speaker.png new file mode 100644 index 000000000..b1ba673cf Binary files /dev/null and b/data/images/now-playing-speaker.png differ diff --git a/data/images/pause-pressed.png b/data/images/pause-pressed.png new file mode 100644 index 000000000..7cb3dc75e Binary files /dev/null and b/data/images/pause-pressed.png differ diff --git a/data/images/pause-rest.png b/data/images/pause-rest.png new file mode 100644 index 000000000..0044d6a99 Binary files /dev/null and b/data/images/pause-rest.png differ diff --git a/data/images/play-pressed.png b/data/images/play-pressed.png new file mode 100644 index 000000000..d2f9ba590 Binary files /dev/null and b/data/images/play-pressed.png differ diff --git a/data/images/play-rest.png b/data/images/play-rest.png new file mode 100644 index 000000000..e8e9bbd81 Binary files /dev/null and b/data/images/play-rest.png differ diff --git a/data/images/playlist-icon.png b/data/images/playlist-icon.png new file mode 100644 index 000000000..99ac953b1 Binary files /dev/null and b/data/images/playlist-icon.png differ diff --git a/data/images/repeat-1-on-pressed.png b/data/images/repeat-1-on-pressed.png new file mode 100644 index 000000000..1c4180701 Binary files /dev/null and b/data/images/repeat-1-on-pressed.png differ diff --git a/data/images/repeat-1-on-rest.png b/data/images/repeat-1-on-rest.png new file mode 100644 index 000000000..cbbb47189 Binary files /dev/null and b/data/images/repeat-1-on-rest.png differ diff --git a/data/images/repeat-all-on-pressed.png b/data/images/repeat-all-on-pressed.png new file mode 100644 index 000000000..bd67f40d5 Binary files /dev/null and b/data/images/repeat-all-on-pressed.png differ diff --git a/data/images/repeat-all-on-rest.png b/data/images/repeat-all-on-rest.png new file mode 100644 index 000000000..f8d4fc101 Binary files /dev/null and b/data/images/repeat-all-on-rest.png differ diff --git a/data/images/repeat-off-pressed.png b/data/images/repeat-off-pressed.png new file mode 100644 index 000000000..61f6f89d9 Binary files /dev/null and b/data/images/repeat-off-pressed.png differ diff --git a/data/images/repeat-off-rest.png b/data/images/repeat-off-rest.png new file mode 100644 index 000000000..8f095afa0 Binary files /dev/null and b/data/images/repeat-off-rest.png differ diff --git a/data/images/search-box-dismiss-x.png b/data/images/search-box-dismiss-x.png new file mode 100644 index 000000000..cdbc019f7 Binary files /dev/null and b/data/images/search-box-dismiss-x.png differ diff --git a/data/images/search-box.png b/data/images/search-box.png new file mode 100644 index 000000000..c6c136689 Binary files /dev/null and b/data/images/search-box.png differ diff --git a/data/images/seek-and-volume-knob-pressed.png b/data/images/seek-and-volume-knob-pressed.png new file mode 100644 index 000000000..bbfc01a0a Binary files /dev/null and b/data/images/seek-and-volume-knob-pressed.png differ diff --git a/data/images/seek-and-volume-knob-rest.png b/data/images/seek-and-volume-knob-rest.png new file mode 100644 index 000000000..2152a6bc9 Binary files /dev/null and b/data/images/seek-and-volume-knob-rest.png differ diff --git a/data/images/seek-slider-bkg.png b/data/images/seek-slider-bkg.png new file mode 100644 index 000000000..59d0869bd Binary files /dev/null and b/data/images/seek-slider-bkg.png differ diff --git a/data/images/seek-slider-level.png b/data/images/seek-slider-level.png new file mode 100644 index 000000000..3589d64a4 Binary files /dev/null and b/data/images/seek-slider-level.png differ diff --git a/data/images/shuffle-off-pressed.png b/data/images/shuffle-off-pressed.png new file mode 100644 index 000000000..215c82016 Binary files /dev/null and b/data/images/shuffle-off-pressed.png differ diff --git a/data/images/shuffle-off-rest.png b/data/images/shuffle-off-rest.png new file mode 100644 index 000000000..cb6d6c099 Binary files /dev/null and b/data/images/shuffle-off-rest.png differ diff --git a/data/images/shuffle-on-pressed.png b/data/images/shuffle-on-pressed.png new file mode 100644 index 000000000..e85018d9a Binary files /dev/null and b/data/images/shuffle-on-pressed.png differ diff --git a/data/images/shuffle-on-rest.png b/data/images/shuffle-on-rest.png new file mode 100644 index 000000000..928095c8b Binary files /dev/null and b/data/images/shuffle-on-rest.png differ diff --git a/data/images/skip-pressed.png b/data/images/skip-pressed.png new file mode 100644 index 000000000..ce8f6fed0 Binary files /dev/null and b/data/images/skip-pressed.png differ diff --git a/data/images/skip-rest.png b/data/images/skip-rest.png new file mode 100644 index 000000000..649c55853 Binary files /dev/null and b/data/images/skip-rest.png differ diff --git a/data/images/source-off-pressed.png b/data/images/source-off-pressed.png new file mode 100644 index 000000000..90ee6e3e2 Binary files /dev/null and b/data/images/source-off-pressed.png differ diff --git a/data/images/source-off-rest.png b/data/images/source-off-rest.png new file mode 100644 index 000000000..9c9b09392 Binary files /dev/null and b/data/images/source-off-rest.png differ diff --git a/data/images/source-on-pressed.png b/data/images/source-on-pressed.png new file mode 100644 index 000000000..4a0f0d3d4 Binary files /dev/null and b/data/images/source-on-pressed.png differ diff --git a/data/images/source-on-rest.png b/data/images/source-on-rest.png new file mode 100644 index 000000000..2877e476e Binary files /dev/null and b/data/images/source-on-rest.png differ diff --git a/data/images/status-alert-icon.png b/data/images/status-alert-icon.png new file mode 100644 index 000000000..a2c2f9f2f Binary files /dev/null and b/data/images/status-alert-icon.png differ diff --git a/data/images/status-bar-bkg.png b/data/images/status-bar-bkg.png new file mode 100644 index 000000000..10ad87e54 Binary files /dev/null and b/data/images/status-bar-bkg.png differ diff --git a/data/images/status-dismiss-x.png b/data/images/status-dismiss-x.png new file mode 100644 index 000000000..0e8827181 Binary files /dev/null and b/data/images/status-dismiss-x.png differ diff --git a/data/images/user-avatar.png b/data/images/user-avatar.png new file mode 100644 index 000000000..5be7b700f Binary files /dev/null and b/data/images/user-avatar.png differ diff --git a/data/images/view-toggle-active-centre.png b/data/images/view-toggle-active-centre.png new file mode 100644 index 000000000..a58a7a2f9 Binary files /dev/null and b/data/images/view-toggle-active-centre.png differ diff --git a/data/images/view-toggle-active-left.png b/data/images/view-toggle-active-left.png new file mode 100644 index 000000000..71f591bf6 Binary files /dev/null and b/data/images/view-toggle-active-left.png differ diff --git a/data/images/view-toggle-active-right.png b/data/images/view-toggle-active-right.png new file mode 100644 index 000000000..2da39f935 Binary files /dev/null and b/data/images/view-toggle-active-right.png differ diff --git a/data/images/view-toggle-icon-artist-active.png b/data/images/view-toggle-icon-artist-active.png new file mode 100644 index 000000000..90d837073 Binary files /dev/null and b/data/images/view-toggle-icon-artist-active.png differ diff --git a/data/images/view-toggle-icon-artist-inactive.png b/data/images/view-toggle-icon-artist-inactive.png new file mode 100644 index 000000000..fd96b230c Binary files /dev/null and b/data/images/view-toggle-icon-artist-inactive.png differ diff --git a/data/images/view-toggle-icon-cloud-active.png b/data/images/view-toggle-icon-cloud-active.png new file mode 100644 index 000000000..17ce67bdb Binary files /dev/null and b/data/images/view-toggle-icon-cloud-active.png differ diff --git a/data/images/view-toggle-icon-cloud-inactive.png b/data/images/view-toggle-icon-cloud-inactive.png new file mode 100644 index 000000000..4546365b1 Binary files /dev/null and b/data/images/view-toggle-icon-cloud-inactive.png differ diff --git a/data/images/view-toggle-icon-list-active.png b/data/images/view-toggle-icon-list-active.png new file mode 100644 index 000000000..9b2f1e25c Binary files /dev/null and b/data/images/view-toggle-icon-list-active.png differ diff --git a/data/images/view-toggle-icon-list-inactive.png b/data/images/view-toggle-icon-list-inactive.png new file mode 100644 index 000000000..aecdccf7e Binary files /dev/null and b/data/images/view-toggle-icon-list-inactive.png differ diff --git a/data/images/view-toggle-inactive-centre.png b/data/images/view-toggle-inactive-centre.png new file mode 100644 index 000000000..577566dfc Binary files /dev/null and b/data/images/view-toggle-inactive-centre.png differ diff --git a/data/images/view-toggle-inactive-left.png b/data/images/view-toggle-inactive-left.png new file mode 100644 index 000000000..eafd06879 Binary files /dev/null and b/data/images/view-toggle-inactive-left.png differ diff --git a/data/images/view-toggle-inactive-right.png b/data/images/view-toggle-inactive-right.png new file mode 100644 index 000000000..1c8d61e73 Binary files /dev/null and b/data/images/view-toggle-inactive-right.png differ diff --git a/data/images/view-toggle-pressed-centre.png b/data/images/view-toggle-pressed-centre.png new file mode 100644 index 000000000..89fe1cc85 Binary files /dev/null and b/data/images/view-toggle-pressed-centre.png differ diff --git a/data/images/view-toggle-pressed-left.png b/data/images/view-toggle-pressed-left.png new file mode 100644 index 000000000..5f1f380a7 Binary files /dev/null and b/data/images/view-toggle-pressed-left.png differ diff --git a/data/images/view-toggle-pressed-right.png b/data/images/view-toggle-pressed-right.png new file mode 100644 index 000000000..649c5ce66 Binary files /dev/null and b/data/images/view-toggle-pressed-right.png differ diff --git a/data/images/volume-icon-full.png b/data/images/volume-icon-full.png new file mode 100644 index 000000000..f5eb34ca3 Binary files /dev/null and b/data/images/volume-icon-full.png differ diff --git a/data/images/volume-icon-muted.png b/data/images/volume-icon-muted.png new file mode 100644 index 000000000..d42a064c2 Binary files /dev/null and b/data/images/volume-icon-muted.png differ diff --git a/data/images/volume-slider-bkg.png b/data/images/volume-slider-bkg.png new file mode 100644 index 000000000..4efcd5013 Binary files /dev/null and b/data/images/volume-slider-bkg.png differ diff --git a/data/images/volume-slider-level.png b/data/images/volume-slider-level.png new file mode 100644 index 000000000..d353bf17b Binary files /dev/null and b/data/images/volume-slider-level.png differ diff --git a/data/topbar-radiobuttons.css b/data/topbar-radiobuttons.css new file mode 100644 index 000000000..077315fd7 --- /dev/null +++ b/data/topbar-radiobuttons.css @@ -0,0 +1,55 @@ +QWidget#widgetRadio { + margin:0; + padding:0; + border: 0; +} + +QRadioButton { + border: 0; + margin:0; + padding:0; + background-repeat: none; + /*width:0; height:0;*/ +} + +QRadioButton::indicator { + width: 29px; + height: 30px; + } + +QRadioButton::indicator::unchecked { + background-image: url(:/data/images/view-toggle-inactive-centre.png); + image: url(:/data/images/view-toggle-icon-artist-inactive.png); +} +QRadioButton::indicator::checked { + background-image: url(:/data/images/view-toggle-active-centre.png); + image: url(:/data/images/view-toggle-icon-artist-active.png); +} +QRadioButton::indicator::pressed { + background-image: url(:/data/images/view-toggle-pressed-centre.png); + image: url(:/data/images/view-toggle-icon-artist-active.png); +} +QRadioButton#radioNormal::indicator::unchecked { + background-image: url(:/data/images/view-toggle-inactive-left.png); + image: url(:/data/images/view-toggle-icon-list-inactive.png); +} +QRadioButton#radioNormal::indicator::checked { + background-image: url(:/data/images/view-toggle-active-left.png); + image: url(:/data/images/view-toggle-icon-list-active.png); +} +QRadioButton#radioNormal::indicator::pressed { + background-image: url(:/data/images/view-toggle-pressed-left.png); + image: url(:/data/images/view-toggle-icon-list-active.png); +} +QRadioButton#radioCloud::indicator::unchecked { + background-image: url(:/data/images/view-toggle-inactive-right.png); + image: url(:/data/images/view-toggle-icon-cloud-inactive.png); +} +QRadioButton#radioCloud::indicator::checked { + background-image: url(:/data/images/view-toggle-active-right.png); + image: url(:/data/images/view-toggle-icon-cloud-active.png); +} +QRadioButton#radioCloud::indicator::pressed { + background-image: url(:/data/images/view-toggle-pressed-right.png); + image: url(:/data/images/view-toggle-icon-cloud-active.png); +} diff --git a/gen_resources.sh b/gen_resources.sh new file mode 100755 index 000000000..a9be92bf2 --- /dev/null +++ b/gen_resources.sh @@ -0,0 +1,13 @@ +#!/bin/bash +echo "" + +datadir="`pwd`/data" + +cd "$datadir" +(find -type f | sed 's/^\.\///g') | while read f +do + ff="${datadir}/$f" + echo "$ff" +done + +echo "" diff --git a/include/tomahawk/album.h b/include/tomahawk/album.h new file mode 100644 index 000000000..8453b36d2 --- /dev/null +++ b/include/tomahawk/album.h @@ -0,0 +1,35 @@ +#ifndef TOMAHAWKALBUM_H +#define TOMAHAWKALBUM_H + +#include +#include + +#include "artist.h" + +namespace Tomahawk +{ + +class Album; +typedef QSharedPointer album_ptr; + +class Album : public QObject +{ +Q_OBJECT + +public: + Album( artist_ptr artist, const QString& name ) + : m_name( name ) + , m_artist( artist ) + {} + + const QString& name() const { return m_name; } + const artist_ptr artist() const { return m_artist; } + +private: + QString m_name; + artist_ptr m_artist; +}; + +}; // ns + +#endif diff --git a/include/tomahawk/artist.h b/include/tomahawk/artist.h new file mode 100644 index 000000000..c5861b268 --- /dev/null +++ b/include/tomahawk/artist.h @@ -0,0 +1,30 @@ +#ifndef TOMAHAWKARTIST_H +#define TOMAHAWKARTIST_H + +#include +#include + +namespace Tomahawk +{ + +class Artist; +typedef QSharedPointer artist_ptr; + +class Artist : public QObject +{ +Q_OBJECT + +public: + Artist( const QString& name ) + : m_name( name ) + {}; + + const QString& name() const { return m_name; } + +private: + QString m_name; +}; + +}; // ns + +#endif diff --git a/include/tomahawk/collection.h b/include/tomahawk/collection.h new file mode 100644 index 000000000..b423a6970 --- /dev/null +++ b/include/tomahawk/collection.h @@ -0,0 +1,100 @@ +/* + The collection - acts as container for someones music library + load() -> async populate by calling addArtists etc, + then finishedLoading() is emitted. + then use artists() etc to get the data. +*/ + +#ifndef TOMAHAWK_COLLECTION_H +#define TOMAHAWK_COLLECTION_H + +#include +#include +#include +#include + +#include "tomahawk/functimeout.h" +#include "tomahawk/playlist.h" +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" + +namespace Tomahawk +{ + +/* + Call load(), then wait for the finishedLoading() signal, + then call tracks() to get all tracks. + */ + +class Collection : public QObject +{ +Q_OBJECT + +public: + Collection( const source_ptr& source, const QString& name, QObject* parent = 0 ); + virtual ~Collection(); + + void invokeSlotTracks( QObject* obj, const char* slotname, const QList& val, collection_ptr collection ); + + virtual QString name() const; + + virtual void loadPlaylists() = 0; + + virtual Tomahawk::playlist_ptr playlist( const QString& guid ); + virtual void addPlaylist( const Tomahawk::playlist_ptr& p ); + virtual void deletePlaylist( const Tomahawk::playlist_ptr& p ); + + /// async calls that fetch data from DB/whatever: + void loadTracks( QObject* obj, const char* slotname ); + + virtual const QList< Tomahawk::playlist_ptr >& playlists() const { return m_playlists; } + + bool isLoaded() const { return m_loaded; } + + const source_ptr& source() const { return m_source; } + unsigned int lastmodified() const { return m_lastmodified; } + + static bool trackSorter( const QVariant& left, const QVariant &right ); + +signals: + void tracksAdded( const QList&, Tomahawk::collection_ptr ); + void tracksRemoved( const QList&, Tomahawk::collection_ptr ); + + void playlistsAdded( const QList& ); + void playlistsDeleted( const QList& ); + +public slots: + virtual void addTracks( const QList &newitems ) = 0; + virtual void removeTracks( const QList &olditems ) = 0; + + void setPlaylists( const QList& plists ) + { + qDebug() << Q_FUNC_INFO << plists.length(); + m_playlists.append( plists ); + if( !m_loaded ) + { + m_loaded = true; + emit playlistsAdded( plists ); + } + } + +protected: + virtual void loadAllTracks( boost::function&, collection_ptr )> callback ) = 0; + + QString m_name; + bool m_loaded; + unsigned int m_lastmodified; // unix time of last change to collection + +private: + source_ptr m_source; + QList< Tomahawk::playlist_ptr > m_playlists; +}; + +}; // ns + +inline uint qHash( const QSharedPointer& key ) +{ + return qHash( (void *)key.data() ); +} + +#endif // TOMAHAWK_COLLECTION_H diff --git a/include/tomahawk/functimeout.h b/include/tomahawk/functimeout.h new file mode 100644 index 000000000..5da67a526 --- /dev/null +++ b/include/tomahawk/functimeout.h @@ -0,0 +1,51 @@ +#ifndef FUNCTIMEOUT_H +#define FUNCTIMEOUT_H + +#include +#include +#include + +#include "boost/function.hpp" +#include "boost/bind.hpp" + +/* + I want to do: + QTimer::singleShot(1000, this, SLOT(doSomething(x))); + instead, I'm doing: + new FuncTimeout(1000, boost::bind(&MyClass::doSomething, this, x)); + + */ +namespace Tomahawk +{ + +class FuncTimeout : public QObject +{ +Q_OBJECT + +public: + FuncTimeout( int ms, boost::function func ) + : m_func( func ) + { + //qDebug() << Q_FUNC_INFO; + QTimer::singleShot( ms, this, SLOT(exec() ) ); + }; + + ~FuncTimeout() + { + //qDebug() << Q_FUNC_INFO; + }; + +public slots: + void exec() + { + m_func(); + this->deleteLater(); + }; + +private: + boost::function m_func; +}; + +}; // ns + +#endif // FUNCTIMEOUT_H diff --git a/include/tomahawk/pipeline.h b/include/tomahawk/pipeline.h new file mode 100644 index 000000000..17dce1895 --- /dev/null +++ b/include/tomahawk/pipeline.h @@ -0,0 +1,70 @@ +#ifndef PIPELINE_H +#define PIPELINE_H + +#include +#include +#include +#include + +#include "tomahawk/typedefs.h" +#include "tomahawk/query.h" +#include "tomahawk/result.h" +#include "tomahawk/resolver.h" + +namespace Tomahawk +{ + +class Resolver; + +class Pipeline : public QObject +{ +Q_OBJECT + +public: + explicit Pipeline( QObject* parent = 0 ); + +// const query_ptr& query( QID qid ) const; +// result_ptr result( RID rid ) const; + + void reportResults( QID qid, const QList< result_ptr >& results ); + + /// sorter to rank resolver priority + static bool resolverSorter( const Resolver* left, const Resolver* right ); + + void addResolver( Resolver* r, bool sort = true ); + void removeResolver( Resolver* r ); + + query_ptr query( const QID& qid ) const + { + return m_qids.value( qid ); + } + + result_ptr result( const RID& rid ) const + { + return m_rids.value( rid ); + } + +public slots: + void add( const query_ptr& q ); + void add( const QList& qlist ); + void databaseReady(); + +private slots: + void shunt( const query_ptr& q ); + void indexReady(); + +private: + QList< Resolver* > m_resolvers; + QMap< QID, query_ptr > m_qids; + QMap< RID, result_ptr > m_rids; + + QMutex m_mut; // for m_qids, m_rids + + // store queries here until DB index is loaded, then shunt them all + QList< query_ptr > m_queries_pending; + bool m_index_ready; +}; + +}; //ns + +#endif // PIPELINE_H diff --git a/include/tomahawk/playlist.h b/include/tomahawk/playlist.h new file mode 100644 index 000000000..1825b358b --- /dev/null +++ b/include/tomahawk/playlist.h @@ -0,0 +1,190 @@ +#ifndef PLAYLIST_H +#define PLAYLIST_H + +#include +#include +#include + +#include "tomahawk/query.h" +#include "tomahawk/typedefs.h" + +class DatabaseCommand_LoadAllPlaylists; +class DatabaseCommand_SetPlaylistRevision; +class DatabaseCommand_CreatePlaylist; + +namespace Tomahawk +{ + +class PlaylistEntry : public QObject +{ +Q_OBJECT +Q_PROPERTY( QString guid READ guid WRITE setGuid ) +Q_PROPERTY( QString annotation READ annotation WRITE setAnnotation ) +Q_PROPERTY( QString resulthint READ resulthint WRITE setResulthint ) +Q_PROPERTY( unsigned int duration READ duration WRITE setDuration ) +Q_PROPERTY( unsigned int lastmodified READ lastmodified WRITE setLastmodified ) +Q_PROPERTY( QVariant query READ queryvariant WRITE setQueryvariant ) + +public: + void setQuery( const Tomahawk::query_ptr& q ) { m_query = q; } + const Tomahawk::query_ptr& query() const { return m_query; } + + // I wish Qt did this for me once i specified the Q_PROPERTIES: + void setQueryvariant( const QVariant& v ); + QVariant queryvariant() const; + + QString guid() const { return m_guid; } + void setGuid( const QString& s ) { m_guid = s; } + + QString annotation() const { return m_annotation; } + void setAnnotation( const QString& s ) { m_annotation = s; } + + QString resulthint() const { return m_resulthint; } + void setResulthint( const QString& s ) { m_resulthint= s; } + + unsigned int duration() const { return m_duration; } + void setDuration( unsigned int i ) { m_duration = i; } + + unsigned int lastmodified() const { return m_lastmodified; } + void setLastmodified( unsigned int i ) { m_lastmodified = i; } + + source_ptr lastsource() const { return m_lastsource; } + void setLastsource( source_ptr s ) { m_lastsource = s ; } + +private: + QString m_guid; + Tomahawk::query_ptr m_query; + QString m_annotation; + unsigned int m_duration; + unsigned int m_lastmodified; + source_ptr m_lastsource; + QString m_resulthint; +}; + + +struct PlaylistRevision +{ + QString revisionguid; + QString oldrevisionguid; + QList newlist; + QList added; + QList removed; + bool applied; // false if conflict +}; + + +class Playlist : public QObject +{ +Q_OBJECT +Q_PROPERTY( QString guid READ guid WRITE setGuid ) +Q_PROPERTY( QString currentrevision READ currentrevision WRITE setCurrentrevision ) +Q_PROPERTY( QString title READ title WRITE setTitle ) +Q_PROPERTY( QString info READ info WRITE setInfo ) +Q_PROPERTY( QString creator READ creator WRITE setCreator ) +Q_PROPERTY( bool shared READ shared WRITE setShared ) + +friend class ::DatabaseCommand_LoadAllPlaylists; +friend class ::DatabaseCommand_SetPlaylistRevision; +friend class ::DatabaseCommand_CreatePlaylist; + +public: + // one CTOR is private, only called by DatabaseCommand_LoadAllPlaylists + static Tomahawk::playlist_ptr create( const source_ptr& author, + const QString& guid, + const QString& title, + const QString& info, + const QString& creator, + bool shared ); + + static bool remove( const playlist_ptr& playlist ); + + virtual void loadRevision( const QString& rev = "" ); + + const source_ptr& author() { return m_source; } + const QString& currentrevision() { return m_currentrevision; } + const QString& title() { return m_title; } + const QString& info() { return m_info; } + const QString& creator() { return m_creator; } + unsigned int lastmodified() { return m_lastmodified; } + const QString& guid() { return m_guid; } + bool shared() const { return m_shared; } + + const QList< plentry_ptr >& entries() { return m_entries; } + void addEntry( const Tomahawk::query_ptr& query, const QString& oldrev ); + void addEntries( const QList& queries, const QString& oldrev ); + + // + // these need to exist and be public for the json serialization stuff + // you SHOULD NOT call them. They are used for an alternate CTOR method from json. + // maybe friend QObjectHelper and make them private? + Playlist( const source_ptr& author ) : + m_source( author ) + , m_lastmodified( 0 ) + { + qDebug() << Q_FUNC_INFO << "JSON"; + } + void setCurrentrevision( const QString& s ) { m_currentrevision = s; } + void setTitle( const QString& s ) { m_title= s; } + void setInfo( const QString& s ) { m_info = s; } + void setCreator( const QString& s ) { m_creator = s; } + void setGuid( const QString& s ) { m_guid = s; } + void setShared( bool b ) { m_shared = b; } + // + +signals: + /// emitted when the playlist revision changes (whenever the playlist changes) + void revisionLoaded( Tomahawk::PlaylistRevision ); + + /// watch for this to see when newly created playlist is synced to DB (if you care) + void created(); + +public slots: + // want to update the playlist from the model? + // generate a newrev using uuid() and call this: + void createNewRevision( const QString& newrev, const QString& oldrev, const QList< plentry_ptr >& entries ); + void reportCreated( const Tomahawk::playlist_ptr& self ); + void reportDeleted( const Tomahawk::playlist_ptr& self ); + + void setRevision( const QString& rev, + const QList& neworderedguids, + const QList& oldorderedguids, + bool is_newest_rev, + const QMap< QString, Tomahawk::plentry_ptr >& addedmap, + bool applied ); + + void resolve(); + +private: + // called from loadAllPlaylists DB cmd: + explicit Playlist( const source_ptr& src, + const QString& currentrevision, + const QString& title, + const QString& info, + const QString& creator, + bool shared, + int lastmod, + const QString& guid = "" ); // populate db + + // called when creating new playlist + explicit Playlist( const source_ptr& author, + const QString& guid, + const QString& title, + const QString& info, + const QString& creator, + bool shared ); + + void rundb(); + + source_ptr m_source; + QString m_currentrevision; + QString m_guid, m_title, m_info, m_creator; + unsigned int m_lastmodified; + bool m_shared; + + QList< plentry_ptr > m_entries; + +}; + +}; + +#endif // PLAYLIST_H diff --git a/include/tomahawk/playlistmodelinterface.h b/include/tomahawk/playlistmodelinterface.h new file mode 100644 index 000000000..73abb6086 --- /dev/null +++ b/include/tomahawk/playlistmodelinterface.h @@ -0,0 +1,34 @@ +#ifndef PLAYLISTINTERFACE_H +#define PLAYLISTINTERFACE_H + +#include "playlistitem.h" +#include "tomahawk/collection.h" +#include "tomahawk/source.h" + +class PlaylistModelInterface +{ +public: + enum RepeatMode { NoRepeat, RepeatOne, RepeatAll }; + + virtual ~PlaylistModelInterface() {} + + virtual PlaylistItem* previousItem() = 0; + virtual PlaylistItem* nextItem() = 0; + virtual PlaylistItem* siblingItem( int itemsAway ) = 0; + virtual void setCurrentItem( const QModelIndex& index ) = 0; + + virtual unsigned int sourceCount() = 0; + virtual unsigned int collectionCount() = 0; + virtual unsigned int trackCount() = 0; + virtual unsigned int artistCount() = 0; + +public slots: + virtual void setRepeatMode( RepeatMode mode ) = 0; + virtual void setShuffled( bool enabled ) = 0; + +signals: + virtual void repeatModeChanged( PlaylistModelInterface::RepeatMode mode ) = 0; + virtual void shuffleModeChanged( bool enabled ) = 0; +}; + +#endif // PLAYLISTINTERFACE_H diff --git a/include/tomahawk/plugin_includes.h b/include/tomahawk/plugin_includes.h new file mode 100644 index 000000000..77c472446 --- /dev/null +++ b/include/tomahawk/plugin_includes.h @@ -0,0 +1,13 @@ +#ifndef PLUGIN_INCLUDES_H +#define PLUGIN_INCLUDES_H + +// all the stuff a plugin needs + +#include "tomahawk/resolver.h" +#include "tomahawk/pluginapi.h" +#include "tomahawk/typedefs.h" +#include "tomahawk/tomahawkplugin.h" +#include "tomahawk/collection.h" +#include "tomahawk/source.h" + +#endif // PLUGIN_INCLUDES_H diff --git a/include/tomahawk/pluginapi.h b/include/tomahawk/pluginapi.h new file mode 100644 index 000000000..3f6ab07fa --- /dev/null +++ b/include/tomahawk/pluginapi.h @@ -0,0 +1,47 @@ +#ifndef PLUGINAPI_H +#define PLUGINAPI_H + +#include +#include + +#include "tomahawk/collection.h" +#include "tomahawk/source.h" + +/* + This is the only API plugins have access to. + This class must proxy calls to internal functions, because plugins can't + get a pointer to any old object and start calling methods on it. +*/ + +namespace Tomahawk +{ +class Resolver; +class Pipeline; + +class PluginAPI : public QObject +{ +Q_OBJECT + +public: + explicit PluginAPI( Pipeline * p ); + + /// call every time new results are available for a running query +// void reportResults( const QString& qid, const QList& results ); + + /// add/remove sources (which have collections) + void addSource( source_ptr s ); + void removeSource( source_ptr s ); + + /// register object capable of searching + void addResolver( Resolver * r ); + + Pipeline * pipeline() const { return m_pipeline; } + +private: + Pipeline * m_pipeline; +}; + + +}; //ns + +#endif // PLUGINAPI_H diff --git a/include/tomahawk/query.h b/include/tomahawk/query.h new file mode 100644 index 000000000..3bb42424e --- /dev/null +++ b/include/tomahawk/query.h @@ -0,0 +1,86 @@ +#ifndef QUERY_H +#define QUERY_H + +#include +#include +#include +#include + +#include "tomahawk/collection.h" +#include "tomahawk/result.h" +#include "tomahawk/typedefs.h" + +namespace Tomahawk +{ + +class Query : public QObject +{ +Q_OBJECT + +public: + explicit Query( const QVariant& v ) + : m_v( v ) + , m_solved( false ) + { + // ensure a QID is present: + QVariantMap m = m_v.toMap(); + if( !m.contains("qid") ) + m.insert( "qid", uuid() ); + + m_v = m; + } + + QVariant toVariant() const { return m_v; } + + /// returns list of all results so far + QList< result_ptr > results() const; + + /// how many results found so far? + unsigned int numResults() const; + + QID id() const; + + /// sorter for list of results + static bool resultSorter( const result_ptr &left, const result_ptr& right ); + + /// solved=true when a perfect result has been found (score of 1.0) + bool solved() const { return m_solved; } + + unsigned int lastPipelineWeight() const { return m_lastpipelineweight; } + void setLastPipelineWeight( unsigned int w ) { m_lastpipelineweight = w;} + + /// for debug output: + QString toString() const + { + return QString( "Query(%1, %2 - %3)" ).arg( id() ).arg( artist() ).arg( track() ); + } + + QString artist() const { return m_v.toMap().value( "artist" ).toString(); } + QString album() const { return m_v.toMap().value( "album" ).toString(); } + QString track() const { return m_v.toMap().value( "track" ).toString(); } + +signals: + void resultsAdded( const QList& ); + void resultsRemoved( Tomahawk::result_ptr ); + void solvedStateChanged( bool state ); + +public slots: + /// (indirectly) called by resolver plugins when results are found + void addResults( const QList< Tomahawk::result_ptr >& ); + void removeResult( Tomahawk::result_ptr ); + +private slots: + void resultUnavailable(); + +private: + mutable QMutex m_mut; + QVariant m_v; + QList< Tomahawk::result_ptr > m_results; + bool m_solved; + mutable QID m_qid; + unsigned int m_lastpipelineweight; +}; + +}; //ns + +#endif // QUERY_H diff --git a/include/tomahawk/resolver.h b/include/tomahawk/resolver.h new file mode 100644 index 000000000..249a89b10 --- /dev/null +++ b/include/tomahawk/resolver.h @@ -0,0 +1,44 @@ +#ifndef RESOLVER_H +#define RESOLVER_H + +#include + +#include "tomahawk/pluginapi.h" + +// implement this if you can resolve queries to content + +/* + Weight: 1-100, 100 being the best + Timeout: some millisecond value, after which we try the next highest + weighted resolver + +*/ +namespace Tomahawk +{ +class PluginAPI; + +class Resolver : public QObject +{ +Q_OBJECT + +public: + Resolver() {}; + + virtual QString name() const = 0; + virtual unsigned int weight() const = 0; + virtual unsigned int preference() const { return 100; }; + virtual unsigned int timeout() const = 0; + virtual void resolve( QVariant ) = 0; + + //virtual QWidget * configUI() { return 0; }; + //etc + + PluginAPI * api() const { return m_api; } + +private: + PluginAPI * m_api; +}; + +}; //ns + +#endif // RESOLVER_H diff --git a/include/tomahawk/result.h b/include/tomahawk/result.h new file mode 100644 index 000000000..b1b936805 --- /dev/null +++ b/include/tomahawk/result.h @@ -0,0 +1,54 @@ +#ifndef RESULT_H +#define RESULT_H + +#include + +#include "collection.h" +#include "source.h" +#include "tomahawk/typedefs.h" + +namespace Tomahawk +{ + +class Result : public QObject +{ +Q_OBJECT + +public: + explicit Result( QVariant v, collection_ptr collection ); + QVariant toVariant() const { return m_v; } + + float score() const; + RID id() const; + collection_ptr collection() const { return m_collection; } + + QString artist() const { return m_v.toMap().value( "artist" ).toString(); } + QString album() const { return m_v.toMap().value( "album" ).toString(); } + QString track() const { return m_v.toMap().value( "track" ).toString(); } + QString url() const { return m_v.toMap().value( "url" ).toString(); } + QString mimetype() const { return m_v.toMap().value( "mimetype" ).toString(); } + + unsigned int duration() const { return m_v.toMap().value( "duration" ).toUInt(); } + unsigned int bitrate() const { return m_v.toMap().value( "bitrate" ).toUInt(); } + unsigned int size() const { return m_v.toMap().value( "size" ).toUInt(); } + unsigned int albumpos() const { return m_v.toMap().value( "albumpos" ).toUInt(); } + + // for debug output: + QString toString() const + { + return QString( "Result(%1 %2\t%3 - %4 %5" ).arg( id() ).arg( score() ).arg( artist() ).arg( track() ).arg( url() ); + } + +signals: + // emitted when the collection this result comes from is going offline: + void becomingUnavailable(); + +private: + QVariant m_v; + mutable RID m_rid; + collection_ptr m_collection; +}; + +}; //ns + +#endif // RESULT_H diff --git a/include/tomahawk/source.h b/include/tomahawk/source.h new file mode 100644 index 000000000..6889f3cb4 --- /dev/null +++ b/include/tomahawk/source.h @@ -0,0 +1,80 @@ +#ifndef SOURCE_H +#define SOURCE_H + +#include +#include +#include + +#include "dbsyncconnection.h" +#include "collection.h" +#include "typedefs.h" + +class ControlConnection; + +namespace Tomahawk +{ + +class Source : public QObject +{ +Q_OBJECT + +public: + explicit Source( const QString& username, ControlConnection* cc ); + explicit Source( const QString& username ); + virtual ~Source(); + + bool isLocal() const { return m_isLocal; } + bool isOnline() const { return m_online; } + + const QString& userName() const { return m_username; } + QString friendlyName() const; + void setFriendlyName( const QString& fname ) { m_friendlyname = fname; } + + collection_ptr collection() const; + void addCollection( QSharedPointer c ); + void removeCollection( QSharedPointer c ); + + unsigned int id() const { return m_id; } + ControlConnection* controlConnection() const { return m_cc; } + + void scanningProgress( unsigned int files ) { emit loadingStateChanged( DBSyncConnection::SCANNING, DBSyncConnection::UNKNOWN, QString::number( files ) ); } + +signals: + void syncedWithDatabase(); + void online(); + void offline(); + + void collectionAdded( QSharedPointer ); + void collectionRemoved( QSharedPointer ); + + void stats( const QVariantMap& ); + void usernameChanged( const QString& ); + + // this signal is emitted from DBSyncConnection: + void loadingStateChanged( DBSyncConnection::State newstate, DBSyncConnection::State oldstate, const QString& info ); + +public slots: + void doDBSync(); + void setStats( const QVariantMap& m ); + +protected: + void setOffline(); + void setOnline(); + +private slots: + void dbLoaded( unsigned int id, const QString& fname ); + void remove(); + +private: + bool m_isLocal; + bool m_online; + QString m_username, m_friendlyname; + unsigned int m_id; + QList< QSharedPointer > m_collections; + ControlConnection* m_cc; + QVariantMap m_stats; +}; + +}; + +#endif // SOURCE_H diff --git a/include/tomahawk/sourcelist.h b/include/tomahawk/sourcelist.h new file mode 100644 index 000000000..e09339d5c --- /dev/null +++ b/include/tomahawk/sourcelist.h @@ -0,0 +1,40 @@ +#ifndef SOURCELIST_H +#define SOURCELIST_H + +#include +#include +#include + +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" + +class SourceList : public QObject +{ +Q_OBJECT + +public: + explicit SourceList( QObject* parent = 0 ); + + const Tomahawk::source_ptr& getLocal(); + void add( const Tomahawk::source_ptr& s ); + void remove( const Tomahawk::source_ptr& s ); + void remove( Tomahawk::Source* s ); + + QList sources() const; + Tomahawk::source_ptr lookup( const QString& username ) const; + Tomahawk::source_ptr lookup( unsigned int id ) const; + unsigned int length() const; + +signals: + void sourceAdded( const Tomahawk::source_ptr& ); + void sourceRemoved( const Tomahawk::source_ptr& ); + +private: + QMap m_sources; + QMap m_sources_id2name; + + Tomahawk::source_ptr m_local; + mutable QMutex m_mut; // mutable so const methods can use a lock +}; + +#endif // SOURCELIST_H diff --git a/include/tomahawk/tomahawkapp.h b/include/tomahawk/tomahawkapp.h new file mode 100644 index 000000000..0cc019c47 --- /dev/null +++ b/include/tomahawk/tomahawkapp.h @@ -0,0 +1,137 @@ +#ifndef TOMAHAWKAPP_H +#define TOMAHAWKAPP_H + +#define APP TomahawkApp::instance() + +#define RESPATH ":/data/" + +#include "headlesscheck.h" + +#include +#include +#include +#include +#include + +#include "QxtHttpServerConnector" +#include "QxtHttpSessionManager" + +#include "tomahawk/functimeout.h" +#include "tomahawk/typedefs.h" +#include "tomahawk/tomahawkplugin.h" +#include "tomahawk/playlist.h" +#include "tomahawk/pipeline.h" + +#include "utils/tomahawkutils.h" + + +#include "sourcelist.h" +#include "servent.h" + +class Database; +class Jabber; +class TomahawkZeroconf; +class TomahawkSettings; + +#ifndef TOMAHAWK_HEADLESS +class AudioEngine; +class TomahawkWindow; +class PlaylistView; + +#ifndef NO_LIBLASTFM +#include +#include "scrobbler.h" +#endif + +#endif + + +// this also acts as a a container for important top-level objects +// that other parts of the app need to find +// (eg, library, pipeline, friends list) +class TomahawkApp : public TOMAHAWK_APPLICATION +{ +Q_OBJECT + +public: + TomahawkApp( int& argc, char *argv[] ); + virtual ~TomahawkApp(); + + static TomahawkApp* instance(); + + Tomahawk::Pipeline* pipeline() { return &m_pipeline; } + Database* database() { return m_db; } + SourceList& sourcelist() { return m_sources; } + Servent& servent() { return m_servent; } + QNetworkAccessManager* nam() { return m_nam; } + const QString& nodeID() const; + +#ifndef TOMAHAWK_HEADLESS + AudioControls* audioControls(); + PlaylistView* playlistView(); + AudioEngine* audioEngine() { return m_audioEngine; } +#endif + + void registerIODeviceFactory( const QString &proto, boost::function(Tomahawk::result_ptr)> fac ); + QSharedPointer localFileIODeviceFactory( const Tomahawk::result_ptr& result ); + QSharedPointer httpIODeviceFactory( const Tomahawk::result_ptr& result ); + + TomahawkSettings* settings() { return m_settings; } + +signals: + void settingsChanged(); + +public slots: + QSharedPointer getIODeviceForUrl( const Tomahawk::result_ptr& result ); + void reconnectJabber(); + +private slots: + void jabberMessage( const QString&, const QString& ); + void jabberPeerOffline( const QString& ); + void jabberPeerOnline( const QString& ); + void jabberAuthError( int code, const QString& msg ); + void jabberDisconnected(); + void jabberConnected(); + + void lanHostFound( const QString&, int, const QString&, const QString& ); + +private: + void initLocalCollection(); + void loadPlugins(); + void registerMetaTypes(); + void startServent(); + void setupDatabase(); + void setupJabber(); + void setupPipeline(); + void startHTTP(); + + QList m_collections; + QList m_plugins; + + Tomahawk::Pipeline m_pipeline; + Database* m_db; + Servent m_servent; + SourceList m_sources; + TomahawkZeroconf* m_zeroconf; + QSharedPointer m_jabber; + +#ifndef TOMAHAWK_HEADLESS + TomahawkWindow* m_mainwindow; + AudioEngine* m_audioEngine; +#ifndef NO_LIBLASTFM + Scrobbler* m_scrobbler; +#endif +#endif + + QMap< QString,boost::function(Tomahawk::result_ptr)> > m_iofactories; + + bool m_headless; + TomahawkSettings* m_settings; + + QNetworkAccessManager* m_nam; + + QxtHttpServerConnector m_connector; + QxtHttpSessionManager m_session; +}; + +#endif // TOMAHAWKAPP_H diff --git a/include/tomahawk/tomahawkplugin.h b/include/tomahawk/tomahawkplugin.h new file mode 100644 index 000000000..f27e2d5ea --- /dev/null +++ b/include/tomahawk/tomahawkplugin.h @@ -0,0 +1,30 @@ +#ifndef TOMAHAWK_PLUGIN_H +#define TOMAHAWK_PLUGIN_H +#include +#include + +#include "tomahawk/pluginapi.h" + +class TomahawkPlugin +{ +public: + TomahawkPlugin(){}; + TomahawkPlugin(Tomahawk::PluginAPI * api) + : m_api(api) {}; + + virtual TomahawkPlugin * factory(Tomahawk::PluginAPI * api) = 0; + + virtual QString name() const = 0; + virtual QString description() const = 0; + +protected: + Tomahawk::PluginAPI * api() const { return m_api; }; + +private: + Tomahawk::PluginAPI * m_api; + +}; + +Q_DECLARE_INTERFACE(TomahawkPlugin, "org.tomahawk.TomahawkPlugin/1.0") + +#endif diff --git a/include/tomahawk/track.h b/include/tomahawk/track.h new file mode 100644 index 000000000..dcb37984c --- /dev/null +++ b/include/tomahawk/track.h @@ -0,0 +1,35 @@ +#ifndef TOMAHAWKTRACK_H +#define TOMAHAWKTRACK_H + +#include +#include + +#include "artist.h" + +namespace Tomahawk +{ + +class Track; +typedef QSharedPointer track_ptr; + +class Track : public QObject +{ +Q_OBJECT + +public: + Track( artist_ptr artist, const QString& name ) + : m_name( name ) + , m_artist( artist ) + {} + + const QString& name() const { return m_name; } + const artist_ptr artist() const { return m_artist; } + +private: + QString m_name; + artist_ptr m_artist; +}; + +}; // ns + +#endif diff --git a/include/tomahawk/typedefs.h b/include/tomahawk/typedefs.h new file mode 100644 index 000000000..41787be9d --- /dev/null +++ b/include/tomahawk/typedefs.h @@ -0,0 +1,39 @@ +#ifndef TYPEDEFS_H +#define TYPEDEFS_H + +#include +#include + +namespace Tomahawk +{ + class Collection; + class Playlist; + class PlaylistEntry; + class Query; + class Result; + class Source; + + typedef QSharedPointer collection_ptr; + typedef QSharedPointer playlist_ptr; + typedef QSharedPointer plentry_ptr; + typedef QSharedPointer query_ptr; + typedef QSharedPointer result_ptr; + typedef QSharedPointer source_ptr; + + // let's keep these typesafe, they are different kinds of GUID: + typedef QString QID; //query id + typedef QString RID; //result id + +}; // ns + +// creates 36char ascii guid without {} around it +inline static QString uuid() +{ + // kinda lame, but + QString q = QUuid::createUuid(); + q.remove( 0, 1 ); + q.chop( 1 ); + return q; +} + +#endif // TYPEDEFS_H diff --git a/libportfwd/CMakeLists.txt b/libportfwd/CMakeLists.txt new file mode 100644 index 000000000..9519e1399 --- /dev/null +++ b/libportfwd/CMakeLists.txt @@ -0,0 +1,50 @@ +PROJECT(libportfwd) +CMAKE_MINIMUM_REQUIRED(VERSION 2.6 FATAL_ERROR) +SET(CMAKE_VERBOSE_MAKEFILE ON) +#SET(CMAKE_INSTALL_PREFIX ".") + +SET(MINIUPNP_DIR "third-party/miniupnpc-20090605/") +SET(NATPMP_DIR "third-party/libnatpmp") + +SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +ADD_DEFINITIONS(-Wall -O2 -DNDEBUG) +IF(WIN32) + ADD_DEFINITIONS(-DWIN32 -DMINIUPNP_EXPORTS ) +ELSE() + ADD_DEFINITIONS(-fPIC) +ENDIF() +INCLUDE_DIRECTORIES(${MINIUPNP_DIR} include) + +ADD_LIBRARY(portfwd STATIC + + # the needed bits of miniupnpc (no python module, no tests, no cli) + ${MINIUPNP_DIR}/igd_desc_parse.c + ${MINIUPNP_DIR}/minisoap.c + ${MINIUPNP_DIR}/minissdpc.c + ${MINIUPNP_DIR}/miniupnpc.c + ${MINIUPNP_DIR}/miniwget.c + ${MINIUPNP_DIR}/minixml.c + ${MINIUPNP_DIR}/minixmlvalid.c + ${MINIUPNP_DIR}/upnpc.c + ${MINIUPNP_DIR}/upnpcommands.c + ${MINIUPNP_DIR}/upnperrors.c + ${MINIUPNP_DIR}/upnpreplyparse.c + + # Our wrapper: + src/portfwd.cpp +) + +IF(WIN32) + TARGET_LINK_LIBRARIES( portfwd "ws2_32.dll" ) +ENDIF() + +#ADD_EXECUTABLE(portfwd-demo +# src/main.cpp +# ) +#TARGET_LINK_LIBRARIES(portfwd-demo portfwd) + +#INSTALL(TARGETS portfwd ARCHIVE DESTINATION lib) +#INSTALL(TARGETS portfwd-demo RUNTIME DESTINATION bin) +#INSTALL(DIRECTORY include/portfwd DESTINATION include PATTERN "*~" EXCLUDE) diff --git a/libportfwd/README b/libportfwd/README new file mode 100644 index 000000000..de68a7eaa --- /dev/null +++ b/libportfwd/README @@ -0,0 +1,29 @@ +libportfwd +---------- +A basic, lightweight c++ wrapper around miniupnp and nat-pmp libraries +for setting up port fwds, detecting max up/downstream bandwidth, and +finding out external IP address. + +See: http://miniupnp.free.fr/ + +Designed to wrap up miniupnpc+natpmp libs into a static lib with a small API +so other projects can easily setup port fwds without shipping extra libs/deps. + +Should detect any upnp or nat-pmp router and automatically use the appropriate +library under the hood. + +Uses cmake to build needed bits of miniupnpc. + +NB/TODO +------- +I don't have a nat-pmp capable device (eg: Airport Express) +so haven't implemented that bit yet. Only supports upnp atm. + +Usage +----- +See the demo in main.cpp, but here's the jist: + + Portfwd pf; + pf.init(2000); // 2000 = ms to wait for response from router + pf.add(1234); // port to fwd to you + pf.remove(1234); // remove port fwding on exit diff --git a/libportfwd/include/portfwd/portfwd.h b/libportfwd/include/portfwd/portfwd.h new file mode 100644 index 000000000..4c4cf51ed --- /dev/null +++ b/libportfwd/include/portfwd/portfwd.h @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +//fwd: +struct UPNPUrls; +struct IGDdatas; + +class Portfwd +{ + public: + Portfwd(); + ~Portfwd(); + /// timeout: milliseconds to wait for a router to respond + /// 2000 is typically enough. + bool init(unsigned int timeout); + void get_status(); + bool add(unsigned short port, unsigned short internal_port ); + bool remove(unsigned short port); + + const std::string& external_ip() const + { return m_externalip; } + const std::string& lan_ip() const + { return m_lanip; } + unsigned int max_upstream_bps() const { return m_upbps; } + unsigned int max_downstream_bps() const { return m_downbps; } + + protected: + struct UPNPUrls* urls; + struct IGDdatas* data; + + std::string m_lanip, m_externalip; + unsigned int m_upbps, m_downbps; +}; + diff --git a/libportfwd/src/main.cpp b/libportfwd/src/main.cpp new file mode 100644 index 000000000..724a54bf6 --- /dev/null +++ b/libportfwd/src/main.cpp @@ -0,0 +1,30 @@ +#include "portfwd/portfwd.h" +int main(int argc, char** argv) +{ + if(argc!=2) + { + printf("Usage: %s \n", argv[0]); + return 1; + } + int port = atoi(argv[1]); + Portfwd pf; + if(!pf.init(2000)) + { + printf("Portfwd.init() failed.\n"); + return 2; + } + printf("External IP: %s\n", pf.external_ip().c_str()); + printf("LAN IP: %s\n", pf.lan_ip().c_str()); + printf("Max upstream: %d bps, max downstream: %d bps\n", + pf.max_upstream_bps(), pf.max_downstream_bps() ); + + printf("%s\n", ((pf.add( port ))?"Added":"Failed to add") ); + + printf("Any key to exit...\n"); + char foo; + scanf("%c",&foo); + + printf("%s\n", ((pf.remove( port ))?"Removed.":"Failed to remove") ); + return 0; +} + diff --git a/libportfwd/src/portfwd.cpp b/libportfwd/src/portfwd.cpp new file mode 100644 index 000000000..c74b156d1 --- /dev/null +++ b/libportfwd/src/portfwd.cpp @@ -0,0 +1,137 @@ +#include "portfwd/portfwd.h" + +#include "miniwget.h" +#include "miniupnpc.h" +#include "upnpcommands.h" + +#ifdef WIN32 +#include +#endif + +Portfwd::Portfwd() + : urls(0), data(0) +{ +} + +Portfwd::~Portfwd() +{ + if(urls) free(urls); + if(data) free(data); +} + +bool +Portfwd::init(unsigned int timeout) +{ +#ifdef WIN32 + WSADATA wsaData; + int nResult = WSAStartup(MAKEWORD(2,2), &wsaData); + if(nResult != NO_ERROR) + { + fprintf(stderr, "WSAStartup() failed.\n"); + return -1; + } +#endif + struct UPNPDev * devlist; + struct UPNPDev * dev; + char * descXML; + int descXMLsize = 0; + printf("Portfwd::init()\n"); + urls = (UPNPUrls*)malloc(sizeof(struct UPNPUrls)); + data = (IGDdatas*)malloc(sizeof(struct IGDdatas)); + memset(urls, 0, sizeof(struct UPNPUrls)); + memset(data, 0, sizeof(struct IGDdatas)); + devlist = upnpDiscover(timeout, NULL, NULL, 0); + if (devlist) + { + dev = devlist; + while (dev) + { + if (strstr (dev->st, "InternetGatewayDevice")) + break; + dev = dev->pNext; + } + if (!dev) + dev = devlist; /* defaulting to first device */ + + printf("UPnP device :\n" + " desc: %s\n st: %s\n", + dev->descURL, dev->st); + + descXML = (char*)miniwget(dev->descURL, &descXMLsize); + if (descXML) + { + parserootdesc (descXML, descXMLsize, data); + free (descXML); descXML = 0; + GetUPNPUrls (urls, data, dev->descURL); + } + // get lan IP: + char lanaddr[16]; + int i; + i = UPNP_GetValidIGD(devlist, urls, data, (char*)&lanaddr, 16); + m_lanip = std::string(lanaddr); + + freeUPNPDevlist(devlist); + get_status(); + return true; + } + return false; +} + +void +Portfwd::get_status() +{ + // get connection speed + UPNP_GetLinkLayerMaxBitRates( + urls->controlURL_CIF, data->servicetype_CIF, &m_downbps, &m_upbps); + + // get external IP adress + char ip[16]; + if( 0 != UPNP_GetExternalIPAddress( urls->controlURL, + data->servicetype, + (char*)&ip ) ) + { + m_externalip = ""; //failed + }else{ + m_externalip = std::string(ip); + } +} + +bool +Portfwd::add( unsigned short port, unsigned short internal_port ) +{ + char port_str[16], port_str_internal[16]; + int r; + printf("Portfwd::add (%s, %d)\n", m_lanip.c_str(), port); + if(urls->controlURL[0] == '\0') + { + printf("Portfwd - the init was not done !\n"); + return false; + } + sprintf(port_str, "%d", port); + sprintf(port_str_internal, "%d", internal_port); + + r = UPNP_AddPortMapping(urls->controlURL, data->servicetype, + port_str, port_str_internal, m_lanip.c_str(), "tomahawk", "TCP", NULL); + if(r!=0) + { + printf("AddPortMapping(%s, %s, %s) failed, code %d\n", port_str, port_str, m_lanip.c_str(), r); + return false; + } + return true; +} + +bool +Portfwd::remove( unsigned short port ) +{ + char port_str[16]; + printf("Portfwd::remove(%d)\n", port); + if(urls->controlURL[0] == '\0') + { + printf("Portfwd - the init was not done !\n"); + return false; + } + sprintf(port_str, "%d", port); + int r = UPNP_DeletePortMapping(urls->controlURL, data->servicetype, port_str, "TCP", NULL); + return r == 0; +} + diff --git a/libportfwd/third-party/libnatpmp/.deps/getgateway.Po b/libportfwd/third-party/libnatpmp/.deps/getgateway.Po new file mode 100644 index 000000000..afb5e4801 --- /dev/null +++ b/libportfwd/third-party/libnatpmp/.deps/getgateway.Po @@ -0,0 +1,133 @@ +getgateway.o: getgateway.c /usr/include/stdio.h /usr/include/features.h \ + /usr/include/sys/cdefs.h /usr/include/bits/wordsize.h \ + /usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \ + /usr/lib/gcc/x86_64-linux-gnu/4.3.3/include/stddef.h \ + /usr/include/bits/types.h /usr/include/bits/typesizes.h \ + /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \ + /usr/lib/gcc/x86_64-linux-gnu/4.3.3/include/stdarg.h \ + /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h \ + /usr/include/bits/stdio.h /usr/include/bits/stdio2.h \ + /usr/include/ctype.h /usr/include/endian.h /usr/include/bits/endian.h \ + /usr/include/bits/byteswap.h /usr/include/netinet/in.h \ + /usr/include/stdint.h /usr/include/bits/wchar.h \ + /usr/include/sys/socket.h /usr/include/sys/uio.h \ + /usr/include/sys/types.h /usr/include/time.h /usr/include/sys/select.h \ + /usr/include/bits/select.h /usr/include/bits/sigset.h \ + /usr/include/bits/time.h /usr/include/sys/sysmacros.h \ + /usr/include/bits/pthreadtypes.h /usr/include/bits/uio.h \ + /usr/include/bits/socket.h /usr/include/bits/sockaddr.h \ + /usr/include/asm/socket.h /usr/include/asm/sockios.h \ + /usr/include/bits/socket2.h /usr/include/bits/in.h \ + /usr/include/sys/param.h \ + /usr/lib/gcc/x86_64-linux-gnu/4.3.3/include-fixed/limits.h \ + /usr/lib/gcc/x86_64-linux-gnu/4.3.3/include-fixed/syslimits.h \ + /usr/include/limits.h /usr/include/bits/posix1_lim.h \ + /usr/include/bits/local_lim.h /usr/include/linux/limits.h \ + /usr/include/bits/posix2_lim.h /usr/include/linux/param.h \ + /usr/include/asm/param.h getgateway.h declspec.h + +/usr/include/stdio.h: + +/usr/include/features.h: + +/usr/include/sys/cdefs.h: + +/usr/include/bits/wordsize.h: + +/usr/include/gnu/stubs.h: + +/usr/include/gnu/stubs-64.h: + +/usr/lib/gcc/x86_64-linux-gnu/4.3.3/include/stddef.h: + +/usr/include/bits/types.h: + +/usr/include/bits/typesizes.h: + +/usr/include/libio.h: + +/usr/include/_G_config.h: + +/usr/include/wchar.h: + +/usr/lib/gcc/x86_64-linux-gnu/4.3.3/include/stdarg.h: + +/usr/include/bits/stdio_lim.h: + +/usr/include/bits/sys_errlist.h: + +/usr/include/bits/stdio.h: + +/usr/include/bits/stdio2.h: + +/usr/include/ctype.h: + +/usr/include/endian.h: + +/usr/include/bits/endian.h: + +/usr/include/bits/byteswap.h: + +/usr/include/netinet/in.h: + +/usr/include/stdint.h: + +/usr/include/bits/wchar.h: + +/usr/include/sys/socket.h: + +/usr/include/sys/uio.h: + +/usr/include/sys/types.h: + +/usr/include/time.h: + +/usr/include/sys/select.h: + +/usr/include/bits/select.h: + +/usr/include/bits/sigset.h: + +/usr/include/bits/time.h: + +/usr/include/sys/sysmacros.h: + +/usr/include/bits/pthreadtypes.h: + +/usr/include/bits/uio.h: + +/usr/include/bits/socket.h: + +/usr/include/bits/sockaddr.h: + +/usr/include/asm/socket.h: + +/usr/include/asm/sockios.h: + +/usr/include/bits/socket2.h: + +/usr/include/bits/in.h: + +/usr/include/sys/param.h: + +/usr/lib/gcc/x86_64-linux-gnu/4.3.3/include-fixed/limits.h: + +/usr/lib/gcc/x86_64-linux-gnu/4.3.3/include-fixed/syslimits.h: + +/usr/include/limits.h: + +/usr/include/bits/posix1_lim.h: + +/usr/include/bits/local_lim.h: + +/usr/include/linux/limits.h: + +/usr/include/bits/posix2_lim.h: + +/usr/include/linux/param.h: + +/usr/include/asm/param.h: + +getgateway.h: + +declspec.h: diff --git a/libportfwd/third-party/libnatpmp/.deps/natpmp.Po b/libportfwd/third-party/libnatpmp/.deps/natpmp.Po new file mode 100644 index 000000000..aeefa79c1 --- /dev/null +++ b/libportfwd/third-party/libnatpmp/.deps/natpmp.Po @@ -0,0 +1,134 @@ +natpmp.o: natpmp.c /usr/include/string.h /usr/include/features.h \ + /usr/include/sys/cdefs.h /usr/include/bits/wordsize.h \ + /usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \ + /usr/lib/gcc/x86_64-linux-gnu/4.3.3/include/stddef.h \ + /usr/include/bits/string.h /usr/include/bits/string2.h \ + /usr/include/endian.h /usr/include/bits/endian.h \ + /usr/include/bits/byteswap.h /usr/include/bits/types.h \ + /usr/include/bits/typesizes.h /usr/include/stdlib.h \ + /usr/include/bits/string3.h /usr/include/time.h \ + /usr/include/bits/time.h /usr/include/sys/time.h \ + /usr/include/sys/select.h /usr/include/bits/select.h \ + /usr/include/bits/sigset.h /usr/include/errno.h \ + /usr/include/bits/errno.h /usr/include/linux/errno.h \ + /usr/include/asm/errno.h /usr/include/asm-generic/errno.h \ + /usr/include/asm-generic/errno-base.h /usr/include/unistd.h \ + /usr/include/bits/posix_opt.h /usr/include/bits/confname.h \ + /usr/include/getopt.h /usr/include/bits/unistd.h /usr/include/fcntl.h \ + /usr/include/bits/fcntl.h /usr/include/sys/types.h \ + /usr/include/sys/sysmacros.h /usr/include/bits/pthreadtypes.h \ + /usr/include/bits/fcntl2.h /usr/include/sys/socket.h \ + /usr/include/sys/uio.h /usr/include/bits/uio.h \ + /usr/include/bits/socket.h /usr/include/bits/sockaddr.h \ + /usr/include/asm/socket.h /usr/include/asm/sockios.h \ + /usr/include/bits/socket2.h natpmp.h /usr/include/netinet/in.h \ + /usr/include/stdint.h /usr/include/bits/wchar.h /usr/include/bits/in.h \ + getgateway.h declspec.h + +/usr/include/string.h: + +/usr/include/features.h: + +/usr/include/sys/cdefs.h: + +/usr/include/bits/wordsize.h: + +/usr/include/gnu/stubs.h: + +/usr/include/gnu/stubs-64.h: + +/usr/lib/gcc/x86_64-linux-gnu/4.3.3/include/stddef.h: + +/usr/include/bits/string.h: + +/usr/include/bits/string2.h: + +/usr/include/endian.h: + +/usr/include/bits/endian.h: + +/usr/include/bits/byteswap.h: + +/usr/include/bits/types.h: + +/usr/include/bits/typesizes.h: + +/usr/include/stdlib.h: + +/usr/include/bits/string3.h: + +/usr/include/time.h: + +/usr/include/bits/time.h: + +/usr/include/sys/time.h: + +/usr/include/sys/select.h: + +/usr/include/bits/select.h: + +/usr/include/bits/sigset.h: + +/usr/include/errno.h: + +/usr/include/bits/errno.h: + +/usr/include/linux/errno.h: + +/usr/include/asm/errno.h: + +/usr/include/asm-generic/errno.h: + +/usr/include/asm-generic/errno-base.h: + +/usr/include/unistd.h: + +/usr/include/bits/posix_opt.h: + +/usr/include/bits/confname.h: + +/usr/include/getopt.h: + +/usr/include/bits/unistd.h: + +/usr/include/fcntl.h: + +/usr/include/bits/fcntl.h: + +/usr/include/sys/types.h: + +/usr/include/sys/sysmacros.h: + +/usr/include/bits/pthreadtypes.h: + +/usr/include/bits/fcntl2.h: + +/usr/include/sys/socket.h: + +/usr/include/sys/uio.h: + +/usr/include/bits/uio.h: + +/usr/include/bits/socket.h: + +/usr/include/bits/sockaddr.h: + +/usr/include/asm/socket.h: + +/usr/include/asm/sockios.h: + +/usr/include/bits/socket2.h: + +natpmp.h: + +/usr/include/netinet/in.h: + +/usr/include/stdint.h: + +/usr/include/bits/wchar.h: + +/usr/include/bits/in.h: + +getgateway.h: + +declspec.h: diff --git a/libportfwd/third-party/libnatpmp/README b/libportfwd/third-party/libnatpmp/README new file mode 100644 index 000000000..50fdd1093 --- /dev/null +++ b/libportfwd/third-party/libnatpmp/README @@ -0,0 +1,4 @@ +libnatpmp is written by Thomas Bernard. +Its homepage is http://miniupnp.tuxfamily.org/libnatpmp.html +This code is from the libnatpmp-20090310 snapshot + diff --git a/libportfwd/third-party/libnatpmp/declspec.h b/libportfwd/third-party/libnatpmp/declspec.h new file mode 100644 index 000000000..ea479d1e6 --- /dev/null +++ b/libportfwd/third-party/libnatpmp/declspec.h @@ -0,0 +1,15 @@ +#ifndef __DECLSPEC_H__ +#define __DECLSPEC_H__ + +#if defined(WIN32) && !defined(STATICLIB) + #ifdef NATPMP_EXPORTS + #define LIBSPEC __declspec(dllexport) + #else + #define LIBSPEC __declspec(dllimport) + #endif +#else + #define LIBSPEC +#endif + +#endif + diff --git a/libportfwd/third-party/libnatpmp/getgateway.c b/libportfwd/third-party/libnatpmp/getgateway.c new file mode 100644 index 000000000..3c84de3a6 --- /dev/null +++ b/libportfwd/third-party/libnatpmp/getgateway.c @@ -0,0 +1,423 @@ +/* $Id: getgateway.c,v 1.13 2009/03/10 10:15:31 nanard Exp $ */ +/* libnatpmp + * Copyright (c) 2007-2008, Thomas BERNARD + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include +#include +#ifndef WIN32 +#include +#endif +#include +/* There is no portable method to get the default route gateway. + * So below are three differents functions implementing this. + * Parsing /proc/net/route is for linux. + * sysctl is the way to access such informations on BSD systems. + * Many systems should provide route information through raw PF_ROUTE + * sockets. */ +#ifdef __linux__ +#define USE_PROC_NET_ROUTE +#undef USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#endif + +#ifdef BSD +#undef USE_PROC_NET_ROUTE +#define USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#endif + +#ifdef __APPLE__ +#undef USE_PROC_NET_ROUTE +#undef USE_SOCKET_ROUTE +#define USE_SYSCTL_NET_ROUTE +#endif + +#if (defined(sun) && defined(__SVR4)) +#undef USE_PROC_NET_ROUTE +#define USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#endif + +#ifdef WIN32 +#undef USE_PROC_NET_ROUTE +#undef USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#define USE_WIN32_CODE +#endif + +#ifdef USE_SYSCTL_NET_ROUTE +#include +#include +#include +#include +#endif +#ifdef USE_SOCKET_ROUTE +#include +#include +#include +#include +#include +#endif +#ifdef WIN32 +#include +#include +#define MAX_KEY_LENGTH 255 +#define MAX_VALUE_LENGTH 16383 +#endif +#include "getgateway.h" + +#ifndef WIN32 +#define SUCCESS (0) +#define FAILED (-1) +#endif + +#ifdef USE_PROC_NET_ROUTE +int getdefaultgateway(in_addr_t * addr) +{ + long d, g; + char buf[256]; + int line = 0; + FILE * f; + char * p; + f = fopen("/proc/net/route", "r"); + if(!f) + return FAILED; + while(fgets(buf, sizeof(buf), f)) { + if(line > 0) { + p = buf; + while(*p && !isspace(*p)) + p++; + while(*p && isspace(*p)) + p++; + if(sscanf(p, "%lx%lx", &d, &g)==2) { + if(d == 0) { /* default */ + *addr = g; + fclose(f); + return SUCCESS; + } + } + } + line++; + } + /* default route not found ! */ + if(f) + fclose(f); + return FAILED; +} +#endif /* #ifdef USE_PROC_NET_ROUTE */ + + +#ifdef USE_SYSCTL_NET_ROUTE + +#define ROUNDUP(a) \ + ((a) > 0 ? (1 + (((a) - 1) | (sizeof(long) - 1))) : sizeof(long)) + +int getdefaultgateway(in_addr_t * addr) +{ +#if 0 + /* net.route.0.inet.dump.0.0 ? */ + int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET, + NET_RT_DUMP, 0, 0/*tableid*/}; +#endif + /* net.route.0.inet.flags.gateway */ + int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET, + NET_RT_FLAGS, RTF_GATEWAY}; + size_t l; + char * buf, * p; + struct rt_msghdr * rt; + struct sockaddr * sa; + struct sockaddr * sa_tab[RTAX_MAX]; + int i; + int r = FAILED; + if(sysctl(mib, sizeof(mib)/sizeof(int), 0, &l, 0, 0) < 0) { + return FAILED; + } + if(l>0) { + buf = malloc(l); + if(sysctl(mib, sizeof(mib)/sizeof(int), buf, &l, 0, 0) < 0) { + free(buf); + return FAILED; + } + for(p=buf; prtm_msglen) { + rt = (struct rt_msghdr *)p; + sa = (struct sockaddr *)(rt + 1); + for(i=0; irtm_addrs & (1 << i)) { + sa_tab[i] = sa; + sa = (struct sockaddr *)((char *)sa + ROUNDUP(sa->sa_len)); + } else { + sa_tab[i] = NULL; + } + } + if( ((rt->rtm_addrs & (RTA_DST|RTA_GATEWAY)) == (RTA_DST|RTA_GATEWAY)) + && sa_tab[RTAX_DST]->sa_family == AF_INET + && sa_tab[RTAX_GATEWAY]->sa_family == AF_INET) { + if(((struct sockaddr_in *)sa_tab[RTAX_DST])->sin_addr.s_addr == 0) { + *addr = ((struct sockaddr_in *)(sa_tab[RTAX_GATEWAY]))->sin_addr.s_addr; + r = SUCCESS; + } + } + } + free(buf); + } + return r; +} +#endif /* #ifdef USE_SYSCTL_NET_ROUTE */ + + +#ifdef USE_SOCKET_ROUTE +/* Thanks to Darren Kenny for this code */ +#define NEXTADDR(w, u) \ + if (rtm_addrs & (w)) {\ + l = sizeof(struct sockaddr); memmove(cp, &(u), l); cp += l;\ + } + +#define rtm m_rtmsg.m_rtm + +struct { + struct rt_msghdr m_rtm; + char m_space[512]; +} m_rtmsg; + +int getdefaultgateway(in_addr_t *addr) +{ + int s, seq, l, rtm_addrs, i; + pid_t pid; + struct sockaddr so_dst, so_mask; + char *cp = m_rtmsg.m_space; + struct sockaddr *gate = NULL, *sa; + struct rt_msghdr *msg_hdr; + + pid = getpid(); + seq = 0; + rtm_addrs = RTA_DST | RTA_NETMASK; + + memset(&so_dst, 0, sizeof(so_dst)); + memset(&so_mask, 0, sizeof(so_mask)); + memset(&rtm, 0, sizeof(struct rt_msghdr)); + + rtm.rtm_type = RTM_GET; + rtm.rtm_flags = RTF_UP | RTF_GATEWAY; + rtm.rtm_version = RTM_VERSION; + rtm.rtm_seq = ++seq; + rtm.rtm_addrs = rtm_addrs; + + so_dst.sa_family = AF_INET; + so_mask.sa_family = AF_INET; + + NEXTADDR(RTA_DST, so_dst); + NEXTADDR(RTA_NETMASK, so_mask); + + rtm.rtm_msglen = l = cp - (char *)&m_rtmsg; + + s = socket(PF_ROUTE, SOCK_RAW, 0); + + if (write(s, (char *)&m_rtmsg, l) < 0) { + close(s); + return FAILED; + } + + do { + l = read(s, (char *)&m_rtmsg, sizeof(m_rtmsg)); + } while (l > 0 && (rtm.rtm_seq != seq || rtm.rtm_pid != pid)); + + close(s); + + msg_hdr = &rtm; + + cp = ((char *)(msg_hdr + 1)); + if (msg_hdr->rtm_addrs) { + for (i = 1; i; i <<= 1) + if (i & msg_hdr->rtm_addrs) { + sa = (struct sockaddr *)cp; + if (i == RTA_GATEWAY ) + gate = sa; + + cp += sizeof(struct sockaddr); + } + } else { + return FAILED; + } + + + if (gate != NULL ) { + *addr = ((struct sockaddr_in *)gate)->sin_addr.s_addr; + return SUCCESS; + } else { + return FAILED; + } +} +#endif /* #ifdef USE_SOCKET_ROUTE */ + +#ifdef USE_WIN32_CODE +int getdefaultgateway(in_addr_t * addr) +{ + HKEY networkCardsKey; + HKEY networkCardKey; + HKEY interfacesKey; + HKEY interfaceKey; + DWORD i = 0; + DWORD numSubKeys = 0; + TCHAR keyName[MAX_KEY_LENGTH]; + DWORD keyNameLength = MAX_KEY_LENGTH; + TCHAR keyValue[MAX_VALUE_LENGTH]; + DWORD keyValueLength = MAX_VALUE_LENGTH; + DWORD keyValueType = REG_SZ; + TCHAR gatewayValue[MAX_VALUE_LENGTH]; + DWORD gatewayValueLength = MAX_VALUE_LENGTH; + DWORD gatewayValueType = REG_MULTI_SZ; + int done = 0; + + char networkCardsPath[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkCards"; + char interfacesPath[] = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"; + + // The windows registry lists its primary network devices in the following location: + // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkCards + // + // Each network device has its own subfolder, named with an index, with various properties: + // -NetworkCards + // -5 + // -Description = Broadcom 802.11n Network Adapter + // -ServiceName = {E35A72F8-5065-4097-8DFE-C7790774EE4D} + // -8 + // -Description = Marvell Yukon 88E8058 PCI-E Gigabit Ethernet Controller + // -ServiceName = {86226414-5545-4335-A9D1-5BD7120119AD} + // + // The above service name is the name of a subfolder within: + // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces + // + // There may be more subfolders in this interfaces path than listed in the network cards path above: + // -Interfaces + // -{3a539854-6a70-11db-887c-806e6f6e6963} + // -DhcpIPAddress = 0.0.0.0 + // -[more] + // -{E35A72F8-5065-4097-8DFE-C7790774EE4D} + // -DhcpIPAddress = 10.0.1.4 + // -DhcpDefaultGateway = 10.0.1.1 + // -[more] + // -{86226414-5545-4335-A9D1-5BD7120119AD} + // -DhcpIpAddress = 10.0.1.5 + // -DhcpDefaultGateay = 10.0.1.1 + // -[more] + // + // In order to extract this information, we enumerate each network card, and extract the ServiceName value. + // This is then used to open the interface subfolder, and attempt to extract a DhcpDefaultGateway value. + // Once one is found, we're done. + // + // It may be possible to simply enumerate the interface folders until we find one with a DhcpDefaultGateway value. + // However, the technique used is the technique most cited on the web, and we assume it to be more correct. + + if(ERROR_SUCCESS != RegOpenKeyEx(HKEY_LOCAL_MACHINE, // Open registry key or predifined key + networkCardsPath, // Name of registry subkey to open + 0, // Reserved - must be zero + KEY_READ, // Mask - desired access rights + &networkCardsKey)) // Pointer to output key + { + // Unable to open network cards keys + return -1; + } + + if(ERROR_SUCCESS != RegOpenKeyEx(HKEY_LOCAL_MACHINE, // Open registry key or predefined key + interfacesPath, // Name of registry subkey to open + 0, // Reserved - must be zero + KEY_READ, // Mask - desired access rights + &interfacesKey)) // Pointer to output key + { + // Unable to open interfaces key + RegCloseKey(networkCardsKey); + return -1; + } + + // Figure out how many subfolders are within the NetworkCards folder + RegQueryInfoKey(networkCardsKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + + //printf( "Number of subkeys: %u\n", (unsigned int)numSubKeys); + + // Enumrate through each subfolder within the NetworkCards folder + for(i = 0; i < numSubKeys && !done; i++) + { + keyNameLength = MAX_KEY_LENGTH; + if(ERROR_SUCCESS == RegEnumKeyEx(networkCardsKey, // Open registry key + i, // Index of subkey to retrieve + keyName, // Buffer that receives the name of the subkey + &keyNameLength, // Variable that receives the size of the above buffer + NULL, // Reserved - must be NULL + NULL, // Buffer that receives the class string + NULL, // Variable that receives the size of the above buffer + NULL)) // Variable that receives the last write time of subkey + { + if(RegOpenKeyEx(networkCardsKey, keyName, 0, KEY_READ, &networkCardKey) == ERROR_SUCCESS) + { + keyValueLength = MAX_VALUE_LENGTH; + if(ERROR_SUCCESS == RegQueryValueEx(networkCardKey, // Open registry key + "ServiceName", // Name of key to query + NULL, // Reserved - must be NULL + &keyValueType, // Receives value type + keyValue, // Receives value + &keyValueLength)) // Receives value length in bytes + { + //printf("keyValue: %s\n", keyValue); + + if(RegOpenKeyEx(interfacesKey, keyValue, 0, KEY_READ, &interfaceKey) == ERROR_SUCCESS) + { + gatewayValueLength = MAX_VALUE_LENGTH; + if(ERROR_SUCCESS == RegQueryValueEx(interfaceKey, // Open registry key + "DhcpDefaultGateway", // Name of key to query + NULL, // Reserved - must be NULL + &gatewayValueType, // Receives value type + gatewayValue, // Receives value + &gatewayValueLength)) // Receives value length in bytes + { + // Check to make sure it's a string + if(gatewayValueType == REG_MULTI_SZ || gatewayValueType == REG_SZ) + { + //printf("gatewayValue: %s\n", gatewayValue); + done = 1; + } + } + else if(ERROR_SUCCESS == RegQueryValueEx(interfaceKey, // Open registry key + "DefaultGateway", // Name of key to query + NULL, // Reserved - must be NULL + &gatewayValueType, // Receives value type + gatewayValue, // Receives value + &gatewayValueLength)) // Receives value length in bytes + { + // Check to make sure it's a string + if(gatewayValueType == REG_MULTI_SZ || gatewayValueType == REG_SZ) + { + //printf("gatewayValue: %s\n", gatewayValue); + done = 1; + } + } + RegCloseKey(interfaceKey); + } + } + RegCloseKey(networkCardKey); + } + } + } + + RegCloseKey(interfacesKey); + RegCloseKey(networkCardsKey); + + if(done) + { + *addr = inet_addr(gatewayValue); + return 0; + } + + return -1; +} +#endif /* #ifdef USE_WIN32_CODE */ + diff --git a/libportfwd/third-party/libnatpmp/getgateway.h b/libportfwd/third-party/libnatpmp/getgateway.h new file mode 100644 index 000000000..cf7794600 --- /dev/null +++ b/libportfwd/third-party/libnatpmp/getgateway.h @@ -0,0 +1,31 @@ +/* $Id: getgateway.h,v 1.3 2008/07/02 22:33:06 nanard Exp $ */ +/* libnatpmp + * Copyright (c) 2007, Thomas BERNARD + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#ifndef __GETGATEWAY_H__ +#define __GETGATEWAY_H__ + +#ifdef WIN32 +#include +#define in_addr_t uint32_t +#endif +#include "declspec.h" + +/* getdefaultgateway() : + * return value : + * 0 : success + * -1 : failure */ +LIBSPEC int getdefaultgateway(in_addr_t * addr); + +#endif diff --git a/libportfwd/third-party/libnatpmp/natpmp.c b/libportfwd/third-party/libnatpmp/natpmp.c new file mode 100644 index 000000000..f1c95b1ad --- /dev/null +++ b/libportfwd/third-party/libnatpmp/natpmp.c @@ -0,0 +1,339 @@ +/* $Id: natpmp.c,v 1.8 2008/07/02 22:33:06 nanard Exp $ */ +/* libnatpmp + * Copyright (c) 2007-2008, Thomas BERNARD + * http://miniupnp.free.fr/libnatpmp.html + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include +#include +#include +#ifdef WIN32 +#include +#include +#include +#define EWOULDBLOCK WSAEWOULDBLOCK +#define ECONNREFUSED WSAECONNREFUSED +#else +#include +#include +#include +#include +#include +#define closesocket close +#endif +#include "natpmp.h" +#include "getgateway.h" + +int initnatpmp(natpmp_t * p) +{ +#ifdef WIN32 + u_long ioctlArg = 1; +#else + int flags; +#endif + struct sockaddr_in addr; + if(!p) + return NATPMP_ERR_INVALIDARGS; + memset(p, 0, sizeof(natpmp_t)); + p->s = socket(PF_INET, SOCK_DGRAM, 0); + if(p->s < 0) + return NATPMP_ERR_SOCKETERROR; +#ifdef WIN32 + if(ioctlsocket(p->s, FIONBIO, &ioctlArg) == SOCKET_ERROR) + return NATPMP_ERR_FCNTLERROR; +#else + if((flags = fcntl(p->s, F_GETFL, 0)) < 0) + return NATPMP_ERR_FCNTLERROR; + if(fcntl(p->s, F_SETFL, flags | O_NONBLOCK) < 0) + return NATPMP_ERR_FCNTLERROR; +#endif + + if(getdefaultgateway(&(p->gateway)) < 0) + return NATPMP_ERR_CANNOTGETGATEWAY; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(NATPMP_PORT); + addr.sin_addr.s_addr = p->gateway; + if(connect(p->s, (struct sockaddr *)&addr, sizeof(addr)) < 0) + return NATPMP_ERR_CONNECTERR; + return 0; +} + +int closenatpmp(natpmp_t * p) +{ + if(!p) + return NATPMP_ERR_INVALIDARGS; + if(closesocket(p->s) < 0) + return NATPMP_ERR_CLOSEERR; + return 0; +} + +static int sendpendingrequest(natpmp_t * p) +{ + int r; +/* struct sockaddr_in addr;*/ + if(!p) + return NATPMP_ERR_INVALIDARGS; +/* memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(NATPMP_PORT); + addr.sin_addr.s_addr = p->gateway; + r = (int)sendto(p->s, p->pending_request, p->pending_request_len, 0, + (struct sockaddr *)&addr, sizeof(addr));*/ + r = (int)send(p->s, p->pending_request, p->pending_request_len, 0); + return (r<0) ? NATPMP_ERR_SENDERR : r; +} + +static int sendnatpmprequest(natpmp_t * p) +{ + int n; + if(!p) + return NATPMP_ERR_INVALIDARGS; + /* TODO : check if no request is allready pending */ + p->has_pending_request = 1; + p->try_number = 1; + n = sendpendingrequest(p); + gettimeofday(&p->retry_time, NULL); // check errors ! + p->retry_time.tv_usec += 250000; /* add 250ms */ + if(p->retry_time.tv_usec >= 1000000) { + p->retry_time.tv_usec -= 1000000; + p->retry_time.tv_sec++; + } + return n; +} + +int getnatpmprequesttimeout(natpmp_t * p, struct timeval * timeout) +{ + struct timeval now; + if(!p || !timeout) + return NATPMP_ERR_INVALIDARGS; + if(!p->has_pending_request) + return NATPMP_ERR_NOPENDINGREQ; + if(gettimeofday(&now, NULL) < 0) + return NATPMP_ERR_GETTIMEOFDAYERR; + timeout->tv_sec = p->retry_time.tv_sec - now.tv_sec; + timeout->tv_usec = p->retry_time.tv_usec - now.tv_usec; + if(timeout->tv_usec < 0) { + timeout->tv_usec += 1000000; + timeout->tv_sec--; + } + return 0; +} + +int sendpublicaddressrequest(natpmp_t * p) +{ + if(!p) + return NATPMP_ERR_INVALIDARGS; + //static const unsigned char request[] = { 0, 0 }; + p->pending_request[0] = 0; + p->pending_request[1] = 0; + p->pending_request_len = 2; + // TODO: return 0 instead of sizeof(request) ?? + return sendnatpmprequest(p); +} + +int sendnewportmappingrequest(natpmp_t * p, int protocol, + uint16_t privateport, uint16_t publicport, + uint32_t lifetime) +{ + if(!p || (protocol!=NATPMP_PROTOCOL_TCP && protocol!=NATPMP_PROTOCOL_UDP)) + return NATPMP_ERR_INVALIDARGS; + p->pending_request[0] = 0; + p->pending_request[1] = protocol; + p->pending_request[2] = 0; + p->pending_request[3] = 0; + *((uint16_t *)(p->pending_request + 4)) = htons(privateport); + *((uint16_t *)(p->pending_request + 6)) = htons(publicport); + *((uint32_t *)(p->pending_request + 8)) = htonl(lifetime); + p->pending_request_len = 12; + return sendnatpmprequest(p); +} + +static int readnatpmpresponse(natpmp_t * p, natpmpresp_t * response) +{ + unsigned char buf[16]; + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + int n; + if(!p) + return NATPMP_ERR_INVALIDARGS; + n = recvfrom(p->s, buf, sizeof(buf), 0, + (struct sockaddr *)&addr, &addrlen); + if(n<0) + switch(errno) { + /*case EAGAIN:*/ + case EWOULDBLOCK: + n = NATPMP_TRYAGAIN; + break; + case ECONNREFUSED: + n = NATPMP_ERR_NOGATEWAYSUPPORT; + break; + default: + n = NATPMP_ERR_RECVFROM; + } + /* check that addr is correct (= gateway) */ + else if(addr.sin_addr.s_addr != p->gateway) + n = NATPMP_ERR_WRONGPACKETSOURCE; + else { + response->resultcode = ntohs(*((uint16_t *)(buf + 2))); + response->epoch = ntohl(*((uint32_t *)(buf + 4))); + if(buf[0] != 0) + n = NATPMP_ERR_UNSUPPORTEDVERSION; + else if(buf[1] < 128 || buf[1] > 130) + n = NATPMP_ERR_UNSUPPORTEDOPCODE; + else if(response->resultcode != 0) { + switch(response->resultcode) { + case 1: + n = NATPMP_ERR_UNSUPPORTEDVERSION; + break; + case 2: + n = NATPMP_ERR_NOTAUTHORIZED; + break; + case 3: + n = NATPMP_ERR_NETWORKFAILURE; + break; + case 4: + n = NATPMP_ERR_OUTOFRESOURCES; + break; + case 5: + n = NATPMP_ERR_UNSUPPORTEDOPCODE; + break; + default: + n = NATPMP_ERR_UNDEFINEDERROR; + } + } else { + response->type = buf[1] & 0x7f; + if(buf[1] == 128) + //response->publicaddress.addr = *((uint32_t *)(buf + 8)); + response->pnu.publicaddress.addr.s_addr = *((uint32_t *)(buf + 8)); + else { + response->pnu.newportmapping.privateport = ntohs(*((uint16_t *)(buf + 8))); + response->pnu.newportmapping.mappedpublicport = ntohs(*((uint16_t *)(buf + 10))); + response->pnu.newportmapping.lifetime = ntohl(*((uint32_t *)(buf + 12))); + } + n = 0; + } + } + return n; +} + +int readnatpmpresponseorretry(natpmp_t * p, natpmpresp_t * response) +{ + int n; + if(!p || !response) + return NATPMP_ERR_INVALIDARGS; + if(!p->has_pending_request) + return NATPMP_ERR_NOPENDINGREQ; + n = readnatpmpresponse(p, response); + if(n<0) { + if(n==NATPMP_TRYAGAIN) { + struct timeval now; + gettimeofday(&now, NULL); // check errors ! + if(timercmp(&now, &p->retry_time, >=)) { + int delay, r; + if(p->try_number >= 9) { + return NATPMP_ERR_NOGATEWAYSUPPORT; + } + /*printf("retry! %d\n", p->try_number);*/ + delay = 250 * (1<try_number); // ms + /*for(i=0; itry_number; i++) + delay += delay;*/ + p->retry_time.tv_sec += (delay / 1000); + p->retry_time.tv_usec += (delay % 1000) * 1000; + if(p->retry_time.tv_usec >= 1000000) { + p->retry_time.tv_usec -= 1000000; + p->retry_time.tv_sec++; + } + p->try_number++; + r = sendpendingrequest(p); + if(r<0) + return r; + } + } + } else { + p->has_pending_request = 0; + } + return n; +} + +#ifdef ENABLE_STRNATPMPERR +const char * strnatpmperr(int r) +{ + const char * s; + switch(r) { + case NATPMP_ERR_INVALIDARGS: + s = "invalid arguments"; + break; + case NATPMP_ERR_SOCKETERROR: + s = "socket() failed"; + break; + case NATPMP_ERR_CANNOTGETGATEWAY: + s = "cannot get default gateway ip address"; + break; + case NATPMP_ERR_CLOSEERR: +#ifdef WIN32 + s = "closesocket() failed"; +#else + s = "close() failed"; +#endif + break; + case NATPMP_ERR_RECVFROM: + s = "recvfrom() failed"; + break; + case NATPMP_ERR_NOPENDINGREQ: + s = "no pending request"; + break; + case NATPMP_ERR_NOGATEWAYSUPPORT: + s = "the gateway does not support nat-pmp"; + break; + case NATPMP_ERR_CONNECTERR: + s = "connect() failed"; + break; + case NATPMP_ERR_WRONGPACKETSOURCE: + s = "packet not received from the default gateway"; + break; + case NATPMP_ERR_SENDERR: + s = "send() failed"; + break; + case NATPMP_ERR_FCNTLERROR: + s = "fcntl() failed"; + break; + case NATPMP_ERR_GETTIMEOFDAYERR: + s = "gettimeofday() failed"; + break; + case NATPMP_ERR_UNSUPPORTEDVERSION: + s = "unsupported nat-pmp version error from server"; + break; + case NATPMP_ERR_UNSUPPORTEDOPCODE: + s = "unsupported nat-pmp opcode error from server"; + break; + case NATPMP_ERR_UNDEFINEDERROR: + s = "undefined nat-pmp server error"; + break; + case NATPMP_ERR_NOTAUTHORIZED: + s = "not authorized"; + break; + case NATPMP_ERR_NETWORKFAILURE: + s = "network failure"; + break; + case NATPMP_ERR_OUTOFRESOURCES: + s = "nat-pmp server out of resources"; + break; + default: + s = "Unknown libnatpmp error"; + } + return s; +} +#endif + diff --git a/libportfwd/third-party/libnatpmp/natpmp.h b/libportfwd/third-party/libnatpmp/natpmp.h new file mode 100644 index 000000000..d1f09c26a --- /dev/null +++ b/libportfwd/third-party/libnatpmp/natpmp.h @@ -0,0 +1,187 @@ +/* $Id: natpmp.h,v 1.11 2009/02/27 22:38:05 nanard Exp $ */ +/* libnatpmp + * Copyright (c) 2007-2008, Thomas BERNARD + * http://miniupnp.free.fr/libnatpmp.html + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#ifndef __NATPMP_H__ +#define __NATPMP_H__ + +/* NAT-PMP Port as defined by the NAT-PMP draft */ +#define NATPMP_PORT (5351) + +#include +#include +#ifdef WIN32 +#include +#include +#define in_addr_t uint32_t +#include "declspec.h" +#else +#define LIBSPEC +#include +#endif + +typedef struct { + int s; /* socket */ + in_addr_t gateway; /* default gateway (IPv4) */ + int has_pending_request; + unsigned char pending_request[12]; + int pending_request_len; + int try_number; + struct timeval retry_time; +} natpmp_t; + +typedef struct { + uint16_t type; /* NATPMP_RESPTYPE_* */ + uint16_t resultcode; /* NAT-PMP response code */ + uint32_t epoch; /* Seconds since start of epoch */ + union { + struct { + //in_addr_t addr; + struct in_addr addr; + } publicaddress; + struct { + uint16_t privateport; + uint16_t mappedpublicport; + uint32_t lifetime; + } newportmapping; + } pnu; +} natpmpresp_t; + +/* possible values for type field of natpmpresp_t */ +#define NATPMP_RESPTYPE_PUBLICADDRESS (0) +#define NATPMP_RESPTYPE_UDPPORTMAPPING (1) +#define NATPMP_RESPTYPE_TCPPORTMAPPING (2) + +/* Values to pass to sendnewportmappingrequest() */ +#define NATPMP_PROTOCOL_UDP (1) +#define NATPMP_PROTOCOL_TCP (2) + +/* return values */ +/* NATPMP_ERR_INVALIDARGS : invalid arguments passed to the function */ +#define NATPMP_ERR_INVALIDARGS (-1) +/* NATPMP_ERR_SOCKETERROR : socket() failed. check errno for details */ +#define NATPMP_ERR_SOCKETERROR (-2) +/* NATPMP_ERR_CANNOTGETGATEWAY : can't get default gateway IP */ +#define NATPMP_ERR_CANNOTGETGATEWAY (-3) +/* NATPMP_ERR_CLOSEERR : close() failed. check errno for details */ +#define NATPMP_ERR_CLOSEERR (-4) +/* NATPMP_ERR_RECVFROM : recvfrom() failed. check errno for details */ +#define NATPMP_ERR_RECVFROM (-5) +/* NATPMP_ERR_NOPENDINGREQ : readnatpmpresponseorretry() called while + * no NAT-PMP request was pending */ +#define NATPMP_ERR_NOPENDINGREQ (-6) +/* NATPMP_ERR_NOGATEWAYSUPPORT : the gateway does not support NAT-PMP */ +#define NATPMP_ERR_NOGATEWAYSUPPORT (-7) +/* NATPMP_ERR_CONNECTERR : connect() failed. check errno for details */ +#define NATPMP_ERR_CONNECTERR (-8) +/* NATPMP_ERR_WRONGPACKETSOURCE : packet not received from the network gateway */ +#define NATPMP_ERR_WRONGPACKETSOURCE (-9) +/* NATPMP_ERR_SENDERR : send() failed. check errno for details */ +#define NATPMP_ERR_SENDERR (-10) +/* NATPMP_ERR_FCNTLERROR : fcntl() failed. check errno for details */ +#define NATPMP_ERR_FCNTLERROR (-11) +/* NATPMP_ERR_GETTIMEOFDAYERR : gettimeofday() failed. check errno for details */ +#define NATPMP_ERR_GETTIMEOFDAYERR (-12) + +/* */ +#define NATPMP_ERR_UNSUPPORTEDVERSION (-14) +#define NATPMP_ERR_UNSUPPORTEDOPCODE (-15) + +/* Errors from the server : */ +#define NATPMP_ERR_UNDEFINEDERROR (-49) +#define NATPMP_ERR_NOTAUTHORIZED (-51) +#define NATPMP_ERR_NETWORKFAILURE (-52) +#define NATPMP_ERR_OUTOFRESOURCES (-53) + +/* NATPMP_TRYAGAIN : no data available for the moment. try again later */ +#define NATPMP_TRYAGAIN (-100) + +/* initnatpmp() + * initialize a natpmp_t object + * Return values : + * 0 = OK + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_SOCKETERROR + * NATPMP_ERR_FCNTLERROR + * NATPMP_ERR_CANNOTGETGATEWAY + * NATPMP_ERR_CONNECTERR */ +LIBSPEC int initnatpmp(natpmp_t * p); + +/* closenatpmp() + * close resources associated with a natpmp_t object + * Return values : + * 0 = OK + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_CLOSEERR */ +LIBSPEC int closenatpmp(natpmp_t * p); + +/* sendpublicaddressrequest() + * send a public address NAT-PMP request to the network gateway + * Return values : + * 2 = OK (size of the request) + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_SENDERR */ +LIBSPEC int sendpublicaddressrequest(natpmp_t * p); + +/* sendnewportmappingrequest() + * send a new port mapping NAT-PMP request to the network gateway + * Arguments : + * protocol is either NATPMP_PROTOCOL_TCP or NATPMP_PROTOCOL_UDP, + * lifetime is in seconds. + * To remove a port mapping, set lifetime to zero. + * To remove all port mappings to the host, set lifetime and both ports + * to zero. + * Return values : + * 12 = OK (size of the request) + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_SENDERR */ +LIBSPEC int sendnewportmappingrequest(natpmp_t * p, int protocol, + uint16_t privateport, uint16_t publicport, + uint32_t lifetime); + +/* getnatpmprequesttimeout() + * fills the timeval structure with the timeout duration of the + * currently pending NAT-PMP request. + * Return values : + * 0 = OK + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_GETTIMEOFDAYERR + * NATPMP_ERR_NOPENDINGREQ */ +LIBSPEC int getnatpmprequesttimeout(natpmp_t * p, struct timeval * timeout); + +/* readnatpmpresponseorretry() + * fills the natpmpresp_t structure if possible + * Return values : + * 0 = OK + * NATPMP_TRYAGAIN + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_NOPENDINGREQ + * NATPMP_ERR_NOGATEWAYSUPPORT + * NATPMP_ERR_RECVFROM + * NATPMP_ERR_WRONGPACKETSOURCE + * NATPMP_ERR_UNSUPPORTEDVERSION + * NATPMP_ERR_UNSUPPORTEDOPCODE + * NATPMP_ERR_NOTAUTHORIZED + * NATPMP_ERR_NETWORKFAILURE + * NATPMP_ERR_OUTOFRESOURCES + * NATPMP_ERR_UNSUPPORTEDOPCODE + * NATPMP_ERR_UNDEFINEDERROR */ +LIBSPEC int readnatpmpresponseorretry(natpmp_t * p, natpmpresp_t * response); + +#ifdef ENABLE_STRNATPMPERR +LIBSPEC const char * strnatpmperr(int t); +#endif + +#endif diff --git a/libportfwd/third-party/miniupnpc-20090605/Changelog.txt b/libportfwd/third-party/miniupnpc-20090605/Changelog.txt new file mode 100644 index 000000000..e34543fec --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/Changelog.txt @@ -0,0 +1,250 @@ +$Id: Changelog.txt,v 1.80 2009/04/17 21:21:19 nanard Exp $ +miniUPnP client Changelog. + +2009/04/17: + updating python module + Use strtoull() when using C99 + +2009/02/28: + Fixed miniwget.c for compiling under sun + +2008/12/18: + cleanup in Makefile (thanks to Paul de Weerd) + minissdpc.c : win32 compatibility + miniupnpc.c : changed xmlns prefix from 'm' to 'u' + Removed NDEBUG (using DEBUG) + +2008/10/14: + Added the ExternalHost argument to DeletePortMapping() + +2008/10/11: + Added the ExternalHost argument to AddPortMapping() + Put a correct User-Agent: header in HTTP requests. + +VERSION 1.2 : + +2008/10/07: + Update docs + +2008/09/25: + Integrated sameport patch from Dario Meloni : Added a "sameport" + argument to upnpDiscover(). + +2008/07/18: + small modif to make Clang happy :) + +2008/07/17: + #define SOAPPREFIX "s" in miniupnpc.c in order to remove SOAP-ENV... + +2008/07/14: + include declspec.h in installation (to /usr/include/miniupnpc) + +VERSION 1.1 : + +2008/07/04: + standard options for install/ln instead of gnu-specific stuff. + +2008/07/03: + now builds a .dll and .lib with win32. (mingw32) + +2008/04/28: + make install now install the binary of the upnpc tool + +2008/04/27: + added testupnpigd.py + added error strings for miniupnpc "internal" errors + improved python module error/exception reporting. + +2008/04/23: + Completely rewrite igd_desc_parse.c in order to be compatible with + Linksys WAG200G + Added testigddescparse + updated python module + +VERSION 1.0 : + +2008/02/21: + put some #ifdef DEBUG around DisplayNameValueList() + +2008/02/18: + Improved error reporting in upnpcommands.c + UPNP_GetStatusInfo() returns LastConnectionError + +2008/02/16: + better error handling in minisoap.c + improving display of "valid IGD found" in upnpc.c + +2008/02/03: + Fixing UPNP_GetValidIGD() + improved make install :) + +2007/12/22: + Adding upnperrors.c/h to provide a strupnperror() function + used to translate UPnP error codes to string. + +2007/12/19: + Fixing getDevicesFromMiniSSDPD() + improved error reporting of UPnP functions + +2007/12/18: + It is now possible to specify a different location for MiniSSDPd socket. + working with MiniSSDPd is now more efficient. + python module improved. + +2007/12/16: + improving error reporting + +2007/12/13: + Try to improve compatibility by using HTTP/1.0 instead of 1.1 and + XML a bit different for SOAP. + +2007/11/25: + fixed select() call for linux + +2007/11/15: + Added -fPIC to CFLAG for better shared library code. + +2007/11/02: + Fixed a potential socket leak in miniwget2() + +2007/10/16: + added a parameter to upnpDiscover() in order to allow the use of another + interface than the default multicast interface. + +2007/10/12: + Fixed the creation of symbolic link in Makefile + +2007/10/08: + Added man page + +2007/10/02: + fixed memory bug in GetUPNPUrls() + +2007/10/01: + fixes in the Makefile + Added UPNP_GetIGDFromUrl() and adapted the sample program accordingly. + Added SONAME in the shared library to please debian :) + fixed MS Windows compilation (minissdpd is not available under MS Windows). + +2007/09/25: + small change to Makefile to be able to install in a different location + (default is /usr) + +2007/09/24: + now compiling both shared and static library + +2007/09/19: + Cosmetic changes on upnpc.c + +2007/09/02: + adapting to new miniSSDPd (release version ?) + +2007/08/31: + Usage of miniSSDPd to skip discovery process. + +2007/08/27: + fixed python module to allow compilation with Python older than Python 2.4 + +2007/06/12: + Added a python module. + +2007/05/19: + Fixed compilation under MinGW + +2007/05/15: + fixed a memory leak in AddPortMapping() + Added testupnpreplyparse executable to check the parsing of + upnp soap messages + minixml now ignore namespace prefixes. + +2007/04/26: + upnpc now displays external ip address with -s or -l + +2007/04/11: + changed MINIUPNPC_URL_MAXSIZE to 128 to accomodate the "BT Voyager 210" + +2007/03/19: + cleanup in miniwget.c + +2007/03/01: + Small typo fix... + +2007/01/30: + Now parsing the HTTP header from SOAP responses in order to + get content-length value. + +2007/01/29: + Fixed the Soap Query to speedup the HTTP request. + added some Win32 DLL stuff... + +2007/01/27: + Fixed some WIN32 compatibility issues + +2006/12/14: + Added UPNPIGD_IsConnected() function in miniupnp.c/.h + Added UPNP_GetValidIGD() in miniupnp.c/.h + cleaned upnpc.c main(). now using UPNP_GetValidIGD() + +2006/12/07: + Version 1.0-RC1 released + +2006/12/03: + Minor changes to compile under SunOS/Solaris + +2006/11/30: + made a minixml parser validator program + updated minixml to handle attributes correctly + +2006/11/22: + Added a -r option to the upnpc sample thanks to Alexander Hubmann. + +2006/11/19: + Cleanup code to make it more ANSI C compliant + +2006/11/10: + detect and display local lan address. + +2006/11/04: + Packets and Bytes Sent/Received are now unsigned int. + +2006/11/01: + Bug fix thanks to Giuseppe D'Angelo + +2006/10/31: + C++ compatibility for .h files. + Added a way to get ip Address on the LAN used to reach the IGD. + +2006/10/25: + Added M-SEARCH to the services in the discovery process. + +2006/10/22: + updated the Makefile to use makedepend, added a "make install" + update Makefile + +2006/10/20: + fixing the description url parsing thanks to patch sent by + Wayne Dawe. + Fixed/translated some comments. + Implemented a better discover process, first looking + for IGD then for root devices (as some devices only reply to + M-SEARCH for root devices). + +2006/09/02: + added freeUPNPDevlist() function. + +2006/08/04: + More command line arguments checking + +2006/08/01: + Added the .bat file to compile under Win32 with minGW32 + +2006/07/31: + Fixed the rootdesc parser (igd_desc_parse.c) + +2006/07/20: + parseMSEARCHReply() is now returning the ST: line as well + starting changes to detect several UPnP devices on the network + +2006/07/19: + using GetCommonLinkProperties to get down/upload bitrate + diff --git a/libportfwd/third-party/miniupnpc-20090605/LICENCE b/libportfwd/third-party/miniupnpc-20090605/LICENCE new file mode 100644 index 000000000..a8cfb5ef3 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/LICENCE @@ -0,0 +1,26 @@ +Copyright (c) 2005-2008, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * 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. + * 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR CONTRIBUTORS 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. + diff --git a/libportfwd/third-party/miniupnpc-20090605/README b/libportfwd/third-party/miniupnpc-20090605/README new file mode 100644 index 000000000..3aa2a361b --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/README @@ -0,0 +1,54 @@ +Project: miniupnp +Project web page: http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ +Author: Thomas Bernard +Copyright (c) 2005-2008 Thomas Bernard +This software is subject to the conditions detailed in the +LICENCE file provided within this distribution. + +For the comfort of Win32 users, bsdqueue.h is included in the distribution. +Its licence is included in the header of the file. +bsdqueue.h is a copy of the sys/queue.h of an OpenBSD system. + +* miniupnp Client * + +To compile, simply run 'gmake' (could be 'make'). +Under win32, to compile with MinGW, type "mingw32make.bat". +The compilation is known to work under linux, FreeBSD, +OpenBSD, MacOS X and cygwin. +To install the library and headers on the system use : +> su +> make install +> exit + +alternatively, to install in a specific location, use : +> INSTALLPREFIX=/usr/local make install + +upnpc.c is a sample client using the libminiupnpc. +To use the libminiupnpc in your application, link it with +libminiupnpc.a and use the following functions found in miniupnpc.h, +upnpcommands.h and miniwget.h : +- upnpDiscover() +- miniwget() +- parserootdesc() +- GetUPNPUrls() +- UPNP_* (calling UPNP methods) + +Note : use #include etc... for the includes +and -lminiupnpc for the link + +Discovery process is speeded up when MiniSSDPd is running on the machine. + +* Python module * + +you can build a python module with 'make pythonmodule' +and install it with 'make installpythonmodule'. +setup.py (and setupmingw32.py) are included in the distribution. + + +Feel free to contact me if you have any problem : +e-mail : miniupnp@free.fr + +If you are using libminiupnpc in your application, please +send me an email ! + + diff --git a/libportfwd/third-party/miniupnpc-20090605/bsdqueue.h b/libportfwd/third-party/miniupnpc-20090605/bsdqueue.h new file mode 100644 index 000000000..1fe0599f5 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/bsdqueue.h @@ -0,0 +1,531 @@ +/* $OpenBSD: queue.h,v 1.31 2005/11/25 08:06:25 otto Exp $ */ +/* $NetBSD: queue.h,v 1.11 1996/05/16 05:17:14 mycroft Exp $ */ + +/* + * Copyright (c) 1991, 1993 + * The Regents of the University of California. 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. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``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 REGENTS OR CONTRIBUTORS 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. + * + * @(#)queue.h 8.5 (Berkeley) 8/20/94 + */ + +#ifndef _SYS_QUEUE_H_ +#define _SYS_QUEUE_H_ + +/* + * This file defines five types of data structures: singly-linked lists, + * lists, simple queues, tail queues, and circular queues. + * + * + * A singly-linked list is headed by a single forward pointer. The elements + * are singly linked for minimum space and pointer manipulation overhead at + * the expense of O(n) removal for arbitrary elements. New elements can be + * added to the list after an existing element or at the head of the list. + * Elements being removed from the head of the list should use the explicit + * macro for this purpose for optimum efficiency. A singly-linked list may + * only be traversed in the forward direction. Singly-linked lists are ideal + * for applications with large datasets and few or no removals or for + * implementing a LIFO queue. + * + * A list is headed by a single forward pointer (or an array of forward + * pointers for a hash table header). The elements are doubly linked + * so that an arbitrary element can be removed without a need to + * traverse the list. New elements can be added to the list before + * or after an existing element or at the head of the list. A list + * may only be traversed in the forward direction. + * + * A simple queue is headed by a pair of pointers, one the head of the + * list and the other to the tail of the list. The elements are singly + * linked to save space, so elements can only be removed from the + * head of the list. New elements can be added to the list before or after + * an existing element, at the head of the list, or at the end of the + * list. A simple queue may only be traversed in the forward direction. + * + * A tail queue is headed by a pair of pointers, one to the head of the + * list and the other to the tail of the list. The elements are doubly + * linked so that an arbitrary element can be removed without a need to + * traverse the list. New elements can be added to the list before or + * after an existing element, at the head of the list, or at the end of + * the list. A tail queue may be traversed in either direction. + * + * A circle queue is headed by a pair of pointers, one to the head of the + * list and the other to the tail of the list. The elements are doubly + * linked so that an arbitrary element can be removed without a need to + * traverse the list. New elements can be added to the list before or after + * an existing element, at the head of the list, or at the end of the list. + * A circle queue may be traversed in either direction, but has a more + * complex end of list detection. + * + * For details on the use of these macros, see the queue(3) manual page. + */ + +#ifdef QUEUE_MACRO_DEBUG +#define _Q_INVALIDATE(a) (a) = ((void *)-1) +#else +#define _Q_INVALIDATE(a) +#endif + +/* + * Singly-linked List definitions. + */ +#define SLIST_HEAD(name, type) \ +struct name { \ + struct type *slh_first; /* first element */ \ +} + +#define SLIST_HEAD_INITIALIZER(head) \ + { NULL } + +#ifdef SLIST_ENTRY +#undef SLIST_ENTRY +#endif + +#define SLIST_ENTRY(type) \ +struct { \ + struct type *sle_next; /* next element */ \ +} + +/* + * Singly-linked List access methods. + */ +#define SLIST_FIRST(head) ((head)->slh_first) +#define SLIST_END(head) NULL +#define SLIST_EMPTY(head) (SLIST_FIRST(head) == SLIST_END(head)) +#define SLIST_NEXT(elm, field) ((elm)->field.sle_next) + +#define SLIST_FOREACH(var, head, field) \ + for((var) = SLIST_FIRST(head); \ + (var) != SLIST_END(head); \ + (var) = SLIST_NEXT(var, field)) + +#define SLIST_FOREACH_PREVPTR(var, varp, head, field) \ + for ((varp) = &SLIST_FIRST((head)); \ + ((var) = *(varp)) != SLIST_END(head); \ + (varp) = &SLIST_NEXT((var), field)) + +/* + * Singly-linked List functions. + */ +#define SLIST_INIT(head) { \ + SLIST_FIRST(head) = SLIST_END(head); \ +} + +#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \ + (elm)->field.sle_next = (slistelm)->field.sle_next; \ + (slistelm)->field.sle_next = (elm); \ +} while (0) + +#define SLIST_INSERT_HEAD(head, elm, field) do { \ + (elm)->field.sle_next = (head)->slh_first; \ + (head)->slh_first = (elm); \ +} while (0) + +#define SLIST_REMOVE_NEXT(head, elm, field) do { \ + (elm)->field.sle_next = (elm)->field.sle_next->field.sle_next; \ +} while (0) + +#define SLIST_REMOVE_HEAD(head, field) do { \ + (head)->slh_first = (head)->slh_first->field.sle_next; \ +} while (0) + +#define SLIST_REMOVE(head, elm, type, field) do { \ + if ((head)->slh_first == (elm)) { \ + SLIST_REMOVE_HEAD((head), field); \ + } else { \ + struct type *curelm = (head)->slh_first; \ + \ + while (curelm->field.sle_next != (elm)) \ + curelm = curelm->field.sle_next; \ + curelm->field.sle_next = \ + curelm->field.sle_next->field.sle_next; \ + _Q_INVALIDATE((elm)->field.sle_next); \ + } \ +} while (0) + +/* + * List definitions. + */ +#define LIST_HEAD(name, type) \ +struct name { \ + struct type *lh_first; /* first element */ \ +} + +#define LIST_HEAD_INITIALIZER(head) \ + { NULL } + +#define LIST_ENTRY(type) \ +struct { \ + struct type *le_next; /* next element */ \ + struct type **le_prev; /* address of previous next element */ \ +} + +/* + * List access methods + */ +#define LIST_FIRST(head) ((head)->lh_first) +#define LIST_END(head) NULL +#define LIST_EMPTY(head) (LIST_FIRST(head) == LIST_END(head)) +#define LIST_NEXT(elm, field) ((elm)->field.le_next) + +#define LIST_FOREACH(var, head, field) \ + for((var) = LIST_FIRST(head); \ + (var)!= LIST_END(head); \ + (var) = LIST_NEXT(var, field)) + +/* + * List functions. + */ +#define LIST_INIT(head) do { \ + LIST_FIRST(head) = LIST_END(head); \ +} while (0) + +#define LIST_INSERT_AFTER(listelm, elm, field) do { \ + if (((elm)->field.le_next = (listelm)->field.le_next) != NULL) \ + (listelm)->field.le_next->field.le_prev = \ + &(elm)->field.le_next; \ + (listelm)->field.le_next = (elm); \ + (elm)->field.le_prev = &(listelm)->field.le_next; \ +} while (0) + +#define LIST_INSERT_BEFORE(listelm, elm, field) do { \ + (elm)->field.le_prev = (listelm)->field.le_prev; \ + (elm)->field.le_next = (listelm); \ + *(listelm)->field.le_prev = (elm); \ + (listelm)->field.le_prev = &(elm)->field.le_next; \ +} while (0) + +#define LIST_INSERT_HEAD(head, elm, field) do { \ + if (((elm)->field.le_next = (head)->lh_first) != NULL) \ + (head)->lh_first->field.le_prev = &(elm)->field.le_next;\ + (head)->lh_first = (elm); \ + (elm)->field.le_prev = &(head)->lh_first; \ +} while (0) + +#define LIST_REMOVE(elm, field) do { \ + if ((elm)->field.le_next != NULL) \ + (elm)->field.le_next->field.le_prev = \ + (elm)->field.le_prev; \ + *(elm)->field.le_prev = (elm)->field.le_next; \ + _Q_INVALIDATE((elm)->field.le_prev); \ + _Q_INVALIDATE((elm)->field.le_next); \ +} while (0) + +#define LIST_REPLACE(elm, elm2, field) do { \ + if (((elm2)->field.le_next = (elm)->field.le_next) != NULL) \ + (elm2)->field.le_next->field.le_prev = \ + &(elm2)->field.le_next; \ + (elm2)->field.le_prev = (elm)->field.le_prev; \ + *(elm2)->field.le_prev = (elm2); \ + _Q_INVALIDATE((elm)->field.le_prev); \ + _Q_INVALIDATE((elm)->field.le_next); \ +} while (0) + +/* + * Simple queue definitions. + */ +#define SIMPLEQ_HEAD(name, type) \ +struct name { \ + struct type *sqh_first; /* first element */ \ + struct type **sqh_last; /* addr of last next element */ \ +} + +#define SIMPLEQ_HEAD_INITIALIZER(head) \ + { NULL, &(head).sqh_first } + +#define SIMPLEQ_ENTRY(type) \ +struct { \ + struct type *sqe_next; /* next element */ \ +} + +/* + * Simple queue access methods. + */ +#define SIMPLEQ_FIRST(head) ((head)->sqh_first) +#define SIMPLEQ_END(head) NULL +#define SIMPLEQ_EMPTY(head) (SIMPLEQ_FIRST(head) == SIMPLEQ_END(head)) +#define SIMPLEQ_NEXT(elm, field) ((elm)->field.sqe_next) + +#define SIMPLEQ_FOREACH(var, head, field) \ + for((var) = SIMPLEQ_FIRST(head); \ + (var) != SIMPLEQ_END(head); \ + (var) = SIMPLEQ_NEXT(var, field)) + +/* + * Simple queue functions. + */ +#define SIMPLEQ_INIT(head) do { \ + (head)->sqh_first = NULL; \ + (head)->sqh_last = &(head)->sqh_first; \ +} while (0) + +#define SIMPLEQ_INSERT_HEAD(head, elm, field) do { \ + if (((elm)->field.sqe_next = (head)->sqh_first) == NULL) \ + (head)->sqh_last = &(elm)->field.sqe_next; \ + (head)->sqh_first = (elm); \ +} while (0) + +#define SIMPLEQ_INSERT_TAIL(head, elm, field) do { \ + (elm)->field.sqe_next = NULL; \ + *(head)->sqh_last = (elm); \ + (head)->sqh_last = &(elm)->field.sqe_next; \ +} while (0) + +#define SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do { \ + if (((elm)->field.sqe_next = (listelm)->field.sqe_next) == NULL)\ + (head)->sqh_last = &(elm)->field.sqe_next; \ + (listelm)->field.sqe_next = (elm); \ +} while (0) + +#define SIMPLEQ_REMOVE_HEAD(head, field) do { \ + if (((head)->sqh_first = (head)->sqh_first->field.sqe_next) == NULL) \ + (head)->sqh_last = &(head)->sqh_first; \ +} while (0) + +/* + * Tail queue definitions. + */ +#define TAILQ_HEAD(name, type) \ +struct name { \ + struct type *tqh_first; /* first element */ \ + struct type **tqh_last; /* addr of last next element */ \ +} + +#define TAILQ_HEAD_INITIALIZER(head) \ + { NULL, &(head).tqh_first } + +#define TAILQ_ENTRY(type) \ +struct { \ + struct type *tqe_next; /* next element */ \ + struct type **tqe_prev; /* address of previous next element */ \ +} + +/* + * tail queue access methods + */ +#define TAILQ_FIRST(head) ((head)->tqh_first) +#define TAILQ_END(head) NULL +#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next) +#define TAILQ_LAST(head, headname) \ + (*(((struct headname *)((head)->tqh_last))->tqh_last)) +/* XXX */ +#define TAILQ_PREV(elm, headname, field) \ + (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last)) +#define TAILQ_EMPTY(head) \ + (TAILQ_FIRST(head) == TAILQ_END(head)) + +#define TAILQ_FOREACH(var, head, field) \ + for((var) = TAILQ_FIRST(head); \ + (var) != TAILQ_END(head); \ + (var) = TAILQ_NEXT(var, field)) + +#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \ + for((var) = TAILQ_LAST(head, headname); \ + (var) != TAILQ_END(head); \ + (var) = TAILQ_PREV(var, headname, field)) + +/* + * Tail queue functions. + */ +#define TAILQ_INIT(head) do { \ + (head)->tqh_first = NULL; \ + (head)->tqh_last = &(head)->tqh_first; \ +} while (0) + +#define TAILQ_INSERT_HEAD(head, elm, field) do { \ + if (((elm)->field.tqe_next = (head)->tqh_first) != NULL) \ + (head)->tqh_first->field.tqe_prev = \ + &(elm)->field.tqe_next; \ + else \ + (head)->tqh_last = &(elm)->field.tqe_next; \ + (head)->tqh_first = (elm); \ + (elm)->field.tqe_prev = &(head)->tqh_first; \ +} while (0) + +#define TAILQ_INSERT_TAIL(head, elm, field) do { \ + (elm)->field.tqe_next = NULL; \ + (elm)->field.tqe_prev = (head)->tqh_last; \ + *(head)->tqh_last = (elm); \ + (head)->tqh_last = &(elm)->field.tqe_next; \ +} while (0) + +#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \ + if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\ + (elm)->field.tqe_next->field.tqe_prev = \ + &(elm)->field.tqe_next; \ + else \ + (head)->tqh_last = &(elm)->field.tqe_next; \ + (listelm)->field.tqe_next = (elm); \ + (elm)->field.tqe_prev = &(listelm)->field.tqe_next; \ +} while (0) + +#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \ + (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \ + (elm)->field.tqe_next = (listelm); \ + *(listelm)->field.tqe_prev = (elm); \ + (listelm)->field.tqe_prev = &(elm)->field.tqe_next; \ +} while (0) + +#define TAILQ_REMOVE(head, elm, field) do { \ + if (((elm)->field.tqe_next) != NULL) \ + (elm)->field.tqe_next->field.tqe_prev = \ + (elm)->field.tqe_prev; \ + else \ + (head)->tqh_last = (elm)->field.tqe_prev; \ + *(elm)->field.tqe_prev = (elm)->field.tqe_next; \ + _Q_INVALIDATE((elm)->field.tqe_prev); \ + _Q_INVALIDATE((elm)->field.tqe_next); \ +} while (0) + +#define TAILQ_REPLACE(head, elm, elm2, field) do { \ + if (((elm2)->field.tqe_next = (elm)->field.tqe_next) != NULL) \ + (elm2)->field.tqe_next->field.tqe_prev = \ + &(elm2)->field.tqe_next; \ + else \ + (head)->tqh_last = &(elm2)->field.tqe_next; \ + (elm2)->field.tqe_prev = (elm)->field.tqe_prev; \ + *(elm2)->field.tqe_prev = (elm2); \ + _Q_INVALIDATE((elm)->field.tqe_prev); \ + _Q_INVALIDATE((elm)->field.tqe_next); \ +} while (0) + +/* + * Circular queue definitions. + */ +#define CIRCLEQ_HEAD(name, type) \ +struct name { \ + struct type *cqh_first; /* first element */ \ + struct type *cqh_last; /* last element */ \ +} + +#define CIRCLEQ_HEAD_INITIALIZER(head) \ + { CIRCLEQ_END(&head), CIRCLEQ_END(&head) } + +#define CIRCLEQ_ENTRY(type) \ +struct { \ + struct type *cqe_next; /* next element */ \ + struct type *cqe_prev; /* previous element */ \ +} + +/* + * Circular queue access methods + */ +#define CIRCLEQ_FIRST(head) ((head)->cqh_first) +#define CIRCLEQ_LAST(head) ((head)->cqh_last) +#define CIRCLEQ_END(head) ((void *)(head)) +#define CIRCLEQ_NEXT(elm, field) ((elm)->field.cqe_next) +#define CIRCLEQ_PREV(elm, field) ((elm)->field.cqe_prev) +#define CIRCLEQ_EMPTY(head) \ + (CIRCLEQ_FIRST(head) == CIRCLEQ_END(head)) + +#define CIRCLEQ_FOREACH(var, head, field) \ + for((var) = CIRCLEQ_FIRST(head); \ + (var) != CIRCLEQ_END(head); \ + (var) = CIRCLEQ_NEXT(var, field)) + +#define CIRCLEQ_FOREACH_REVERSE(var, head, field) \ + for((var) = CIRCLEQ_LAST(head); \ + (var) != CIRCLEQ_END(head); \ + (var) = CIRCLEQ_PREV(var, field)) + +/* + * Circular queue functions. + */ +#define CIRCLEQ_INIT(head) do { \ + (head)->cqh_first = CIRCLEQ_END(head); \ + (head)->cqh_last = CIRCLEQ_END(head); \ +} while (0) + +#define CIRCLEQ_INSERT_AFTER(head, listelm, elm, field) do { \ + (elm)->field.cqe_next = (listelm)->field.cqe_next; \ + (elm)->field.cqe_prev = (listelm); \ + if ((listelm)->field.cqe_next == CIRCLEQ_END(head)) \ + (head)->cqh_last = (elm); \ + else \ + (listelm)->field.cqe_next->field.cqe_prev = (elm); \ + (listelm)->field.cqe_next = (elm); \ +} while (0) + +#define CIRCLEQ_INSERT_BEFORE(head, listelm, elm, field) do { \ + (elm)->field.cqe_next = (listelm); \ + (elm)->field.cqe_prev = (listelm)->field.cqe_prev; \ + if ((listelm)->field.cqe_prev == CIRCLEQ_END(head)) \ + (head)->cqh_first = (elm); \ + else \ + (listelm)->field.cqe_prev->field.cqe_next = (elm); \ + (listelm)->field.cqe_prev = (elm); \ +} while (0) + +#define CIRCLEQ_INSERT_HEAD(head, elm, field) do { \ + (elm)->field.cqe_next = (head)->cqh_first; \ + (elm)->field.cqe_prev = CIRCLEQ_END(head); \ + if ((head)->cqh_last == CIRCLEQ_END(head)) \ + (head)->cqh_last = (elm); \ + else \ + (head)->cqh_first->field.cqe_prev = (elm); \ + (head)->cqh_first = (elm); \ +} while (0) + +#define CIRCLEQ_INSERT_TAIL(head, elm, field) do { \ + (elm)->field.cqe_next = CIRCLEQ_END(head); \ + (elm)->field.cqe_prev = (head)->cqh_last; \ + if ((head)->cqh_first == CIRCLEQ_END(head)) \ + (head)->cqh_first = (elm); \ + else \ + (head)->cqh_last->field.cqe_next = (elm); \ + (head)->cqh_last = (elm); \ +} while (0) + +#define CIRCLEQ_REMOVE(head, elm, field) do { \ + if ((elm)->field.cqe_next == CIRCLEQ_END(head)) \ + (head)->cqh_last = (elm)->field.cqe_prev; \ + else \ + (elm)->field.cqe_next->field.cqe_prev = \ + (elm)->field.cqe_prev; \ + if ((elm)->field.cqe_prev == CIRCLEQ_END(head)) \ + (head)->cqh_first = (elm)->field.cqe_next; \ + else \ + (elm)->field.cqe_prev->field.cqe_next = \ + (elm)->field.cqe_next; \ + _Q_INVALIDATE((elm)->field.cqe_prev); \ + _Q_INVALIDATE((elm)->field.cqe_next); \ +} while (0) + +#define CIRCLEQ_REPLACE(head, elm, elm2, field) do { \ + if (((elm2)->field.cqe_next = (elm)->field.cqe_next) == \ + CIRCLEQ_END(head)) \ + (head).cqh_last = (elm2); \ + else \ + (elm2)->field.cqe_next->field.cqe_prev = (elm2); \ + if (((elm2)->field.cqe_prev = (elm)->field.cqe_prev) == \ + CIRCLEQ_END(head)) \ + (head).cqh_first = (elm2); \ + else \ + (elm2)->field.cqe_prev->field.cqe_next = (elm2); \ + _Q_INVALIDATE((elm)->field.cqe_prev); \ + _Q_INVALIDATE((elm)->field.cqe_next); \ +} while (0) + +#endif /* !_SYS_QUEUE_H_ */ diff --git a/libportfwd/third-party/miniupnpc-20090605/codelength.h b/libportfwd/third-party/miniupnpc-20090605/codelength.h new file mode 100644 index 000000000..f11e5e936 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/codelength.h @@ -0,0 +1,24 @@ +/* $Id: codelength.h,v 1.1 2008/10/06 22:04:06 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas BERNARD + * copyright (c) 2005-2008 Thomas Bernard + * This software is subjet to the conditions detailed in the + * provided LICENCE file. */ +#ifndef __CODELENGTH_H__ +#define __CODELENGTH_H__ + +/* Encode length by using 7bit per Byte : + * Most significant bit of each byte specifies that the + * following byte is part of the code */ +#define DECODELENGTH(n, p) n = 0; \ + do { n = (n << 7) | (*p & 0x7f); } \ + while(*(p++)&0x80); + +#define CODELENGTH(n, p) if(n>=268435456) *(p++) = (n >> 28) | 0x80; \ + if(n>=2097152) *(p++) = (n >> 21) | 0x80; \ + if(n>=16384) *(p++) = (n >> 14) | 0x80; \ + if(n>=128) *(p++) = (n >> 7) | 0x80; \ + *(p++) = n & 0x7f; + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/declspec.h b/libportfwd/third-party/miniupnpc-20090605/declspec.h new file mode 100644 index 000000000..b804247d2 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/declspec.h @@ -0,0 +1,15 @@ +#ifndef __DECLSPEC_H__ +#define __DECLSPEC_H__ + +#if defined(WIN32) && !defined(STATICLIB) + #ifdef MINIUPNP_EXPORTS + #define LIBSPEC __declspec(dllexport) + #else + #define LIBSPEC __declspec(dllimport) + #endif +#else + #define LIBSPEC +#endif + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/igd_desc_parse.c b/libportfwd/third-party/miniupnpc-20090605/igd_desc_parse.c new file mode 100644 index 000000000..e839ff4c1 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/igd_desc_parse.c @@ -0,0 +1,113 @@ +/* $Id: igd_desc_parse.c,v 1.8 2008/04/23 11:51:06 nanard Exp $ */ +/* Project : miniupnp + * http://miniupnp.free.fr/ + * Author : Thomas Bernard + * Copyright (c) 2005-2008 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * */ +#include "igd_desc_parse.h" +#include +#include + +/* TODO : rewrite this code so it correctly handle descriptions with + * both WANIPConnection and/or WANPPPConnection */ + +/* Start element handler : + * update nesting level counter and copy element name */ +void IGDstartelt(void * d, const char * name, int l) +{ + struct IGDdatas * datas = (struct IGDdatas *)d; + memcpy( datas->cureltname, name, l); + datas->cureltname[l] = '\0'; + datas->level++; + if( (l==7) && !memcmp(name, "service", l) ) { + datas->controlurl_tmp[0] = '\0'; + datas->eventsuburl_tmp[0] = '\0'; + datas->scpdurl_tmp[0] = '\0'; + datas->servicetype_tmp[0] = '\0'; + } +} + +/* End element handler : + * update nesting level counter and update parser state if + * service element is parsed */ +void IGDendelt(void * d, const char * name, int l) +{ + struct IGDdatas * datas = (struct IGDdatas *)d; + datas->level--; + /*printf("endelt %2d %.*s\n", datas->level, l, name);*/ + if( (l==7) && !memcmp(name, "service", l) ) + { + /* + if( datas->state < 1 + && !strcmp(datas->servicetype, + // "urn:schemas-upnp-org:service:WANIPConnection:1") ) + "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1")) + datas->state ++; + */ + if(0==strcmp(datas->servicetype_tmp, + "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1")) { + memcpy(datas->controlurl_CIF, datas->controlurl_tmp, MINIUPNPC_URL_MAXSIZE); + memcpy(datas->eventsuburl_CIF, datas->eventsuburl_tmp, MINIUPNPC_URL_MAXSIZE); + memcpy(datas->scpdurl_CIF, datas->scpdurl_tmp, MINIUPNPC_URL_MAXSIZE); + memcpy(datas->servicetype_CIF, datas->servicetype_tmp, MINIUPNPC_URL_MAXSIZE); + } else if(0==strcmp(datas->servicetype_tmp, + "urn:schemas-upnp-org:service:WANIPConnection:1") + || 0==strcmp(datas->servicetype_tmp, + "urn:schemas-upnp-org:service:WANPPPConnection:1") ) { + memcpy(datas->controlurl, datas->controlurl_tmp, MINIUPNPC_URL_MAXSIZE); + memcpy(datas->eventsuburl, datas->eventsuburl_tmp, MINIUPNPC_URL_MAXSIZE); + memcpy(datas->scpdurl, datas->scpdurl_tmp, MINIUPNPC_URL_MAXSIZE); + memcpy(datas->servicetype, datas->servicetype_tmp, MINIUPNPC_URL_MAXSIZE); + } + } +} + +/* Data handler : + * copy data depending on the current element name and state */ +void IGDdata(void * d, const char * data, int l) +{ + struct IGDdatas * datas = (struct IGDdatas *)d; + char * dstmember = 0; + /*printf("%2d %s : %.*s\n", + datas->level, datas->cureltname, l, data); */ + if( !strcmp(datas->cureltname, "URLBase") ) + dstmember = datas->urlbase; + else if( !strcmp(datas->cureltname, "serviceType") ) + dstmember = datas->servicetype_tmp; + else if( !strcmp(datas->cureltname, "controlURL") ) + dstmember = datas->controlurl_tmp; + else if( !strcmp(datas->cureltname, "eventSubURL") ) + dstmember = datas->eventsuburl_tmp; + else if( !strcmp(datas->cureltname, "SCPDURL") ) + dstmember = datas->scpdurl_tmp; +/* else if( !strcmp(datas->cureltname, "deviceType") ) + dstmember = datas->devicetype_tmp;*/ + if(dstmember) + { + if(l>=MINIUPNPC_URL_MAXSIZE) + l = MINIUPNPC_URL_MAXSIZE-1; + memcpy(dstmember, data, l); + dstmember[l] = '\0'; + } +} + +void printIGD(struct IGDdatas * d) +{ + printf("urlbase = %s\n", d->urlbase); + printf("WAN Device (Common interface config) :\n"); + /*printf(" deviceType = %s\n", d->devicetype_CIF);*/ + printf(" serviceType = %s\n", d->servicetype_CIF); + printf(" controlURL = %s\n", d->controlurl_CIF); + printf(" eventSubURL = %s\n", d->eventsuburl_CIF); + printf(" SCPDURL = %s\n", d->scpdurl_CIF); + printf("WAN Connection Device (IP or PPP Connection):\n"); + /*printf(" deviceType = %s\n", d->devicetype);*/ + printf(" servicetype = %s\n", d->servicetype); + printf(" controlURL = %s\n", d->controlurl); + printf(" eventSubURL = %s\n", d->eventsuburl); + printf(" SCPDURL = %s\n", d->scpdurl); +} + + diff --git a/libportfwd/third-party/miniupnpc-20090605/igd_desc_parse.h b/libportfwd/third-party/miniupnpc-20090605/igd_desc_parse.h new file mode 100644 index 000000000..aabcb087d --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/igd_desc_parse.h @@ -0,0 +1,47 @@ +/* $Id: igd_desc_parse.h,v 1.6 2008/04/23 11:51:07 nanard Exp $ */ +/* Project : miniupnp + * http://miniupnp.free.fr/ + * Author : Thomas Bernard + * Copyright (c) 2005-2008 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * */ +#ifndef __IGD_DESC_PARSE_H__ +#define __IGD_DESC_PARSE_H__ + +/* Structure to store the result of the parsing of UPnP + * descriptions of Internet Gateway Devices */ +#define MINIUPNPC_URL_MAXSIZE (128) +struct IGDdatas { + char cureltname[MINIUPNPC_URL_MAXSIZE]; + char urlbase[MINIUPNPC_URL_MAXSIZE]; + int level; + /*int state;*/ + /* "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" */ + char controlurl_CIF[MINIUPNPC_URL_MAXSIZE]; + char eventsuburl_CIF[MINIUPNPC_URL_MAXSIZE]; + char scpdurl_CIF[MINIUPNPC_URL_MAXSIZE]; + char servicetype_CIF[MINIUPNPC_URL_MAXSIZE]; + /*char devicetype_CIF[MINIUPNPC_URL_MAXSIZE];*/ + /* "urn:schemas-upnp-org:service:WANIPConnection:1" + * "urn:schemas-upnp-org:service:WANPPPConnection:1" */ + char controlurl[MINIUPNPC_URL_MAXSIZE]; + char eventsuburl[MINIUPNPC_URL_MAXSIZE]; + char scpdurl[MINIUPNPC_URL_MAXSIZE]; + char servicetype[MINIUPNPC_URL_MAXSIZE]; + /*char devicetype[MINIUPNPC_URL_MAXSIZE];*/ + /* tmp */ + char controlurl_tmp[MINIUPNPC_URL_MAXSIZE]; + char eventsuburl_tmp[MINIUPNPC_URL_MAXSIZE]; + char scpdurl_tmp[MINIUPNPC_URL_MAXSIZE]; + char servicetype_tmp[MINIUPNPC_URL_MAXSIZE]; + /*char devicetype_tmp[MINIUPNPC_URL_MAXSIZE];*/ +}; + +void IGDstartelt(void *, const char *, int); +void IGDendelt(void *, const char *, int); +void IGDdata(void *, const char *, int); +void printIGD(struct IGDdatas *); + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/man3/miniupnpc.3 b/libportfwd/third-party/miniupnpc-20090605/man3/miniupnpc.3 new file mode 100644 index 000000000..9d0e23e88 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/man3/miniupnpc.3 @@ -0,0 +1,52 @@ +\" $Id: miniupnpc.3,v 1.2 2008/10/07 13:51:55 nanard Exp $ +.TH miniupnpc 3 +.SH NAME +miniupnpc \- UPnP client library +.SH SYNOPSIS +.SH DESCRIPTION +The miniupnpc library implement the UPnP protocol defined +to dialog with Internet Gateway Devices. It also has +the ability to use data gathered by minissdpd(1) about +UPnP devices up on the network in order to skip the +long UPnP device discovery process. +.PP +At first, upnpDiscover(3) has to be used to discover UPnP IGD present +on the network. Then UPNP_GetValidIGD(3) to select the right one. +Alternatively, UPNP_GetIGDFromUrl(3) could be used to bypass discovery +process if the root description url of the device to use is known. +Then all the UPNP_* functions can be used, such as +UPNP_GetConnectionTypeInfo(3), UPNP_AddPortMapping(3), etc... +.SH "HEADER FILES" +.IP miniupnpc.h +That's the main header file for the miniupnpc library API. +It contains all the functions and structures related to device discovery. +.IP upnpcommands.h +This header file contain the UPnP IGD methods that are accessible +through the miniupnpc API. The name of the C functions are matching +the UPnP methods names. ie: GetGenericPortMappingEntry is +UPNP_GetGenericPortMappingEntry. +.SH "API FUNCTIONS" +.IP "struct UPNPDev * upnpDiscover(int delay, const char * multicastif, const char * minissdpdsock, int sameport);" +execute the discovery process. +delay (in millisecond) is the maximum time for waiting any device response. +If available, device list will be obtained from MiniSSDPd. +Default path for minissdpd socket will be used if minissdpdsock argument is NULL. +If multicastif is not NULL, it will be used instead of the default multicast interface for sending SSDP discover packets. +If sameport is not null, SSDP packets will be sent from the source port 1900 (same as destination port) otherwise system assign a source port. +.IP "void freeUPNPDevlist(struct UPNPDev * devlist);" +free the list returned by upnpDiscover(). +.IP "int UPNP_GetValidIGD(struct UPNPDev * devlist, struct UPNPUrls * urls, struct IGDdatas * data, char * lanaddr, int lanaddrlen);" +browse the list of device returned by upnpDiscover(), find +a live UPnP internet gateway device and fill structures passed as arguments +with data used for UPNP methods invokation. +.IP "int UPNP_GetIGDFromUrl(const char * rootdescurl, struct UPNPUrls * urls, struct IGDdatas * data, char * lanaddr, int lanaddrlen);" +permit to bypass the upnpDiscover() call if the xml root description +URL of the UPnP IGD is known. +Fill structures passed as arguments +with data used for UPNP methods invokation. +.IP "void GetUPNPUrls(struct UPNPUrls *, struct IGDdatas *, const char *);" +.IP "void FreeUPNPUrls(struct UPNPUrls *);" + +.SH "SEE ALSO" +minissdpd(1) +.SH BUGS diff --git a/libportfwd/third-party/miniupnpc-20090605/mingw32make.bat b/libportfwd/third-party/miniupnpc-20090605/mingw32make.bat new file mode 100644 index 000000000..3e59be59b --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/mingw32make.bat @@ -0,0 +1,7 @@ +@mingw32-make -f Makefile.mingw %1 +@if errorlevel 1 goto end +@strip upnpc-static.exe +@upx --best upnpc-static.exe +@strip upnpc-shared.exe +@upx --best upnpc-shared.exe +:end diff --git a/libportfwd/third-party/miniupnpc-20090605/minisoap.c b/libportfwd/third-party/miniupnpc-20090605/minisoap.c new file mode 100644 index 000000000..04b1a92b8 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/minisoap.c @@ -0,0 +1,113 @@ +/* $Id: minisoap.c,v 1.16 2008/10/11 16:39:29 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas Bernard + * Copyright (c) 2005 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * + * Minimal SOAP implementation for UPnP protocol. + */ +#include +#include +#ifdef WIN32 +#include +#include +#define snprintf _snprintf +#else +#include +#include +#include +#endif +#include "minisoap.h" +#include "miniupnpcstrings.h" + +/* only for malloc */ +#include + +#ifdef WIN32 +#define PRINT_SOCKET_ERROR(x) printf("Socket error: %s, %d\n", x, WSAGetLastError()); +#else +#define PRINT_SOCKET_ERROR(x) perror(x) +#endif + +/* httpWrite sends the headers and the body to the socket + * and returns the number of bytes sent */ +static int +httpWrite(int fd, const char * body, int bodysize, + const char * headers, int headerssize) +{ + int n = 0; + /*n = write(fd, headers, headerssize);*/ + /*if(bodysize>0) + n += write(fd, body, bodysize);*/ + /* Note : my old linksys router only took into account + * soap request that are sent into only one packet */ + char * p; + /* TODO: AVOID MALLOC */ + p = malloc(headerssize+bodysize); + if(!p) + return 0; + memcpy(p, headers, headerssize); + memcpy(p+headerssize, body, bodysize); + /*n = write(fd, p, headerssize+bodysize);*/ + n = send(fd, p, headerssize+bodysize, 0); + if(n<0) { + PRINT_SOCKET_ERROR("send"); + } + /* disable send on the socket */ + /* draytek routers dont seems to like that... */ +#if 0 +#ifdef WIN32 + if(shutdown(fd, SD_SEND)<0) { +#else + if(shutdown(fd, SHUT_WR)<0) { /*SD_SEND*/ +#endif + PRINT_SOCKET_ERROR("shutdown"); + } +#endif + free(p); + return n; +} + +/* self explanatory */ +int soapPostSubmit(int fd, + const char * url, + const char * host, + unsigned short port, + const char * action, + const char * body) +{ + int bodysize; + char headerbuf[512]; + int headerssize; + char portstr[8]; + bodysize = (int)strlen(body); + /* We are not using keep-alive HTTP connections. + * HTTP/1.1 needs the header Connection: close to do that. + * This is the default with HTTP/1.0 */ + /* Connection: Close is normally there only in HTTP/1.1 but who knows */ + portstr[0] = '\0'; + if(port != 80) + snprintf(portstr, sizeof(portstr), ":%hu", port); + headerssize = snprintf(headerbuf, sizeof(headerbuf), + "POST %s HTTP/1.1\r\n" +/* "POST %s HTTP/1.0\r\n"*/ + "Host: %s%s\r\n" + "User-Agent: " OS_STRING ", UPnP/1.0, MiniUPnPc/" MINIUPNPC_VERSION_STRING "\r\n" + "Content-Length: %d\r\n" + "Content-Type: text/xml\r\n" + "SOAPAction: \"%s\"\r\n" + "Connection: Close\r\n" + "Cache-Control: no-cache\r\n" /* ??? */ + "Pragma: no-cache\r\n" + "\r\n", + url, host, portstr, bodysize, action); +#ifdef DEBUG + printf("SOAP request : headersize=%d bodysize=%d\n", + headerssize, bodysize); + /*printf("%s", headerbuf);*/ +#endif + return httpWrite(fd, body, bodysize, headerbuf, headerssize); +} + + diff --git a/libportfwd/third-party/miniupnpc-20090605/minisoap.h b/libportfwd/third-party/miniupnpc-20090605/minisoap.h new file mode 100644 index 000000000..9fa297fd3 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/minisoap.h @@ -0,0 +1,15 @@ +/* $Id: minisoap.h,v 1.3 2006/11/19 22:32:34 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas Bernard + * Copyright (c) 2005 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. */ +#ifndef __MINISOAP_H__ +#define __MINISOAP_H__ + +/*int httpWrite(int, const char *, int, const char *);*/ +int soapPostSubmit(int, const char *, const char *, unsigned short, + const char *, const char *); + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/minissdpc.c b/libportfwd/third-party/miniupnpc-20090605/minissdpc.c new file mode 100644 index 000000000..557b65acf --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/minissdpc.c @@ -0,0 +1,115 @@ +/* $Id: minissdpc.c,v 1.7 2008/12/18 17:45:48 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas BERNARD + * copyright (c) 2005-2008 Thomas Bernard + * This software is subjet to the conditions detailed in the + * provided LICENCE file. */ +/*#include */ +#include +#include +#include +#include +#include +#ifdef WIN32 +#include +#include +#include +/* Hack */ +#define UNIX_PATH_LEN 108 +struct sockaddr_un { + uint16_t sun_family; + char sun_path[UNIX_PATH_LEN]; +}; +#else +#include +#include +#endif + +#include "minissdpc.h" +#include "miniupnpc.h" + +#include "codelength.h" + +struct UPNPDev * +getDevicesFromMiniSSDPD(const char * devtype, const char * socketpath) +{ + struct UPNPDev * tmp; + struct UPNPDev * devlist = NULL; + unsigned char buffer[2048]; + ssize_t n; + unsigned char * p; + unsigned char * url; + unsigned int i; + unsigned int urlsize, stsize, usnsize, l; + int s; + struct sockaddr_un addr; + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if(s < 0) + { + /*syslog(LOG_ERR, "socket(unix): %m");*/ + perror("socket(unix)"); + return NULL; + } + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socketpath, sizeof(addr.sun_path)); + if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) + { + /*syslog(LOG_WARNING, "connect(\"%s\"): %m", socketpath);*/ + close(s); + return NULL; + } + stsize = strlen(devtype); + buffer[0] = 1; /* request type 1 : request devices/services by type */ + p = buffer + 1; + l = stsize; CODELENGTH(l, p); + memcpy(p, devtype, stsize); + p += stsize; + if(write(s, buffer, p - buffer) < 0) + { + /*syslog(LOG_ERR, "write(): %m");*/ + perror("minissdpc.c: write()"); + close(s); + return NULL; + } + n = read(s, buffer, sizeof(buffer)); + if(n<=0) + { + perror("minissdpc.c: read()"); + close(s); + return NULL; + } + p = buffer + 1; + for(i = 0; i < buffer[0]; i++) + { + if(p+2>=buffer+sizeof(buffer)) + break; + DECODELENGTH(urlsize, p); + if(p+urlsize+2>=buffer+sizeof(buffer)) + break; + url = p; + p += urlsize; + DECODELENGTH(stsize, p); + if(p+stsize+2>=buffer+sizeof(buffer)) + break; + tmp = (struct UPNPDev *)malloc(sizeof(struct UPNPDev)+urlsize+stsize); + tmp->pNext = devlist; + tmp->descURL = tmp->buffer; + tmp->st = tmp->buffer + 1 + urlsize; + memcpy(tmp->buffer, url, urlsize); + tmp->buffer[urlsize] = '\0'; + memcpy(tmp->buffer + urlsize + 1, p, stsize); + p += stsize; + tmp->buffer[urlsize+1+stsize] = '\0'; + devlist = tmp; + /* added for compatibility with recent versions of MiniSSDPd + * >= 2007/12/19 */ + DECODELENGTH(usnsize, p); + p += usnsize; + if(p>buffer + sizeof(buffer)) + break; + } + close(s); + return devlist; +} + diff --git a/libportfwd/third-party/miniupnpc-20090605/minissdpc.h b/libportfwd/third-party/miniupnpc-20090605/minissdpc.h new file mode 100644 index 000000000..25e91ce31 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/minissdpc.h @@ -0,0 +1,15 @@ +/* $Id: minissdpc.h,v 1.1 2007/08/31 15:15:33 nanard Exp $ */ +/* Project: miniupnp + * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * Author: Thomas Bernard + * Copyright (c) 2005-2007 Thomas Bernard + * This software is subjects to the conditions detailed + * in the LICENCE file provided within this distribution */ +#ifndef __MINISSDPC_H__ +#define __MINISSDPC_H__ + +struct UPNPDev * +getDevicesFromMiniSSDPD(const char * devtype, const char * socketpath); + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/miniupnpc.c b/libportfwd/third-party/miniupnpc-20090605/miniupnpc.c new file mode 100644 index 000000000..3794d2b07 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/miniupnpc.c @@ -0,0 +1,748 @@ +/* $Id: miniupnpc.c,v 1.57 2008/12/18 17:46:36 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas BERNARD + * copyright (c) 2005-2007 Thomas Bernard + * This software is subjet to the conditions detailed in the + * provided LICENCE file. */ +#include +#include +#include +#ifdef WIN32 +/* Win32 Specific includes and defines */ +#include +#include +#include +#define snprintf _snprintf +#if defined(_MSC_VER) && (_MSC_VER >= 1400) +#define strncasecmp _memicmp +#else +#define strncasecmp memicmp +#endif +#define MAXHOSTNAMELEN 64 +#else +/* Standard POSIX includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#define closesocket close +#endif +#include "miniupnpc.h" +#include "minissdpc.h" +#include "miniwget.h" +#include "minisoap.h" +#include "minixml.h" +#include "upnpcommands.h" + +#ifdef WIN32 +#define PRINT_SOCKET_ERROR(x) printf("Socket error: %s, %d\n", x, WSAGetLastError()); +#else +#define PRINT_SOCKET_ERROR(x) perror(x) +#endif + +#define SOAPPREFIX "s" +#define SERVICEPREFIX "u" +#define SERVICEPREFIX2 'u' + +/* root description parsing */ +void parserootdesc(const char * buffer, int bufsize, struct IGDdatas * data) +{ + struct xmlparser parser; + /* xmlparser object */ + parser.xmlstart = buffer; + parser.xmlsize = bufsize; + parser.data = data; + parser.starteltfunc = IGDstartelt; + parser.endeltfunc = IGDendelt; + parser.datafunc = IGDdata; + parser.attfunc = 0; + parsexml(&parser); +#ifdef DEBUG + printIGD(data); +#endif +} + +/* Content-length: nnn */ +static int getcontentlenfromline(const char * p, int n) +{ + static const char contlenstr[] = "content-length"; + const char * p2 = contlenstr; + int a = 0; + while(*p2) + { + if(n==0) + return -1; + if(*p2 != *p && *p2 != (*p + 32)) + return -1; + p++; p2++; n--; + } + if(n==0) + return -1; + if(*p != ':') + return -1; + p++; n--; + while(*p == ' ') + { + if(n==0) + return -1; + p++; n--; + } + while(*p >= '0' && *p <= '9') + { + if(n==0) + return -1; + a = (a * 10) + (*p - '0'); + p++; n--; + } + return a; +} + +static void +getContentLengthAndHeaderLength(char * p, int n, + int * contentlen, int * headerlen) +{ + char * line; + int linelen; + int r; + line = p; + while(line < p + n) + { + linelen = 0; + while(line[linelen] != '\r' && line[linelen] != '\r') + { + if(line+linelen >= p+n) + return; + linelen++; + } + r = getcontentlenfromline(line, linelen); + if(r>0) + *contentlen = r; + line = line + linelen + 2; + if(line[0] == '\r' && line[1] == '\n') + { + *headerlen = (line - p) + 2; + return; + } + } +} + +/* simpleUPnPcommand : + * not so simple ! + * return values : + * 0 - OK + * -1 - error */ +int simpleUPnPcommand(int s, const char * url, const char * service, + const char * action, struct UPNParg * args, + char * buffer, int * bufsize) +{ + if(!url) return -1; + struct sockaddr_in dest; + char hostname[MAXHOSTNAMELEN+1]; + unsigned short port = 0; + char * path; + char soapact[128]; + char soapbody[2048]; + char * buf; + int buffree; + int n; + int contentlen, headerlen; /* for the response */ + snprintf(soapact, sizeof(soapact), "%s#%s", service, action); + if(args==NULL) + { + /*soapbodylen = */snprintf(soapbody, sizeof(soapbody), + "\r\n" + "<" SOAPPREFIX ":Envelope " + "xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" " + SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<" SOAPPREFIX ":Body>" + "<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">" + "" + "" + "\r\n", action, service, action); + } + else + { + char * p; + const char * pe, * pv; + int soapbodylen; + soapbodylen = snprintf(soapbody, sizeof(soapbody), + "\r\n" + "<" SOAPPREFIX ":Envelope " + "xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" " + SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<" SOAPPREFIX ":Body>" + "<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">", + action, service); + p = soapbody + soapbodylen; + while(args->elt) + { + /* check that we are never overflowing the string... */ + if(soapbody + sizeof(soapbody) <= p + 100) + { + /* we keep a margin of at least 100 bytes */ + *bufsize = 0; + return -1; + } + *(p++) = '<'; + pe = args->elt; + while(*pe) + *(p++) = *(pe++); + *(p++) = '>'; + if((pv = args->val)) + { + while(*pv) + *(p++) = *(pv++); + } + *(p++) = '<'; + *(p++) = '/'; + pe = args->elt; + while(*pe) + *(p++) = *(pe++); + *(p++) = '>'; + args++; + } + *(p++) = '<'; + *(p++) = '/'; + *(p++) = SERVICEPREFIX2; + *(p++) = ':'; + pe = action; + while(*pe) + *(p++) = *(pe++); + strncpy(p, ">\r\n", + soapbody + sizeof(soapbody) - p); + } + if(!parseURL(url, hostname, &port, &path)) return -1; + if(s<0) + { + s = socket(PF_INET, SOCK_STREAM, 0); + if(s<0) + { + PRINT_SOCKET_ERROR("socket"); + *bufsize = 0; + return -1; + } + dest.sin_family = AF_INET; + dest.sin_port = htons(port); + dest.sin_addr.s_addr = inet_addr(hostname); + if(connect(s, (struct sockaddr *)&dest, sizeof(struct sockaddr))<0) + { + PRINT_SOCKET_ERROR("connect"); + closesocket(s); + *bufsize = 0; + return -1; + } + } + + n = soapPostSubmit(s, path, hostname, port, soapact, soapbody); + if(n<=0) { +#ifdef DEBUG + printf("Error sending SOAP request\n"); +#endif + closesocket(s); + return -1; + } + + contentlen = -1; + headerlen = -1; + buf = buffer; + buffree = *bufsize; + *bufsize = 0; + while ((n = ReceiveData(s, buf, buffree, 5000)) > 0) { + buffree -= n; + buf += n; + *bufsize += n; + getContentLengthAndHeaderLength(buffer, *bufsize, + &contentlen, &headerlen); +#ifdef DEBUG + printf("received n=%dbytes bufsize=%d ContLen=%d HeadLen=%d\n", + n, *bufsize, contentlen, headerlen); +#endif + /* break if we received everything */ + if(contentlen > 0 && headerlen > 0 && *bufsize >= contentlen+headerlen) + break; + } + + closesocket(s); + return 0; +} + +/* parseMSEARCHReply() + * the last 4 arguments are filled during the parsing : + * - location/locationsize : "location:" field of the SSDP reply packet + * - st/stsize : "st:" field of the SSDP reply packet. + * The strings are NOT null terminated */ +static void +parseMSEARCHReply(const char * reply, int size, + const char * * location, int * locationsize, + const char * * st, int * stsize) +{ + int a, b, i; + i = 0; + a = i; /* start of the line */ + b = 0; + while(ipNext = devlist; + tmp->descURL = tmp->buffer; + tmp->st = tmp->buffer + 1 + urlsize; + memcpy(tmp->buffer, descURL, urlsize); + tmp->buffer[urlsize] = '\0'; + memcpy(tmp->buffer + urlsize + 1, st, stsize); + tmp->buffer[urlsize+1+stsize] = '\0'; + devlist = tmp; + } + } + } +} + +/* freeUPNPDevlist() should be used to + * free the chained list returned by upnpDiscover() */ +void freeUPNPDevlist(struct UPNPDev * devlist) +{ + struct UPNPDev * next; + while(devlist) + { + next = devlist->pNext; + free(devlist); + devlist = next; + } +} + +static void +url_cpy_or_cat(char * dst, const char * src, int n) +{ + if( (src[0] == 'h') + &&(src[1] == 't') + &&(src[2] == 't') + &&(src[3] == 'p') + &&(src[4] == ':') + &&(src[5] == '/') + &&(src[6] == '/')) + { + strncpy(dst, src, n); + } + else + { + int l = strlen(dst); + if(src[0] != '/') + dst[l++] = '/'; + if(l<=n) + strncpy(dst + l, src, n - l); + } +} + +/* Prepare the Urls for usage... + */ +void GetUPNPUrls(struct UPNPUrls * urls, struct IGDdatas * data, + const char * descURL) +{ + char * p; + int n1, n2, n3; + n1 = strlen(data->urlbase); + if(n1==0) + n1 = strlen(descURL); + n1 += 2; /* 1 byte more for Null terminator, 1 byte for '/' if needed */ + n2 = n1; n3 = n1; + n1 += strlen(data->scpdurl); + n2 += strlen(data->controlurl); + n3 += strlen(data->controlurl_CIF); + + urls->ipcondescURL = (char *)malloc(n1); + urls->controlURL = (char *)malloc(n2); + urls->controlURL_CIF = (char *)malloc(n3); + /* maintenant on chope la desc du WANIPConnection */ + if(data->urlbase[0] != '\0') + strncpy(urls->ipcondescURL, data->urlbase, n1); + else + strncpy(urls->ipcondescURL, descURL, n1); + p = strchr(urls->ipcondescURL+7, '/'); + if(p) p[0] = '\0'; + strncpy(urls->controlURL, urls->ipcondescURL, n2); + strncpy(urls->controlURL_CIF, urls->ipcondescURL, n3); + + url_cpy_or_cat(urls->ipcondescURL, data->scpdurl, n1); + + url_cpy_or_cat(urls->controlURL, data->controlurl, n2); + + url_cpy_or_cat(urls->controlURL_CIF, data->controlurl_CIF, n3); + +#ifdef DEBUG + printf("urls->ipcondescURL='%s' %d n1=%d\n", urls->ipcondescURL, + strlen(urls->ipcondescURL), n1); + printf("urls->controlURL='%s' %d n2=%d\n", urls->controlURL, + strlen(urls->controlURL), n2); + printf("urls->controlURL_CIF='%s' %d n3=%d\n", urls->controlURL_CIF, + strlen(urls->controlURL_CIF), n3); +#endif +} + +void +FreeUPNPUrls(struct UPNPUrls * urls) +{ + if(!urls) + return; + free(urls->controlURL); + urls->controlURL = 0; + free(urls->ipcondescURL); + urls->ipcondescURL = 0; + free(urls->controlURL_CIF); + urls->controlURL_CIF = 0; +} + + +int ReceiveData(int socket, char * data, int length, int timeout) +{ + int n; +#ifndef WIN32 + struct pollfd fds[1]; /* for the poll */ + fds[0].fd = socket; + fds[0].events = POLLIN; + n = poll(fds, 1, timeout); + if(n < 0) + { + PRINT_SOCKET_ERROR("poll"); + return -1; + } + else if(n == 0) + { + return 0; + } +#else + fd_set socketSet; + TIMEVAL timeval; + FD_ZERO(&socketSet); + FD_SET(socket, &socketSet); + timeval.tv_sec = timeout / 1000; + timeval.tv_usec = (timeout % 1000) * 1000; + /*n = select(0, &socketSet, NULL, NULL, &timeval);*/ + n = select(FD_SETSIZE, &socketSet, NULL, NULL, &timeval); + if(n < 0) + { + PRINT_SOCKET_ERROR("select"); + return -1; + } + else if(n == 0) + { + return 0; + } +#endif + n = recv(socket, data, length, 0); + if(n<0) + { + PRINT_SOCKET_ERROR("recv"); + } + return n; +} + +int +UPNPIGD_IsConnected(struct UPNPUrls * urls, struct IGDdatas * data) +{ + char status[64]; + unsigned int uptime; + status[0] = '\0'; + UPNP_GetStatusInfo(urls->controlURL, data->servicetype, + status, &uptime, NULL); + if(0 == strcmp("Connected", status)) + { + return 1; + } + else + return 0; +} + + +/* UPNP_GetValidIGD() : + * return values : + * 0 = NO IGD found + * 1 = A valid connected IGD has been found + * 2 = A valid IGD has been found but it reported as + * not connected + * 3 = an UPnP device has been found but was not recognized as an IGD + * + * In any non zero return case, the urls and data structures + * passed as parameters are set. Donc forget to call FreeUPNPUrls(urls) to + * free allocated memory. + */ +int +UPNP_GetValidIGD(struct UPNPDev * devlist, + struct UPNPUrls * urls, + struct IGDdatas * data, + char * lanaddr, int lanaddrlen) +{ + char * descXML; + int descXMLsize = 0; + struct UPNPDev * dev; + int ndev = 0; + int state; /* state 1 : IGD connected. State 2 : IGD. State 3 : anything */ + if(!devlist) + { +#ifdef DEBUG + printf("Empty devlist\n"); +#endif + return 0; + } + for(state = 1; state <= 3; state++) + { + for(dev = devlist; dev; dev = dev->pNext) + { + /* we should choose an internet gateway device. + * with st == urn:schemas-upnp-org:device:InternetGatewayDevice:1 */ + descXML = miniwget_getaddr(dev->descURL, &descXMLsize, + lanaddr, lanaddrlen); + if(descXML) + { + ndev++; + memset(data, 0, sizeof(struct IGDdatas)); + memset(urls, 0, sizeof(struct UPNPUrls)); + parserootdesc(descXML, descXMLsize, data); + free(descXML); + descXML = NULL; + if(0==strcmp(data->servicetype_CIF, + "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1") + || state >= 3 ) + { + GetUPNPUrls(urls, data, dev->descURL); + +#ifdef DEBUG + printf("UPNPIGD_IsConnected(%s) = %d\n", + urls->controlURL, + UPNPIGD_IsConnected(urls, data)); +#endif + if((state >= 2) || UPNPIGD_IsConnected(urls, data)) + return state; + FreeUPNPUrls(urls); + } + memset(data, 0, sizeof(struct IGDdatas)); + } +#ifdef DEBUG + else + { + printf("error getting XML description %s\n", dev->descURL); + } +#endif + } + } + return 0; +} + +/* UPNP_GetIGDFromUrl() + * Used when skipping the discovery process. + * return value : + * 0 - Not ok + * 1 - OK */ +int +UPNP_GetIGDFromUrl(const char * rootdescurl, + struct UPNPUrls * urls, + struct IGDdatas * data, + char * lanaddr, int lanaddrlen) +{ + char * descXML; + int descXMLsize = 0; + descXML = miniwget_getaddr(rootdescurl, &descXMLsize, + lanaddr, lanaddrlen); + if(descXML) { + memset(data, 0, sizeof(struct IGDdatas)); + memset(urls, 0, sizeof(struct UPNPUrls)); + parserootdesc(descXML, descXMLsize, data); + free(descXML); + descXML = NULL; + GetUPNPUrls(urls, data, rootdescurl); + return 1; + } else { + return 0; + } +} + diff --git a/libportfwd/third-party/miniupnpc-20090605/miniupnpc.def b/libportfwd/third-party/miniupnpc-20090605/miniupnpc.def new file mode 100644 index 000000000..21e66a4c3 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/miniupnpc.def @@ -0,0 +1,31 @@ +LIBRARY +; miniupnpc library + +EXPORTS +; miniupnpc + upnpDiscover + freeUPNPDevlist + parserootdesc + UPNP_GetValidIGD + UPNP_GetIGDFromUrl + GetUPNPUrls + FreeUPNPUrls +; miniwget + miniwget + miniwget_getaddr +; upnpcommands + UPNP_GetTotalBytesSent + UPNP_GetTotalBytesReceived + UPNP_GetTotalPacketsSent + UPNP_GetTotalPacketsReceived + UPNP_GetStatusInfo + UPNP_GetConnectionTypeInfo + UPNP_GetExternalIPAddress + UPNP_GetLinkLayerMaxBitRates + UPNP_AddPortMapping + UPNP_DeletePortMapping + UPNP_GetPortMappingNumberOfEntries + UPNP_GetSpecificPortMappingEntry + UPNP_GetGenericPortMappingEntry +; upnperrors + strupnperror \ No newline at end of file diff --git a/libportfwd/third-party/miniupnpc-20090605/miniupnpc.h b/libportfwd/third-party/miniupnpc-20090605/miniupnpc.h new file mode 100644 index 000000000..bce314e82 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/miniupnpc.h @@ -0,0 +1,110 @@ +/* $Id: miniupnpc.h,v 1.18 2008/09/25 18:02:50 nanard Exp $ */ +/* Project: miniupnp + * http://miniupnp.free.fr/ + * Author: Thomas Bernard + * Copyright (c) 2005-2006 Thomas Bernard + * This software is subjects to the conditions detailed + * in the LICENCE file provided within this distribution */ +#ifndef __MINIUPNPC_H__ +#define __MINIUPNPC_H__ + +#include "declspec.h" +#include "igd_desc_parse.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Structures definitions : */ +struct UPNParg { const char * elt; const char * val; }; + +int simpleUPnPcommand(int, const char *, const char *, + const char *, struct UPNParg *, + char *, int *); + +struct UPNPDev { + struct UPNPDev * pNext; + char * descURL; + char * st; + char buffer[2]; +}; + +/* upnpDiscover() + * discover UPnP devices on the network. + * The discovered devices are returned as a chained list. + * It is up to the caller to free the list with freeUPNPDevlist(). + * delay (in millisecond) is the maximum time for waiting any device + * response. + * If available, device list will be obtained from MiniSSDPd. + * Default path for minissdpd socket will be used if minissdpdsock argument + * is NULL. + * If multicastif is not NULL, it will be used instead of the default + * multicast interface for sending SSDP discover packets. + * If sameport is not null, SSDP packets will be sent from the source port + * 1900 (same as destination port) otherwise system assign a source port. */ +LIBSPEC struct UPNPDev * upnpDiscover(int delay, const char * multicastif, + const char * minissdpdsock, int sameport); +/* freeUPNPDevlist() + * free list returned by upnpDiscover() */ +LIBSPEC void freeUPNPDevlist(struct UPNPDev * devlist); + +/* parserootdesc() : + * parse root XML description of a UPnP device and fill the IGDdatas + * structure. */ +LIBSPEC void parserootdesc(const char *, int, struct IGDdatas *); + +/* structure used to get fast access to urls + * controlURL: controlURL of the WANIPConnection + * ipcondescURL: url of the description of the WANIPConnection + * controlURL_CIF: controlURL of the WANCommonInterfaceConfig + */ +struct UPNPUrls { + char * controlURL; + char * ipcondescURL; + char * controlURL_CIF; +}; + +/* UPNP_GetValidIGD() : + * return values : + * 0 = NO IGD found + * 1 = A valid connected IGD has been found + * 2 = A valid IGD has been found but it reported as + * not connected + * 3 = an UPnP device has been found but was not recognized as an IGD + * + * In any non zero return case, the urls and data structures + * passed as parameters are set. Donc forget to call FreeUPNPUrls(urls) to + * free allocated memory. + */ +LIBSPEC int +UPNP_GetValidIGD(struct UPNPDev * devlist, + struct UPNPUrls * urls, + struct IGDdatas * data, + char * lanaddr, int lanaddrlen); + +/* UPNP_GetIGDFromUrl() + * Used when skipping the discovery process. + * return value : + * 0 - Not ok + * 1 - OK */ +LIBSPEC int +UPNP_GetIGDFromUrl(const char * rootdescurl, + struct UPNPUrls * urls, + struct IGDdatas * data, + char * lanaddr, int lanaddrlen); + +LIBSPEC void GetUPNPUrls(struct UPNPUrls *, struct IGDdatas *, const char *); + +LIBSPEC void FreeUPNPUrls(struct UPNPUrls *); + +/* Reads data from the specified socket. + * Returns the number of bytes read if successful, zero if no bytes were + * read or if we timed out. Returns negative if there was an error. */ +int ReceiveData(int socket, char * data, int length, int timeout); + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/miniupnpcmodule.c b/libportfwd/third-party/miniupnpc-20090605/miniupnpcmodule.c new file mode 100644 index 000000000..121ff2052 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/miniupnpcmodule.c @@ -0,0 +1,460 @@ +/* $Id: miniupnpcmodule.c,v 1.13 2009/04/17 20:59:42 nanard Exp $*/ +/* Project : miniupnp + * Author : Thomas BERNARD + * website : http://miniupnp.tuxfamily.org/ + * copyright (c) 2007 Thomas Bernard + * This software is subjet to the conditions detailed in the + * provided LICENCE file. */ +#include +#define STATICLIB +#include "structmember.h" +#include "miniupnpc.h" +#include "upnpcommands.h" +#include "upnperrors.h" + +/* for compatibility with Python < 2.4 */ +#ifndef Py_RETURN_NONE +#define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None +#endif + +#ifndef Py_RETURN_TRUE +#define Py_RETURN_TRUE return Py_INCREF(Py_True), Py_True +#endif + +#ifndef Py_RETURN_FALSE +#define Py_RETURN_FALSE return Py_INCREF(Py_False), Py_False +#endif + +typedef struct { + PyObject_HEAD + /* Type-specific fields go here. */ + struct UPNPDev * devlist; + struct UPNPUrls urls; + struct IGDdatas data; + unsigned int discoverdelay; /* value passed to upnpDiscover() */ + char lanaddr[16]; /* our ip address on the LAN */ + char * multicastif; + char * minissdpdsocket; +} UPnPObject; + +static PyMemberDef UPnP_members[] = { + {"lanaddr", T_STRING_INPLACE, offsetof(UPnPObject, lanaddr), + READONLY, "ip address on the LAN" + }, + {"discoverdelay", T_UINT, offsetof(UPnPObject, discoverdelay), + 0/*READWRITE*/, "value in ms used to wait for SSDP responses" + }, + /* T_STRING is allways readonly :( */ + {"multicastif", T_STRING, offsetof(UPnPObject, multicastif), + 0, "IP of the network interface to be used for multicast operations" + }, + {"minissdpdsocket", T_STRING, offsetof(UPnPObject, multicastif), + 0, "path of the MiniSSDPd unix socket" + }, + {NULL} +}; + +static void +UPnPObject_dealloc(UPnPObject *self) +{ + freeUPNPDevlist(self->devlist); + FreeUPNPUrls(&self->urls); + self->ob_type->tp_free((PyObject*)self); +} + +static PyObject * +UPnP_discover(UPnPObject *self) +{ + struct UPNPDev * dev; + int i; + PyObject *res = NULL; + if(self->devlist) + { + freeUPNPDevlist(self->devlist); + self->devlist = 0; + } + self->devlist = upnpDiscover((int)self->discoverdelay/*timeout in ms*/, + 0/* multicast if*/, + 0/*minissdpd socket*/, + 0/*sameport flag*/); + /* Py_RETURN_NONE ??? */ + for(dev = self->devlist, i = 0; dev; dev = dev->pNext) + i++; + res = Py_BuildValue("i", i); + return res; +} + +static PyObject * +UPnP_selectigd(UPnPObject *self) +{ + if(UPNP_GetValidIGD(self->devlist, &self->urls, &self->data, + self->lanaddr, sizeof(self->lanaddr))) + { + return Py_BuildValue("s", self->urls.controlURL); + } + else + { + /* TODO: have our own exception type ! */ + PyErr_SetString(PyExc_Exception, "No UPnP device discovered"); + return NULL; + } +} + +static PyObject * +UPnP_totalbytesent(UPnPObject *self) +{ + return Py_BuildValue("I", + UPNP_GetTotalBytesSent(self->urls.controlURL_CIF, + self->data.servicetype_CIF)); +} + +static PyObject * +UPnP_totalbytereceived(UPnPObject *self) +{ + return Py_BuildValue("I", + UPNP_GetTotalBytesReceived(self->urls.controlURL_CIF, + self->data.servicetype_CIF)); +} + +static PyObject * +UPnP_totalpacketsent(UPnPObject *self) +{ + return Py_BuildValue("I", + UPNP_GetTotalPacketsSent(self->urls.controlURL_CIF, + self->data.servicetype_CIF)); +} + +static PyObject * +UPnP_totalpacketreceived(UPnPObject *self) +{ + return Py_BuildValue("I", + UPNP_GetTotalPacketsReceived(self->urls.controlURL_CIF, + self->data.servicetype_CIF)); +} + +static PyObject * +UPnP_statusinfo(UPnPObject *self) +{ + char status[64]; + char lastconnerror[64]; + unsigned int uptime = 0; + int r; + status[0] = '\0'; + lastconnerror[0] = '\0'; + r = UPNP_GetStatusInfo(self->urls.controlURL, self->data.servicetype, + status, &uptime, lastconnerror); + if(r==UPNPCOMMAND_SUCCESS) { + return Py_BuildValue("(s,I,s)", status, uptime, lastconnerror); + } else { + /* TODO: have our own exception type ! */ + PyErr_SetString(PyExc_Exception, strupnperror(r)); + return NULL; + } +} + +static PyObject * +UPnP_connectiontype(UPnPObject *self) +{ + char connectionType[64]; + int r; + connectionType[0] = '\0'; + r = UPNP_GetConnectionTypeInfo(self->urls.controlURL, + self->data.servicetype, + connectionType); + if(r==UPNPCOMMAND_SUCCESS) { + return Py_BuildValue("s", connectionType); + } else { + /* TODO: have our own exception type ! */ + PyErr_SetString(PyExc_Exception, strupnperror(r)); + return NULL; + } +} + +static PyObject * +UPnP_externalipaddress(UPnPObject *self) +{ + char externalIPAddress[16]; + int r; + externalIPAddress[0] = '\0'; + r = UPNP_GetExternalIPAddress(self->urls.controlURL, + self->data.servicetype, + externalIPAddress); + if(r==UPNPCOMMAND_SUCCESS) { + return Py_BuildValue("s", externalIPAddress); + } else { + /* TODO: have our own exception type ! */ + PyErr_SetString(PyExc_Exception, strupnperror(r)); + return NULL; + } +} + +/* AddPortMapping(externalPort, protocol, internalHost, internalPort, desc, + * remoteHost) + * protocol is 'UDP' or 'TCP' */ +static PyObject * +UPnP_addportmapping(UPnPObject *self, PyObject *args) +{ + char extPort[6]; + unsigned short ePort; + char inPort[6]; + unsigned short iPort; + const char * proto; + const char * host; + const char * desc; + const char * remoteHost; + int r; + if (!PyArg_ParseTuple(args, "HssHss", &ePort, &proto, + &host, &iPort, &desc, &remoteHost)) + return NULL; + sprintf(extPort, "%hu", ePort); + sprintf(inPort, "%hu", iPort); + r = UPNP_AddPortMapping(self->urls.controlURL, self->data.servicetype, + extPort, inPort, host, desc, proto, remoteHost); + if(r==UPNPCOMMAND_SUCCESS) + { + Py_RETURN_TRUE; + } + else + { + // TODO: RAISE an Exception. See upnpcommands.h for errors codes. + // upnperrors.c + //Py_RETURN_FALSE; + /* TODO: have our own exception type ! */ + PyErr_SetString(PyExc_Exception, strupnperror(r)); + return NULL; + } +} + +/* DeletePortMapping(extPort, proto, removeHost='') + * proto = 'UDP', 'TCP' */ +static PyObject * +UPnP_deleteportmapping(UPnPObject *self, PyObject *args) +{ + char extPort[6]; + unsigned short ePort; + const char * proto; + const char * remoteHost = ""; + int r; + if(!PyArg_ParseTuple(args, "Hs|z", &ePort, &proto, &remoteHost)) + return NULL; + sprintf(extPort, "%hu", ePort); + r = UPNP_DeletePortMapping(self->urls.controlURL, self->data.servicetype, + extPort, proto, remoteHost); + if(r==UPNPCOMMAND_SUCCESS) { + Py_RETURN_TRUE; + } else { + /* TODO: have our own exception type ! */ + PyErr_SetString(PyExc_Exception, strupnperror(r)); + return NULL; + } +} + +static PyObject * +UPnP_getportmappingnumberofentries(UPnPObject *self) +{ + unsigned int n = 0; + int r; + r = UPNP_GetPortMappingNumberOfEntries(self->urls.controlURL, + self->data.servicetype, + &n); + if(r==UPNPCOMMAND_SUCCESS) { + return Py_BuildValue("I", n); + } else { + /* TODO: have our own exception type ! */ + PyErr_SetString(PyExc_Exception, strupnperror(r)); + return NULL; + } +} + +/* GetSpecificPortMapping(ePort, proto) + * proto = 'UDP' or 'TCP' */ +static PyObject * +UPnP_getspecificportmapping(UPnPObject *self, PyObject *args) +{ + char extPort[6]; + unsigned short ePort; + const char * proto; + char intClient[16]; + char intPort[6]; + unsigned short iPort; + if(!PyArg_ParseTuple(args, "Hs", &ePort, &proto)) + return NULL; + sprintf(extPort, "%hu", ePort); + UPNP_GetSpecificPortMappingEntry(self->urls.controlURL, + self->data.servicetype, + extPort, proto, + intClient, intPort); + if(intClient[0]) + { + iPort = (unsigned short)atoi(intPort); + return Py_BuildValue("(s,H)", intClient, iPort); + } + else + { + Py_RETURN_NONE; + } +} + +/* GetGenericPortMapping(index) */ +static PyObject * +UPnP_getgenericportmapping(UPnPObject *self, PyObject *args) +{ + int i, r; + char index[8]; + char intClient[16]; + char intPort[6]; + unsigned short iPort; + char extPort[6]; + unsigned short ePort; + char protocol[4]; + char desc[80]; + char enabled[6]; + char rHost[64]; + char duration[16]; /* lease duration */ + unsigned int dur; + if(!PyArg_ParseTuple(args, "i", &i)) + return NULL; + snprintf(index, sizeof(index), "%d", i); + rHost[0] = '\0'; enabled[0] = '\0'; + duration[0] = '\0'; desc[0] = '\0'; + extPort[0] = '\0'; intPort[0] = '\0'; intClient[0] = '\0'; + r = UPNP_GetGenericPortMappingEntry(self->urls.controlURL, + self->data.servicetype, + index, + extPort, intClient, intPort, + protocol, desc, enabled, rHost, + duration); + if(r==UPNPCOMMAND_SUCCESS) + { + ePort = (unsigned short)atoi(extPort); + iPort = (unsigned short)atoi(intPort); + dur = (unsigned int)strtoul(duration, 0, 0); + return Py_BuildValue("(H,s,(s,H),s,s,s,I)", + ePort, protocol, intClient, iPort, + desc, enabled, rHost, dur); + } + else + { + Py_RETURN_NONE; + } +} + +/* miniupnpc.UPnP object Method Table */ +static PyMethodDef UPnP_methods[] = { + {"discover", (PyCFunction)UPnP_discover, METH_NOARGS, + "discover UPnP IGD devices on the network" + }, + {"selectigd", (PyCFunction)UPnP_selectigd, METH_NOARGS, + "select a valid UPnP IGD among discovered devices" + }, + {"totalbytesent", (PyCFunction)UPnP_totalbytesent, METH_NOARGS, + "return the total number of bytes sent by UPnP IGD" + }, + {"totalbytereceived", (PyCFunction)UPnP_totalbytereceived, METH_NOARGS, + "return the total number of bytes received by UPnP IGD" + }, + {"totalpacketsent", (PyCFunction)UPnP_totalpacketsent, METH_NOARGS, + "return the total number of packets sent by UPnP IGD" + }, + {"totalpacketreceived", (PyCFunction)UPnP_totalpacketreceived, METH_NOARGS, + "return the total number of packets received by UPnP IGD" + }, + {"statusinfo", (PyCFunction)UPnP_statusinfo, METH_NOARGS, + "return status and uptime" + }, + {"connectiontype", (PyCFunction)UPnP_connectiontype, METH_NOARGS, + "return IGD WAN connection type" + }, + {"externalipaddress", (PyCFunction)UPnP_externalipaddress, METH_NOARGS, + "return external IP address" + }, + {"addportmapping", (PyCFunction)UPnP_addportmapping, METH_VARARGS, + "add a port mapping" + }, + {"deleteportmapping", (PyCFunction)UPnP_deleteportmapping, METH_VARARGS, + "delete a port mapping" + }, + {"getportmappingnumberofentries", (PyCFunction)UPnP_getportmappingnumberofentries, METH_NOARGS, + "-- non standard --" + }, + {"getspecificportmapping", (PyCFunction)UPnP_getspecificportmapping, METH_VARARGS, + "get details about a specific port mapping entry" + }, + {"getgenericportmapping", (PyCFunction)UPnP_getgenericportmapping, METH_VARARGS, + "get all details about the port mapping at index" + }, + {NULL} /* Sentinel */ +}; + +static PyTypeObject UPnPType = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "miniupnpc.UPnP", /*tp_name*/ + sizeof(UPnPObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)UPnPObject_dealloc,/*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + "UPnP objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + UPnP_methods, /* tp_methods */ + UPnP_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0,/*(initproc)UPnP_init,*/ /* tp_init */ + 0, /* tp_alloc */ +#ifndef WIN32 + PyType_GenericNew,/*UPnP_new,*/ /* tp_new */ +#else + 0, +#endif +}; + +/* module methods */ +static PyMethodDef miniupnpc_methods[] = { + {NULL} /* Sentinel */ +}; + +#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ +#define PyMODINIT_FUNC void +#endif +PyMODINIT_FUNC +initminiupnpc(void) +{ + PyObject* m; + +#ifdef WIN32 + UPnPType.tp_new = PyType_GenericNew; +#endif + if (PyType_Ready(&UPnPType) < 0) + return; + + m = Py_InitModule3("miniupnpc", miniupnpc_methods, + "miniupnpc module."); + + Py_INCREF(&UPnPType); + PyModule_AddObject(m, "UPnP", (PyObject *)&UPnPType); +} + diff --git a/libportfwd/third-party/miniupnpc-20090605/miniupnpcstrings.h b/libportfwd/third-party/miniupnpc-20090605/miniupnpcstrings.h new file mode 100644 index 000000000..8f1b6c26d --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/miniupnpcstrings.h @@ -0,0 +1,15 @@ +/* $Id: miniupnpcstrings.h,v 1.3 2009/06/04 09:05:56 nanard Exp $ */ +/* Project: miniupnp + * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * Author: Thomas Bernard + * Copyright (c) 2005-2009 Thomas Bernard + * This software is subjects to the conditions detailed + * in the LICENCE file provided within this distribution */ +#ifndef __MINIUPNPCSTRINGS_H__ +#define __MINIUPNPCSTRINGS_H__ + +#define OS_STRING "OpenBSD/4.3" +#define MINIUPNPC_VERSION_STRING "1.3" + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/miniwget.c b/libportfwd/third-party/miniupnpc-20090605/miniwget.c new file mode 100644 index 000000000..0a026cbb1 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/miniwget.c @@ -0,0 +1,222 @@ +/* $Id: miniwget.c,v 1.22 2009/02/28 10:36:35 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas Bernard + * Copyright (c) 2005 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * */ +#include +#include +#include +#include "miniupnpc.h" +#ifdef WIN32 +#include +#include +#define MAXHOSTNAMELEN 64 +#define MIN(x,y) (((x)<(y))?(x):(y)) +#define snprintf _snprintf +#define herror +#define socklen_t int +#else +#include +#include +#include +#include +#include +#include +#define closesocket close +#endif +#if defined(__sun) || defined(sun) +#define MIN(x,y) (((x)<(y))?(x):(y)) +#endif + +#include "miniupnpcstrings.h" + +/* miniwget2() : + * */ +static void * +miniwget2(const char * url, const char * host, + unsigned short port, const char * path, + int * size, char * addr_str, int addr_str_len) +{ + char buf[2048]; + int s; + struct sockaddr_in dest; + struct hostent *hp; + *size = 0; + hp = gethostbyname(host); + if(hp==NULL) + { + herror(host); + return NULL; + } + /* memcpy((char *)&dest.sin_addr, hp->h_addr, hp->h_length); */ + memcpy(&dest.sin_addr, hp->h_addr, sizeof(dest.sin_addr)); + memset(dest.sin_zero, 0, sizeof(dest.sin_zero)); + s = socket(PF_INET, SOCK_STREAM, 0); + if(s < 0) + { + perror("socket"); + return NULL; + } + dest.sin_family = AF_INET; + dest.sin_port = htons(port); + if(connect(s, (struct sockaddr *)&dest, sizeof(struct sockaddr_in))<0) + { + perror("connect"); + closesocket(s); + return NULL; + } + + /* get address for caller ! */ + if(addr_str) + { + struct sockaddr_in saddr; + socklen_t len; + + len = sizeof(saddr); + getsockname(s, (struct sockaddr *)&saddr, &len); +#ifndef WIN32 + inet_ntop(AF_INET, &saddr.sin_addr, addr_str, addr_str_len); +#else + /* using INT WINAPI WSAAddressToStringA(LPSOCKADDR, DWORD, LPWSAPROTOCOL_INFOA, LPSTR, LPDWORD); + * But his function make a string with the port : nn.nn.nn.nn:port */ +/* if(WSAAddressToStringA((SOCKADDR *)&saddr, sizeof(saddr), + NULL, addr_str, (DWORD *)&addr_str_len)) + { + printf("WSAAddressToStringA() failed : %d\n", WSAGetLastError()); + }*/ + strncpy(addr_str, inet_ntoa(saddr.sin_addr), addr_str_len); +#endif +#ifdef DEBUG + printf("address miniwget : %s\n", addr_str); +#endif + } + + snprintf(buf, sizeof(buf), + "GET %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: Close\r\n" + "User-Agent: " OS_STRING ", UPnP/1.0, MiniUPnPc/" MINIUPNPC_VERSION_STRING "\r\n" + + "\r\n", + path, host, port); + /*write(s, buf, strlen(buf));*/ + send(s, buf, strlen(buf), 0); + { + int n, headers=1; + char * respbuffer = NULL; + int allreadyread = 0; + /*while((n = recv(s, buf, 2048, 0)) > 0)*/ + while((n = ReceiveData(s, buf, 2048, 5000)) > 0) + { + if(headers) + { + int i=0; + while(ip3)) + { + strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p3-p1))); + *port = 80; + } + else + { + strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p2-p1))); + *port = 0; + p2++; + while( (*p2 >= '0') && (*p2 <= '9')) + { + *port *= 10; + *port += (unsigned short)(*p2 - '0'); + p2++; + } + } + *path = p3; + return 1; +} + +void * miniwget(const char * url, int * size) +{ + unsigned short port; + char * path; + /* protocol://host:port/chemin */ + char hostname[MAXHOSTNAMELEN+1]; + *size = 0; + if(!parseURL(url, hostname, &port, &path)) + return NULL; + return miniwget2(url, hostname, port, path, size, 0, 0); +} + +void * miniwget_getaddr(const char * url, int * size, char * addr, int addrlen) +{ + unsigned short port; + char * path; + /* protocol://host:port/chemin */ + char hostname[MAXHOSTNAMELEN+1]; + *size = 0; + if(addr) + addr[0] = '\0'; + if(!parseURL(url, hostname, &port, &path)) + return NULL; + return miniwget2(url, hostname, port, path, size, addr, addrlen); +} + diff --git a/libportfwd/third-party/miniupnpc-20090605/miniwget.h b/libportfwd/third-party/miniupnpc-20090605/miniwget.h new file mode 100644 index 000000000..12c062c6d --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/miniwget.h @@ -0,0 +1,28 @@ +/* $Id: miniwget.h,v 1.5 2007/01/29 20:27:23 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas Bernard + * Copyright (c) 2005 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * */ +#ifndef __MINIWGET_H__ +#define __MINIWGET_H__ + +#include "declspec.h" + +#ifdef __cplusplus +extern "C" { +#endif + +LIBSPEC void * miniwget(const char *, int *); + +LIBSPEC void * miniwget_getaddr(const char *, int *, char *, int); + +int parseURL(const char *, char *, unsigned short *, char * *); + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/minixml.c b/libportfwd/third-party/miniupnpc-20090605/minixml.c new file mode 100644 index 000000000..dce690e2e --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/minixml.c @@ -0,0 +1,191 @@ +/* $Id: minixml.c,v 1.6 2007/05/15 18:14:08 nanard Exp $ */ +/* minixml.c : the minimum size a xml parser can be ! */ +/* Project : miniupnp + * webpage: http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * Author : Thomas Bernard + +Copyright (c) 2005-2007, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * 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. + * 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR CONTRIBUTORS 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 "minixml.h" + +/* parseatt : used to parse the argument list + * return 0 (false) in case of success and -1 (true) if the end + * of the xmlbuffer is reached. */ +int parseatt(struct xmlparser * p) +{ + const char * attname; + int attnamelen; + const char * attvalue; + int attvaluelen; + while(p->xml < p->xmlend) + { + if(*p->xml=='/' || *p->xml=='>') + return 0; + if( !IS_WHITE_SPACE(*p->xml) ) + { + char sep; + attname = p->xml; + attnamelen = 0; + while(*p->xml!='=' && !IS_WHITE_SPACE(*p->xml) ) + { + attnamelen++; p->xml++; + if(p->xml >= p->xmlend) + return -1; + } + while(*(p->xml++) != '=') + { + if(p->xml >= p->xmlend) + return -1; + } + while(IS_WHITE_SPACE(*p->xml)) + { + p->xml++; + if(p->xml >= p->xmlend) + return -1; + } + sep = *p->xml; + if(sep=='\'' || sep=='\"') + { + p->xml++; + if(p->xml >= p->xmlend) + return -1; + attvalue = p->xml; + attvaluelen = 0; + while(*p->xml != sep) + { + attvaluelen++; p->xml++; + if(p->xml >= p->xmlend) + return -1; + } + } + else + { + attvalue = p->xml; + attvaluelen = 0; + while( !IS_WHITE_SPACE(*p->xml) + && *p->xml != '>' && *p->xml != '/') + { + attvaluelen++; p->xml++; + if(p->xml >= p->xmlend) + return -1; + } + } + /*printf("%.*s='%.*s'\n", + attnamelen, attname, attvaluelen, attvalue);*/ + if(p->attfunc) + p->attfunc(p->data, attname, attnamelen, attvalue, attvaluelen); + } + p->xml++; + } + return -1; +} + +/* parseelt parse the xml stream and + * call the callback functions when needed... */ +void parseelt(struct xmlparser * p) +{ + int i; + const char * elementname; + while(p->xml < (p->xmlend - 1)) + { + if((p->xml)[0]=='<' && (p->xml)[1]!='?') + { + i = 0; elementname = ++p->xml; + while( !IS_WHITE_SPACE(*p->xml) + && (*p->xml!='>') && (*p->xml!='/') + ) + { + i++; p->xml++; + if (p->xml >= p->xmlend) + return; + /* to ignore namespace : */ + if(*p->xml==':') + { + i = 0; + elementname = ++p->xml; + } + } + if(i>0) + { + if(p->starteltfunc) + p->starteltfunc(p->data, elementname, i); + if(parseatt(p)) + return; + if(*p->xml!='/') + { + const char * data; + i = 0; data = ++p->xml; + if (p->xml >= p->xmlend) + return; + while( IS_WHITE_SPACE(*p->xml) ) + { + p->xml++; + if (p->xml >= p->xmlend) + return; + } + while(*p->xml!='<') + { + i++; p->xml++; + if (p->xml >= p->xmlend) + return; + } + if(i>0 && p->datafunc) + p->datafunc(p->data, data, i); + } + } + else if(*p->xml == '/') + { + i = 0; elementname = ++p->xml; + if (p->xml >= p->xmlend) + return; + while((*p->xml != '>')) + { + i++; p->xml++; + if (p->xml >= p->xmlend) + return; + } + if(p->endeltfunc) + p->endeltfunc(p->data, elementname, i); + p->xml++; + } + } + else + { + p->xml++; + } + } +} + +/* the parser must be initialized before calling this function */ +void parsexml(struct xmlparser * parser) +{ + parser->xml = parser->xmlstart; + parser->xmlend = parser->xmlstart + parser->xmlsize; + parseelt(parser); +} + + diff --git a/libportfwd/third-party/miniupnpc-20090605/minixml.h b/libportfwd/third-party/miniupnpc-20090605/minixml.h new file mode 100644 index 000000000..857c70ee9 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/minixml.h @@ -0,0 +1,37 @@ +/* $Id: minixml.h,v 1.6 2006/11/30 11:47:21 nanard Exp $ */ +/* minimal xml parser + * + * Project : miniupnp + * Website : http://miniupnp.free.fr/ + * Author : Thomas Bernard + * Copyright (c) 2005 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * */ +#ifndef __MINIXML_H__ +#define __MINIXML_H__ +#define IS_WHITE_SPACE(c) ((c==' ') || (c=='\t') || (c=='\r') || (c=='\n')) + +/* if a callback function pointer is set to NULL, + * the function is not called */ +struct xmlparser { + const char *xmlstart; + const char *xmlend; + const char *xml; /* pointer to current character */ + int xmlsize; + void * data; + void (*starteltfunc) (void *, const char *, int); + void (*endeltfunc) (void *, const char *, int); + void (*datafunc) (void *, const char *, int); + void (*attfunc) (void *, const char *, int, const char *, int); +}; + +/* parsexml() + * the xmlparser structure must be initialized before the call + * the following structure members have to be initialized : + * xmlstart, xmlsize, data, *func + * xml is for internal usage, xmlend is computed automatically */ +void parsexml(struct xmlparser *); + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/minixmlvalid.c b/libportfwd/third-party/miniupnpc-20090605/minixmlvalid.c new file mode 100644 index 000000000..e7967befa --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/minixmlvalid.c @@ -0,0 +1,149 @@ +/* $Id: minixmlvalid.c,v 1.2 2006/11/30 11:31:55 nanard Exp $ */ +/* MiniUPnP Project + * http://miniupnp.tuxfamily.org/ or http://miniupnp.free.fr/ + * minixmlvalid.c : + * validation program for the minixml parser + * + * (c) 2006 Thomas Bernard */ + +#include +#include +#include +#include "minixml.h" + +/* xml event structure */ +struct event { + enum { ELTSTART, ELTEND, ATT, CHARDATA } type; + const char * data; + int len; +}; + +struct eventlist { + int n; + struct event * events; +}; + +/* compare 2 xml event lists + * return 0 if the two lists are equals */ +int evtlistcmp(struct eventlist * a, struct eventlist * b) +{ + int i; + struct event * ae, * be; + if(a->n != b->n) + return 1; + for(i=0; in; i++) + { + ae = a->events + i; + be = b->events + i; + if( (ae->type != be->type) + ||(ae->len != be->len) + ||memcmp(ae->data, be->data, ae->len)) + { + printf("Found a difference : %d '%.*s' != %d '%.*s'\n", + ae->type, ae->len, ae->data, + be->type, be->len, be->data); + return 1; + } + } + return 0; +} + +/* Test data */ +static const char xmldata[] = +"\n" +" " +"character data" +" \n \t" +"" +" \tchardata1chardata2" +""; + +static const struct event evtref[] = +{ + {ELTSTART, "xmlroot", 7}, + {ELTSTART, "elt1", 4}, + /* attributes */ + {CHARDATA, "character data", 14}, + {ELTEND, "elt1", 4}, + {ELTSTART, "elt1b", 5}, + {ELTSTART, "elt2a", 5}, + {ELTSTART, "elt2b", 5}, + {CHARDATA, "chardata1", 9}, + {ELTEND, "elt2b", 5}, + {ELTSTART, "elt2b", 5}, + {CHARDATA, "chardata2", 9}, + {ELTEND, "elt2b", 5}, + {ELTEND, "elt2a", 5}, + {ELTEND, "xmlroot", 7} +}; + +void startelt(void * data, const char * p, int l) +{ + struct eventlist * evtlist = data; + struct event * evt; + evt = evtlist->events + evtlist->n; + /*printf("startelt : %.*s\n", l, p);*/ + evt->type = ELTSTART; + evt->data = p; + evt->len = l; + evtlist->n++; +} + +void endelt(void * data, const char * p, int l) +{ + struct eventlist * evtlist = data; + struct event * evt; + evt = evtlist->events + evtlist->n; + /*printf("endelt : %.*s\n", l, p);*/ + evt->type = ELTEND; + evt->data = p; + evt->len = l; + evtlist->n++; +} + +void chardata(void * data, const char * p, int l) +{ + struct eventlist * evtlist = data; + struct event * evt; + evt = evtlist->events + evtlist->n; + /*printf("chardata : '%.*s'\n", l, p);*/ + evt->type = CHARDATA; + evt->data = p; + evt->len = l; + evtlist->n++; +} + +int testxmlparser(const char * xml, int size) +{ + int r; + struct eventlist evtlist; + struct eventlist evtlistref; + struct xmlparser parser; + evtlist.n = 0; + evtlist.events = malloc(sizeof(struct event)*100); + memset(&parser, 0, sizeof(parser)); + parser.xmlstart = xml; + parser.xmlsize = size; + parser.data = &evtlist; + parser.starteltfunc = startelt; + parser.endeltfunc = endelt; + parser.datafunc = chardata; + parsexml(&parser); + printf("%d events\n", evtlist.n); + /* compare */ + evtlistref.n = sizeof(evtref)/sizeof(struct event); + evtlistref.events = (struct event *)evtref; + r = evtlistcmp(&evtlistref, &evtlist); + free(evtlist.events); + return r; +} + +int main(int argc, char * * argv) +{ + int r; + r = testxmlparser(xmldata, sizeof(xmldata)-1); + if(r) + printf("minixml validation test failed\n"); + return r; +} + diff --git a/libportfwd/third-party/miniupnpc-20090605/pymoduletest.py b/libportfwd/third-party/miniupnpc-20090605/pymoduletest.py new file mode 100644 index 000000000..d35a3b092 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/pymoduletest.py @@ -0,0 +1,52 @@ +#! /usr/bin/python +# MiniUPnP project +# Author : Thomas Bernard +# This Sample code is public domain. +# website : http://miniupnp.tuxfamily.org/ + +# import the python miniupnpc module +import miniupnpc +import sys + +# create the object +u = miniupnpc.UPnP() +print 'inital(default) values :' +print ' discoverdelay', u.discoverdelay +print ' lanaddr', u.lanaddr +print ' multicastif', u.multicastif +print ' minissdpdsocket', u.minissdpdsocket +u.discoverdelay = 200; +#u.minissdpdsocket = '../minissdpd/minissdpd.sock' +# discovery process, it usualy takes several seconds (2 seconds or more) +print 'Discovering... delay=%ums' % u.discoverdelay +print u.discover(), 'device(s) detected' +# select an igd +try: + u.selectigd() +except Exception, e: + print 'Exception :', e + sys.exit(1) +# display information about the IGD and the internet connection +print 'local ip address :', u.lanaddr +print 'external ip address :', u.externalipaddress() +print u.statusinfo(), u.connectiontype() + +#print u.addportmapping(64000, 'TCP', +# '192.168.1.166', 63000, 'port mapping test', '') +#print u.deleteportmapping(64000, 'TCP') + +port = 0 +proto = 'UDP' +# list the redirections : +i = 0 +while True: + p = u.getgenericportmapping(i) + if p==None: + break + print i, p + (port, proto, (ihost,iport), desc, c, d, e) = p + #print port, desc + i = i + 1 + +print u.getspecificportmapping(port, proto) + diff --git a/libportfwd/third-party/miniupnpc-20090605/setup.py b/libportfwd/third-party/miniupnpc-20090605/setup.py new file mode 100644 index 000000000..8ecb5f392 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/setup.py @@ -0,0 +1,15 @@ +#! /usr/bin/python +# $Id: setup.py,v 1.3 2009/04/17 20:59:42 nanard Exp $ +# the MiniUPnP Project (c) 2007 Thomas Bernard +# http://miniupnp.tuxfamily.org/ or http://miniupnp.free.fr/ +# +# python script to build the miniupnpc module under unix +# +# replace libminiupnpc.a by libminiupnpc.so for shared library usage +from distutils.core import setup, Extension +setup(name="miniupnpc", version="1.3", + ext_modules=[ + Extension(name="miniupnpc", sources=["miniupnpcmodule.c"], + extra_objects=["libminiupnpc.a"]) + ]) + diff --git a/libportfwd/third-party/miniupnpc-20090605/setupmingw32.py b/libportfwd/third-party/miniupnpc-20090605/setupmingw32.py new file mode 100755 index 000000000..62433e4ac --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/setupmingw32.py @@ -0,0 +1,15 @@ +#! /usr/bin/python +# $Id: setupmingw32.py,v 1.1 2007/06/12 23:04:13 nanard Exp $ +# the MiniUPnP Project (c) 2007 Thomas Bernard +# http://miniupnp.tuxfamily.org/ or http://miniupnp.free.fr/ +# +# python script to build the miniupnpc module under unix +# +from distutils.core import setup, Extension +setup(name="miniupnpc", version="1.0-RC6", + ext_modules=[ + Extension(name="miniupnpc", sources=["miniupnpcmodule.c"], + libraries=["ws2_32"], + extra_objects=["libminiupnpc.a"]) + ]) + diff --git a/libportfwd/third-party/miniupnpc-20090605/testigddescparse.c b/libportfwd/third-party/miniupnpc-20090605/testigddescparse.c new file mode 100644 index 000000000..527f5e111 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/testigddescparse.c @@ -0,0 +1,57 @@ +/* $Id: testigddescparse.c,v 1.1 2008/04/23 11:53:45 nanard Exp $ */ +/* Project : miniupnp + * http://miniupnp.free.fr/ + * Author : Thomas Bernard + * Copyright (c) 2008 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * */ +#include +#include +#include +#include "igd_desc_parse.h" +#include "minixml.h" + +int test_igd_desc_parse(char * buffer, int len) +{ + struct IGDdatas igd; + struct xmlparser parser; + memset(&igd, 0, sizeof(struct IGDdatas)); + memset(&parser, 0, sizeof(struct xmlparser)); + parser.xmlstart = buffer; + parser.xmlsize = len; + parser.data = &igd; + parser.starteltfunc = IGDstartelt; + parser.endeltfunc = IGDendelt; + parser.datafunc = IGDdata; + parsexml(&parser); + printIGD(&igd); + return 0; +} + +int main(int argc, char * * argv) +{ + FILE * f; + char * buffer; + int len; + int r = 0; + if(argc<2) { + fprintf(stderr, "Usage: %s file.xml\n", argv[0]); + return 1; + } + f = fopen(argv[1], "r"); + if(!f) { + fprintf(stderr, "Cannot open %s for reading.\n", argv[1]); + return 1; + } + fseek(f, 0, SEEK_END); + len = ftell(f); + fseek(f, 0, SEEK_SET); + buffer = malloc(len); + fread(buffer, 1, len, f); + fclose(f); + r = test_igd_desc_parse(buffer, len); + free(buffer); + return r; +} + diff --git a/libportfwd/third-party/miniupnpc-20090605/testminixml.c b/libportfwd/third-party/miniupnpc-20090605/testminixml.c new file mode 100644 index 000000000..3d82527b7 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/testminixml.c @@ -0,0 +1,88 @@ +/* $Id: testminixml.c,v 1.6 2006/11/19 22:32:35 nanard Exp $ + * testminixml.c + * test program for the "minixml" functions. + * Author : Thomas Bernard. + */ +#include +#include +#include +#include "minixml.h" +#include "igd_desc_parse.h" + +#ifdef WIN32 +#define NO_BZERO +#endif + +#ifdef NO_BZERO +#define bzero(p, n) memset(p, 0, n) +#endif + +/* ---------------------------------------------------------------------- */ +void printeltname1(void * d, const char * name, int l) +{ + int i; + printf("element "); + for(i=0;i %s port %u TCP' % (externalipaddress, eport, u.lanaddr, httpd.server_port) + + b = u.addportmapping(eport, 'TCP', u.lanaddr, httpd.server_port, + 'UPnP IGD Tester port %u' % eport, '') + if b: + print 'Success. Now waiting for some HTTP request on http://%s:%u' % (externalipaddress ,eport) + try: + httpd.handle_request() + httpd.server_close() + except KeyboardInterrupt, details: + print "CTRL-C exception!", details + b = u.deleteportmapping(eport, 'TCP') + if b: + print 'Successfully deleted port mapping' + else: + print 'Failed to remove port mapping' + else: + print 'Failed' + + httpd.server_close() + +except Exception, e: + print 'Exception :', e diff --git a/libportfwd/third-party/miniupnpc-20090605/testupnpreplyparse.c b/libportfwd/third-party/miniupnpc-20090605/testupnpreplyparse.c new file mode 100644 index 000000000..a02e8f6a6 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/testupnpreplyparse.c @@ -0,0 +1,44 @@ +/* $Id: testupnpreplyparse.c,v 1.2 2008/02/21 13:05:27 nanard Exp $ */ +/* MiniUPnP project + * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * (c) 2006-2007 Thomas Bernard + * This software is subject to the conditions detailed + * in the LICENCE file provided within the distribution */ +#include +#include +#include "upnpreplyparse.h" + +void +test_parsing(const char * buf, int len) +{ + struct NameValueParserData pdata; + ParseNameValue(buf, len, &pdata); + ClearNameValueList(&pdata); +} + +int main(int argc, char * * argv) +{ + FILE * f; + char buffer[4096]; + int l; + if(argc<2) + { + fprintf(stderr, "Usage: %s file.xml\n", argv[0]); + return 1; + } + f = fopen(argv[1], "r"); + if(!f) + { + fprintf(stderr, "Error : can not open file %s\n", argv[1]); + return 2; + } + l = fread(buffer, 1, sizeof(buffer)-1, f); + fclose(f); + buffer[l] = '\0'; +#ifdef DEBUG + DisplayNameValueList(buffer, l); +#endif + test_parsing(buffer, l); + return 0; +} + diff --git a/libportfwd/third-party/miniupnpc-20090605/updateminiupnpcstrings.sh b/libportfwd/third-party/miniupnpc-20090605/updateminiupnpcstrings.sh new file mode 100755 index 000000000..f7b95a560 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/updateminiupnpcstrings.sh @@ -0,0 +1,36 @@ +#! /bin/sh +# $Id: updateminiupnpcstrings.sh,v 1.2 2009/06/04 09:13:53 nanard Exp $ + +FILE=miniupnpcstrings.h + +# detecting the OS name and version +OS_NAME=`uname -s` +OS_VERSION=`uname -r` +if [ -f /etc/debian_version ]; then + OS_NAME=Debian + OS_VERSION=`cat /etc/debian_version` +fi +# use lsb_release (Linux Standard Base) when available +LSB_RELEASE=`which lsb_release` +if [ 0 -eq $? ]; then + OS_NAME=`${LSB_RELEASE} -i -s` + OS_VERSION=`${LSB_RELEASE} -r -s` + case $OS_NAME in + Debian) + #OS_VERSION=`${LSB_RELEASE} -c -s` + ;; + Ubuntu) + #OS_VERSION=`${LSB_RELEASE} -c -s` + ;; + esac +fi + +echo "Detected OS [$OS_NAME] version [$OS_VERSION]" + +EXPR="s/OS_STRING \".*\"/OS_STRING \"${OS_NAME}\/${OS_VERSION}\"/" +#echo $EXPR +echo "Backuping $FILE to $FILE.bak." +cp $FILE $FILE.bak +echo "setting OS_STRING macro value to ${OS_NAME}/${OS_VERSION} in $FILE." +cat $FILE.bak | sed -e "$EXPR" > $FILE + diff --git a/libportfwd/third-party/miniupnpc-20090605/upnpc.c b/libportfwd/third-party/miniupnpc-20090605/upnpc.c new file mode 100644 index 000000000..2c5debe31 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/upnpc.c @@ -0,0 +1,374 @@ +/* $Id: upnpc.c,v 1.65 2008/10/14 18:05:27 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas Bernard + * Copyright (c) 2005-2008 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided in this distribution. + * */ +#include +#include +#include +#ifdef WIN32 +#include +#define snprintf _snprintf +#endif +#include "miniwget.h" +#include "miniupnpc.h" +#include "upnpcommands.h" +#include "upnperrors.h" + +/* protofix() checks if protocol is "UDP" or "TCP" + * returns NULL if not */ +const char * protofix(const char * proto) +{ + static const char proto_tcp[4] = { 'T', 'C', 'P', 0}; + static const char proto_udp[4] = { 'U', 'D', 'P', 0}; + int i, b; + for(i=0, b=1; i<4; i++) + b = b && ( (proto[i] == proto_tcp[i]) + || (proto[i] == (proto_tcp[i] | 32)) ); + if(b) + return proto_tcp; + for(i=0, b=1; i<4; i++) + b = b && ( (proto[i] == proto_udp[i]) + || (proto[i] == (proto_udp[i] | 32)) ); + if(b) + return proto_udp; + return 0; +} + +static void DisplayInfos(struct UPNPUrls * urls, + struct IGDdatas * data) +{ + char externalIPAddress[16]; + char connectionType[64]; + char status[64]; + char lastconnerr[64]; + unsigned int uptime; + unsigned int brUp, brDown; + int r; + UPNP_GetConnectionTypeInfo(urls->controlURL, + data->servicetype, + connectionType); + if(connectionType[0]) + printf("Connection Type : %s\n", connectionType); + else + printf("GetConnectionTypeInfo failed.\n"); + UPNP_GetStatusInfo(urls->controlURL, data->servicetype, + status, &uptime, lastconnerr); + printf("Status : %s, uptime=%u, LastConnectionError : %s\n", + status, uptime, lastconnerr); + UPNP_GetLinkLayerMaxBitRates(urls->controlURL_CIF, data->servicetype_CIF, + &brDown, &brUp); + printf("MaxBitRateDown : %u bps MaxBitRateUp %u bps\n", brDown, brUp); + r = UPNP_GetExternalIPAddress(urls->controlURL, + data->servicetype, + externalIPAddress); + if(r != UPNPCOMMAND_SUCCESS) + printf("GetExternalIPAddress() returned %d\n", r); + if(externalIPAddress[0]) + printf("ExternalIPAddress = %s\n", externalIPAddress); + else + printf("GetExternalIPAddress failed.\n"); +} + +static void GetConnectionStatus(struct UPNPUrls * urls, + struct IGDdatas * data) +{ + unsigned int bytessent, bytesreceived, packetsreceived, packetssent; + DisplayInfos(urls, data); + bytessent = UPNP_GetTotalBytesSent(urls->controlURL_CIF, data->servicetype_CIF); + bytesreceived = UPNP_GetTotalBytesReceived(urls->controlURL_CIF, data->servicetype_CIF); + packetssent = UPNP_GetTotalPacketsSent(urls->controlURL_CIF, data->servicetype_CIF); + packetsreceived = UPNP_GetTotalPacketsReceived(urls->controlURL_CIF, data->servicetype_CIF); + printf("Bytes: Sent: %8u\tRecv: %8u\n", bytessent, bytesreceived); + printf("Packets: Sent: %8u\tRecv: %8u\n", packetssent, packetsreceived); +} + +static void ListRedirections(struct UPNPUrls * urls, + struct IGDdatas * data) +{ + int r; + int i = 0; + char index[6]; + char intClient[16]; + char intPort[6]; + char extPort[6]; + char protocol[4]; + char desc[80]; + char enabled[6]; + char rHost[64]; + char duration[16]; + /*unsigned int num=0; + UPNP_GetPortMappingNumberOfEntries(urls->controlURL, data->servicetype, &num); + printf("PortMappingNumberOfEntries : %u\n", num);*/ + do { + snprintf(index, 6, "%d", i); + rHost[0] = '\0'; enabled[0] = '\0'; + duration[0] = '\0'; desc[0] = '\0'; + extPort[0] = '\0'; intPort[0] = '\0'; intClient[0] = '\0'; + r = UPNP_GetGenericPortMappingEntry(urls->controlURL, data->servicetype, + index, + extPort, intClient, intPort, + protocol, desc, enabled, + rHost, duration); + if(r==0) + /* + printf("%02d - %s %s->%s:%s\tenabled=%s leaseDuration=%s\n" + " desc='%s' rHost='%s'\n", + i, protocol, extPort, intClient, intPort, + enabled, duration, + desc, rHost); + */ + printf("%2d %s %5s->%s:%-5s '%s' '%s'\n", + i, protocol, extPort, intClient, intPort, + desc, rHost); + else + printf("GetGenericPortMappingEntry() returned %d (%s)\n", + r, strupnperror(r)); + i++; + } while(r==0); +} + +/* Test function + * 1 - get connection type + * 2 - get extenal ip address + * 3 - Add port mapping + * 4 - get this port mapping from the IGD */ +static void SetRedirectAndTest(struct UPNPUrls * urls, + struct IGDdatas * data, + const char * iaddr, + const char * iport, + const char * eport, + const char * proto) +{ + char externalIPAddress[16]; + char intClient[16]; + char intPort[6]; + int r; + + if(!iaddr || !iport || !eport || !proto) + { + fprintf(stderr, "Wrong arguments\n"); + return; + } + proto = protofix(proto); + if(!proto) + { + fprintf(stderr, "invalid protocol\n"); + return; + } + + UPNP_GetExternalIPAddress(urls->controlURL, + data->servicetype, + externalIPAddress); + if(externalIPAddress[0]) + printf("ExternalIPAddress = %s\n", externalIPAddress); + else + printf("GetExternalIPAddress failed.\n"); + + r = UPNP_AddPortMapping(urls->controlURL, data->servicetype, + eport, iport, iaddr, 0, proto, 0); + if(r!=UPNPCOMMAND_SUCCESS) + printf("AddPortMapping(%s, %s, %s) failed with code %d\n", + eport, iport, iaddr, r); + + r = UPNP_GetSpecificPortMappingEntry(urls->controlURL, + data->servicetype, + eport, proto, + intClient, intPort); + if(r!=UPNPCOMMAND_SUCCESS) + printf("GetSpecificPortMappingEntry() failed with code %d\n", r); + + if(intClient[0]) { + printf("InternalIP:Port = %s:%s\n", intClient, intPort); + printf("external %s:%s %s is redirected to internal %s:%s\n", + externalIPAddress, eport, proto, intClient, intPort); + } +} + +static void +RemoveRedirect(struct UPNPUrls * urls, + struct IGDdatas * data, + const char * eport, + const char * proto) +{ + int r; + if(!proto || !eport) + { + fprintf(stderr, "invalid arguments\n"); + return; + } + proto = protofix(proto); + if(!proto) + { + fprintf(stderr, "protocol invalid\n"); + return; + } + r = UPNP_DeletePortMapping(urls->controlURL, data->servicetype, eport, proto, 0); + printf("UPNP_DeletePortMapping() returned : %d\n", r); +} + + +/* sample upnp client program */ +int main(int argc, char ** argv) +{ + char command = 0; + char ** commandargv = 0; + int commandargc = 0; + struct UPNPDev * devlist = 0; + char lanaddr[16]; /* my ip address on the LAN */ + int i; + const char * rootdescurl = 0; + const char * multicastif = 0; + const char * minissdpdpath = 0; + +#ifdef WIN32 + WSADATA wsaData; + int nResult = WSAStartup(MAKEWORD(2,2), &wsaData); + if(nResult != NO_ERROR) + { + fprintf(stderr, "WSAStartup() failed.\n"); + return -1; + } +#endif + printf("upnpc : miniupnpc library test client. (c) 2006-2008 Thomas Bernard\n"); + printf("Go to http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/\n" + "for more information.\n"); + /* command line processing */ + for(i=1; ipNext) + { + printf(" desc: %s\n st: %s\n\n", + device->descURL, device->st); + } + } + i = 1; + if( (rootdescurl && UPNP_GetIGDFromUrl(rootdescurl, &urls, &data, lanaddr, sizeof(lanaddr))) + || (i = UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr)))) + { + switch(i) { + case 1: + printf("Found valid IGD : %s\n", urls.controlURL); + break; + case 2: + printf("Found a (not connected?) IGD : %s\n", urls.controlURL); + printf("Trying to continue anyway\n"); + break; + case 3: + printf("UPnP device found. Is it an IGD ? : %s\n", urls.controlURL); + printf("Trying to continue anyway\n"); + break; + default: + printf("Found device (igd ?) : %s\n", urls.controlURL); + printf("Trying to continue anyway\n"); + } + printf("Local LAN ip address : %s\n", lanaddr); + #if 0 + printf("getting \"%s\"\n", urls.ipcondescURL); + descXML = miniwget(urls.ipcondescURL, &descXMLsize); + if(descXML) + { + /*fwrite(descXML, 1, descXMLsize, stdout);*/ + free(descXML); descXML = NULL; + } + #endif + + switch(command) + { + case 'l': + DisplayInfos(&urls, &data); + ListRedirections(&urls, &data); + break; + case 'a': + SetRedirectAndTest(&urls, &data, + commandargv[0], commandargv[1], + commandargv[2], commandargv[3]); + break; + case 'd': + RemoveRedirect(&urls, &data, commandargv[0], commandargv[1]); + break; + case 's': + GetConnectionStatus(&urls, &data); + break; + case 'r': + for(i=0; i +#include +#include +#include "upnpcommands.h" +#include "miniupnpc.h" + +static UNSIGNED_INTEGER +my_atoui(const char * s) +{ + return s ? ((UNSIGNED_INTEGER)STRTOUI(s, NULL, 0)) : 0; +} + +/* + * */ +UNSIGNED_INTEGER +UPNP_GetTotalBytesSent(const char * controlURL, + const char * servicetype) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + unsigned int r = 0; + char * p; + simpleUPnPcommand(-1, controlURL, servicetype, "GetTotalBytesSent", 0, buffer, &bufsize); + ParseNameValue(buffer, bufsize, &pdata); + /*DisplayNameValueList(buffer, bufsize);*/ + p = GetValueFromNameValueList(&pdata, "NewTotalBytesSent"); + r = my_atoui(p); + ClearNameValueList(&pdata); + return r; +} + +/* + * */ +UNSIGNED_INTEGER +UPNP_GetTotalBytesReceived(const char * controlURL, + const char * servicetype) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + unsigned int r = 0; + char * p; + simpleUPnPcommand(-1, controlURL, servicetype, "GetTotalBytesReceived", 0, buffer, &bufsize); + ParseNameValue(buffer, bufsize, &pdata); + /*DisplayNameValueList(buffer, bufsize);*/ + p = GetValueFromNameValueList(&pdata, "NewTotalBytesReceived"); + r = my_atoui(p); + ClearNameValueList(&pdata); + return r; +} + +/* + * */ +UNSIGNED_INTEGER +UPNP_GetTotalPacketsSent(const char * controlURL, + const char * servicetype) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + unsigned int r = 0; + char * p; + simpleUPnPcommand(-1, controlURL, servicetype, "GetTotalPacketsSent", 0, buffer, &bufsize); + ParseNameValue(buffer, bufsize, &pdata); + /*DisplayNameValueList(buffer, bufsize);*/ + p = GetValueFromNameValueList(&pdata, "NewTotalPacketsSent"); + r = my_atoui(p); + ClearNameValueList(&pdata); + return r; +} + +/* + * */ +UNSIGNED_INTEGER +UPNP_GetTotalPacketsReceived(const char * controlURL, + const char * servicetype) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + unsigned int r = 0; + char * p; + simpleUPnPcommand(-1, controlURL, servicetype, "GetTotalPacketsReceived", 0, buffer, &bufsize); + ParseNameValue(buffer, bufsize, &pdata); + /*DisplayNameValueList(buffer, bufsize);*/ + p = GetValueFromNameValueList(&pdata, "NewTotalPacketsReceived"); + r = my_atoui(p); + ClearNameValueList(&pdata); + return r; +} + +/* UPNP_GetStatusInfo() call the corresponding UPNP method + * returns the current status and uptime */ +int UPNP_GetStatusInfo(const char * controlURL, + const char * servicetype, + char * status, + unsigned int * uptime, + char * lastconnerror) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + char * p; + char * up; + char * err; + int ret = UPNPCOMMAND_UNKNOWN_ERROR; + + if(!status && !uptime) + return UPNPCOMMAND_INVALID_ARGS; + + simpleUPnPcommand(-1, controlURL, servicetype, "GetStatusInfo", 0, buffer, &bufsize); + ParseNameValue(buffer, bufsize, &pdata); + /*DisplayNameValueList(buffer, bufsize);*/ + up = GetValueFromNameValueList(&pdata, "NewUptime"); + p = GetValueFromNameValueList(&pdata, "NewConnectionStatus"); + err = GetValueFromNameValueList(&pdata, "NewLastConnectionError"); + if(p && up) + ret = UPNPCOMMAND_SUCCESS; + + if(status) { + if(p){ + strncpy(status, p, 64 ); + status[63] = '\0'; + }else + status[0]= '\0'; + } + + if(uptime) { + if(up) + sscanf(up,"%u",uptime); + else + uptime = 0; + } + + if(lastconnerror) { + if(err) { + strncpy(lastconnerror, err, 64 ); + lastconnerror[63] = '\0'; + } else + lastconnerror[0] = '\0'; + } + + p = GetValueFromNameValueList(&pdata, "errorCode"); + if(p) { + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(p, "%d", &ret); + } + ClearNameValueList(&pdata); + return ret; +} + +/* UPNP_GetConnectionTypeInfo() call the corresponding UPNP method + * returns the connection type */ +int UPNP_GetConnectionTypeInfo(const char * controlURL, + const char * servicetype, + char * connectionType) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + char * p; + int ret = UPNPCOMMAND_UNKNOWN_ERROR; + + if(!connectionType) + return UPNPCOMMAND_INVALID_ARGS; + + simpleUPnPcommand(-1, controlURL, servicetype, + "GetConnectionTypeInfo", 0, buffer, &bufsize); + ParseNameValue(buffer, bufsize, &pdata); + p = GetValueFromNameValueList(&pdata, "NewConnectionType"); + /*p = GetValueFromNameValueList(&pdata, "NewPossibleConnectionTypes");*/ + /* PossibleConnectionTypes will have several values.... */ + if(p) { + strncpy(connectionType, p, 64 ); + connectionType[63] = '\0'; + ret = UPNPCOMMAND_SUCCESS; + } else + connectionType[0] = '\0'; + p = GetValueFromNameValueList(&pdata, "errorCode"); + if(p) { + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(p, "%d", &ret); + } + ClearNameValueList(&pdata); + return ret; +} + +/* UPNP_GetLinkLayerMaxBitRate() call the corresponding UPNP method. + * Returns 2 values: Downloadlink bandwidth and Uplink bandwidth. + * One of the values can be null + * Note : GetLinkLayerMaxBitRates belongs to WANPPPConnection:1 only + * We can use the GetCommonLinkProperties from WANCommonInterfaceConfig:1 */ +int UPNP_GetLinkLayerMaxBitRates(const char * controlURL, const char * servicetype, unsigned int * bitrateDown, unsigned int* bitrateUp) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + int ret = UPNPCOMMAND_UNKNOWN_ERROR; + char * down; + char * up; + char * p; + + if(!bitrateDown && !bitrateUp) + return UPNPCOMMAND_INVALID_ARGS; + + /* shouldn't we use GetCommonLinkProperties ? */ + simpleUPnPcommand(-1, controlURL, servicetype, + "GetCommonLinkProperties", 0, buffer, &bufsize); + /*"GetLinkLayerMaxBitRates", 0, buffer, &bufsize);*/ + /*DisplayNameValueList(buffer, bufsize);*/ + ParseNameValue(buffer, bufsize, &pdata); + /*down = GetValueFromNameValueList(&pdata, "NewDownstreamMaxBitRate");*/ + /*up = GetValueFromNameValueList(&pdata, "NewUpstreamMaxBitRate");*/ + down = GetValueFromNameValueList(&pdata, "NewLayer1DownstreamMaxBitRate"); + up = GetValueFromNameValueList(&pdata, "NewLayer1UpstreamMaxBitRate"); + /*GetValueFromNameValueList(&pdata, "NewWANAccessType");*/ + /*GetValueFromNameValueList(&pdata, "NewPhysicalLinkSatus");*/ + if(down && up) + ret = UPNPCOMMAND_SUCCESS; + + if(bitrateDown) + { + if(down) + sscanf(down,"%u",bitrateDown); + else + *bitrateDown = 0; + } + + if(bitrateUp) + { + if(up) + sscanf(up,"%u",bitrateUp); + else + *bitrateUp = 0; + } + p = GetValueFromNameValueList(&pdata, "errorCode"); + if(p) { + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(p, "%d", &ret); + } + ClearNameValueList(&pdata); + return ret; +} + + +/* UPNP_GetExternalIPAddress() call the corresponding UPNP method. + * if the third arg is not null the value is copied to it. + * at least 16 bytes must be available + * + * Return values : + * 0 : SUCCESS + * NON ZERO : ERROR Either an UPnP error code or an unknown error. + * + * 402 Invalid Args - See UPnP Device Architecture section on Control. + * 501 Action Failed - See UPnP Device Architecture section on Control. + */ +int UPNP_GetExternalIPAddress(const char * controlURL, + const char * servicetype, + char * extIpAdd) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + char * p; + int ret = UPNPCOMMAND_UNKNOWN_ERROR; + + if(!extIpAdd || !controlURL || !servicetype) + return UPNPCOMMAND_INVALID_ARGS; + + simpleUPnPcommand(-1, controlURL, servicetype, "GetExternalIPAddress", 0, buffer, &bufsize); + /*DisplayNameValueList(buffer, bufsize);*/ + ParseNameValue(buffer, bufsize, &pdata); + /*printf("external ip = %s\n", GetValueFromNameValueList(&pdata, "NewExternalIPAddress") );*/ + p = GetValueFromNameValueList(&pdata, "NewExternalIPAddress"); + if(p) { + strncpy(extIpAdd, p, 16 ); + extIpAdd[15] = '\0'; + ret = UPNPCOMMAND_SUCCESS; + } else + extIpAdd[0] = '\0'; + + p = GetValueFromNameValueList(&pdata, "errorCode"); + if(p) { + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(p, "%d", &ret); + } + + ClearNameValueList(&pdata); + return ret; +} + +int +UPNP_AddPortMapping(const char * controlURL, const char * servicetype, + const char * extPort, + const char * inPort, + const char * inClient, + const char * desc, + const char * proto, + const char * remoteHost) +{ + struct UPNParg * AddPortMappingArgs; + char buffer[4096]; + int bufsize = 4096; + struct NameValueParserData pdata; + const char * resVal; + int ret; + + if(!inPort || !inClient || !proto || !extPort) + return UPNPCOMMAND_INVALID_ARGS; + + AddPortMappingArgs = calloc(9, sizeof(struct UPNParg)); + AddPortMappingArgs[0].elt = "NewRemoteHost"; + AddPortMappingArgs[0].val = remoteHost; + AddPortMappingArgs[1].elt = "NewExternalPort"; + AddPortMappingArgs[1].val = extPort; + AddPortMappingArgs[2].elt = "NewProtocol"; + AddPortMappingArgs[2].val = proto; + AddPortMappingArgs[3].elt = "NewInternalPort"; + AddPortMappingArgs[3].val = inPort; + AddPortMappingArgs[4].elt = "NewInternalClient"; + AddPortMappingArgs[4].val = inClient; + AddPortMappingArgs[5].elt = "NewEnabled"; + AddPortMappingArgs[5].val = "1"; + AddPortMappingArgs[6].elt = "NewPortMappingDescription"; + AddPortMappingArgs[6].val = desc?desc:"libminiupnpc"; + AddPortMappingArgs[7].elt = "NewLeaseDuration"; + AddPortMappingArgs[7].val = "0"; + simpleUPnPcommand(-1, controlURL, servicetype, "AddPortMapping", AddPortMappingArgs, buffer, &bufsize); + /*DisplayNameValueList(buffer, bufsize);*/ + /*buffer[bufsize] = '\0';*/ + /*puts(buffer);*/ + ParseNameValue(buffer, bufsize, &pdata); + resVal = GetValueFromNameValueList(&pdata, "errorCode"); + if(resVal) { + /*printf("AddPortMapping errorCode = '%s'\n", resVal); */ + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(resVal, "%d", &ret); + } else { + ret = UPNPCOMMAND_SUCCESS; + } + ClearNameValueList(&pdata); + free(AddPortMappingArgs); + return ret; +} + +int +UPNP_DeletePortMapping(const char * controlURL, const char * servicetype, + const char * extPort, const char * proto, + const char * remoteHost) +{ + /*struct NameValueParserData pdata;*/ + struct UPNParg * DeletePortMappingArgs; + char buffer[4096]; + int bufsize = 4096; + struct NameValueParserData pdata; + const char * resVal; + int ret; + + if(!extPort || !proto) + return UPNPCOMMAND_INVALID_ARGS; + + DeletePortMappingArgs = calloc(4, sizeof(struct UPNParg)); + DeletePortMappingArgs[0].elt = "NewRemoteHost"; + DeletePortMappingArgs[0].val = remoteHost; + DeletePortMappingArgs[1].elt = "NewExternalPort"; + DeletePortMappingArgs[1].val = extPort; + DeletePortMappingArgs[2].elt = "NewProtocol"; + DeletePortMappingArgs[2].val = proto; + simpleUPnPcommand(-1, controlURL, servicetype, + "DeletePortMapping", + DeletePortMappingArgs, buffer, &bufsize); + /*DisplayNameValueList(buffer, bufsize);*/ + ParseNameValue(buffer, bufsize, &pdata); + resVal = GetValueFromNameValueList(&pdata, "errorCode"); + if(resVal) { + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(resVal, "%d", &ret); + } else { + ret = UPNPCOMMAND_SUCCESS; + } + ClearNameValueList(&pdata); + free(DeletePortMappingArgs); + return ret; +} + +int UPNP_GetGenericPortMappingEntry(const char * controlURL, + const char * servicetype, + const char * index, + char * extPort, + char * intClient, + char * intPort, + char * protocol, + char * desc, + char * enabled, + char * rHost, + char * duration) +{ + struct NameValueParserData pdata; + struct UPNParg * GetPortMappingArgs; + char buffer[4096]; + int bufsize = 4096; + char * p; + int r = UPNPCOMMAND_UNKNOWN_ERROR; + if(!index) + return UPNPCOMMAND_INVALID_ARGS; + intClient[0] = '\0'; + intPort[0] = '\0'; + GetPortMappingArgs = calloc(2, sizeof(struct UPNParg)); + GetPortMappingArgs[0].elt = "NewPortMappingIndex"; + GetPortMappingArgs[0].val = index; + simpleUPnPcommand(-1, controlURL, servicetype, + "GetGenericPortMappingEntry", + GetPortMappingArgs, buffer, &bufsize); + ParseNameValue(buffer, bufsize, &pdata); + p = GetValueFromNameValueList(&pdata, "NewRemoteHost"); + if(p && rHost) + { + strncpy(rHost, p, 64); + rHost[63] = '\0'; + } + p = GetValueFromNameValueList(&pdata, "NewExternalPort"); + if(p && extPort) + { + strncpy(extPort, p, 6); + extPort[5] = '\0'; + r = UPNPCOMMAND_SUCCESS; + } + p = GetValueFromNameValueList(&pdata, "NewProtocol"); + if(p && protocol) + { + strncpy(protocol, p, 4); + protocol[3] = '\0'; + } + p = GetValueFromNameValueList(&pdata, "NewInternalClient"); + if(p && intClient) + { + strncpy(intClient, p, 16); + intClient[15] = '\0'; + r = 0; + } + p = GetValueFromNameValueList(&pdata, "NewInternalPort"); + if(p && intPort) + { + strncpy(intPort, p, 6); + intPort[5] = '\0'; + } + p = GetValueFromNameValueList(&pdata, "NewEnabled"); + if(p && enabled) + { + strncpy(enabled, p, 4); + enabled[3] = '\0'; + } + p = GetValueFromNameValueList(&pdata, "NewPortMappingDescription"); + if(p && desc) + { + strncpy(desc, p, 80); + desc[79] = '\0'; + } + p = GetValueFromNameValueList(&pdata, "NewLeaseDuration"); + if(p && duration) + { + strncpy(duration, p, 16); + duration[15] = '\0'; + } + p = GetValueFromNameValueList(&pdata, "errorCode"); + if(p) { + r = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(p, "%d", &r); + } + ClearNameValueList(&pdata); + free(GetPortMappingArgs); + return r; +} + +int UPNP_GetPortMappingNumberOfEntries(const char * controlURL, const char * servicetype, unsigned int * numEntries) +{ + struct NameValueParserData pdata; + char buffer[4096]; + int bufsize = 4096; + char* p; + int ret = UPNPCOMMAND_UNKNOWN_ERROR; + simpleUPnPcommand(-1, controlURL, servicetype, "GetPortMappingNumberOfEntries", 0, buffer, &bufsize); +#ifdef DEBUG + DisplayNameValueList(buffer, bufsize); +#endif + ParseNameValue(buffer, bufsize, &pdata); + + p = GetValueFromNameValueList(&pdata, "NewPortMappingNumberOfEntries"); + if(numEntries && p) { + *numEntries = 0; + sscanf(p, "%u", numEntries); + ret = UPNPCOMMAND_SUCCESS; + } + + p = GetValueFromNameValueList(&pdata, "errorCode"); + if(p) { + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(p, "%d", &ret); + } + + ClearNameValueList(&pdata); + return ret; +} + +/* UPNP_GetSpecificPortMappingEntry retrieves an existing port mapping + * the result is returned in the intClient and intPort strings + * please provide 16 and 6 bytes of data */ +int +UPNP_GetSpecificPortMappingEntry(const char * controlURL, + const char * servicetype, + const char * extPort, + const char * proto, + char * intClient, + char * intPort) +{ + struct NameValueParserData pdata; + struct UPNParg * GetPortMappingArgs; + char buffer[4096]; + int bufsize = 4096; + char * p; + int ret = UPNPCOMMAND_UNKNOWN_ERROR; + + if(!intPort || !intClient || !extPort || !proto) + return UPNPCOMMAND_INVALID_ARGS; + + GetPortMappingArgs = calloc(4, sizeof(struct UPNParg)); + GetPortMappingArgs[0].elt = "NewRemoteHost"; + GetPortMappingArgs[1].elt = "NewExternalPort"; + GetPortMappingArgs[1].val = extPort; + GetPortMappingArgs[2].elt = "NewProtocol"; + GetPortMappingArgs[2].val = proto; + simpleUPnPcommand(-1, controlURL, servicetype, + "GetSpecificPortMappingEntry", + GetPortMappingArgs, buffer, &bufsize); + /*fd = simpleUPnPcommand(fd, controlURL, data.servicetype, "GetSpecificPortMappingEntry", AddPortMappingArgs, buffer, &bufsize); */ + /*DisplayNameValueList(buffer, bufsize);*/ + ParseNameValue(buffer, bufsize, &pdata); + + p = GetValueFromNameValueList(&pdata, "NewInternalClient"); + if(p) { + strncpy(intClient, p, 16); + intClient[15] = '\0'; + ret = UPNPCOMMAND_SUCCESS; + } else + intClient[0] = '\0'; + + p = GetValueFromNameValueList(&pdata, "NewInternalPort"); + if(p) { + strncpy(intPort, p, 6); + intPort[5] = '\0'; + } else + intPort[0] = '\0'; + + p = GetValueFromNameValueList(&pdata, "errorCode"); + if(p) { + ret = UPNPCOMMAND_UNKNOWN_ERROR; + sscanf(p, "%d", &ret); + } + + ClearNameValueList(&pdata); + free(GetPortMappingArgs); + return ret; +} + + diff --git a/libportfwd/third-party/miniupnpc-20090605/upnpcommands.h b/libportfwd/third-party/miniupnpc-20090605/upnpcommands.h new file mode 100644 index 000000000..217c472be --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/upnpcommands.h @@ -0,0 +1,193 @@ +/* $Id: upnpcommands.h,v 1.17 2009/04/17 21:21:19 nanard Exp $ */ +/* Miniupnp project : http://miniupnp.free.fr/ + * Author : Thomas Bernard + * Copyright (c) 2005-2008 Thomas Bernard + * This software is subject to the conditions detailed in the + * LICENCE file provided within this distribution */ +#ifndef __UPNPCOMMANDS_H__ +#define __UPNPCOMMANDS_H__ + +#include "upnpreplyparse.h" +#include "declspec.h" + +/* MiniUPnPc return codes : */ +#define UPNPCOMMAND_SUCCESS (0) +#define UPNPCOMMAND_UNKNOWN_ERROR (-1) +#define UPNPCOMMAND_INVALID_ARGS (-2) + +#ifdef __cplusplus +extern "C" { +#endif + +#if (defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L) +#define UNSIGNED_INTEGER unsigned long long +#define STRTOUI strtoull +#else +#define UNSIGNED_INTEGER unsigned int +#define STRTOUI strtoul +#endif + +LIBSPEC UNSIGNED_INTEGER +UPNP_GetTotalBytesSent(const char * controlURL, + const char * servicetype); + +LIBSPEC UNSIGNED_INTEGER +UPNP_GetTotalBytesReceived(const char * controlURL, + const char * servicetype); + +LIBSPEC UNSIGNED_INTEGER +UPNP_GetTotalPacketsSent(const char * controlURL, + const char * servicetype); + +LIBSPEC UNSIGNED_INTEGER +UPNP_GetTotalPacketsReceived(const char * controlURL, + const char * servicetype); + +/* UPNP_GetStatusInfo() + * status and lastconnerror are 64 byte buffers + * Return values : + * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR + * or a UPnP Error code */ +LIBSPEC int +UPNP_GetStatusInfo(const char * controlURL, + const char * servicetype, + char * status, + unsigned int * uptime, + char * lastconnerror); + +/* UPNP_GetConnectionTypeInfo() + * argument connectionType is a 64 character buffer + * Return Values : + * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR + * or a UPnP Error code */ +LIBSPEC int +UPNP_GetConnectionTypeInfo(const char * controlURL, + const char * servicetype, + char * connectionType); + +/* UPNP_GetExternalIPAddress() call the corresponding UPNP method. + * if the third arg is not null the value is copied to it. + * at least 16 bytes must be available + * + * Return values : + * 0 : SUCCESS + * NON ZERO : ERROR Either an UPnP error code or an unknown error. + * + * possible UPnP Errors : + * 402 Invalid Args - See UPnP Device Architecture section on Control. + * 501 Action Failed - See UPnP Device Architecture section on Control. */ +LIBSPEC int +UPNP_GetExternalIPAddress(const char * controlURL, + const char * servicetype, + char * extIpAdd); + +/* UPNP_GetLinkLayerMaxBitRates() + * call WANCommonInterfaceConfig:1#GetCommonLinkProperties + * + * return values : + * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR + * or a UPnP Error Code. */ +LIBSPEC int +UPNP_GetLinkLayerMaxBitRates(const char* controlURL, + const char* servicetype, + unsigned int * bitrateDown, + unsigned int * bitrateUp); + +/* UPNP_AddPortMapping() + * if desc is NULL, it will be defaulted to "libminiupnpc" + * remoteHost is usually NULL because IGD don't support it. + * + * Return values : + * 0 : SUCCESS + * NON ZERO : ERROR. Either an UPnP error code or an unknown error. + * + * List of possible UPnP errors for AddPortMapping : + * errorCode errorDescription (short) - Description (long) + * 402 Invalid Args - See UPnP Device Architecture section on Control. + * 501 Action Failed - See UPnP Device Architecture section on Control. + * 715 WildCardNotPermittedInSrcIP - The source IP address cannot be + * wild-carded + * 716 WildCardNotPermittedInExtPort - The external port cannot be wild-carded + * 718 ConflictInMappingEntry - The port mapping entry specified conflicts + * with a mapping assigned previously to another client + * 724 SamePortValuesRequired - Internal and External port values + * must be the same + * 725 OnlyPermanentLeasesSupported - The NAT implementation only supports + * permanent lease times on port mappings + * 726 RemoteHostOnlySupportsWildcard - RemoteHost must be a wildcard + * and cannot be a specific IP address or DNS name + * 727 ExternalPortOnlySupportsWildcard - ExternalPort must be a wildcard and + * cannot be a specific port value */ +LIBSPEC int +UPNP_AddPortMapping(const char * controlURL, const char * servicetype, + const char * extPort, + const char * inPort, + const char * inClient, + const char * desc, + const char * proto, + const char * remoteHost); + +/* UPNP_DeletePortMapping() + * Use same argument values as what was used for AddPortMapping(). + * remoteHost is usually NULL because IGD don't support it. + * Return Values : + * 0 : SUCCESS + * NON ZERO : error. Either an UPnP error code or an undefined error. + * + * List of possible UPnP errors for DeletePortMapping : + * 402 Invalid Args - See UPnP Device Architecture section on Control. + * 714 NoSuchEntryInArray - The specified value does not exist in the array */ +LIBSPEC int +UPNP_DeletePortMapping(const char * controlURL, const char * servicetype, + const char * extPort, const char * proto, + const char * remoteHost); + +/* UPNP_GetPortMappingNumberOfEntries() + * not supported by all routers */ +LIBSPEC int +UPNP_GetPortMappingNumberOfEntries(const char* controlURL, const char* servicetype, unsigned int * num); + +/* UPNP_GetSpecificPortMappingEntry retrieves an existing port mapping + * the result is returned in the intClient and intPort strings + * please provide 16 and 6 bytes of data + * + * return value : + * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR + * or a UPnP Error Code. */ +LIBSPEC int +UPNP_GetSpecificPortMappingEntry(const char * controlURL, + const char * servicetype, + const char * extPort, + const char * proto, + char * intClient, + char * intPort); + +/* UPNP_GetGenericPortMappingEntry() + * + * return value : + * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR + * or a UPnP Error Code. + * + * Possible UPNP Error codes : + * 402 Invalid Args - See UPnP Device Architecture section on Control. + * 713 SpecifiedArrayIndexInvalid - The specified array index is out of bounds + */ +LIBSPEC int +UPNP_GetGenericPortMappingEntry(const char * controlURL, + const char * servicetype, + const char * index, + char * extPort, + char * intClient, + char * intPort, + char * protocol, + char * desc, + char * enabled, + char * rHost, + char * duration); + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/upnperrors.c b/libportfwd/third-party/miniupnpc-20090605/upnperrors.c new file mode 100644 index 000000000..8a05349b1 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/upnperrors.c @@ -0,0 +1,66 @@ +/* $Id: upnperrors.c,v 1.3 2008/04/27 17:21:51 nanard Exp $ */ +/* Project : miniupnp + * Author : Thomas BERNARD + * copyright (c) 2007 Thomas Bernard + * All Right reserved. + * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * This software is subjet to the conditions detailed in the + * provided LICENCE file. */ +#include +#include "upnperrors.h" +#include "upnpcommands.h" + +const char * strupnperror(int err) +{ + const char * s = NULL; + switch(err) { + case UPNPCOMMAND_SUCCESS: + s = "Success"; + break; + case UPNPCOMMAND_UNKNOWN_ERROR: + s = "Miniupnpc Unknown Error"; + break; + case UPNPCOMMAND_INVALID_ARGS: + s = "Miniupnpc Invalid Arguments"; + break; + case 401: + s = "Invalid Action"; + break; + case 402: + s = "Invalid Args"; + break; + case 501: + s = "Action Failed"; + break; + case 713: + s = "SpecifiedArrayIndexInvalid"; + break; + case 714: + s = "NoSuchEntryInArray"; + break; + case 715: + s = "WildCardNotPermittedInSrcIP"; + break; + case 716: + s = "WildCardNotPermittedInExtPort"; + break; + case 718: + s = "ConflictInMappingEntry"; + break; + case 724: + s = "SamePortValuesRequired"; + break; + case 725: + s = "OnlyPermanentLeasesSupported"; + break; + case 726: + s = "RemoteHostOnlySupportsWildcard"; + break; + case 727: + s = "ExternalPortOnlySupportsWildcard"; + break; + default: + s = NULL; + } + return s; +} diff --git a/libportfwd/third-party/miniupnpc-20090605/upnperrors.h b/libportfwd/third-party/miniupnpc-20090605/upnperrors.h new file mode 100644 index 000000000..2c544c97c --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/upnperrors.h @@ -0,0 +1,26 @@ +/* $Id: upnperrors.h,v 1.2 2008/07/02 23:31:15 nanard Exp $ */ +/* (c) 2007 Thomas Bernard + * All rights reserved. + * MiniUPnP Project. + * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * This software is subjet to the conditions detailed in the + * provided LICENCE file. */ +#ifndef __UPNPERRORS_H__ +#define __UPNPERRORS_H__ + +#include "declspec.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* strupnperror() + * Return a string description of the UPnP error code + * or NULL for undefinded errors */ +LIBSPEC const char * strupnperror(int err); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libportfwd/third-party/miniupnpc-20090605/upnpreplyparse.c b/libportfwd/third-party/miniupnpc-20090605/upnpreplyparse.c new file mode 100644 index 000000000..c72b4c825 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/upnpreplyparse.c @@ -0,0 +1,127 @@ +/* $Id: upnpreplyparse.c,v 1.10 2008/02/21 13:05:27 nanard Exp $ */ +/* MiniUPnP project + * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * (c) 2006 Thomas Bernard + * This software is subject to the conditions detailed + * in the LICENCE file provided within the distribution */ + +#include +#include +#include + +#include "upnpreplyparse.h" +#include "minixml.h" + +static void +NameValueParserStartElt(void * d, const char * name, int l) +{ + struct NameValueParserData * data = (struct NameValueParserData *)d; + if(l>63) + l = 63; + memcpy(data->curelt, name, l); + data->curelt[l] = '\0'; +} + +static void +NameValueParserGetData(void * d, const char * datas, int l) +{ + struct NameValueParserData * data = (struct NameValueParserData *)d; + struct NameValue * nv; + nv = malloc(sizeof(struct NameValue)); + if(l>63) + l = 63; + strncpy(nv->name, data->curelt, 64); + nv->name[63] = '\0'; + memcpy(nv->value, datas, l); + nv->value[l] = '\0'; + LIST_INSERT_HEAD( &(data->head), nv, entries); +} + +void +ParseNameValue(const char * buffer, int bufsize, + struct NameValueParserData * data) +{ + struct xmlparser parser; + LIST_INIT(&(data->head)); + /* init xmlparser object */ + parser.xmlstart = buffer; + parser.xmlsize = bufsize; + parser.data = data; + parser.starteltfunc = NameValueParserStartElt; + parser.endeltfunc = 0; + parser.datafunc = NameValueParserGetData; + parser.attfunc = 0; + parsexml(&parser); +} + +void +ClearNameValueList(struct NameValueParserData * pdata) +{ + struct NameValue * nv; + while((nv = pdata->head.lh_first) != NULL) + { + LIST_REMOVE(nv, entries); + free(nv); + } +} + +char * +GetValueFromNameValueList(struct NameValueParserData * pdata, + const char * Name) +{ + struct NameValue * nv; + char * p = NULL; + for(nv = pdata->head.lh_first; + (nv != NULL) && (p == NULL); + nv = nv->entries.le_next) + { + if(strcmp(nv->name, Name) == 0) + p = nv->value; + } + return p; +} + +#if 0 +/* useless now that minixml ignores namespaces by itself */ +char * +GetValueFromNameValueListIgnoreNS(struct NameValueParserData * pdata, + const char * Name) +{ + struct NameValue * nv; + char * p = NULL; + char * pname; + for(nv = pdata->head.lh_first; + (nv != NULL) && (p == NULL); + nv = nv->entries.le_next) + { + pname = strrchr(nv->name, ':'); + if(pname) + pname++; + else + pname = nv->name; + if(strcmp(pname, Name)==0) + p = nv->value; + } + return p; +} +#endif + +/* debug all-in-one function + * do parsing then display to stdout */ +#ifdef DEBUG +void +DisplayNameValueList(char * buffer, int bufsize) +{ + struct NameValueParserData pdata; + struct NameValue * nv; + ParseNameValue(buffer, bufsize, &pdata); + for(nv = pdata.head.lh_first; + nv != NULL; + nv = nv->entries.le_next) + { + printf("%s = %s\n", nv->name, nv->value); + } + ClearNameValueList(&pdata); +} +#endif + diff --git a/libportfwd/third-party/miniupnpc-20090605/upnpreplyparse.h b/libportfwd/third-party/miniupnpc-20090605/upnpreplyparse.h new file mode 100644 index 000000000..a92763431 --- /dev/null +++ b/libportfwd/third-party/miniupnpc-20090605/upnpreplyparse.h @@ -0,0 +1,62 @@ +/* $Id: upnpreplyparse.h,v 1.8 2008/02/21 13:05:27 nanard Exp $ */ +/* MiniUPnP project + * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ + * (c) 2006 Thomas Bernard + * This software is subject to the conditions detailed + * in the LICENCE file provided within the distribution */ + +#ifndef __UPNPREPLYPARSE_H__ +#define __UPNPREPLYPARSE_H__ + +#if defined(NO_SYS_QUEUE_H) || defined(WIN32) +#include "bsdqueue.h" +#else +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +struct NameValue { + LIST_ENTRY(NameValue) entries; + char name[64]; + char value[64]; +}; + +struct NameValueParserData { + LIST_HEAD(listhead, NameValue) head; + char curelt[64]; +}; + +/* ParseNameValue() */ +void +ParseNameValue(const char * buffer, int bufsize, + struct NameValueParserData * data); + +/* ClearNameValueList() */ +void +ClearNameValueList(struct NameValueParserData * pdata); + +/* GetValueFromNameValueList() */ +char * +GetValueFromNameValueList(struct NameValueParserData * pdata, + const char * Name); + +/* GetValueFromNameValueListIgnoreNS() */ +char * +GetValueFromNameValueListIgnoreNS(struct NameValueParserData * pdata, + const char * Name); + +/* DisplayNameValueList() */ +#ifdef DEBUG +void +DisplayNameValueList(char * buffer, int bufsize); +#endif + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/qxtweb-standalone/CMakeLists.txt b/qxtweb-standalone/CMakeLists.txt new file mode 100644 index 000000000..cbea35025 --- /dev/null +++ b/qxtweb-standalone/CMakeLists.txt @@ -0,0 +1,107 @@ +PROJECT(libqxtweb-standalone) +CMAKE_MINIMUM_REQUIRED(VERSION 2.8 FATAL_ERROR) +SET(CMAKE_VERBOSE_MAKEFILE ON) +SET(CMAKE_INSTALL_PREFIX ".") + +FIND_PACKAGE( Qt4 4.6.0 COMPONENTS QtCore QtNetwork REQUIRED ) +set(QT_USE_QTNETWORK TRUE) +include( ${QT_USE_FILE} ) + +SET(qxtweb "qxtweb") + +SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +ADD_DEFINITIONS(-Wall -O2 -DNDEBUG) +IF(NOT WIN32) + ADD_DEFINITIONS(-fPIC) +ENDIF() +ADD_DEFINITIONS( -DBUILD_QXT_CORE -DBUILD_QXT_WEB ) + + +INCLUDE_DIRECTORIES( ${qxtweb} ) + + +SET( sources + # QxtWeb: + ${qxtweb}/qxtabstracthttpconnector.cpp + ${qxtweb}/qxtabstractwebservice.cpp + ${qxtweb}/qxtabstractwebsessionmanager.cpp + ${qxtweb}/qxthtmltemplate.cpp + ${qxtweb}/qxthttpserverconnector.cpp + ${qxtweb}/qxthttpsessionmanager.cpp + ${qxtweb}/qxtscgiserverconnector.cpp + ${qxtweb}/qxtwebcontent.cpp + ${qxtweb}/qxtwebevent.cpp + ${qxtweb}/qxtwebservicedirectory.cpp + ${qxtweb}/qxtwebslotservice.cpp + ${qxtweb}/qxtwebcgiservice.cpp + + # Ripped bits of QxtCore: + ${qxtweb}/qxtmetaobject.cpp + ${qxtweb}/qxtnull.cpp +) + +SET( headers + # QxtWeb: + ${qxtweb}/qxtabstracthttpconnector.h + ${qxtweb}/qxtabstractwebservice.h + ${qxtweb}/qxtabstractwebsessionmanager.h + ${qxtweb}/qxtabstractwebsessionmanager_p.h +# ${qxtweb}/qxthtmltemplate.h + ${qxtweb}/qxthttpsessionmanager.h +# ${qxtweb}/qxtweb.h + ${qxtweb}/qxtwebcontent.h +# ${qxtweb}/qxtwebevent.h + ${qxtweb}/qxtwebservicedirectory.h + ${qxtweb}/qxtwebservicedirectory_p.h + ${qxtweb}/qxtwebslotservice.h + ${qxtweb}/qxtwebcgiservice.h + ${qxtweb}/qxtwebcgiservice_p.h + + # Ripped bits of QxtCore: +# ${qxtweb}/qxtmetaobject.h +# ${qxtweb}/qxtnullable.h +# ${qxtweb}/qxtnull.h + ${qxtweb}/qxtboundfunction.h +# ${qxtweb}/qxtboundfunctionbase.h +# ${qxtweb}/qxtboundcfunction.h +# ${qxtweb}/qxtmetatype.h +) + +qt4_wrap_cpp( mocstuff ${headers} ) + +# DLL on windows due to linker issues, otherwise static +IF(WIN32) + ADD_LIBRARY(qxtweb-standalone SHARED + ${mocstuff} + ${headers} + ${sources} + ) +ELSE() + ADD_LIBRARY(qxtweb-standalone STATIC + ${mocstuff} + ${headers} + ${sources} + ) +ENDIF() + + +target_link_libraries( qxtweb-standalone + ${QT_LIBRARIES} +) + +# Also build small example app from qxt demos: + +#qt4_wrap_cpp( mocex "example/myservice.h" ) + +#ADD_EXECUTABLE( example-webserver +# ${mocex} +# example/main.cpp +# example/myservice.h +# ) + +#TARGET_LINK_LIBRARIES( example-webserver +# ${QT_LIBRARIES} +# "${CMAKE_CURRENT_SOURCE_DIR}/libqxtweb-standalone.a" +# ) diff --git a/qxtweb-standalone/README.txt b/qxtweb-standalone/README.txt new file mode 100644 index 000000000..a114d0813 --- /dev/null +++ b/qxtweb-standalone/README.txt @@ -0,0 +1 @@ +QxtWeb from libqxt.org project, standalone and cmakeified. For embeddable webserver diff --git a/qxtweb-standalone/example/main.cpp b/qxtweb-standalone/example/main.cpp new file mode 100644 index 000000000..c7199bca4 --- /dev/null +++ b/qxtweb-standalone/example/main.cpp @@ -0,0 +1,22 @@ +#include + +#include "myservice.h" + + +int main(int argc, char ** argv){ + + QCoreApplication app(argc,argv); + + QxtHttpServerConnector connector; + + QxtHttpSessionManager session; + session.setPort(8080); + session.setConnector(&connector); + + MyService s1(&session); + session.setStaticContentService ( &s1); + + session.start(); + return app.exec(); +} + diff --git a/qxtweb-standalone/example/myservice.h b/qxtweb-standalone/example/myservice.h new file mode 100644 index 000000000..d52ec13c0 --- /dev/null +++ b/qxtweb-standalone/example/myservice.h @@ -0,0 +1,23 @@ +#ifndef MYSERVICE +#define MYSERVICE + +#include +#include +#include +#include + + +class MyService : public QxtWebSlotService{ + Q_OBJECT; +public: + MyService(QxtAbstractWebSessionManager * sm, QObject * parent = 0 ): QxtWebSlotService(sm,parent){ + } +public slots: + void index(QxtWebRequestEvent* event) + { + postEvent(new QxtWebPageEvent(event->sessionID, event->requestID, "

It Works!

")); + } +}; + +#endif + diff --git a/qxtweb-standalone/qxtweb/QxtAbstractHttpConnector b/qxtweb-standalone/qxtweb/QxtAbstractHttpConnector new file mode 100644 index 000000000..f845ea44a --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtAbstractHttpConnector @@ -0,0 +1,2 @@ +#include "qxtabstracthttpconnector.h" + diff --git a/qxtweb-standalone/qxtweb/QxtAbstractWebService b/qxtweb-standalone/qxtweb/QxtAbstractWebService new file mode 100644 index 000000000..a2d76ab29 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtAbstractWebService @@ -0,0 +1,2 @@ +#include "qxtabstractwebservice.h" + diff --git a/qxtweb-standalone/qxtweb/QxtAbstractWebSessionManager b/qxtweb-standalone/qxtweb/QxtAbstractWebSessionManager new file mode 100644 index 000000000..3247dce20 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtAbstractWebSessionManager @@ -0,0 +1,2 @@ +#include "qxtabstractwebsessionmanager.h" + diff --git a/qxtweb-standalone/qxtweb/QxtHtmlTemplate b/qxtweb-standalone/qxtweb/QxtHtmlTemplate new file mode 100644 index 000000000..8493b0860 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtHtmlTemplate @@ -0,0 +1,2 @@ +#include "qxthtmltemplate.h" + diff --git a/qxtweb-standalone/qxtweb/QxtHttpServerConnector b/qxtweb-standalone/qxtweb/QxtHttpServerConnector new file mode 100644 index 000000000..7d03e7b68 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtHttpServerConnector @@ -0,0 +1 @@ +#include "qxtabstracthttpconnector.h" diff --git a/qxtweb-standalone/qxtweb/QxtHttpSessionManager b/qxtweb-standalone/qxtweb/QxtHttpSessionManager new file mode 100644 index 000000000..55d2d00be --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtHttpSessionManager @@ -0,0 +1,2 @@ +#include "qxthttpsessionmanager.h" + diff --git a/qxtweb-standalone/qxtweb/QxtMail b/qxtweb-standalone/qxtweb/QxtMail new file mode 100644 index 000000000..024eba08f --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtMail @@ -0,0 +1,2 @@ +#include "qxtmail.h" + diff --git a/qxtweb-standalone/qxtweb/QxtScgiServerConnector b/qxtweb-standalone/qxtweb/QxtScgiServerConnector new file mode 100644 index 000000000..7d03e7b68 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtScgiServerConnector @@ -0,0 +1 @@ +#include "qxtabstracthttpconnector.h" diff --git a/qxtweb-standalone/qxtweb/QxtSendmail b/qxtweb-standalone/qxtweb/QxtSendmail new file mode 100644 index 000000000..074a6c107 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtSendmail @@ -0,0 +1,2 @@ +#include "qxtsendmail.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWeb b/qxtweb-standalone/qxtweb/QxtWeb new file mode 100644 index 000000000..bb110419d --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWeb @@ -0,0 +1 @@ +#include "qxtweb.h" diff --git a/qxtweb-standalone/qxtweb/QxtWebCgiService b/qxtweb-standalone/qxtweb/QxtWebCgiService new file mode 100644 index 000000000..edc82ad17 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebCgiService @@ -0,0 +1 @@ +#include "qxtwebcgiservice.h" diff --git a/qxtweb-standalone/qxtweb/QxtWebContent b/qxtweb-standalone/qxtweb/QxtWebContent new file mode 100644 index 000000000..657e7eec3 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebContent @@ -0,0 +1,2 @@ +#include "qxtwebcontent.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebErrorEvent b/qxtweb-standalone/qxtweb/QxtWebErrorEvent new file mode 100644 index 000000000..9f04f93f8 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebErrorEvent @@ -0,0 +1,2 @@ +#include "qxtwebevent.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebEvent b/qxtweb-standalone/qxtweb/QxtWebEvent new file mode 100644 index 000000000..9f04f93f8 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebEvent @@ -0,0 +1,2 @@ +#include "qxtwebevent.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebFileUploadEvent b/qxtweb-standalone/qxtweb/QxtWebFileUploadEvent new file mode 100644 index 000000000..c6ac30eb4 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebFileUploadEvent @@ -0,0 +1 @@ +#include "qxtwebevent.h" diff --git a/qxtweb-standalone/qxtweb/QxtWebPageEvent b/qxtweb-standalone/qxtweb/QxtWebPageEvent new file mode 100644 index 000000000..9f04f93f8 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebPageEvent @@ -0,0 +1,2 @@ +#include "qxtwebevent.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebRedirectEvent b/qxtweb-standalone/qxtweb/QxtWebRedirectEvent new file mode 100644 index 000000000..9f04f93f8 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebRedirectEvent @@ -0,0 +1,2 @@ +#include "qxtwebevent.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebRemoveCookieEvent b/qxtweb-standalone/qxtweb/QxtWebRemoveCookieEvent new file mode 100644 index 000000000..c6ac30eb4 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebRemoveCookieEvent @@ -0,0 +1 @@ +#include "qxtwebevent.h" diff --git a/qxtweb-standalone/qxtweb/QxtWebRequestEvent b/qxtweb-standalone/qxtweb/QxtWebRequestEvent new file mode 100644 index 000000000..9f04f93f8 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebRequestEvent @@ -0,0 +1,2 @@ +#include "qxtwebevent.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebServiceDirectory b/qxtweb-standalone/qxtweb/QxtWebServiceDirectory new file mode 100644 index 000000000..c2f20d518 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebServiceDirectory @@ -0,0 +1,2 @@ +#include "qxtwebservicedirectory.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebSlotService b/qxtweb-standalone/qxtweb/QxtWebSlotService new file mode 100644 index 000000000..d00698c37 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebSlotService @@ -0,0 +1,2 @@ +#include "qxtwebslotservice.h" + diff --git a/qxtweb-standalone/qxtweb/QxtWebStoreCookieEvent b/qxtweb-standalone/qxtweb/QxtWebStoreCookieEvent new file mode 100644 index 000000000..c6ac30eb4 --- /dev/null +++ b/qxtweb-standalone/qxtweb/QxtWebStoreCookieEvent @@ -0,0 +1 @@ +#include "qxtwebevent.h" diff --git a/qxtweb-standalone/qxtweb/qxtabstracthttpconnector.cpp b/qxtweb-standalone/qxtweb/qxtabstracthttpconnector.cpp new file mode 100644 index 000000000..374ae8b8e --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtabstracthttpconnector.cpp @@ -0,0 +1,249 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtAbstractHttpConnector + +\inmodule QxtWeb + +\brief The QxtAbstractHttpConnector class is a base class for defining +HTTP-based protocols for use with QxtHttpSessionManager + +QxtHttpSessionManager does the work of managing sessions and state for the +otherwise stateless HTTP protocol, but it relies on QxtAbstractHttpConnector +subclasses to implement the protocol used to communicate with the web server. + +Subclasses are responsible for accepting new connections (by implementing +listen(const QHostAddress&, quint16) and invoking addConnection(QIODevice*)), +for informing the session manager when request headers are available (by +implementing canParseRequest(const QByteArray&)), for parsing the request +headers (by implementing parseRequest(QByteArray&)), and for writing response +headers (by implementing writeHeaders(QIODevice*, const QHttpResponseHeader&)). + +\sa QxtHttpSessionManager +*/ + +#include "qxthttpsessionmanager.h" +#include "qxtwebcontent.h" +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN +class QxtAbstractHttpConnectorPrivate : public QxtPrivate +{ +public: + QxtHttpSessionManager* manager; + QReadWriteLock bufferLock, requestLock; + QHash buffers; // connection->buffer + QHash requests; // requestID->connection + QHash > dataSources; // iodevices providing result data + quint32 nextRequestID; + + inline quint32 getNextRequestID(QIODevice* connection) + { + QWriteLocker locker(&requestLock); + do + { + nextRequestID++; + if (nextRequestID == 0xFFFFFFFF) nextRequestID = 1; + } + while (requests.contains(nextRequestID)); // yeah, right + requests[nextRequestID] = connection; + return nextRequestID; + } + + inline void doneWithRequest(quint32 requestID) + { + QWriteLocker locker(&requestLock); + requests.remove(requestID); + dataSources.remove(requestID); + } + + inline QIODevice* getRequestConnection(quint32 requestID) + { + QReadLocker locker(&requestLock); + return requests[requestID]; + } + + inline void setRequestDataSource( quint32 requestID, QSharedPointer& dataSource ) + { + QWriteLocker locker(&requestLock); + dataSources.insert( requestID, dataSource ); + } + + inline QSharedPointer& getRequestDataSource(quint32 requestID) + { + QReadLocker locker(&requestLock); + return dataSources[requestID]; + } +}; +#endif + +/*! + * Creates a QxtAbstractHttpConnector with the specified \a parent. + * + * Note that this is an abstract class and cannot be instantiated directly. + */ +QxtAbstractHttpConnector::QxtAbstractHttpConnector(QObject* parent) : QObject(parent) +{ + QXT_INIT_PRIVATE(QxtAbstractHttpConnector); + qxt_d().nextRequestID = 0; +} + +/*! + * \internal + */ +void QxtAbstractHttpConnector::setSessionManager(QxtHttpSessionManager* manager) +{ + qxt_d().manager = manager; +} + +/*! + * Returns the session manager into which the connector is installed. + * + * \sa QxtHttpSessionManager::setConnector + */ +QxtHttpSessionManager* QxtAbstractHttpConnector::sessionManager() const +{ + return qxt_d().manager; +} + +/*! + * \internal + * Returns the QIODevice associated with a \a requestID. + * + * The request ID is generated internally and used by the session manager. + */ +QIODevice* QxtAbstractHttpConnector::getRequestConnection(quint32 requestID) +{ + return qxt_d().getRequestConnection(requestID); +} + +void QxtAbstractHttpConnector::setRequestDataSource( quint32 requestID, QSharedPointer& dataSource ) +{ + qxt_d().setRequestDataSource( requestID, dataSource ); +} + +QSharedPointer& QxtAbstractHttpConnector::getRequestDataSource(quint32 requestID) +{ + return qxt_d().getRequestDataSource( requestID ); +} + +void QxtAbstractHttpConnector::doneWithRequest(quint32 requestID) +{ + qxt_d().doneWithRequest( requestID ); +} + +/*! + * Starts managing a new connection from \a device. + * + * This function should be invoked by a subclass to attach incoming connections + * to the session manager. + */ +void QxtAbstractHttpConnector::addConnection(QIODevice* device) +{ + QWriteLocker locker(&qxt_d().bufferLock); + qxt_d().buffers[device] = QByteArray(); + QObject::connect(device, SIGNAL(readyRead()), this, SLOT(incomingData())); + QObject::connect(device, SIGNAL(aboutToClose()), this, SLOT(disconnected())); + QObject::connect(device, SIGNAL(disconnected()), this, SLOT(disconnected())); + QObject::connect(device, SIGNAL(destroyed()), this, SLOT(disconnected())); +} + +/*! + * \internal + */ +void QxtAbstractHttpConnector::incomingData(QIODevice* device) +{ + if (!device) + { + device = qobject_cast(sender()); + if (!device) return; + } + QReadLocker locker(&qxt_d().bufferLock); + QByteArray& buffer = qxt_d().buffers[device]; + buffer.append(device->readAll()); + if (!canParseRequest(buffer)) return; + QHttpRequestHeader header = parseRequest(buffer); + QxtWebContent* content = 0; + QByteArray start; + if (header.contentLength() > 0) + { + start = buffer.left(header.value("content-length").toInt()); + buffer = buffer.mid(header.value("content-length").toInt()); + content = new QxtWebContent(header.contentLength(), start, device); + } + else if (header.hasKey("connection") && header.value("connection").toLower() == "close") + { + start = buffer; + buffer.clear(); + content = new QxtWebContent(header.contentLength(), start, device); + } // else no content + quint32 requestID = qxt_d().getNextRequestID(device); + sessionManager()->incomingRequest(requestID, header, content); +} + +/*! + * \internal + */ +void QxtAbstractHttpConnector::disconnected() +{ + QIODevice* device = qobject_cast(sender()); + if (!device) return; + QWriteLocker locker(&qxt_d().bufferLock); + qxt_d().buffers.remove(device); + sessionManager()->disconnected(device); +} + +/*! + * \fn virtual bool QxtAbstractHttpConnector::listen(const QHostAddress& interface, quint16 port) + * Invoked by the session manager to indicate that the connector should listen + * for incoming connections on the specified \a interface and \a port. + * + * If the interface is QHostAddress::Any, the server will listen on all network interfaces. + * + * Returns true on success, or false if the server could not begin listening. + * + * \sa addConnection(QIODevice*) + */ + +/*! + * \fn virtual bool QxtAbstractHttpConnector::canParseRequest(const QByteArray& buffer) + * Returns true if a complete set of request headers can be extracted from the provided \a buffer. + */ + +/*! + * \fn virtual QHttpRequestHeader QxtAbstractHttpConnector::parseRequest(QByteArray& buffer) + * Extracts a set of request headers from the provided \a buffer. + * + * Subclasses implementing this function must be sure to remove the parsed data from the buffer. + */ + +/*! + * \fn virtual void QxtAbstractHttpConnector::writeHeaders(QIODevice* device, const QHttpResponseHeader& header) + * Writes a the response \a header to the specified \a device. + */ diff --git a/qxtweb-standalone/qxtweb/qxtabstracthttpconnector.h b/qxtweb-standalone/qxtweb/qxtabstracthttpconnector.h new file mode 100644 index 000000000..78aa1fb9b --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtabstracthttpconnector.h @@ -0,0 +1,121 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTABSTRACTHTTPCONNECTOR_H +#define QXTABSTRACTHTTPCONNECTOR_H + +#include "qxtglobal.h" +#include +#include +#include +#include + +QT_FORWARD_DECLARE_CLASS(QIODevice) +class QxtHttpSessionManager; + +class QxtAbstractHttpConnectorPrivate; +class QXT_WEB_EXPORT QxtAbstractHttpConnector : public QObject +{ + friend class QxtHttpSessionManager; + Q_OBJECT +public: + QxtAbstractHttpConnector(QObject* parent = 0); + virtual bool listen(const QHostAddress& iface, quint16 port) = 0; + +protected: + QxtHttpSessionManager* sessionManager() const; + + void addConnection(QIODevice* device); + QIODevice* getRequestConnection(quint32 requestID); + void setRequestDataSource( quint32 requestID, QSharedPointer& dataSource ); + QSharedPointer& getRequestDataSource(quint32 requestID); + void doneWithRequest(quint32 requestID); + virtual bool canParseRequest(const QByteArray& buffer) = 0; + virtual QHttpRequestHeader parseRequest(QByteArray& buffer) = 0; + virtual void writeHeaders(QIODevice* device, const QHttpResponseHeader& header) = 0; + +private Q_SLOTS: + void incomingData(QIODevice* device = 0); + void disconnected(); + +private: + void setSessionManager(QxtHttpSessionManager* manager); + QXT_DECLARE_PRIVATE(QxtAbstractHttpConnector) +}; + +class QxtHttpServerConnectorPrivate; +class QXT_WEB_EXPORT QxtHttpServerConnector : public QxtAbstractHttpConnector +{ + Q_OBJECT +public: + QxtHttpServerConnector(QObject* parent = 0); + virtual bool listen(const QHostAddress& iface, quint16 port); + +protected: + virtual bool canParseRequest(const QByteArray& buffer); + virtual QHttpRequestHeader parseRequest(QByteArray& buffer); + virtual void writeHeaders(QIODevice* device, const QHttpResponseHeader& header); + +private Q_SLOTS: + void acceptConnection(); + +private: + QXT_DECLARE_PRIVATE(QxtHttpServerConnector) +}; + +class QxtScgiServerConnectorPrivate; +class QXT_WEB_EXPORT QxtScgiServerConnector : public QxtAbstractHttpConnector +{ + Q_OBJECT +public: + QxtScgiServerConnector(QObject* parent = 0); + virtual bool listen(const QHostAddress& iface, quint16 port); + +protected: + virtual bool canParseRequest(const QByteArray& buffer); + virtual QHttpRequestHeader parseRequest(QByteArray& buffer); + virtual void writeHeaders(QIODevice* device, const QHttpResponseHeader& header); + +private Q_SLOTS: + void acceptConnection(); + +private: + QXT_DECLARE_PRIVATE(QxtScgiServerConnector) +}; +/* Commented out pending implementation + +class QxtFcgiConnectorPrivate; +class QXT_WEB_EXPORT QxtFcgiConnector : public QxtAbstractHttpConnector { +Q_OBJECT +public: + QxtFcgiConnector(QObject* parent = 0); + virtual bool listen(const QHostAddress& iface, quint16 port); + +private: + QXT_DECLARE_PRIVATE(QxtFcgiConnector) +}; +*/ + +#endif // QXTABSTRACTHTTPCONNECTOR_H diff --git a/qxtweb-standalone/qxtweb/qxtabstractwebservice.cpp b/qxtweb-standalone/qxtweb/qxtabstractwebservice.cpp new file mode 100644 index 000000000..ae64e3067 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtabstractwebservice.cpp @@ -0,0 +1,121 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtAbstractWebService + +\inmodule QxtWeb + +\brief The QxtAbstractWebService class is a base interface for web services + +QxtAbstractWebService provides a common interface for all web service classes. +It uses an event-driven design instead of the more traditional request-response +design used by many web scripting languages. When the user requests a web +page, the service receives a pageRequestedEvent; after the service assembles +the response, it must post a QxtWebPageEvent (or a subclass, such as +QxtWebRedirectEvent or QxtWebErrorEvent). + +Usually, an application providing web services will instantiate one +QxtAbstractWebService object for each session, but this is not a requirement. +For services that do not require session management, such as those that serve +only static content, the session factory may return the same pointer for +every invocation, or it may use some more exotic scheme. + +When using one service object per session, each service's data members are +independent and may be used to track state across requests. A service object +shared among multiple sessions will retain state across requests as well but +it must implement its own mechanism for separating non-shared data. + +The QxtWeb architecture is not multithreaded; that is, QxtAbstractWebService +does not automatically spawn a process for every session. However, +QxtAbstractWebSessionManager performs thread-safe event dispatching, so +subclasses of QxtAbstractWebService are free to create threads themselves. + +A web service object may delete itself (see QObject::deleteLater()) to end +the associated session. + +\sa QxtAbstractWebSessionManager::ServiceFactory +*/ + +/* + * TODO: + * The current architecture only allows for two behaviors: creating a new session + * for every connection without a session cookie, or not using sessions at all. + * This needs to be fixed by adding a function to the session manager to explicitly + * create a new session. + */ + +#include "qxtabstractwebservice.h" + +#ifndef QXT_DOXYGEN_RUN +class QxtAbstractWebServicePrivate : public QxtPrivate +{ +public: + QXT_DECLARE_PUBLIC(QxtAbstractWebService) + QxtAbstractWebServicePrivate() {} + + QxtAbstractWebSessionManager* manager; +}; +#endif + +/*! + * Creates a QxtAbstractWebService with the specified \a parent and session \a manager. + * + * Often, the session manager will also be the parent, but this is not a requirement. + * + * Note that this is an abstract class and cannot be instantiated directly. + */ +QxtAbstractWebService::QxtAbstractWebService(QxtAbstractWebSessionManager* manager, QObject* parent) : QObject(parent) +{ + QXT_INIT_PRIVATE(QxtAbstractWebService); + qxt_d().manager = manager; +} + +/*! + * Returns the session manager associated with the web service. + */ +QxtAbstractWebSessionManager* QxtAbstractWebService::sessionManager() const +{ + return qxt_d().manager; +} + +/*! + * \fn void QxtAbstractWebService::postEvent(QxtWebEvent* event) + * Posts an \a event to a web browser that has made a request to the service. + */ + +/*! + * \fn virtual void QxtAbstractWebService::pageRequestedEvent(QxtWebRequestEvent* event) + * This \a event handler must be reimplemented in subclasses to receive page + * request events. + * + * Every page request event received MUST be responded to with a QxtWebPageEvent + * or a QxtWebPageEvent subclass. This response does not have to be posted + * during the execution of this function, to support asynchronous design, but + * failure to post an event will cause the web browser making the request to + * wait until it times out. + * + * \sa QxtWebRequestEvent + */ diff --git a/qxtweb-standalone/qxtweb/qxtabstractwebservice.h b/qxtweb-standalone/qxtweb/qxtabstractwebservice.h new file mode 100644 index 000000000..98497819c --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtabstractwebservice.h @@ -0,0 +1,53 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTABSTRACTWEBSERVICE_H +#define QXTABSTRACTWEBSERVICE_H + +#include +#include "qxtabstractwebsessionmanager.h" +class QxtWebEvent; +class QxtWebRequestEvent; + +class QxtAbstractWebServicePrivate; +class QXT_WEB_EXPORT QxtAbstractWebService : public QObject +{ + Q_OBJECT +public: + explicit QxtAbstractWebService(QxtAbstractWebSessionManager* manager, QObject* parent = 0); + + QxtAbstractWebSessionManager* sessionManager() const; + inline void postEvent(QxtWebEvent* event) + { + sessionManager()->postEvent(event); + } + virtual void pageRequestedEvent(QxtWebRequestEvent* event) = 0; + // virtual void functionInvokedEvent(QxtWebRequestEvent* event) = 0; // todo: implement + +private: + QXT_DECLARE_PRIVATE(QxtAbstractWebService) +}; + +#endif // QXTABSTRACTWEBSERVICE_H diff --git a/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager.cpp b/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager.cpp new file mode 100644 index 000000000..260b63ab1 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager.cpp @@ -0,0 +1,201 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtAbstractWebSessionManager + +\inmodule QxtWeb + +\brief The QxtAbstractWebSessionManager class is a base class for QxtWeb session managers + +QxtAbstractWebSessionManager is the base class for all QxtWeb session managers. + +Session managers are responsible for managing connections between web browsers +and web services, for creating sessions and their corresponding service objects, +and for managing and dispatching events between browsers and services. + +Note that the session manager is not responsible for destroying service objects. +A service object that wishes to end its corresponding session may destroy itself +(see QObject::deleteLater()) and QxtAbstractWebSessionManager will automatically +clean up its internal session tracking data. + +\sa QxtAbstractWebService +*/ + +/*! + * \typedef QxtAbstractWebSessionManager::ServiceFactory + * \brief Pointer to a function that generates QxtAbstractWebService objects + * + * \bold TYPEDEF: The ServiceFactory type represents a pointer to a function that takes two + * parameters -- a QxtAbstractWebSessionManager* pointer and an int session ID. + * The function must return a QxtAbstractWebService* pointer. + * + * Usually, an application providing web services will instantiate one + * QxtAbstractWebService object for each session. For services that do not + * require session management, such as those that serve only static pages, a + * single service object may be shared for all requests, or it may use some + * more exotic scheme. See QxtAbstractWebService for more details. + */ + +#include "qxtabstractwebsessionmanager.h" +#include "qxtabstractwebsessionmanager_p.h" +#include "qxtabstractwebservice.h" +#include "qxtmetaobject.h" +#include + +#ifndef QXT_DOXYGEN_RUN +QxtAbstractWebSessionManagerPrivate::QxtAbstractWebSessionManagerPrivate() : factory(0), maxID(1) +{ + // initializers only +} + +void QxtAbstractWebSessionManagerPrivate::sessionDestroyed(int sessionID) +{ + if (sessions.contains(sessionID)) + { + freeList.enqueue(sessionID); + sessions.remove(sessionID); + } +} + +int QxtAbstractWebSessionManagerPrivate::getNextID() +{ + if (freeList.empty()) + { + int next = maxID; + maxID++; + return next; + } + return freeList.dequeue(); +} +#endif + +/*! + * Creates a QxtAbstractWebSessionManager with the specified \a parent. + * + * Note that this is an abstract class and cannot be instantiated directly. + */ +QxtAbstractWebSessionManager::QxtAbstractWebSessionManager(QObject* parent) : QObject(parent) +{ + QXT_INIT_PRIVATE(QxtAbstractWebSessionManager); +} + +/*! + * Sets the service \a factory for the session manager. + * + * The service factory is invoked every time the session manager creates a new + * session. Usually, an application providing web services will instantiate one + * QxtAbstractWebService object for each session. For services that do not + * require separate sessions, such as those that serve only static pages, the + * factory may return a pointer to the same object for multiple requests. + * + * \sa QxtAbstractWebSessionManager::ServiceFactory + */ +void QxtAbstractWebSessionManager::setServiceFactory(ServiceFactory* factory) +{ + qxt_d().factory = factory; +} + +/*! + * Returns the service factory in use by the session manager. + * + * \sa setServiceFactory(ServiceFactory*) + */ +QxtAbstractWebSessionManager::ServiceFactory* QxtAbstractWebSessionManager::serviceFactory() const +{ + return qxt_d().factory; +} + +/*! + * Returns the service object corresponding to the provided \a sessionID. + */ +QxtAbstractWebService* QxtAbstractWebSessionManager::session(int sessionID) const +{ + if (qxt_d().sessions.contains(sessionID)) + return qxt_d().sessions[sessionID]; + return 0; +} + +/*! + * Creates a new session and returns its session ID. + * + * This function uses the serviceFactory() to request an instance of the web service. + * \sa serviceFactory() + */ +int QxtAbstractWebSessionManager::createService() +{ + int sessionID = qxt_d().getNextID(); + if (!qxt_d().factory) return sessionID; + + QxtAbstractWebService* service = serviceFactory()(this, sessionID); + qxt_d().sessions[sessionID] = service; + // Using QxtBoundFunction to bind the sessionID to the slot invocation + QxtMetaObject::connect(service, SIGNAL(destroyed()), QxtMetaObject::bind(&qxt_d(), SLOT(sessionDestroyed(int)), Q_ARG(int, sessionID)), Qt::QueuedConnection); + return sessionID; // you can always get the service with this +} + +/*! + * \fn virtual bool QxtAbstractWebSessionManager::start() + * Starts the session manager. + * + * Session managers should not create sessions before start() is invoked. + * Subclasses are encouraged to refrain from accepting connections until the + * session manager is started. + */ + +/*! + * \fn virtual void QxtAbstractWebSessionManager::postEvent(QxtWebEvent* event) + * Adds the event to the event queue for its associated session. + * + * Since different protocols may require different event processing behavior, + * there is no default implementation in QxtAbstractWebSessionManager. Subclasses + * are responsible for maintaining event queues and deciding when and where to + * dispatch events. + * + * Depending on the subclass's implementation posted events may not be dispatched + * for some time, and is is possible that an event may never be dispatched if + * the session is terminated before the event is handled. + * + * \sa QxtWebEvent + */ + +/*! + * \fn virtual void QxtAbstractWebSessionManager::processEvents() + * Processes pending events for all sessions. + * + * Since different protocols may require different event processing behavior, + * there is no default implementation in QxtAbstractWebSessionManager. Subclasses + * are responsible for maintaining event queues and deciding when and where to + * dispatch events. + * + * processEvents() is not required to dispatch all events immediately. In + * particular, some events may require certain conditions to be met before + * they may be fully processed. (For example, because HTTP cookies are sent + * as response headers, QxtHttpServerConnector may not dispatch a + * QxtWebStoreCookieEvent until a QxtWebPageEvent for the same session is + * available.) Unprocessed events may remain in the event queue. + * + * \sa QxtWebEvent + */ diff --git a/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager.h b/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager.h new file mode 100644 index 000000000..8f040376a --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager.h @@ -0,0 +1,60 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTABSTRACTWEBSESSIONMANAGER_H +#define QXTABSTRACTWEBSESSIONMANAGER_H + +#include +#include +class QxtAbstractWebService; +class QxtWebEvent; + +class QxtAbstractWebSessionManagerPrivate; +class QXT_WEB_EXPORT QxtAbstractWebSessionManager : public QObject +{ + Q_OBJECT +public: + typedef QxtAbstractWebService* ServiceFactory(QxtAbstractWebSessionManager*, int); + + QxtAbstractWebSessionManager(QObject* parent = 0); + + virtual bool start() = 0; + virtual void postEvent(QxtWebEvent* event) = 0; + void setServiceFactory(ServiceFactory* factory); + ServiceFactory* serviceFactory() const; + + QxtAbstractWebService* session(int sessionID) const; + +protected: + int createService(); + +protected Q_SLOTS: + virtual void processEvents() = 0; + +private: + QXT_DECLARE_PRIVATE(QxtAbstractWebSessionManager) +}; + +#endif // QXTABSTRACTWEBSESSIONMANAGER_H diff --git a/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager_p.h b/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager_p.h new file mode 100644 index 000000000..b285e26da --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtabstractwebsessionmanager_p.h @@ -0,0 +1,55 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTABSTRACTWEBSESSIONMANAGER_P_H +#define QXTABSTRACTWEBSESSIONMANAGER_P_H + +#include +#include +#include +#include +#include "qxtabstractwebsessionmanager.h" + +#ifndef QXT_DOXYGEN_RUN +class QxtAbstractWebSessionManagerPrivate : public QObject, public QxtPrivate +{ + Q_OBJECT +public: + QxtAbstractWebSessionManagerPrivate(); + QXT_DECLARE_PUBLIC(QxtAbstractWebSessionManager) + + QxtAbstractWebSessionManager::ServiceFactory* factory; + QHash sessions; + QQueue freeList; + int maxID; + + int getNextID(); + +public Q_SLOTS: + void sessionDestroyed(int sessionID); +}; +#endif // QXT_DOXYGEN_RUN + +#endif // QXTABSTRACTWEBSESSIONMANAGER_P_H diff --git a/qxtweb-standalone/qxtweb/qxtboundcfunction.h b/qxtweb-standalone/qxtweb/qxtboundcfunction.h new file mode 100644 index 000000000..3e49142dc --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtboundcfunction.h @@ -0,0 +1,633 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTBOUNDCFUNCTION_H +#define QXTBOUNDCFUNCTION_H + +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN + +#define QXT_RETURN(fp) *reinterpret_cast(returnValue.data()) = (*reinterpret_cast(fp)) +#define QXT_INVOKE(fp) (*reinterpret_cast(fp)) +#define QXT_PARAM(i) *reinterpret_cast(p ## i .data()) + +template < typename RETURN, typename T1 = void, typename T2 = void, typename T3 = void, typename T4 = void, typename T5 = void, +typename T6 = void, typename T7 = void, typename T8 = void, typename T9 = void, typename T10 = void > +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7), QXT_PARAM(8), QXT_PARAM(9), QXT_PARAM(10)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3, T4); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3, T4, T5); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3, T4, T5, T6); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7)); + return true; + } +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7, T8); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7), QXT_PARAM(8)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction_return : public QxtGenericFunctionPointer +{ +public: + typedef RETURN(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7, T8, T9); + bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_RETURN(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7), QXT_PARAM(8), QXT_PARAM(9)); + return true; + } +private: + qxt_cfunction_return(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template < typename T1 = void, typename T2 = void, typename T3 = void, typename T4 = void, typename T5 = void, +typename T6 = void, typename T7 = void, typename T8 = void, typename T9 = void, typename T10 = void > +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7), QXT_PARAM(8), QXT_PARAM(9), QXT_PARAM(10)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template <> +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3, T4); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3, T4, T5); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3, T4, T5, T6); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7, T8); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7), QXT_PARAM(8)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template +class /*QXT_CORE_EXPORT*/ qxt_cfunction : public QxtGenericFunctionPointer +{ +public: + typedef void(*FUNCTION)(T1, T2, T3, T4, T5, T6, T7, T8, T9); + bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + QXT_10_UNUSED; + QXT_INVOKE(funcPtr)(QXT_PARAM(1), QXT_PARAM(2), QXT_PARAM(3), QXT_PARAM(4), QXT_PARAM(5), QXT_PARAM(6), QXT_PARAM(7), QXT_PARAM(8), QXT_PARAM(9)); + return true; + } +private: + qxt_cfunction(voidFunc* ptr, const QByteArray& typeIdName) : QxtGenericFunctionPointer(ptr, typeIdName) {} +}; + +template < typename RETURN = void, typename T1 = void, typename T2 = void, typename T3 = void, typename T4 = void, typename T5 = void, +typename T6 = void, typename T7 = void, typename T8 = void, typename T9 = void, typename T10 = void > +class /*QXT_CORE_EXPORT*/ QxtBoundCFunction : public QxtBoundFunctionBase +{ +public: + QxtGenericFunctionPointer funcPtr; + + QxtBoundCFunction(QObject* parent, QxtGenericFunctionPointer funcPointer, QGenericArgument* params[10], QByteArray types[10]) : QxtBoundFunctionBase(parent, params, types), funcPtr(funcPointer) + { + // initializers only, thanks to template magic + } + + virtual bool invokeImpl(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_IMPL_10ARGS(QGenericArgument)) + { + if (type != Qt::AutoConnection && type != Qt::DirectConnection) + { + qWarning() << "QxtBoundCFunction::invoke: Cannot invoke non-Qt functions using a queued connection"; + return false; + } + return reinterpret_cast*>(&funcPtr)->invoke(returnValue, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + } +}; + +template +class /*QXT_CORE_EXPORT*/ QxtBoundCFunction : public QxtBoundFunctionBase +{ +public: + QxtGenericFunctionPointer funcPtr; + + QxtBoundCFunction(QObject* parent, QxtGenericFunctionPointer funcPointer, QGenericArgument* params[10], QByteArray types[10]) : QxtBoundFunctionBase(parent, params, types), funcPtr(funcPointer) + { + // initializers only, thanks to template magic + } + + virtual bool invokeImpl(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_IMPL_10ARGS(QGenericArgument)) + { + Q_UNUSED(returnValue); + if (type != Qt::AutoConnection && type != Qt::DirectConnection) + { + qWarning() << "QxtBoundCFunction::invoke: Cannot invoke non-Qt functions using a queued connection"; + return false; + } + return reinterpret_cast*>(&funcPtr)->invoke(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + } +}; + +#undef QXT_RETURN +#undef QXT_INVOKE +#undef QXT_PARAM +#endif + +namespace QxtMetaObject +{ + /*! + * \relates QxtMetaObject + * \sa QxtMetaObject::connect + * \sa qxtFuncPtr + * \sa QxtBoundFunction + * \sa QXT_BIND + * + * Creates a binding to the provided C/C++ function using the provided parameter list. + * Use the qxtFuncPtr function to wrap a bare function pointer for use in this function. + * Use the Q_ARG macro to specify constant parameters, or use the QXT_BIND macro to + * relay a parameter from a connected signal or passed via the QxtBoundFunction::invoke() + * method. + * + * The first template parameter must match the return type of the function, or + * void if the function does not return a value. The remaining template parameters must + * match the types of the function's parameters. If any type does not match, this + * function returns NULL. + * + * The returned QxtBoundFunction will not have a parent. Assigning a parent using + * QObject::setParent() is strongly recommended to avoid memory leaks. + */ + template + QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QXT_IMPL_10ARGS(QGenericArgument)) + { + // Make sure the template parameters make a function pointer equivalent to the one passed in + if (funcPointer.typeName != typeid(typename qxt_cfunction_return::FUNCTION).name()) + { + qWarning() << "QxtMetaObject::bind: parameter list mismatch, check template arguments"; + return 0; + } + + QGenericArgument* args[10] = { &p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8, &p9, &p10 }; + for (int i = 0; i < 10; i++) + { + if (args[i]->name() == 0) break; // done + if (QByteArray(args[i]->name()) == "QxtBoundArgument") + { + Q_ASSERT_X((quintptr)(args[i]->data()) > 0 && (quintptr)(args[i]->data()) <= 10, "QXT_BIND", "invalid argument number"); + } + } + + QByteArray types[10]; + types[0] = QxtMetaType::name(); + types[1] = QxtMetaType::name(); + types[2] = QxtMetaType::name(); + types[3] = QxtMetaType::name(); + types[4] = QxtMetaType::name(); + types[5] = QxtMetaType::name(); + types[6] = QxtMetaType::name(); + types[7] = QxtMetaType::name(); + types[8] = QxtMetaType::name(); + types[9] = QxtMetaType::name(); + + return new QxtBoundCFunction(0, funcPointer, args, types); + } + + /*! + * \relates QxtMetaObject + * \sa QxtMetaObject::connect + * \sa qxtFuncPtr + * \sa QxtBoundFunction + * + * Creates a binding to the provided C/C++ function using the provided parameter list. + * Use the qxtFuncPtr function to wrap a bare function pointer for use in this function. + * The type of each argument is deduced from the type of the QVariant. This function + * cannot bind positional arguments; see the overload using QGenericArgument. + * + * The first template parameter must match the return type of the function, or + * void if the function does not return a value. The remaining template parameters must + * match the types of the function's parameters. If any type does not match, this + * function returns NULL. + * + * The returned QxtBoundFunction will not have a parent. Assigning a parent using + * QObject::setParent() is strongly recommended to avoid memory leaks. + */ + template + QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QXT_IMPL_10ARGS(QVariant)) + { + QVariant* args[10] = { &p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8, &p9, &p10 }; + return QxtMetaObject::bind(funcPointer, QXT_VAR_ARG(1), QXT_VAR_ARG(2), QXT_VAR_ARG(3), QXT_VAR_ARG(4), + QXT_VAR_ARG(5), QXT_VAR_ARG(6), QXT_VAR_ARG(7), QXT_VAR_ARG(8), QXT_VAR_ARG(9), QXT_VAR_ARG(10)); + } + +// The following overloads exist because C++ doesn't support default parameters in function templates +#ifndef QXT_DOXYGEN_RUN + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer) + { + return bind(funcPointer, + QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1) + { + return bind(funcPointer, + p1, QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2) + { + return bind(funcPointer, + p1, p2, QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2, QGenericArgument p3) + { + return bind(funcPointer, + p1, p2, p3, QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2, QGenericArgument p3, QGenericArgument p4) + { + return bind(funcPointer, + p1, p2, p3, p4, QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2, QGenericArgument p3, QGenericArgument p4, QGenericArgument p5) + { + return bind(funcPointer, + p1, p2, p3, p4, p5, QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2, QGenericArgument p3, QGenericArgument p4, QGenericArgument p5, QGenericArgument p6) + { + return bind(funcPointer, + p1, p2, p3, p4, p5, p6, QGenericArgument(), QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2, QGenericArgument p3, QGenericArgument p4, QGenericArgument p5, QGenericArgument p6, QGenericArgument p7) + { + return bind(funcPointer, + p1, p2, p3, p4, p5, p6, p7, QGenericArgument(), QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2, QGenericArgument p3, QGenericArgument p4, QGenericArgument p5, + QGenericArgument p6, QGenericArgument p7, QGenericArgument p8) + { + return bind(funcPointer, p1, p2, p3, p4, p5, p6, p7, p8, QGenericArgument(), QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QGenericArgument p1, QGenericArgument p2, QGenericArgument p3, QGenericArgument p4, QGenericArgument p5, + QGenericArgument p6, QGenericArgument p7, QGenericArgument p8, QGenericArgument p9) + { + return bind(funcPointer, p1, p2, p3, p4, p5, p6, p7, p8, p9, QGenericArgument()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1) + { + return bind(funcPointer, p1, QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2) + { + return bind(funcPointer, p1, p2, QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2, QVariant p3) + { + return bind(funcPointer, p1, p2, p3, QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2, QVariant p3, QVariant p4) + { + return bind(funcPointer, p1, p2, p3, p4, QVariant(), QVariant(), QVariant(), QVariant(), QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2, QVariant p3, QVariant p4, QVariant p5) + { + return bind(funcPointer, p1, p2, p3, p4, p5, QVariant(), QVariant(), QVariant(), QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2, QVariant p3, QVariant p4, QVariant p5, QVariant p6) + { + return bind(funcPointer, p1, p2, p3, p4, p5, p6, QVariant(), QVariant(), QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2, QVariant p3, QVariant p4, QVariant p5, QVariant p6, QVariant p7) + { + return bind(funcPointer, p1, p2, p3, p4, p5, p6, p7, QVariant(), QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2, QVariant p3, QVariant p4, QVariant p5, QVariant p6, QVariant p7, QVariant p8) + { + return bind(funcPointer, p1, p2, p3, p4, p5, p6, p7, p8, QVariant(), QVariant()); + } + + template + inline QxtBoundFunction* bind(QxtGenericFunctionPointer funcPointer, QVariant p1, QVariant p2, QVariant p3, QVariant p4, QVariant p5, QVariant p6, QVariant p7, QVariant p8, QVariant p9) + { + return bind(funcPointer, p1, p2, p3, p4, p5, p6, p7, p8, p9, QVariant()); + } +#endif +} +#endif diff --git a/qxtweb-standalone/qxtweb/qxtboundfunction.h b/qxtweb-standalone/qxtweb/qxtboundfunction.h new file mode 100644 index 000000000..f2255c419 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtboundfunction.h @@ -0,0 +1,264 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTBOUNDFUNCTION_H +#define QXTBOUNDFUNCTION_H + +#include +#include +#include +#include +#include +#include +#include + +/*! +\class QxtBoundFunction + +\inmodule QxtCore + +\brief Binds parameters to a function call + + * A bound function is very similar to what the C++ FAQ Lite refers to as "functionoids." + * (http://www.parashift.com/c++-faq-lite/pointers-to-members.html#faq-33.10) + * It is similar in use to a function pointer, but allows any or all parameters to be + * pre-filled with constant values. The remaining parameters are specified when the + * function is invoked, for instance, by a Qt signal connection. + * + * By far, the most common expected use is to provide a parameter to a slot when the + * signal doesn't have offer one. Many developers new to Qt try to write code like this: + * \code + * connect(button, SIGNAL(clicked()), lineEdit, SLOT(setText("Hello, world"))); + * \endcode + * Experienced Qt developers will immediately spot the flaw here. The typical solution + * is to create a short, one-line wrapper slot that invokes the desired function. Some + * clever developers may even use QSignalMapper to handle slots that only need one + * int or QString parameter. + * + * QxtBoundFunction enables the previous connect statement to be written like this: + * \code + * connect(button, SIGNAL(clicked()), QxtMetaObject::bind(lineEdit, SLOT(setText(QString)), Q_ARG(QString, "Hello, world!"))); + * \code + * This accomplishes the same result without having to create a new slot, or worse, + * an entire object, just to pass a constant value. + * + * Additionally, through the use of the QXT_BIND macro, parameters from the signal + * can be rearranged, skipped, or passed alongside constant arguments provided + * with the Q_ARG macro. This can be used to provide stateful callbacks to a + * generic function, for example. + * + * Many kinds of functions can be bound. The most common binding applies to + * Qt signals and slots, but standard C/C++ functions can be bound as well. + * Future development may add the ability to bind to C++ member functions, + * and developers can make custom QxtBoundFunction subclasses for even more + * flexibility if necessary. + * + * + */ +class QXT_CORE_EXPORT QxtBoundFunction : public QObject +{ + Q_OBJECT +public: + /*! + * Invokes the bound function and returns a value. + * + * The template parameter should be the return type of the invoked function. This overload accepts + * QVariant parameters and will guess the data type of each parameter based on the type of the QVariant. + */ + template + inline QxtNullable invoke(QXT_PROTO_10ARGS(QVariant)) + { + if (!parent() || QThread::currentThread() == parent()->thread()) + return invoke(Qt::DirectConnection, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); +#if QT_VERSION >= 0x040300 + return invoke(Qt::BlockingQueuedConnection, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); +#else + qWarning() << "QxtBoundFunction::invoke: Cannot return a value using a queued connection"; + return QxtNull(); +#endif + } + + /*! + * Invokes the bound function and returns a value. + * + * The template parameter should be the return type of the invoked function. This overload accepts + * QGenericArgument parameters, expressed using the Q_ARG() macro. + */ + template + QxtNullable invoke(Qt::ConnectionType type, QVariant p1, QXT_PROTO_9ARGS(QVariant)) + { + if (type == Qt::QueuedConnection) + { + qWarning() << "QxtBoundFunction::invoke: Cannot return a value using a queued connection"; + return QxtNull(); + } + T retval; + // I know this is a totally ugly function call + if (invoke(type, QGenericReturnArgument(qVariantFromValue(*reinterpret_cast(0)).typeName(), reinterpret_cast(&retval)), + p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)) + { + return retval; + } + else + { + return QxtNull(); + } + } + + /*! + * Invokes the bound function, discarding the return value. + * + * This overload accepts QVariant parameters and will guess the data type of each + * parameter based on the type of the QVariant. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + inline bool invoke(QVariant p1, QXT_PROTO_9ARGS(QVariant)) + { + return invoke(Qt::AutoConnection, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + } + /*! + * Invokes the bound function, discarding the return value. + * + * This overload accepts QVariant parameters and will guess the data type of each + * parameter based on the type of the QVariant. It also allows you to specify the + * connection type, allowing the bound function to be invoked across threads using + * the Qt event loop. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + bool invoke(Qt::ConnectionType, QVariant p1, QXT_PROTO_9ARGS(QVariant)); + + /*! + * Invokes the bound function, discarding the return value. + * + * This overload accepts QGenericArgument parameters, expressed using the Q_ARG() + * macro. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + inline bool invoke(QXT_PROTO_10ARGS(QGenericArgument)) + { + return invoke(Qt::AutoConnection, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + } + /*! + * Invokes the bound function, discarding the return value. + * + * This overload accepts QGenericArgument parameters, expressed using the Q_ARG() + * macro. It also allows you to specify the connection type, allowing the bound + * function to be invoked across threads using the Qt event loop. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + inline bool invoke(Qt::ConnectionType type, QXT_PROTO_10ARGS(QGenericArgument)) + { + return invoke(type, QGenericReturnArgument(), p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + } + + /*! + * Invokes the bound function and assigns the return value to a parameter passed by reference. + * + * Use the Q_RETURN_ARG() macro to pass a reference to an assignable object of the function's + * return type. When the function completes, its return value will be stored in that object. + * + * This overload accepts QVariant parameters and will guess the data type of each + * parameter based on the type of the QVariant. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + inline bool invoke(QGenericReturnArgument returnValue, QVariant p1, QXT_PROTO_9ARGS(QVariant)) + { + return invoke(Qt::AutoConnection, returnValue, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + } + /*! + * Invokes the bound function and assigns the return value to a parameter passed by reference. + * + * Use the Q_RETURN_ARG() macro to pass a reference to an assignable object of the function's + * return type. When the function completes, its return value will be stored in that object. + * + * This overload accepts QVariant parameters and will guess the data type of each + * parameter based on the type of the QVariant. It also allows you to specify the + * connection type, allowing the bound function to be invoked across threads using + * the Qt event loop. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + bool invoke(Qt::ConnectionType type, QGenericReturnArgument returnValue, QVariant p1, QXT_PROTO_9ARGS(QVariant)); + + /*! + * Invokes the bound function and assigns the return value to a parameter passed by reference. + * + * Use the Q_RETURN_ARG() macro to pass a reference to an assignable object of the function's + * return type. When the function completes, its return value will be stored in that object. + * + * This overload accepts QGenericArgument parameters, expressed using the Q_ARG() + * macro. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + inline bool invoke(QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) + { + return invoke(Qt::AutoConnection, returnValue, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); + } + /*! + * Invokes the bound function and assigns the return value to a parameter passed by reference. + * + * Use the Q_RETURN_ARG() macro to pass a reference to an assignable object of the function's + * return type. When the function completes, its return value will be stored in that object. + * + * This overload accepts QGenericArgument parameters, expressed using the Q_ARG() + * macro. It also allows you to specify the connection type, allowing the bound + * function to be invoked across threads using the Qt event loop. + * + * This function returns true if the invocation was successful, otherwise it + * returns false. + */ + bool invoke(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)); + +protected: +#ifndef QXT_DOXYGEN_RUN + QxtBoundFunction(QObject* parent = 0); +#endif + + /*! + * Performs the work of invoking the bound function. + * + * This function is pure virtual. The various QxtMetaObject::bind() functions return opaque subclasses + * of QxtBoundFunction. If you wish to create a new kind of bound function, reimplement this function to + * perform the invocation and assign the function's return value, if any, to the returnValue parameter. + * + * This function should return true if the invocation is successful and false if an error occurs. + */ + virtual bool invokeImpl(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)) = 0; +}; + +#endif diff --git a/qxtweb-standalone/qxtweb/qxtboundfunctionbase.h b/qxtweb-standalone/qxtweb/qxtboundfunctionbase.h new file mode 100644 index 000000000..47da5f1d9 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtboundfunctionbase.h @@ -0,0 +1,61 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +// This file exists for the convenience of QxtBoundCFunction. +// It is not part of the public API and is subject to change. +// +// We mean it. + +#ifndef QXTBOUNDFUNCTIONBASE_H +#define QXTBOUNDFUNCTIONBASE_H + +#include +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN + +#define QXT_10_UNUSED Q_UNUSED(p1) Q_UNUSED(p2) Q_UNUSED(p3) Q_UNUSED(p4) Q_UNUSED(p5) Q_UNUSED(p6) Q_UNUSED(p7) Q_UNUSED(p8) Q_UNUSED(p9) Q_UNUSED(p10) + +class QXT_CORE_EXPORT QxtBoundFunctionBase : public QxtBoundFunction +{ +public: + QByteArray bindTypes[10]; + QGenericArgument arg[10], p[10]; + void* data[10]; + + QxtBoundFunctionBase(QObject* parent, QGenericArgument* params[10], QByteArray types[10]); + virtual ~QxtBoundFunctionBase(); + + int qt_metacall(QMetaObject::Call _c, int _id, void **_a); + bool invokeBase(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_PROTO_10ARGS(QGenericArgument)); +}; + +#define QXT_ARG(i) ((argCount>i)?QGenericArgument(p ## i .typeName(), p ## i .constData()):QGenericArgument()) +#define QXT_VAR_ARG(i) (p ## i .isValid())?QGenericArgument(p ## i .typeName(), p ## i .constData()):QGenericArgument() +#endif +#endif diff --git a/qxtweb-standalone/qxtweb/qxtglobal.cpp b/qxtweb-standalone/qxtweb/qxtglobal.cpp new file mode 100644 index 000000000..fe7b9e782 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtglobal.cpp @@ -0,0 +1,241 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#include "qxtglobal.h" + +/*! + \headerfile + \title Global Qxt Declarations + \inmodule QxtCore + + \brief The header provides basic declarations and + is included by all other Qxt headers. + */ + +/*! + \macro QXT_VERSION + \relates + + This macro expands a numeric value of the form 0xMMNNPP (MM = + major, NN = minor, PP = patch) that specifies Qxt's version + number. For example, if you compile your application against Qxt + 0.4.0, the QXT_VERSION macro will expand to 0x000400. + + You can use QXT_VERSION to use the latest Qt features where + available. For example: + \code + #if QXT_VERSION >= 0x000400 + qxtTabWidget->setTabMovementMode(QxtTabWidget::InPlaceMovement); + #endif + \endcode + + \sa QXT_VERSION_STR, qxtVersion() + */ + +/*! + \macro QXT_VERSION_STR + \relates + + This macro expands to a string that specifies Qxt's version number + (for example, "0.4.0"). This is the version against which the + application is compiled. + + \sa qxtVersion(), QXT_VERSION + */ + +/*! + \relates + + Returns the version number of Qxt at run-time as a string (for + example, "0.4.0"). This may be a different version than the + version the application was compiled against. + + \sa QXT_VERSION_STR + */ +const char* qxtVersion() +{ + return QXT_VERSION_STR; +} + +/*! +\headerfile +\title The Qxt private implementation +\inmodule QxtCore + +\brief The header provides tools for hiding +details of a class. + +Application code generally doesn't have to be concerned about hiding its +implementation details, but when writing library code it is important to +maintain a constant interface, both source and binary. Maintaining a constant +source interface is easy enough, but keeping the binary interface constant +means moving implementation details into a private class. The PIMPL, or +d-pointer, idiom is a common method of implementing this separation. QxtPimpl +offers a convenient way to connect the public and private sides of your class. + +\section1 Getting Started +Before you declare the public class, you need to make a forward declaration +of the private class. The private class must have the same name as the public +class, followed by the word Private. For example, a class named MyTest would +declare the private class with: +\code +class MyTestPrivate; +\endcode + +\section1 The Public Class +Generally, you shouldn't keep any data members in the public class without a +good reason. Functions that are part of the public interface should be declared +in the public class, and functions that need to be available to subclasses (for +calling or overriding) should be in the protected section of the public class. +To connect the private class to the public class, include the +QXT_DECLARE_PRIVATE macro in the private section of the public class. In the +example above, the private class is connected as follows: +\code +private: + QXT_DECLARE_PRIVATE(MyTest) +\endcode + +Additionally, you must include the QXT_INIT_PRIVATE macro in the public class's +constructor. Continuing with the MyTest example, your constructor might look +like this: +\code +MyTest::MyTest() { + // initialization + QXT_INIT_PRIVATE(MyTest); +} +\endcode + +\section1 The Private Class +As mentioned above, data members should usually be kept in the private class. +This allows the memory layout of the private class to change without breaking +binary compatibility for the public class. Functions that exist only as +implementation details, or functions that need access to private data members, +should be implemented here. + +To define the private class, inherit from the template QxtPrivate class, and +include the QXT_DECLARE_PUBLIC macro in its public section. The template +parameter should be the name of the public class. For example: +\code +class MyTestPrivate : public QxtPrivate { +public: + MyTestPrivate(); + QXT_DECLARE_PUBLIC(MyTest) +}; +\endcode + +\section1 Accessing Private Members +Use the qxt_d() function (actually a function-like object) from functions in +the public class to access the private class. Similarly, functions in the +private class can invoke functions in the public class by using the qxt_p() +function (this one's actually a function). + +For example, assume that MyTest has methods named getFoobar and doBaz(), +and MyTestPrivate has a member named foobar and a method named doQuux(). +The code might resemble this example: +\code +int MyTest::getFoobar() { + return qxt_d().foobar; +} + +void MyTestPrivate::doQuux() { + qxt_p().doBaz(foobar); +} +\endcode +*/ + +/*! + * \macro QXT_DECLARE_PRIVATE(PUB) + * \relates + * Declares that a public class has a related private class. + * + * This shuold be put in the private section of the public class. The parameter is the name of the public class. + */ + +/*! + * \macro QXT_DECLARE_PUBLIC(PUB) + * \relates + * Declares that a private class has a related public class. + * + * This may be put anywhere in the declaration of the private class. The parameter is the name of the public class. + */ + +/*! + * \macro QXT_INIT_PRIVATE(PUB) + * \relates + * Initializes resources owned by the private class. + * + * This should be called from the public class's constructor, + * before qxt_d() is used for the first time. The parameter is the name of the public class. + */ + +/*! + * \macro QXT_D(PUB) + * \relates + * Returns a reference in the current scope named "d" to the private class. + * + * This function is only available in a class using \a QXT_DECLARE_PRIVATE. + */ + +/*! + * \macro QXT_P(PUB) + * \relates + * Creates a reference in the current scope named "q" to the public class. + * + * This macro only works in a class using \a QXT_DECLARE_PUBLIC. + */ + +/*! + * \fn QxtPrivate& PUB::qxt_d() + * \relates + * Returns a reference to the private class. + * + * This function is only available in a class using \a QXT_DECLARE_PRIVATE. + */ + +/*! + * \fn const QxtPrivate& PUB::qxt_d() const + * \relates + * Returns a const reference to the private class. + * + * This function is only available in a class using \a QXT_DECLARE_PRIVATE. + * This overload will be automatically used in const functions. + */ + +/*! + * \fn PUB& QxtPrivate::qxt_p() + * \relates + * Returns a reference to the public class. + * + * This function is only available in a class using \a QXT_DECLARE_PUBLIC. + */ + +/*! + * \fn const PUB& QxtPrivate::qxt_p() const + * \relates + * Returns a const reference to the public class. + * + * This function is only available in a class using \a QXT_DECLARE_PUBLIC. + * This overload will be automatically used in const functions. + */ diff --git a/qxtweb-standalone/qxtweb/qxtglobal.h b/qxtweb-standalone/qxtweb/qxtglobal.h new file mode 100644 index 000000000..575372dcd --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtglobal.h @@ -0,0 +1,207 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTGLOBAL_H +#define QXTGLOBAL_H + +#include + +#define QXT_VERSION 0x000600 +#define QXT_VERSION_STR "0.6.0" + +//--------------------------global macros------------------------------ + +#ifndef QXT_NO_MACROS + +#endif // QXT_NO_MACROS + +//--------------------------export macros------------------------------ + +#define QXT_DLLEXPORT DO_NOT_USE_THIS_ANYMORE + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_CORE) +# define QXT_CORE_EXPORT Q_DECL_EXPORT +# else +# define QXT_CORE_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_CORE_EXPORT +#endif // BUILD_QXT_CORE + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_GUI) +# define QXT_GUI_EXPORT Q_DECL_EXPORT +# else +# define QXT_GUI_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_GUI_EXPORT +#endif // BUILD_QXT_GUI + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_NETWORK) +# define QXT_NETWORK_EXPORT Q_DECL_EXPORT +# else +# define QXT_NETWORK_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_NETWORK_EXPORT +#endif // BUILD_QXT_NETWORK + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_SQL) +# define QXT_SQL_EXPORT Q_DECL_EXPORT +# else +# define QXT_SQL_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_SQL_EXPORT +#endif // BUILD_QXT_SQL + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_WEB) +# define QXT_WEB_EXPORT Q_DECL_EXPORT +# else +# define QXT_WEB_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_WEB_EXPORT +#endif // BUILD_QXT_WEB + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_BERKELEY) +# define QXT_BERKELEY_EXPORT Q_DECL_EXPORT +# else +# define QXT_BERKELEY_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_BERKELEY_EXPORT +#endif // BUILD_QXT_BERKELEY + +#if !defined(QXT_STATIC) +# if defined(BUILD_QXT_ZEROCONF) +# define QXT_ZEROCONF_EXPORT Q_DECL_EXPORT +# else +# define QXT_ZEROCONF_EXPORT Q_DECL_IMPORT +# endif +#else +# define QXT_ZEROCONF_EXPORT +#endif // QXT_ZEROCONF_EXPORT + +#if defined BUILD_QXT_CORE || defined BUILD_QXT_GUI || defined BUILD_QXT_SQL || defined BUILD_QXT_NETWORK || defined BUILD_QXT_WEB || defined BUILD_QXT_BERKELEY || defined BUILD_QXT_ZEROCONF +# define BUILD_QXT +#endif + +QXT_CORE_EXPORT const char* qxtVersion(); + +#ifndef QT_BEGIN_NAMESPACE +#define QT_BEGIN_NAMESPACE +#endif + +#ifndef QT_END_NAMESPACE +#define QT_END_NAMESPACE +#endif + +#ifndef QT_FORWARD_DECLARE_CLASS +#define QT_FORWARD_DECLARE_CLASS(Class) class Class; +#endif + +/**************************************************************************** +** This file is derived from code bearing the following notice: +** The sole author of this file, Adam Higerd, has explicitly disclaimed all +** copyright interest and protection for the content within. This file has +** been placed in the public domain according to United States copyright +** statute and case law. In jurisdictions where this public domain dedication +** is not legally recognized, anyone who receives a copy of this file is +** permitted to use, modify, duplicate, and redistribute this file, in whole +** or in part, with no restrictions or conditions. In these jurisdictions, +** this file shall be copyright (C) 2006-2008 by Adam Higerd. +****************************************************************************/ + +#define QXT_DECLARE_PRIVATE(PUB) friend class PUB##Private; QxtPrivateInterface qxt_d; +#define QXT_DECLARE_PUBLIC(PUB) friend class PUB; +#define QXT_INIT_PRIVATE(PUB) qxt_d.setPublic(this); +#define QXT_D(PUB) PUB##Private& d = qxt_d() +#define QXT_P(PUB) PUB& p = qxt_p() + +template +class QxtPrivate +{ +public: + virtual ~QxtPrivate() + {} + inline void QXT_setPublic(PUB* pub) + { + qxt_p_ptr = pub; + } + +protected: + inline PUB& qxt_p() + { + return *qxt_p_ptr; + } + inline const PUB& qxt_p() const + { + return *qxt_p_ptr; + } + +private: + PUB* qxt_p_ptr; +}; + +template +class QxtPrivateInterface +{ + friend class QxtPrivate; +public: + QxtPrivateInterface() + { + pvt = new PVT; + } + ~QxtPrivateInterface() + { + delete pvt; + } + + inline void setPublic(PUB* pub) + { + pvt->QXT_setPublic(pub); + } + inline PVT& operator()() + { + return *static_cast(pvt); + } + inline const PVT& operator()() const + { + return *static_cast(pvt); + } +private: + QxtPrivateInterface(const QxtPrivateInterface&) { } + QxtPrivateInterface& operator=(const QxtPrivateInterface&) { } + QxtPrivate* pvt; +}; + +#endif // QXT_GLOBAL diff --git a/qxtweb-standalone/qxtweb/qxthtmltemplate.cpp b/qxtweb-standalone/qxtweb/qxthtmltemplate.cpp new file mode 100644 index 000000000..c8ded5081 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxthtmltemplate.cpp @@ -0,0 +1,156 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! + \class QxtHtmlTemplate + \inmodule QxtWeb + \brief The QxtHtmlTemplate class provides a basic HTML template engine + + open a file containing html code and php style variables. + use the square bracket operators to assign content for a variable + + \code + QxtHtmlTemplate index; + if(!index.open) + return 404; + index["content"]="hello world"; + echo()< + + Test Page + + + + \endcode + + funny storry: whe are using this class to make our documentation (eat your own dogfood, you know ;). + but when we where parsing exactly this file you read right now the first time, QxtHtmlTemplate got stuck in an infinite loop. guess why. becouse of that example above :D + So be warned: when you assign content to a variable that contains the variable name itself, render() will never return. + + +*/ + +/*! + \fn QxtHtmlTemplate::open(const QString& filename) + Opens \a filename. Returns \c true on success and \c false on failure. + Note that it will also return false for an empty html file. + */ + +/*! + \fn QString QxtHtmlTemplate::render() const + Uses the variables you set and renders the opened file. + returns an empty string on failure. + Does NOT take care of not assigned variables, they will remain in the returned string + */ + +#include "qxthtmltemplate.h" +#include +#include + +/*! + Constructs a new QxtHtmlTemplate. + */ +QxtHtmlTemplate::QxtHtmlTemplate() : QMap() +{} + +/*! + Loads data \a d. + */ +void QxtHtmlTemplate::load(const QString& d) +{ + data = d; +} + +bool QxtHtmlTemplate::open(const QString& filename) +{ + QFile f(filename); + f.open(QIODevice::ReadOnly); + data = QString::fromLocal8Bit(f.readAll()); + f.close(); + if (data.isEmpty()) + { + qWarning("QxtHtmlTemplate::open(\"%s\") empty or nonexistent", qPrintable(filename)); + return false; + } + return true; +} + +QString QxtHtmlTemplate::render() const +{ + ///try to preserve indention by parsing char by char and saving the last non-space character + + + QString output = data; + int lastnewline = 0; + + + for (int i = 0;i < output.count();i++) + { + if (output.at(i) == '\n') + { + lastnewline = i; + } + + if (output.at(i) == '<' && output.at(i + 1) == '?' && output.at(i + 2) == '=') + { + int j = i + 3; + QString var; + + for (int jj = j;jj < output.count();jj++) + { + if (output.at(jj) == '?' && output.at(jj + 1) == '>') + { + j = jj; + break; + } + var += output.at(jj); + } + + + if (j == i) + { + qWarning("QxtHtmlTemplate::render() unterminated + ** + ****************************************************************************/ + +#ifndef QXTHTMLTEMPLATE_H +#define QXTHTMLTEMPLATE_H + +#include +#include +#include +#include + +class QXT_WEB_EXPORT QxtHtmlTemplate : public QMap +{ +public: + QxtHtmlTemplate(); + bool open(const QString& filename); + void load(const QString& data); + + QString render() const; + +private: + QString data; +}; + +#endif // QXTHTMLTEMPLATE_H + diff --git a/qxtweb-standalone/qxtweb/qxthttpserverconnector.cpp b/qxtweb-standalone/qxtweb/qxthttpserverconnector.cpp new file mode 100644 index 000000000..910ee3789 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxthttpserverconnector.cpp @@ -0,0 +1,125 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtHttpServerConnector + +\inmodule QxtWeb + +\brief The QxtHttpServerConnector class provides a built-in HTTP server for QxtHttpSessionManager + +QxtHttpSessionManager does the work of managing sessions and state for the +otherwise stateless HTTP protocol, but it relies on QxtAbstractHttpConnector +subclasses to implement the protocol used to communicate with the web server. + +QxtHttpServerConnector implements a complete HTTP server internally and does +not require an external web server to function. However, it provides very +little control over the behavior of the web server and may not suitable for +high traffic scenarios or virtual hosting configurations. + +\sa QxtHttpSessionManager +*/ +#include "qxthttpsessionmanager.h" +#include "qxtwebevent.h" +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN +class QxtHttpServerConnectorPrivate : public QxtPrivate +{ +public: + QTcpServer* server; +}; +#endif + +/*! + * Creates a QxtHttpServerConnector with the given \a parent. + */ +QxtHttpServerConnector::QxtHttpServerConnector(QObject* parent) : QxtAbstractHttpConnector(parent) +{ + QXT_INIT_PRIVATE(QxtHttpServerConnector); + qxt_d().server = new QTcpServer(this); + QObject::connect(qxt_d().server, SIGNAL(newConnection()), this, SLOT(acceptConnection())); +} + +/*! + * \reimp + */ +bool QxtHttpServerConnector::listen(const QHostAddress& iface, quint16 port) +{ + return qxt_d().server->listen(iface, port); +} + +/*! + * \internal + */ +void QxtHttpServerConnector::acceptConnection() +{ + QTcpSocket* socket = qxt_d().server->nextPendingConnection(); + addConnection(socket); +} + +/*! + * \reimp + */ +bool QxtHttpServerConnector::canParseRequest(const QByteArray& buffer) +{ + if (buffer.indexOf("\r\n\r\n") >= 0) return true; // 1.0+ + if (buffer.indexOf("\r\n") >= 0 && buffer.indexOf("HTTP/") == -1) return true; // 0.9 + return false; +} + +/*! + * \reimp + */ +QHttpRequestHeader QxtHttpServerConnector::parseRequest(QByteArray& buffer) +{ + int pos = buffer.indexOf("\r\n\r\n"), endpos = pos + 3; + if (pos == -1) + { + pos = buffer.indexOf("\r\n"); // 0.9 + endpos = pos + 1; + } + + QHttpRequestHeader header(QString::fromUtf8(buffer.left(endpos))); + QByteArray firstLine = buffer.left(buffer.indexOf('\r')); + if (firstLine.indexOf("HTTP/") == -1) + { + header.setRequest(header.method(), header.path(), 0, 9); + } + buffer.remove(0, endpos + 1); + return header; +} + +/*! + * \reimp + */ +void QxtHttpServerConnector::writeHeaders(QIODevice* device, const QHttpResponseHeader& header) +{ + if (header.majorVersion() == 0) return; // 0.9 doesn't have headers + device->write(header.toString().toUtf8()); +} diff --git a/qxtweb-standalone/qxtweb/qxthttpsessionmanager.cpp b/qxtweb-standalone/qxtweb/qxthttpsessionmanager.cpp new file mode 100644 index 000000000..ce9ccc907 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxthttpsessionmanager.cpp @@ -0,0 +1,700 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtHttpSessionManager + +\inmodule QxtWeb + +\brief The QxtHttpSessionManager class provides a session manager for HTTP-based protocols + +QxtHttpSessionManager is a QxtWeb session manager that adds session management +support to the normally stateless HTTP model. + +In addition to session management, QxtHttpSessionManager also supports a +static service, which can serve content that does not require session management, +such as static web pages. The static service is also used to respond to HTTP/0.9 +clients that do not support cookies and HTTP/1.0 and HTTP/1.1 clients that are +rejecting cookies. If no static service is provided, these clients will only +see an "Internal Configuration Error", so it is recommended to supply a static +service, even one that only returns a more useful error message. + +QxtHttpSessionManager attempts to be thread-safe in accepting connections and +posting events. It is reentrant for all other functionality. + +\sa QxtAbstractWebService +*/ + +#include "qxthttpsessionmanager.h" +#include "qxtwebevent.h" +#include "qxtwebcontent.h" +#include "qxtabstractwebservice.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN +class QxtHttpSessionManagerPrivate : public QxtPrivate +{ +public: + struct ConnectionState + { + QxtBoundFunction* onBytesWritten; + bool readyRead; + bool finishedTransfer; + bool keepAlive; + bool streaming; + int httpMajorVersion; + int httpMinorVersion; + int sessionID; + }; + + QxtHttpSessionManagerPrivate() : iface(QHostAddress::Any), port(80), sessionCookieName("sessionID"), connector(0), staticService(0), autoCreateSession(true), + eventLock(QMutex::Recursive), sessionLock(QMutex::Recursive) {} + QXT_DECLARE_PUBLIC(QxtHttpSessionManager) + + QHostAddress iface; + quint16 port; + QByteArray sessionCookieName; + QxtAbstractHttpConnector* connector; + QxtAbstractWebService* staticService; + bool autoCreateSession; + + QMutex eventLock; + QList eventQueue; + + QMutex sessionLock; + QHash sessionKeys; // sessionKey->sessionID + QHash connectionState; // connection->state + + Qt::HANDLE mainThread; +}; +#endif + +/*! + * Constructs a new QxtHttpSessionManager with the specified \a parent. + */ +QxtHttpSessionManager::QxtHttpSessionManager(QObject* parent) : QxtAbstractWebSessionManager(parent) +{ + QXT_INIT_PRIVATE(QxtHttpSessionManager); + qxt_d().mainThread = QThread::currentThreadId(); +} + +/*! + * Returns the interface on which the session manager will listen for incoming connections. + * \sa setInterface + */ +QHostAddress QxtHttpSessionManager::listenInterface() const + { + return qxt_d().iface; + } + +/*! + * Sets the interface \a iface on which the session manager will listen for incoming + * connections. + * + * The default value is QHostAddress::Any, which will cause the session manager + * to listen on all network interfaces. + * + * \sa QxtAbstractHttpConnector::listen + */ +void QxtHttpSessionManager::setListenInterface(const QHostAddress& iface) +{ + qxt_d().iface = iface; +} + +/*! + * Returns the port on which the session manager will listen for incoming connections. + * \sa setInterface + */ +quint16 QxtHttpSessionManager::port() const +{ + return qxt_d().port; +} + +/*! + * Sets the \a port on which the session manager will listen for incoming connections. + * + * The default value is to listen on port 80. This is an acceptable value when + * using QxtHttpServerConnector, but it is not likely to be desirable for other + * connectors. + * + * \sa port + */ +void QxtHttpSessionManager::setPort(quint16 port) +{ + qxt_d().port = port; +} + +/*! + * \reimp + */ +bool QxtHttpSessionManager::start() +{ + Q_ASSERT(qxt_d().connector); + return connector()->listen(listenInterface(), port()); +} + +/*! + * Returns the name of the HTTP cookie used to track sessions in the web browser. + * \sa setSessionCookieName + */ +QByteArray QxtHttpSessionManager::sessionCookieName() const +{ + return qxt_d().sessionCookieName; +} + +/*! + * Sets the \a name of the HTTP cookie used to track sessions in the web browser. + * + * The default value is "sessionID". + * + * \sa sessionCookieName + */ +void QxtHttpSessionManager::setSessionCookieName(const QByteArray& name) +{ + qxt_d().sessionCookieName = name; +} + +/*! + * Sets the \a connector used to manage connections to web browsers. + * + * \sa connector + */ +void QxtHttpSessionManager::setConnector(QxtAbstractHttpConnector* connector) +{ + connector->setSessionManager(this); + qxt_d().connector = connector; +} + +/*! + * Sets the \a connector used to manage connections to web browsers. + * + * This overload is provided for convenience and can construct the predefined + * connectors provided with Qxt. + * + * \sa connector + */ +void QxtHttpSessionManager::setConnector(Connector connector) +{ + if (connector == HttpServer) + setConnector(new QxtHttpServerConnector(this)); + else if (connector == Scgi) + setConnector(new QxtScgiServerConnector(this)); + /* commented out pending implementation + + else if(connector == Fcgi) + setConnector(new QxtFcgiConnector(this)); + */ +} + +/*! + * Returns the connector used to manage connections to web browsers. + * \sa setConnector + */ +QxtAbstractHttpConnector* QxtHttpSessionManager::connector() const +{ + return qxt_d().connector; +} + +/*! + * Returns \c true if sessions are automatically created for every connection + * that does not already have a session cookie associated with it; otherwise + * returns \c false. + * \sa setAutoCreateSession + */ +bool QxtHttpSessionManager::autoCreateSession() const +{ + return qxt_d().autoCreateSession; +} + +/*! + * Sets \a enabled whether sessions are automatically created for every connection + * that does not already have a session cookie associated with it. + * + * Sessions are only created for clients that support HTTP cookies. HTTP/0.9 + * clients will never generate a session. + * + * \sa autoCreateSession + */ +void QxtHttpSessionManager::setAutoCreateSession(bool enable) +{ + qxt_d().autoCreateSession = enable; +} + +/*! + * Returns the QxtAbstractWebService that is used to respond to requests from + * connections that are not associated with a session. + * + * \sa setStaticContentService + */ +QxtAbstractWebService* QxtHttpSessionManager::staticContentService() const +{ + return qxt_d().staticService; +} + +/*! + * Sets the \a service that is used to respond to requests from + * connections that are not associated with a session. + * + * If no static content service is set, connections that are not associated + * with a session will receive an "Internal Configuration Error". + * + * \sa staticContentService + */ +void QxtHttpSessionManager::setStaticContentService(QxtAbstractWebService* service) +{ + qxt_d().staticService = service; +} + +/*! + * \reimp + */ +void QxtHttpSessionManager::postEvent(QxtWebEvent* h) +{ + qxt_d().eventLock.lock(); + qxt_d().eventQueue.append(h); + qxt_d().eventLock.unlock(); + // if(h->type() == QxtWebEvent::Page) + QMetaObject::invokeMethod(this, "processEvents", Qt::QueuedConnection); +} + +/*! + * Creates a new session and sends the session key to the web browser. + * + * Subclasses may override this function to perform custom session initialization, + * but they must call the base class implementation in order to update the internal + * session database and fetch a new session ID. + */ +int QxtHttpSessionManager::newSession() +{ + QMutexLocker locker(&qxt_d().sessionLock); + int sessionID = createService(); + QUuid key; + do + { + key = QUuid::createUuid(); + } + while (qxt_d().sessionKeys.contains(key)); + qxt_d().sessionKeys[key] = sessionID; + postEvent(new QxtWebStoreCookieEvent(sessionID, qxt_d().sessionCookieName, key)); + return sessionID; +} + +/*! + * Handles incoming HTTP requests and dispatches them to the appropriate service. + * + * The \a requestID is an opaque value generated by the connector. + * + * Subclasses may override this function to perform preprocessing on each + * request, but they must call the base class implementation in order to + * generate and dispatch the appropriate events. + */ +void QxtHttpSessionManager::incomingRequest(quint32 requestID, const QHttpRequestHeader& header, QxtWebContent* content) +{ + QMultiHash cookies; + foreach(const QString& cookie, header.allValues("cookie")) // QHttpHeader is case-insensitive, thankfully + { + foreach(const QString& kv, cookie.split("; ")) + { + int pos = kv.indexOf('='); + if (pos == -1) continue; + cookies.insert(kv.left(pos), kv.mid(pos + 1)); + } + } + + int sessionID; + QString sessionCookie = cookies.value(qxt_d().sessionCookieName); + + qxt_d().sessionLock.lock(); + if (qxt_d().sessionKeys.contains(sessionCookie)) + { + sessionID = qxt_d().sessionKeys[sessionCookie]; + } + else if (header.majorVersion() > 0 && qxt_d().autoCreateSession) + { + sessionID = newSession(); + } + else + { + sessionID = 0; + } + + QIODevice* device = connector()->getRequestConnection(requestID); + QxtHttpSessionManagerPrivate::ConnectionState& state = qxt_d().connectionState[device]; + state.sessionID = sessionID; + state.httpMajorVersion = header.majorVersion(); + state.httpMinorVersion = header.minorVersion(); + if (state.httpMajorVersion == 0 || (state.httpMajorVersion == 1 && state.httpMinorVersion == 0) || header.value("connection").toLower() == "close") + state.keepAlive = false; + else + state.keepAlive = true; + qxt_d().sessionLock.unlock(); + + QxtWebRequestEvent* event = new QxtWebRequestEvent(sessionID, requestID, QUrl(header.path())); + QTcpSocket* socket = qobject_cast(device); + if (socket) + { + event->remoteAddress = socket->peerAddress().toString(); + } + event->method = header.method(); + event->cookies = cookies; + event->url.setScheme("http"); + if (event->url.host().isEmpty()) + event->url.setHost(header.value("host")); + if (event->url.port() == -1) + event->url.setPort(port()); + event->contentType = header.contentType(); + event->content = content; + typedef QPair StringPair; + foreach(const StringPair& line, header.values()) + { + if (line.first.toLower() == "cookie") continue; + event->headers.insert(line.first, line.second); + } + event->headers.insert("X-Request-Protocol", "HTTP/" + QString::number(state.httpMajorVersion) + '.' + QString::number(state.httpMinorVersion)); + if (sessionID && session(sessionID)) + { + session(sessionID)->pageRequestedEvent(event); + } + else if (qxt_d().staticService) + { + qxt_d().staticService->pageRequestedEvent(event); + } + else + { + postEvent(new QxtWebErrorEvent(0, requestID, 500, "Internal Configuration Error")); + } +} + +/*! + * \internal + */ +void QxtHttpSessionManager::disconnected(QIODevice* device) +{ + QMutexLocker locker(&qxt_d().sessionLock); + if (qxt_d().connectionState.contains(device)) + delete qxt_d().connectionState[device].onBytesWritten; + qxt_d().connectionState.remove(device); +} + +/*! + * \reimp + */ +void QxtHttpSessionManager::processEvents() +{ + if (QThread::currentThreadId() != qxt_d().mainThread) + { + QMetaObject::invokeMethod(this, "processEvents", Qt::QueuedConnection); + return; + } + QxtHttpSessionManagerPrivate& d = qxt_d(); + QMutexLocker locker(&d.eventLock); + if (!d.eventQueue.count()) return; + + int ct = d.eventQueue.count(), sessionID = 0, requestID = 0, pagePos = -1; + QxtWebRedirectEvent* re = 0; + QxtWebPageEvent* pe = 0; + for (int i = 0; i < ct; i++) + { + if (d.eventQueue[i]->type() != QxtWebEvent::Page && d.eventQueue[i]->type() != QxtWebEvent::Redirect) continue; + pagePos = i; + sessionID = d.eventQueue[i]->sessionID; + if (d.eventQueue[pagePos]->type() == QxtWebEvent::Redirect) + { + re = static_cast(d.eventQueue[pagePos]); + } + pe = static_cast(d.eventQueue[pagePos]); + requestID = pe->requestID; + break; + } + if (pagePos == -1) return; // no pages to send yet + + QHttpResponseHeader header; + QList removeIDs; + QxtWebEvent* e = 0; + for (int i = 0; i < pagePos; i++) + { + if (d.eventQueue[i]->sessionID != sessionID) continue; + e = d.eventQueue[i]; + if (e->type() == QxtWebEvent::StoreCookie) + { + QxtWebStoreCookieEvent* ce = static_cast(e); + QString cookie = ce->name + '=' + ce->data; + if (ce->expiration.isValid()) + { + cookie += "; max-age=" + QString::number(QDateTime::currentDateTime().secsTo(ce->expiration)) + + "; expires=" + ce->expiration.toUTC().toString("ddd, dd-MMM-YYYY hh:mm:ss GMT"); + } + header.addValue("set-cookie", cookie); + removeIDs.push_front(i); + } + else if (e->type() == QxtWebEvent::RemoveCookie) + { + QxtWebRemoveCookieEvent* ce = static_cast(e); + header.addValue("set-cookie", ce->name + "=; max-age=0; expires=" + QDateTime(QDate(1970, 1, 1)).toString("ddd, dd-MMM-YYYY hh:mm:ss GMT")); + removeIDs.push_front(i); + } + } + removeIDs.push_front(pagePos); + + QIODevice* device = connector()->getRequestConnection(requestID); + QxtWebContent* content = qobject_cast(device); + // TODO: This should only be invoked when pipelining occurs + // In theory it shouldn't cause any problems as POST is specced to not be pipelined + if (content) content->ignoreRemainingContent(); + + QxtHttpSessionManagerPrivate::ConnectionState& state = qxt_d().connectionState[connector()->getRequestConnection(requestID)]; + + header.setStatusLine(pe->status, pe->statusMessage, state.httpMajorVersion, state.httpMinorVersion); + + if (re) + { + header.setValue("location", re->destination); + } + + // Set custom header values + for (QMultiHash::iterator it = pe->headers.begin(); it != pe->headers.end(); ++it) + { + header.setValue(it.key(), it.value()); + } + + header.setContentType(pe->contentType); + if (state.httpMajorVersion == 0 || (state.httpMajorVersion == 1 && state.httpMinorVersion == 0)) + pe->chunked = false; + + connector()->setRequestDataSource( pe->requestID, pe->dataSource ); + QSharedPointer source( pe->dataSource ); + state.finishedTransfer = false; + bool emptyContent = !source->bytesAvailable() && !pe->streaming; + state.readyRead = source->bytesAvailable(); + state.streaming = pe->streaming; + + if (emptyContent) + { + header.setValue("connection", "close"); + connector()->writeHeaders(device, header); + closeConnection(requestID); + } + else + { + if (state.onBytesWritten) delete state.onBytesWritten; // disconnect old handler + if (!pe->chunked) + { + state.keepAlive = false; + state.onBytesWritten = QxtMetaObject::bind(this, SLOT(sendNextBlock(int)), + Q_ARG(int, requestID)); + + QxtMetaObject::connect(source.data(), SIGNAL(readyRead()), + QxtMetaObject::bind(this, SLOT(blockReadyRead(int)), + Q_ARG(int, requestID)), + Qt::QueuedConnection); + + QxtMetaObject::connect(source.data(), SIGNAL(aboutToClose()), + QxtMetaObject::bind(this, SLOT(closeConnection(int)), + Q_ARG(int, requestID)), + Qt::QueuedConnection); + } + else + { + header.setValue("transfer-encoding", "chunked"); + state.onBytesWritten = QxtMetaObject::bind(this, SLOT(sendNextChunk(int)), + Q_ARG(int, requestID)); + + QxtMetaObject::connect(source.data(), SIGNAL(readyRead()), + QxtMetaObject::bind(this, SLOT(chunkReadyRead(int)), + Q_ARG(int, requestID)), + Qt::QueuedConnection); + + QxtMetaObject::connect(source.data(), SIGNAL(aboutToClose()), + QxtMetaObject::bind(this, SLOT(sendEmptyChunk(int)), + Q_ARG(int, requestID)), + Qt::QueuedConnection); + } + QxtMetaObject::connect(device, SIGNAL(bytesWritten(qint64)), state.onBytesWritten, Qt::QueuedConnection); + + if (state.keepAlive) + { + header.setValue("connection", "keep-alive"); + } + else + { + header.setValue("connection", "close"); + } + connector()->writeHeaders(device, header); + if (state.readyRead) + { + if (pe->chunked) + sendNextChunk(requestID); + else + sendNextBlock(requestID); + } + } + + foreach(int id, removeIDs) + { + delete d.eventQueue.takeAt(id); + } + + if (d.eventQueue.count()) + QMetaObject::invokeMethod(this, "processEvents", Qt::QueuedConnection); +} + +/*! + * \internal + */ +void QxtHttpSessionManager::chunkReadyRead(int requestID) +{ + const QSharedPointer& dataSource = connector()->getRequestDataSource( requestID ); + if (!dataSource->bytesAvailable()) return; + QIODevice* device = connector()->getRequestConnection(requestID); + if (!device->bytesToWrite() || qxt_d().connectionState[device].readyRead == false) + { + qxt_d().connectionState[device].readyRead = true; + sendNextChunk(requestID); + } +} + +/*! + * \internal + */ +void QxtHttpSessionManager::sendNextChunk(int requestID) +{ + const QSharedPointer& dataSource = connector()->getRequestDataSource( requestID ); + QIODevice* device = connector()->getRequestConnection(requestID); + QxtHttpSessionManagerPrivate::ConnectionState& state = qxt_d().connectionState[device]; + if (state.finishedTransfer) + { + // This is just the last block written; we're done with it + return; + } + if (!dataSource->bytesAvailable()) + { + state.readyRead = false; + return; + } + QByteArray chunk = dataSource->read(32768); // this is a good chunk size + if (chunk.size()) + { + QByteArray data = QString::number(chunk.size(), 16).toUtf8() + "\r\n" + chunk + "\r\n"; + device->write(data); + } + state.readyRead = false; + if (!state.streaming && !dataSource->bytesAvailable()) + QMetaObject::invokeMethod(this, "sendEmptyChunk", Q_ARG(int, requestID)); +} + +/*! + * \internal + */ +void QxtHttpSessionManager::sendEmptyChunk(int requestID) +{ + QIODevice* device = connector()->getRequestConnection(requestID); + if (!qxt_d().connectionState.contains(device)) return; // in case a disconnect signal and a bytesWritten signal get fired in the wrong order + QxtHttpSessionManagerPrivate::ConnectionState& state = qxt_d().connectionState[device]; + if (state.finishedTransfer) return; + state.finishedTransfer = true; + device->write("0\r\n\r\n"); + + if (state.keepAlive) + { + delete state.onBytesWritten; + state.onBytesWritten = 0; + QSharedPointer& dataSource = connector()->getRequestDataSource( requestID ); + dataSource.clear(); + connector()->incomingData(device); + } + else + { + closeConnection(requestID); + } +} + +/*! + * \internal + */ +void QxtHttpSessionManager::closeConnection(int requestID) +{ + QIODevice* device = connector()->getRequestConnection(requestID); + if( !device ) return; // already closing/closed + QxtHttpSessionManagerPrivate::ConnectionState& state = qxt_d().connectionState[device]; + state.finishedTransfer = true; + state.onBytesWritten = NULL; + QTcpSocket* socket = qobject_cast(device); + if (socket) + socket->disconnectFromHost(); + else + device->close(); + + connector()->doneWithRequest( requestID ); +} + +/*! + * \internal + */ +void QxtHttpSessionManager::blockReadyRead(int requestID) +{ + const QSharedPointer& dataSource = connector()->getRequestDataSource( requestID ); + if (!dataSource->bytesAvailable()) return; + + QIODevice* device = connector()->getRequestConnection(requestID); + if (!device->bytesToWrite() || qxt_d().connectionState[device].readyRead == false) + { + qxt_d().connectionState[device].readyRead = true; + sendNextBlock(requestID); + } +} + +/*! + * \internal + */ +void QxtHttpSessionManager::sendNextBlock(int requestID) +{ + QSharedPointer& dataSource = connector()->getRequestDataSource( requestID ); + QIODevice* device = connector()->getRequestConnection(requestID); + if (!qxt_d().connectionState.contains(device)) return; // in case a disconnect signal and a bytesWritten signal get fired in the wrong order + QxtHttpSessionManagerPrivate::ConnectionState& state = qxt_d().connectionState[device]; + if (state.finishedTransfer) return; + if (!dataSource->bytesAvailable()) + { + state.readyRead = false; + return; + } + QByteArray chunk = dataSource->read(32768); // this is a good chunk size + device->write(chunk); + state.readyRead = false; + if (!state.streaming && !dataSource->bytesAvailable()) + { + closeConnection(requestID); + } +} diff --git a/qxtweb-standalone/qxtweb/qxthttpsessionmanager.h b/qxtweb-standalone/qxtweb/qxthttpsessionmanager.h new file mode 100644 index 000000000..6ded5a032 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxthttpsessionmanager.h @@ -0,0 +1,91 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTHTTPSESSIONMANAGER_H +#define QXTHTTPSESSIONMANAGER_H + +#include "qxtabstractwebsessionmanager.h" +#include "qxtabstracthttpconnector.h" +#include +#include +#include + +class QxtWebEvent; +class QxtWebContent; + +class QxtHttpSessionManagerPrivate; +class QXT_WEB_EXPORT QxtHttpSessionManager : public QxtAbstractWebSessionManager +{ + friend class QxtAbstractHttpConnector; + Q_OBJECT +public: + enum Connector { HttpServer, Scgi, Fcgi }; + + QxtHttpSessionManager(QObject* parent = 0); + + virtual void postEvent(QxtWebEvent*); + + QHostAddress listenInterface() const; + void setListenInterface(const QHostAddress& iface); + + quint16 port() const; + void setPort(quint16 port); + + QByteArray sessionCookieName() const; + void setSessionCookieName(const QByteArray& name); + + bool autoCreateSession() const; + void setAutoCreateSession(bool enable); + + QxtAbstractWebService* staticContentService() const; + void setStaticContentService(QxtAbstractWebService* service); + + void setConnector(QxtAbstractHttpConnector* connector); + void setConnector(Connector connector); + QxtAbstractHttpConnector* connector() const; + + virtual bool start(); + +protected: + virtual int newSession(); + virtual void incomingRequest(quint32 requestID, const QHttpRequestHeader& header, QxtWebContent* device); + +protected Q_SLOTS: + virtual void processEvents(); + +private Q_SLOTS: + void closeConnection(int requestID); + void chunkReadyRead(int requestID); + void sendNextChunk(int requestID); + void sendEmptyChunk(int requestID); + void blockReadyRead(int requestID); + void sendNextBlock(int requestID); + +private: + void disconnected(QIODevice* device); + QXT_DECLARE_PRIVATE(QxtHttpSessionManager) +}; + +#endif diff --git a/qxtweb-standalone/qxtweb/qxtmetaobject.cpp b/qxtweb-standalone/qxtweb/qxtmetaobject.cpp new file mode 100644 index 000000000..cad4f038f --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtmetaobject.cpp @@ -0,0 +1,371 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\namespace QxtMetaObject + +\inmodule QxtCore + +\brief The QxtMetaObject namespace provides extensions to QMetaObject + +including QxtMetaObject::bind + +*/ + +#include "qxtmetaobject.h" +#include "qxtboundfunction.h" +#include "qxtboundcfunction.h" +#include "qxtmetatype.h" + +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN +class QxtBoundArgument +{ + // This class intentionally left blank +}; +Q_DECLARE_METATYPE(QxtBoundArgument) + +class QxtBoundFunctionBase; + +QxtBoundFunction::QxtBoundFunction(QObject* parent) : QObject(parent) +{ + // initializer only +} +#endif + +bool QxtBoundFunction::invoke(Qt::ConnectionType type, QXT_IMPL_10ARGS(QVariant)) +{ + return invoke(type, QXT_VAR_ARG(1), QXT_VAR_ARG(2), QXT_VAR_ARG(3), QXT_VAR_ARG(4), QXT_VAR_ARG(5), QXT_VAR_ARG(6), QXT_VAR_ARG(7), QXT_VAR_ARG(8), QXT_VAR_ARG(9), QXT_VAR_ARG(10)); +} + +bool QxtBoundFunction::invoke(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_IMPL_10ARGS(QVariant)) +{ + return invoke(type, returnValue, QXT_VAR_ARG(1), QXT_VAR_ARG(2), QXT_VAR_ARG(3), QXT_VAR_ARG(4), QXT_VAR_ARG(5), QXT_VAR_ARG(6), QXT_VAR_ARG(7), QXT_VAR_ARG(8), QXT_VAR_ARG(9), QXT_VAR_ARG(10)); +} + +QxtBoundFunctionBase::QxtBoundFunctionBase(QObject* parent, QGenericArgument* params[10], QByteArray types[10]) : QxtBoundFunction(parent) +{ + for (int i = 0; i < 10; i++) + { + if (!params[i]) break; + if (QByteArray(params[i]->name()) == "QxtBoundArgument") + { + arg[i] = QGenericArgument("QxtBoundArgument", params[i]->data()); + } + else + { + data[i] = qxtConstructFromGenericArgument(*params[i]); + arg[i] = p[i] = QGenericArgument(params[i]->name(), data[i]); + } + bindTypes[i] = types[i]; + } +} + +QxtBoundFunctionBase::~QxtBoundFunctionBase() +{ + for (int i = 0; i < 10; i++) + { + if (arg[i].name() == 0) return; + if (QByteArray(arg[i].name()) != "QxtBoundArgument") qxtDestroyFromGenericArgument(arg[i]); + } +} + +int QxtBoundFunctionBase::qt_metacall(QMetaObject::Call _c, int _id, void **_a) +{ + _id = QObject::qt_metacall(_c, _id, _a); + if (_id < 0) + return _id; + if (_c == QMetaObject::InvokeMetaMethod) + { + if (_id == 0) + { + for (int i = 0; i < 10; i++) + { + if (QByteArray(arg[i].name()) == "QxtBoundArgument") + { + p[i] = QGenericArgument(bindTypes[i].constData(), _a[(quintptr)(arg[i].data())]); + } + } + invokeImpl(Qt::DirectConnection, QGenericReturnArgument(), p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9]); + } + _id = -1; + } + return _id; +} + +bool QxtBoundFunctionBase::invokeBase(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_IMPL_10ARGS(QGenericArgument)) +{ + QGenericArgument* args[10] = { &p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8, &p9, &p10 }; + for (int i = 0; i < 10; i++) + { + if (QByteArray(arg[i].name()) == "QxtBoundArgument") + { + p[i] = *args[(quintptr)(arg[i].data()) - 1]; + } + } + return invokeImpl(type, returnValue, p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9]); +} + +bool QxtBoundFunction::invoke(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_IMPL_10ARGS(QGenericArgument)) +{ + return reinterpret_cast(this)->invokeBase(type, returnValue, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); +} + +#ifndef QXT_DOXYGEN_RUN +class QxtBoundSlot : public QxtBoundFunctionBase +{ +public: + QByteArray sig; + + QxtBoundSlot(QObject* receiver, const char* invokable, QGenericArgument* params[10], QByteArray types[10]) : QxtBoundFunctionBase(receiver, params, types), sig(invokable) + { + // initializers only + } + + virtual bool invokeImpl(Qt::ConnectionType type, QGenericReturnArgument returnValue, QXT_IMPL_10ARGS(QGenericArgument)) + { + if (!QMetaObject::invokeMethod(parent(), QxtMetaObject::methodName(sig.constData()), type, returnValue, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)) + { + qWarning() << "QxtBoundFunction: call to" << sig << "failed"; + return false; + } + return true; + } +}; +#endif + +namespace QxtMetaObject +{ + + /*! + Returns the name of the given method. + + Example usage: + \code + QByteArray method = QxtMetaObject::methodName(" int foo ( int bar, double baz )"); + // method is now "foo" + \endcode + */ + QByteArray methodName(const char* method) + { + QByteArray name = methodSignature(method); + const int idx = name.indexOf("("); + if (idx != -1) + name.truncate(idx); + return name; + } + + /*! + Returns the signature of the given method. + */ + QByteArray methodSignature(const char* method) + { + QByteArray name = QMetaObject::normalizedSignature(method); + if(name[0] >= '0' && name[0] <= '9') + return name.mid(1); + return name; + } + + /*! + Checks if \a method contains parentheses and begins with 1 or 2. + */ + bool isSignalOrSlot(const char* method) + { + QByteArray m(method); + return (m.count() && (m[0] >= '0' && m[0] <= '9') && m.contains('(') && m.contains(')')); + } + + /*! + * Creates a binding to the provided signal, slot, or Q_INVOKABLE method using the + * provided parameter list. The type of each argument is deduced from the type of + * the QVariant. This function cannot bind positional arguments; see the + * overload using QGenericArgument. + * + * If the provided QObject does not implement the requested method, or if the + * argument list is incompatible with the method's function signature, this + * function returns NULL. + * + * The returned QxtBoundFunction is created as a child of the receiver. + * Changing the parent will result in undefined behavior. + * + * \sa QxtMetaObject::connect, QxtBoundFunction + */ + QxtBoundFunction* bind(QObject* recv, const char* invokable, QXT_IMPL_10ARGS(QVariant)) + { + if (!recv) + { + qWarning() << "QxtMetaObject::bind: cannot connect to null QObject"; + return 0; + } + + QVariant* args[10] = { &p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8, &p9, &p10 }; + QByteArray connSlot("2"), recvSlot(QMetaObject::normalizedSignature(invokable)); + const QMetaObject* meta = recv->metaObject(); + int methodID = meta->indexOfMethod(QxtMetaObject::methodSignature(recvSlot.constData())); + if (methodID == -1) + { + qWarning() << "QxtMetaObject::bind: no such method " << recvSlot; + return 0; + } + QMetaMethod method = meta->method(methodID); + int argCount = method.parameterTypes().count(); + const QList paramTypes = method.parameterTypes(); + + for (int i = 0; i < argCount; i++) + { + if (paramTypes[i] == "QxtBoundArgument") continue; + int type = QMetaType::type(paramTypes[i].constData()); + if (!args[i]->canConvert((QVariant::Type)type)) + { + qWarning() << "QxtMetaObject::bind: incompatible parameter list for " << recvSlot; + return 0; + } + } + + return QxtMetaObject::bind(recv, invokable, QXT_ARG(1), QXT_ARG(2), QXT_ARG(3), QXT_ARG(4), QXT_ARG(5), QXT_ARG(6), QXT_ARG(7), QXT_ARG(8), QXT_ARG(9), QXT_ARG(10)); + } + + /*! + * Creates a binding to the provided signal, slot, or Q_INVOKABLE method using the + * provided parameter list. Use the Q_ARG macro to specify constant parameters, or + * use the QXT_BIND macro to relay a parameter from a connected signal or passed + * via the QxtBoundFunction::invoke() method. + * + * If the provided QObject does not implement the requested method, or if the + * argument list is incompatible with the method's function signature, this + * function returns NULL. + * + * The returned QxtBoundFunction is created as a child of the receiver. + * Changing the parent will result in undefined behavior. + * + * \sa QxtMetaObject::connect, QxtBoundFunction, QXT_BIND + */ + QxtBoundFunction* bind(QObject* recv, const char* invokable, QXT_IMPL_10ARGS(QGenericArgument)) + { + if (!recv) + { + qWarning() << "QxtMetaObject::bind: cannot connect to null QObject"; + return 0; + } + + QGenericArgument* args[10] = { &p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8, &p9, &p10 }; + QByteArray connSlot("2"), recvSlot(QMetaObject::normalizedSignature(invokable)), bindTypes[10]; + const QMetaObject* meta = recv->metaObject(); + int methodID = meta->indexOfMethod(QxtMetaObject::methodSignature(recvSlot.constData()).constData()); + if (methodID == -1) + { + qWarning() << "QxtMetaObject::bind: no such method " << recvSlot; + return 0; + } + QMetaMethod method = meta->method(methodID); + int argCount = method.parameterTypes().count(); + + connSlot += QxtMetaObject::methodName(invokable) + '('; + for (int i = 0; i < 10; i++) + { + if (args[i]->name() == 0) break; // done + if (i >= argCount) + { + qWarning() << "QxtMetaObject::bind: too many arguments passed to " << invokable; + return 0; + } + if (i > 0) connSlot += ','; // argument separator + if (QByteArray(args[i]->name()) == "QxtBoundArgument") + { + Q_ASSERT_X((quintptr)(args[i]->data()) > 0 && (quintptr)(args[i]->data()) <= 10, "QXT_BIND", "invalid argument number"); + connSlot += method.parameterTypes()[i]; + bindTypes[i] = method.parameterTypes()[i]; + } + else + { + connSlot += args[i]->name(); // type name + } + } + connSlot = QMetaObject::normalizedSignature(connSlot += ')'); + + if (!QMetaObject::checkConnectArgs(recvSlot.constData(), connSlot.constData())) + { + qWarning() << "QxtMetaObject::bind: provided parameters " << connSlot.mid(connSlot.indexOf('(')) << " is incompatible with " << invokable; + return 0; + } + + return new QxtBoundSlot(recv, invokable, args, bindTypes); + } + + /*! + Connects a signal to a QxtBoundFunction. + */ + bool connect(QObject* sender, const char* signal, QxtBoundFunction* slot, Qt::ConnectionType type) + { + const QMetaObject* meta = sender->metaObject(); + int methodID = meta->indexOfMethod(meta->normalizedSignature(signal).mid(1).constData()); + if (methodID < 0) + { + qWarning() << "QxtMetaObject::connect: no such signal: " << QByteArray(signal).mid(1); + return false; + } + + return QMetaObject::connect(sender, methodID, slot, QObject::staticMetaObject.methodCount(), (int)(type)); + } + + /*! + \relates QxtMetaObject + This overload always invokes the member using the connection type Qt::AutoConnection. + + \sa QMetaObject::invokeMethod() + */ + bool invokeMethod(QObject* object, const char* member, const QVariant& arg0, + const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, + const QVariant& arg4, const QVariant& arg5, const QVariant& arg6, + const QVariant& arg7, const QVariant& arg8, const QVariant& arg9) + { + return invokeMethod(object, member, Qt::AutoConnection, + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); + } + + /*! + \relates QxtMetaObject + + Invokes the \a member (a signal or a slot name) on the \a object. + Returns \c true if the member could be invoked. Returns \c false + if there is no such member or the parameters did not match. + + \sa QMetaObject::invokeMethod() + */ + bool invokeMethod(QObject* object, const char* member, Qt::ConnectionType type, + const QVariant& arg0, const QVariant& arg1, const QVariant& arg2, + const QVariant& arg3, const QVariant& arg4, const QVariant& arg5, + const QVariant& arg6, const QVariant& arg7, const QVariant& arg8, const QVariant& arg9) + { + #define QXT_MO_ARG(i) QGenericArgument(arg ## i.typeName(), arg ## i.constData()) + return QMetaObject::invokeMethod(object, methodName(member), type, + QXT_MO_ARG(0), QXT_MO_ARG(1), QXT_MO_ARG(2), QXT_MO_ARG(3), QXT_MO_ARG(4), + QXT_MO_ARG(5), QXT_MO_ARG(6), QXT_MO_ARG(7), QXT_MO_ARG(8), QXT_MO_ARG(9)); + } +} diff --git a/qxtweb-standalone/qxtweb/qxtmetaobject.h b/qxtweb-standalone/qxtweb/qxtmetaobject.h new file mode 100644 index 000000000..a2acdfe99 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtmetaobject.h @@ -0,0 +1,106 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#ifndef QXTMETAOBJECT_H +#define QXTMETAOBJECT_H + +#include +#include +#include +#include +#include "qxtnullable.h" +#include "qxtglobal.h" +QT_FORWARD_DECLARE_CLASS(QByteArray) +class QxtBoundArgument; +class QxtBoundFunction; + +#define QXT_PROTO_10ARGS(T) T p1 = T(), T p2 = T(), T p3 = T(), T p4 = T(), T p5 = T(), T p6 = T(), T p7 = T(), T p8 = T(), T p9 = T(), T p10 = T() +#define QXT_PROTO_9ARGS(T) T p2 = T(), T p3 = T(), T p4 = T(), T p5 = T(), T p6 = T(), T p7 = T(), T p8 = T(), T p9 = T(), T p10 = T() +#define QXT_IMPL_10ARGS(T) T p1, T p2, T p3, T p4, T p5, T p6, T p7, T p8, T p9, T p10 + +class QXT_CORE_EXPORT QxtGenericFunctionPointer +{ + template + friend QxtGenericFunctionPointer qxtFuncPtr(FUNCTION funcPtr); +public: + QxtGenericFunctionPointer(const QxtGenericFunctionPointer& other) + { + funcPtr = other.funcPtr; + typeName = other.typeName; + } + + typedef void(voidFunc)(); + voidFunc* funcPtr; + QByteArray typeName; + +protected: + QxtGenericFunctionPointer(voidFunc* ptr, const QByteArray& typeIdName) + { + funcPtr = ptr; + typeName = typeIdName; + } +}; + +template +QxtGenericFunctionPointer qxtFuncPtr(FUNCTION funcPtr) +{ + return QxtGenericFunctionPointer(reinterpret_cast(funcPtr), typeid(funcPtr).name()); +} + +namespace QxtMetaObject +{ + QXT_CORE_EXPORT QByteArray methodName(const char* method); + QXT_CORE_EXPORT QByteArray methodSignature(const char* method); + + QXT_CORE_EXPORT bool isSignalOrSlot(const char* method); + + QXT_CORE_EXPORT QxtBoundFunction* bind(QObject* recv, const char* invokable, QXT_PROTO_10ARGS(QGenericArgument)); + QXT_CORE_EXPORT QxtBoundFunction* bind(QObject* recv, const char* invokable, QVariant p1, QXT_PROTO_9ARGS(QVariant)); + QXT_CORE_EXPORT bool connect(QObject* sender, const char* signal, QxtBoundFunction* slot, + Qt::ConnectionType type = Qt::AutoConnection); + + QXT_CORE_EXPORT bool invokeMethod(QObject* object, const char* member, + const QVariant& arg0 = QVariant(), const QVariant& arg1 = QVariant(), + const QVariant& arg2 = QVariant(), const QVariant& arg3 = QVariant(), + const QVariant& arg4 = QVariant(), const QVariant& arg5 = QVariant(), + const QVariant& arg6 = QVariant(), const QVariant& arg7 = QVariant(), + const QVariant& arg8 = QVariant(), const QVariant& arg9 = QVariant()); + + QXT_CORE_EXPORT bool invokeMethod(QObject* object, const char* member, Qt::ConnectionType type, + const QVariant& arg0 = QVariant(), const QVariant& arg1 = QVariant(), + const QVariant& arg2 = QVariant(), const QVariant& arg3 = QVariant(), + const QVariant& arg4 = QVariant(), const QVariant& arg5 = QVariant(), + const QVariant& arg6 = QVariant(), const QVariant& arg7 = QVariant(), + const QVariant& arg8 = QVariant(), const QVariant& arg9 = QVariant()); +} + +/*! + * \relates QxtMetaObject + * Refers to the n'th parameter of QxtBoundFunction::invoke() or of a signal connected to + * a QxtBoundFunction. + * \sa QxtMetaObject::bind + */ +#define QXT_BIND(n) QGenericArgument("QxtBoundArgument", reinterpret_cast(n)) + +#endif // QXTMETAOBJECT_H diff --git a/qxtweb-standalone/qxtweb/qxtmetatype.h b/qxtweb-standalone/qxtweb/qxtmetatype.h new file mode 100644 index 000000000..34bde7a05 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtmetatype.h @@ -0,0 +1,132 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTMETATYPE_H +#define QXTMETATYPE_H + +#include +#include +#include +#include +#include + +template +class /*QXT_CORE_EXPORT*/ QxtMetaType +{ +public: + static inline T* construct(const T* copy = 0) + { + return QMetaType::construct(qMetaTypeId(), reinterpret_cast(copy)); + } + + static inline void destroy(T* data) + { + QMetaType::destroy(qMetaTypeId(), data); + } + + // no need to reimplement isRegistered since this class will fail at compile time if it isn't + + static inline bool load(QDataStream& stream, T* data) + { + return QMetaType::load(stream, qMetaTypeId(), reinterpret_cast(data)); + } + + static inline bool save(QDataStream& stream, const T* data) + { + return QMetaType::save(stream, qMetaTypeId(), reinterpret_cast(data)); + } + + static inline int type() + { + return qMetaTypeId(); + } + + static inline const char* name() + { + return QMetaType::typeName(qMetaTypeId()); + } +}; + +template <> +class /*QXT_CORE_EXPORT*/ QxtMetaType +{ +public: + static inline void* construct(const void* copy = 0) + { + Q_UNUSED(copy); + return 0; + } + + static inline void destroy(void* data) + { + Q_UNUSED(data); + } + + static inline bool load(QDataStream& stream, void* data) + { + Q_UNUSED(stream); + Q_UNUSED(data); + return false; + } + + static inline bool save(QDataStream& stream, const void* data) + { + Q_UNUSED(stream); + Q_UNUSED(data); + return false; + } + + static inline int type() + { + return 0; + } + + static inline const char* name() + { + return 0; + } +}; + +inline void* qxtConstructByName(const char* typeName, const void* copy = 0) +{ + return QMetaType::construct(QMetaType::type(typeName), copy); +} + +inline void qxtDestroyByName(const char* typeName, void* data) +{ + QMetaType::destroy(QMetaType::type(typeName), data); +} + +inline void* qxtConstructFromGenericArgument(QGenericArgument arg) +{ + return qxtConstructByName(arg.name(), arg.data()); +} + +inline void qxtDestroyFromGenericArgument(QGenericArgument arg) +{ + qxtDestroyByName(arg.name(), arg.data()); +} + +#endif diff --git a/qxtweb-standalone/qxtweb/qxtnull.cpp b/qxtweb-standalone/qxtweb/qxtnull.cpp new file mode 100644 index 000000000..68f342be3 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtnull.cpp @@ -0,0 +1,27 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#include "qxtnull.h" + +// nothing here diff --git a/qxtweb-standalone/qxtweb/qxtnull.h b/qxtweb-standalone/qxtweb/qxtnull.h new file mode 100644 index 000000000..9e8f7e9b8 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtnull.h @@ -0,0 +1,62 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTNULL_H +#define QXTNULL_H + +#include + +/*! +\class QxtNull QxtNull + +\inmodule QxtCore + +\brief An object representing the "null" value for QxtNullable. + +\sa QxtNullable +*/ + +struct QXT_CORE_EXPORT QxtNull +{ + /*! integer cast operator + * In expressions, QxtNull behaves as an integer zero for compatibility with generic functions. + */ + operator int() const + { + return 0; + } + enum { isNull = true }; +}; + +#ifndef QXT_NO_MACROS + +/*! \relates QxtNull + * A convenience alias for QxtNull(). + */ +#define QXT_NULL QxtNull() + +#endif // QXT_NO_MACROS + +#endif // QXTNULL_H diff --git a/qxtweb-standalone/qxtweb/qxtnullable.h b/qxtweb-standalone/qxtweb/qxtnullable.h new file mode 100644 index 000000000..eb2a9e291 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtnullable.h @@ -0,0 +1,146 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtCore module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtNullable QxtNullable +\inmodule QxtCore +\brief distinct null value compatible with any data type. + +in general it's a templated abstraction to allow any data type to be +expressed with a null value distinct from any real value. An example +of such a use is for optional arguments. +\n +prepare a function for argument skipping: + +\code +void somefunction( qxtNull(int,a) , qxtNull(int,b) ) +{ + +if (!a.isNull()) + { + int i = a.value(); + //do something with i + } + if (!b.isNull()) + { + int x = b.value(); + //do something with x + } +} +\endcode + +usage: +\code + +somefunction(SKIP,1,2); +somefunction(3,4); +somefunction(3,SKIP,6); +somefunction(1); +\endcode + +*/ + +#ifndef QXTNULLABLE_H +#define QXTNULLABLE_H +#include + +/*! \relates QxtNullable + * defines a skipable argument with type \a t and variable name \a n + */ +#define qxtNull(t,n) QxtNullable n = QxtNullable() + +#include + +template +class /*QXT_CORE_EXPORT*/ QxtNullable +{ +public: + QxtNullable(QxtNull); + QxtNullable(const T& p); + QxtNullable(); + + ///determinates if the Value is set to something meaningfull + bool isNull() const; + + ///delete Value + void nullify(); + + T& value() const; + operator T() const; + void operator=(const T& p); + +private: + T* val; +}; + +template +QxtNullable::QxtNullable(QxtNull) +{ + val = 0; +} + +template +QxtNullable::QxtNullable(const T& p) +{ + val = const_cast(&p); +} + +template +QxtNullable::QxtNullable() +{ + val = 0; +} + +template +QxtNullable::operator T() const +{ + return *val; +} + +template +T& QxtNullable::value() const +{ + return *val; +} + +template +bool QxtNullable::isNull() const +{ + return (val == 0); +} + +template +void QxtNullable::nullify() +{ + val = 0; +} + +template +void QxtNullable::operator=(const T & p) +{ + val = const_cast(&p); +} + +#endif diff --git a/qxtweb-standalone/qxtweb/qxtscgiserverconnector.cpp b/qxtweb-standalone/qxtweb/qxtscgiserverconnector.cpp new file mode 100644 index 000000000..3297b94ff --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtscgiserverconnector.cpp @@ -0,0 +1,191 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtScgiServerConnector + +\inmodule QxtWeb + +\brief The QxtScgiServerConnector class provides an SCGI connector for QxtHttpSessionManager + + +QxtScgiServerConnector implements the SCGI protocoll supported by almost all modern web servers. + + + +\sa QxtHttpSessionManager +*/ +#include "qxthttpsessionmanager.h" +#include "qxtwebevent.h" +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN +class QxtScgiServerConnectorPrivate : public QxtPrivate +{ +public: + QTcpServer* server; +}; +#endif + +/*! + * Creates a QxtHttpServerConnector with the given \a parent. + */ +QxtScgiServerConnector::QxtScgiServerConnector(QObject* parent) : QxtAbstractHttpConnector(parent) +{ + QXT_INIT_PRIVATE(QxtScgiServerConnector); + qxt_d().server = new QTcpServer(this); + QObject::connect(qxt_d().server, SIGNAL(newConnection()), this, SLOT(acceptConnection())); +} + +/*! + * \reimp + */ +bool QxtScgiServerConnector::listen(const QHostAddress& iface, quint16 port) +{ + return qxt_d().server->listen(iface, port); +} + +/*! + * \internal + */ +void QxtScgiServerConnector::acceptConnection() +{ + QTcpSocket* socket = qxt_d().server->nextPendingConnection(); + addConnection(socket); +} + +/*! + * \reimp + */ +bool QxtScgiServerConnector::canParseRequest(const QByteArray& buffer) +{ + if (buffer.size() < 10) + return false; + QString expectedsize; + for (int i = 0;i < 10;i++) + { + if (buffer.at(i) == ':') + { + break; + } + else + { + expectedsize += buffer.at(i); + } + } + + if (expectedsize.isEmpty()) + { + //protocoll error + return false; + } + + return (buffer.size() > expectedsize.toInt()); +} + +/*! + * \reimp + */ +QHttpRequestHeader QxtScgiServerConnector::parseRequest(QByteArray& buffer) +{ + QString expectedsize_s; + for (int i = 0;i < 20;i++) + { + if (buffer.at(i) == ':') + { + break; + } + else + { + expectedsize_s += buffer.at(i); + } + } + + if (expectedsize_s.isEmpty()) + { + //protocoll error + return QHttpRequestHeader(); + } + + + buffer = buffer.right(buffer.size() - (expectedsize_s.count() + 1)); + + + QHttpRequestHeader request_m; + + QByteArray name; + int i = 0; + while ((i = buffer.indexOf('\0')) > -1) + { + if (name.isEmpty()) + { + name = buffer.left(i); + } + else + { + request_m.setValue(QString::fromAscii(name).toLower(), QString::fromAscii(buffer.left(i))); + name = ""; + } + buffer = buffer.mid(i + 1); + } + + + request_m.setRequest(request_m.value("request_method"), request_m.value("request_uri"), 1, 0); + + + foreach(const QString& key, request_m.keys()) + { + if (key.startsWith(QString("http_"))) + { + request_m.setValue(key.right(key.size() - 5), request_m.value(key)); + } + } + + request_m.setValue("Connection", "close"); + + + buffer.chop(1); + + + return request_m; +} + +/*! + * \reimp + */ +void QxtScgiServerConnector::writeHeaders(QIODevice* device, const QHttpResponseHeader& response_m) +{ + + device->write(("Status:" + QString::number(response_m.statusCode()) + ' ' + response_m.reasonPhrase() + "\r\n").toAscii()); + + foreach(const QString& key, response_m.keys()) + { + device->write((key + ':' + response_m.value(key) + "\r\n").toAscii()); + } + device->write("\r\n"); +} diff --git a/qxtweb-standalone/qxtweb/qxtweb.h b/qxtweb-standalone/qxtweb/qxtweb.h new file mode 100644 index 000000000..0940ba88e --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtweb.h @@ -0,0 +1,39 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ +#ifndef QXTWEB_H_INCLUDED +#define QXTWEB_H_INCLUDED + +#include "qxtabstracthttpconnector.h" +#include "qxtabstractwebservice.h" +#include "qxtabstractwebsessionmanager.h" +#include "qxtwebcgiservice.h" +#include "qxthtmltemplate.h" +#include "qxthttpsessionmanager.h" +#include "qxtwebcontent.h" +#include "qxtwebevent.h" +#include "qxtwebservicedirectory.h" +#include "qxtwebslotservice.h" + +#endif // QXTWEB_H_INCLUDED diff --git a/qxtweb-standalone/qxtweb/qxtwebcgiservice.cpp b/qxtweb-standalone/qxtweb/qxtwebcgiservice.cpp new file mode 100644 index 000000000..c522554e0 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebcgiservice.cpp @@ -0,0 +1,427 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtWebCgiService + +\inmodule QxtWeb + +\brief The QxtWebCgiService class provides a CGI/1.1 gateway for QxtWeb + +TODO: write docs +TODO: implement timeout +*/ + +#include "qxtwebcgiservice.h" +#include "qxtwebcgiservice_p.h" +#include "qxtwebevent.h" +#include "qxtwebcontent.h" +#include +#include +#include +#include + +QxtCgiRequestInfo::QxtCgiRequestInfo() : sessionID(0), requestID(0), eventSent(false), terminateSent(false) {} +QxtCgiRequestInfo::QxtCgiRequestInfo(QxtWebRequestEvent* req) : sessionID(req->sessionID), requestID(req->requestID), eventSent(false), terminateSent(false) {} + +/*! + * Constructs a QxtWebCgiService object with the specified session \a manager and \a parent. + * This service will invoke the specified \a binary to handle incoming requests. + * + * Often, the session manager will also be the parent, but this is not a requirement. + */ +QxtWebCgiService::QxtWebCgiService(const QString& binary, QxtAbstractWebSessionManager* manager, QObject* parent) : QxtAbstractWebService(manager, parent) +{ + QXT_INIT_PRIVATE(QxtWebCgiService); + qxt_d().binary = binary; + QObject::connect(&qxt_d().timeoutMapper, SIGNAL(mapped(QObject*)), &qxt_d(), SLOT(terminateProcess(QObject*))); +} + +/*! + * Returns the path to the CGI script that will be executed to handle requests. + * + * \sa setBinary() + */ +QString QxtWebCgiService::binary() const +{ + return qxt_d().binary; +} + +/*! + * Sets the path to the CGI script \a bin that will be executed to handle requests. + * + * \sa binary() + */ +void QxtWebCgiService::setBinary(const QString& bin) +{ + if (!QFile::exists(bin) || !(QFile::permissions(bin) & (QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther))) + { + qWarning() << "QxtWebCgiService::setBinary: " + bin + " does not appear to be executable."; + } + qxt_d().binary = bin; +} + +/*! + * Returns the maximum time a CGI script may execute, in milliseconds. + * + * The default value is 0, which indicates that CGI scripts will not be terminated + * due to long running times. + * + * \sa setTimeout() + */ +int QxtWebCgiService::timeout() const +{ + return qxt_d().timeout; +} + +/*! + * Sets the maximum \a time a CGI script may execute, in milliseconds. + * + * The timer is started when the script is launched. After the timeout elapses once, + * the script will be asked to stop, as QProcess::terminate(). (That is, the script + * will receive WM_CLOSE on Windows or SIGTERM on UNIX.) If the process has still + * failed to terminate after another timeout, it will be forcibly terminated, as + * QProcess::kill(). (That is, the script will receive TerminateProcess on Windows + * or SIGKILL on UNIX.) + * + * Set the timeout to 0 to disable this behavior; scripts will not be terminated + * due to excessive run time. This is the default behavior. + * + * CAUTION: Keep in mind that the timeout applies to the real running time of the + * script, not processor time used. A script that initiates a lengthy download + * may be interrupted while transferring data to the web browser. To avoid this + * behavior, see the timeoutOverride property to allow the script to request + * an extended timeout, or use a different QxtAbstractWebService object for + * serving streaming content or large files. + * + * + * \sa timeout(), timeoutOverride(), setTimeoutOverride(), QProcess::terminate(), QProcess::kill() + */ +void QxtWebCgiService::setTimeout(int time) +{ + qxt_d().timeout = time; +} + +/*! + * Returns whether or not to allow scripts to override the timeout. + * + * \sa setTimeoutOverride(), setTimeout() + */ +bool QxtWebCgiService::timeoutOverride() const +{ + return qxt_d().timeoutOverride; +} + +/*! + * Sets whether or not to allow scripts to override the timeout. + * Scripts are allowed to override if \a enable is \c true. + * + * As an extension to the CGI/1.1 gateway specification, a CGI script may + * output a "X-QxtWeb-Timeout" header to change the termination timeout + * on a per-script basis. Only enable this option if you trust the scripts + * being executed. + * + * \sa timeoutOverride(), setTimeout() + */ +void QxtWebCgiService::setTimeoutOverride(bool enable) +{ + qxt_d().timeoutOverride = enable; +} + +/*! + * \reimp + */ +void QxtWebCgiService::pageRequestedEvent(QxtWebRequestEvent* event) +{ + // Create the process object and initialize connections + QProcess* process = new QProcess(this); + qxt_d().requests[process] = QxtCgiRequestInfo(event); + qxt_d().processes[event->content] = process; + QxtCgiRequestInfo& requestInfo = qxt_d().requests[process]; + QObject::connect(process, SIGNAL(readyRead()), &qxt_d(), SLOT(processReadyRead())); + QObject::connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), &qxt_d(), SLOT(processFinished())); + QObject::connect(process, SIGNAL(error(QProcess::ProcessError)), &qxt_d(), SLOT(processFinished())); + requestInfo.timeout = new QTimer(process); + qxt_d().timeoutMapper.setMapping(requestInfo.timeout, process); + QObject::connect(requestInfo.timeout, SIGNAL(timeout()), &qxt_d().timeoutMapper, SLOT(map())); + + // Initialize the system environment + QStringList s_env = process->systemEnvironment(); + QMap env; + foreach(const QString& entry, s_env) + { + int pos = entry.indexOf('='); + env[entry.left(pos)] = entry.mid(pos + 1); + } + + // Populate CGI/1.1 environment variables + env["SERVER_SOFTWARE"] = QString("QxtWeb/" QXT_VERSION_STR); + env["SERVER_NAME"] = event->url.host(); + env["GATEWAY_INTERFACE"] = "CGI/1.1"; + if (event->headers.contains("X-Request-Protocol")) + env["SERVER_PROTOCOL"] = event->headers.value("X-Request-Protocol"); + else + env.remove("SERVER_PROTOCOL"); + if (event->url.port() != -1) + env["SERVER_PORT"] = QString::number(event->url.port()); + else + env.remove("SERVER_PORT"); + env["REQUEST_METHOD"] = event->method; + env["PATH_INFO"] = event->url.path(); + env["PATH_TRANSLATED"] = event->url.path(); // CGI/1.1 says we should resolve this, but we have no logical interpretation + env["SCRIPT_NAME"] = event->originalUrl.path().remove(QRegExp(QRegExp::escape(event->url.path()) + '$')); + env["SCRIPT_FILENAME"] = qxt_d().binary; // CGI/1.1 doesn't define this but PHP demands it + env.remove("REMOTE_HOST"); + env["REMOTE_ADDR"] = event->remoteAddress; + // TODO: If we ever support HTTP authentication, we should use these + env.remove("AUTH_TYPE"); + env.remove("REMOTE_USER"); + env.remove("REMOTE_IDENT"); + if (event->contentType.isEmpty()) + { + env.remove("CONTENT_TYPE"); + env.remove("CONTENT_LENGTH"); + } + else + { + env["CONTENT_TYPE"] = event->contentType; + env["CONTENT_LENGTH"] = QString::number(event->content->unreadBytes()); + } + env["QUERY_STRING"] = event->url.encodedQuery(); + + // Populate HTTP header environment variables + QMultiHash::const_iterator iter = event->headers.begin(); + while (iter != event->headers.end()) + { + QString key = "HTTP_" + iter.key().toUpper().replace('-', '_'); + if (key != "HTTP_CONTENT_TYPE" && key != "HTTP_CONTENT_LENGTH") + env[key] = iter.value(); + iter++; + } + + // Populate HTTP_COOKIE parameter + iter = event->cookies.begin(); + QString cookies; + while (iter != event->cookies.end()) + { + if (!cookies.isEmpty()) + cookies += "; "; + cookies += iter.key() + '=' + iter.value(); + iter++; + } + if (!cookies.isEmpty()) + env["HTTP_COOKIE"] = cookies; + + // Load environment into process space + QStringList p_env; + QMap::iterator env_iter = env.begin(); + while (env_iter != env.end()) + { + p_env << env_iter.key() + '=' + env_iter.value(); + env_iter++; + } + process->setEnvironment(p_env); + + // Launch process + if (event->url.hasQuery() && event->url.encodedQuery().contains('=')) + { + // CGI/1.1 spec says to pass the query on the command line if there's no embedded = sign + process->start(qxt_d().binary + ' ' + QUrl::fromPercentEncoding(event->url.encodedQuery()), QIODevice::ReadWrite); + } + else + { + process->start(qxt_d().binary, QIODevice::ReadWrite); + } + + // Start the timeout + if(qxt_d().timeout > 0) + { + requestInfo.timeout->start(qxt_d().timeout); + } + + // Transmit POST data + if (event->content) + { + QObject::connect(event->content, SIGNAL(readyRead()), &qxt_d(), SLOT(browserReadyRead())); + qxt_d().browserReadyRead(event->content); + } +} + +/*! + * \internal + */ +void QxtWebCgiServicePrivate::browserReadyRead(QObject* o_content) +{ + if (!o_content) o_content = sender(); + QxtWebContent* content = static_cast(o_content); // this is a private class, no worries about type safety + + // Read POST data and copy it to the process + QByteArray data = content->readAll(); + if (!data.isEmpty()) + processes[content]->write(data); + + // If no POST data remains unsent, clean up + if (!content->unreadBytes() && processes.contains(content)) + { + processes[content]->closeWriteChannel(); + processes.remove(content); + } +} + +/*! + * \internal + */ +void QxtWebCgiServicePrivate::processReadyRead() +{ + QProcess* process = static_cast(sender()); + QxtCgiRequestInfo& request = requests[process]; + + QByteArray line; + while (process->canReadLine()) + { + // Read in a CGI/1.1 header line + line = process->readLine().replace(QByteArray("\r"), ""); //krazy:exclude=doublequote_chars + if (line == "\n") + { + // An otherwise-empty line indicates the end of CGI/1.1 headers and the start of content + QObject::disconnect(process, SIGNAL(readyRead()), this, 0); + QxtWebPageEvent* event = 0; + int code = 200; + if (request.headers.contains("status")) + { + // CGI/1.1 defines a "Status:" header that dictates the HTTP response code + code = request.headers["status"].left(3).toInt(); + if (code >= 300 && code < 400) // redirect + { + event = new QxtWebRedirectEvent(request.sessionID, request.requestID, request.headers["location"], code); + } + } + // If a previous header (currently just status) hasn't created an event, create a normal page event here + if (!event) + { + event = new QxtWebPageEvent(request.sessionID, request.requestID, QSharedPointer(process) ); + event->status = code; + } + // Add other response headers passed from CGI (currently only Content-Type is supported) + if (request.headers.contains("content-type")) + event->contentType = request.headers["content-type"].toUtf8(); + // TODO: QxtWeb doesn't support transmitting arbitrary HTTP headers right now, but it may be desirable + // for applications that know what kind of server frontend they're using to allow scripts to send + // protocol-specific headers. + + // Post the event + qxt_p().postEvent(event); + request.eventSent = true; + return; + } + else + { + // Since we haven't reached the end of headers yet, parse a header + int pos = line.indexOf(": "); + QByteArray hdrName = line.left(pos).toLower(); + QByteArray hdrValue = line.mid(pos + 2).replace(QByteArray("\n"), ""); //krazy:exclude=doublequote_chars + if (hdrName == "set-cookie") + { + // Parse a new cookie and post an event to send it to the client + QList cookies = hdrValue.split(','); + foreach(const QByteArray& cookie, cookies) + { + int equals = cookie.indexOf("="); + int semi = cookie.indexOf(";"); + QByteArray cookieName = cookie.left(equals); + int age = cookie.toLower().indexOf("max-age=", semi); + int secs = -1; + if (age >= 0) + secs = cookie.mid(age + 8, cookie.indexOf(";", age) - age - 8).toInt(); + if (secs == 0) + { + qxt_p().postEvent(new QxtWebRemoveCookieEvent(request.sessionID, cookieName)); + } + else + { + QByteArray cookieValue = cookie.mid(equals + 1, semi - equals - 1); + QDateTime cookieExpires; + if (secs != -1) + cookieExpires = QDateTime::currentDateTime().addSecs(secs); + qxt_p().postEvent(new QxtWebStoreCookieEvent(request.sessionID, cookieName, cookieValue, cookieExpires)); + } + } + } + else if(hdrName == "x-qxtweb-timeout") + { + if(timeoutOverride) + request.timeout->setInterval(hdrValue.toInt()); + } + else + { + // Store other headers for later inspection + request.headers[hdrName] = hdrValue; + } + } + } +} + +/*! + * \internal + */ +void QxtWebCgiServicePrivate::processFinished() +{ + QProcess* process = static_cast(sender()); + QxtCgiRequestInfo& request = requests[process]; + + if (!request.eventSent) + { + // If no event was posted, issue an internal error + qxt_p().postEvent(new QxtWebErrorEvent(request.sessionID, request.requestID, 500, "Internal Server Error")); + } + + // Clean up data structures + process->close(); + QxtWebContent* key = processes.key(process); + if (key) processes.remove(key); + timeoutMapper.removeMappings(request.timeout); + requests.remove(process); +} + +/*! + * \internal + */ +void QxtWebCgiServicePrivate::terminateProcess(QObject* o_process) +{ + QProcess* process = static_cast(o_process); + QxtCgiRequestInfo& request = requests[process]; + + if(request.terminateSent) + { + // kill with fire + process->kill(); + } + else + { + // kill nicely + process->terminate(); + request.terminateSent = true; + } +} diff --git a/qxtweb-standalone/qxtweb/qxtwebcgiservice.h b/qxtweb-standalone/qxtweb/qxtwebcgiservice.h new file mode 100644 index 000000000..3f68e7946 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebcgiservice.h @@ -0,0 +1,58 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTWEBCGISERVICE_H +#define QXTWEBCGISERVICE_H + +#include +#include +#include "qxtabstractwebsessionmanager.h" +#include "qxtabstractwebservice.h" +class QxtWebEvent; +class QxtWebRequestEvent; + +class QxtWebCgiServicePrivate; +class QXT_WEB_EXPORT QxtWebCgiService : public QxtAbstractWebService +{ + Q_OBJECT +public: + QxtWebCgiService(const QString& binary, QxtAbstractWebSessionManager* manager, QObject* parent = 0); + + QString binary() const; + void setBinary(const QString& bin); + + int timeout() const; + void setTimeout(int time); + + bool timeoutOverride() const; + void setTimeoutOverride(bool enable); + + virtual void pageRequestedEvent(QxtWebRequestEvent* event); + +private: + QXT_DECLARE_PRIVATE(QxtWebCgiService) +}; + +#endif // QXTWEBCGISERVICE_H diff --git a/qxtweb-standalone/qxtweb/qxtwebcgiservice_p.h b/qxtweb-standalone/qxtweb/qxtwebcgiservice_p.h new file mode 100644 index 000000000..b7355a3d0 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebcgiservice_p.h @@ -0,0 +1,71 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTWEBCGISERVICE_P_H +#define QXTWEBCGISERVICE_P_H + +#include "qxtwebcgiservice.h" +#include +#include +#include +#include +#include + +#ifndef QXT_DOXYGEN_RUN +QT_FORWARD_DECLARE_CLASS(QProcess) +class QxtWebContent; + +struct QxtCgiRequestInfo +{ + QxtCgiRequestInfo(); + QxtCgiRequestInfo(QxtWebRequestEvent* req); + int sessionID, requestID; + QHash headers; + bool eventSent, terminateSent; + QTimer* timeout; +}; + +class QxtWebCgiServicePrivate : public QObject, public QxtPrivate +{ + Q_OBJECT +public: + QXT_DECLARE_PUBLIC(QxtWebCgiService) + + QHash requests; + QHash processes; + QString binary; + int timeout; + bool timeoutOverride; + QSignalMapper timeoutMapper; + +public Q_SLOTS: + void browserReadyRead(QObject* o_content = 0); + void processReadyRead(); + void processFinished(); + void terminateProcess(QObject* o_process); +}; +#endif // QXT_DOXYGEN_RUN + +#endif // QXTWEBSERVICEDIRECTORY_P_H diff --git a/qxtweb-standalone/qxtweb/qxtwebcontent.cpp b/qxtweb-standalone/qxtweb/qxtwebcontent.cpp new file mode 100644 index 000000000..ff5866356 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebcontent.cpp @@ -0,0 +1,259 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtWebContent + +\inmodule QxtWeb + +\brief The QxtWebContent class provides and I/O device for data sent by the web browser + +QxtWebContent is a read-only QIODevice subclass that encapsulates data sent +from the web browser, for instance in a POST or PUT request. + +In order to avoid delays while reading content sent from the client, and to +insulate multiple pipelined requests on the same connection from each other, +QxtWeb uses QxtWebContent as an abstraction for streaming data. + +\sa QxtAbstractWebService +*/ + +#include "qxtwebcontent.h" +#include +#include + +#ifndef QXT_DOXYGEN_RUN +class QxtWebContentPrivate : public QxtPrivate +{ +public: + QxtWebContentPrivate() : ignoreRemaining(false) {} + QXT_DECLARE_PUBLIC(QxtWebContent) + + void init(int contentLength, const QByteArray& start, QIODevice* device) + { + this->start = start; + this->device = device; + if (contentLength <= 0) + bytesRemaining = -1; + else + bytesRemaining = contentLength - start.length(); + if (device) + { + QObject::connect(device, SIGNAL(readyRead()), &qxt_p(), SIGNAL(readyRead())); + // QObject::connect(device, SIGNAL(aboutToClose()), this, SIGNAL(aboutToClose())); + // QObject::connect(device, SIGNAL(destroyed()), this, SIGNAL(aboutToClose())); + // ask the object if it has an error signal + if (device->metaObject()->indexOfSignal(QMetaObject::normalizedSignature(SIGNAL(error(QAbstractSocket::SocketError)))) >= 0) + { + QObject::connect(device, SIGNAL(error(QAbstractSocket::SocketError)), &qxt_p(), SLOT(errorReceived(QAbstractSocket::SocketError))); + } + } + qxt_p().setOpenMode(QIODevice::ReadOnly); + } + + qint64 bytesRemaining; + QByteArray start; + QIODevice* device; + bool ignoreRemaining; +}; +#endif + +/*! + * Constructs a QxtWebContent object. + * + * The content provided by this constructor is the first \a contentLength bytes + * read from the provided \a device. + * + * The QxtWebContent object is parented to the \a device. + */ +QxtWebContent::QxtWebContent(int contentLength, QIODevice* device) : QIODevice(device) +{ + QXT_INIT_PRIVATE(QxtWebContent); + qxt_d().init(contentLength, QByteArray(), device); +} + +/*! + * Constructs a QxtWebContent object. + * + * The content provided by this constructor is the data contained in \a start, + * followed by enough data read from the provided \a device to fill the desired + * \a contentLength. + * + * The QxtWebContent object is parented to the \a device. + */ +QxtWebContent::QxtWebContent(int contentLength, const QByteArray& start, QIODevice* device) : QIODevice(device) +{ + QXT_INIT_PRIVATE(QxtWebContent); + qxt_d().init(contentLength, start, device); +} + +/*! + * Constructs a QxtWebContent object with the specified \a parent. + * + * The content provided by this constructor is exactly the data contained in + * \a content. + */ +QxtWebContent::QxtWebContent(const QByteArray& content, QObject* parent) : QIODevice(parent) +{ + QXT_INIT_PRIVATE(QxtWebContent); + qxt_d().init(content.size(), content, 0); +} + +/*! + * \reimp + */ +qint64 QxtWebContent::bytesAvailable() const +{ + qint64 available = QIODevice::bytesAvailable() + (qxt_d().device ? qxt_d().device->bytesAvailable() : 0) + qxt_d().start.count(); + if (available > qxt_d().bytesRemaining) + return qxt_d().bytesRemaining; + return available; +} + +/*! + * \reimp + */ +qint64 QxtWebContent::readData(char* data, qint64 maxSize) +{ + char* writePtr = data; + // read more than 32k; TCP ideally handles 48k blocks but we need wiggle room + if (maxSize > 32768) maxSize = 32768; + + // don't read more than the content-length + int sz = qxt_d().start.count(); + if (sz > 0 && maxSize > sz) + { + memcpy(writePtr, qxt_d().start.constData(), sz); + writePtr += sz; + maxSize -= sz; + qxt_d().start.clear(); + } + else if (sz > 0 && sz > maxSize) + { + memcpy(writePtr, qxt_d().start.constData(), maxSize); + qxt_d().start = qxt_d().start.mid(maxSize); + return maxSize; + } + + if (qxt_d().device == 0) + { + return sz; + } + else if (qxt_d().bytesRemaining >= 0) + { + qint64 readBytes = qxt_d().device->read(writePtr, (maxSize > qxt_d().bytesRemaining) ? qxt_d().bytesRemaining : maxSize); + qxt_d().bytesRemaining -= readBytes; + if (qxt_d().bytesRemaining == 0) QMetaObject::invokeMethod(this, "aboutToClose", Qt::QueuedConnection); + return sz + readBytes; + } + else + { + return sz + qxt_d().device->read(writePtr, maxSize); + } +} + +/*! + * Returns the number of bytes of content that have not yet been read. + * + * Note that not all of the remaining content may be immediately available for + * reading. This function returns the content length, minus the number of + * bytes that have already been read. + */ +qint64 QxtWebContent::unreadBytes() const +{ + return qxt_d().start.size() + qxt_d().bytesRemaining; +} + +/*! + * \reimp + */ +qint64 QxtWebContent::writeData(const char*, qint64) +{ + // always an error to write + return -1; +} + +/*! + * \internal + */ +void QxtWebContent::errorReceived(QAbstractSocket::SocketError) +{ + setErrorString(qxt_d().device->errorString()); +} + +/*! + * Blocks until all of the streaming data has been read from the browser. + * + * Note that this function will block events for the thread on which it is called. + * If the main thread is blocked, QxtWeb will be unable to process additional + * requests until the content has been received. + */ +void QxtWebContent::waitForAllContent() +{ + if (!qxt_d().device) return; + QByteArray buffer; + while (qxt_d().device && qxt_d().bytesRemaining > 0) + { + buffer = qxt_d().device->readAll(); + qxt_d().start += buffer; + qxt_d().bytesRemaining -= buffer.size(); + if (qxt_d().bytesRemaining > 0) qxt_d().device->waitForReadyRead(-1); + } +} + +/*! + * Discards any data not yet read. + * + * After invoking this function, any further data received from the browser + * is silently discarded. + */ +void QxtWebContent::ignoreRemainingContent() +{ + if (qxt_d().bytesRemaining <= 0 || !qxt_d().device) return; + if (!qxt_d().ignoreRemaining) + { + qxt_d().ignoreRemaining = true; + QObject::connect(qxt_d().device, SIGNAL(readyRead()), this, SLOT(ignoreRemainingContent())); + } +} + +#ifndef QXT_DOXYGEN_RUN +typedef QPair QxtQueryItem; +#endif + +/*! + * Extracts the key/value pairs from application/x-www-form-urlencoded \a data, + * such as the query string from the URL or the form data from a POST request. + */ +QHash QxtWebContent::parseUrlEncodedQuery(const QString& data) +{ + QUrl post("/?" + data); + QHash rv; + foreach(const QxtQueryItem& item, post.queryItems()) + { + rv.insertMulti(item.first, item.second); + } + return rv; +} diff --git a/qxtweb-standalone/qxtweb/qxtwebcontent.h b/qxtweb-standalone/qxtweb/qxtwebcontent.h new file mode 100644 index 000000000..06e88a808 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebcontent.h @@ -0,0 +1,63 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTWEBCONTENT_H +#define QXTWEBCONTENT_H + +#include +#include +#include +#include + +class QxtWebContentPrivate; +class QXT_WEB_EXPORT QxtWebContent : public QIODevice +{ + Q_OBJECT +public: + QxtWebContent(int contentLength, const QByteArray& start, QIODevice* device); + QxtWebContent(int contentLength, QIODevice* device); + explicit QxtWebContent(const QByteArray& content, QObject* parent = 0); + static QHash parseUrlEncodedQuery(const QString& data); + + virtual qint64 bytesAvailable() const; + qint64 unreadBytes() const; + + void waitForAllContent(); + +public Q_SLOTS: + void ignoreRemainingContent(); + +protected: + virtual qint64 readData(char* data, qint64 maxSize); + virtual qint64 writeData(const char* data, qint64 maxSize); + +private Q_SLOTS: + void errorReceived(QAbstractSocket::SocketError); + +private: + QXT_DECLARE_PRIVATE(QxtWebContent) +}; + +#endif // QXTWEBCONTENT_H diff --git a/qxtweb-standalone/qxtweb/qxtwebevent.cpp b/qxtweb-standalone/qxtweb/qxtwebevent.cpp new file mode 100644 index 000000000..cd0d75755 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebevent.cpp @@ -0,0 +1,401 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#include "qxtwebevent.h" +#include "qxtwebcontent.h" +#include + +/*! +\class QxtWebEvent + +\inmodule QxtWeb + +\brief The QxtWebEvent class is a base class of all QxtWeb event types + +QxtWebEvent is the base class for all QxtWeb event classes. Event objects +contain event parameters. + +The base QxtWebEvent class contains the type of the event and a session ID for +the session it relates to. Subclasses of QxtWebEvent contain additional +parameters describing the particular event. +*/ + +/*! + \enum QxtWebEvent::EventType + + \value None Not an event. + \value Request A request event. + \value FileUpload A file upload event. + \value Page A page event. + \value StoreCookie A store cookie event. + \value RemoveCookie A remove cookie event. + \value Redirect A redirect event. +*/ + +/*! + * Constructs a QxtWebEvent of the specified \a type for the specified \a sessionID. + */ +QxtWebEvent::QxtWebEvent(EventType type, int sessionID) + : sessionID(sessionID), m_type(type) {} + +/*! + * Destroys the event. + */ +QxtWebEvent::~QxtWebEvent() {} + +/*! + * \fn EventType QxtWebEvent::type() const + * Returns the event type. + */ + +/*! + * \variable QxtWebEvent::sessionID + * Contains the ID of the session the event is related to. + */ + +/*! +\class QxtWebRequestEvent + +\inmodule QxtWeb + +\brief The QxtWebRequestEvent class describes a request from a web browser + +The QxtWebRequestEvent class contains information about a request from a web +browser. +*/ + +/*! + * Constructs a QxtWebRequestEvent for the specified \a sessionID, \a requestID and \a url. + */ +QxtWebRequestEvent::QxtWebRequestEvent(int sessionID, int requestID, const QUrl& url) + : QxtWebEvent(QxtWebEvent::Request, sessionID), requestID(requestID), url(url), originalUrl(url) {} + +/*! + * Destroys the event and any content that may still be associated with it. + */ +QxtWebRequestEvent::~QxtWebRequestEvent() +{ + if (content) delete content; +} + +/*! + * \variable QxtWebRequestEvent::requestID + * Contains an opaque value generated by the session manager. This request ID + * must be included in the QxtWebPageEvent or QxtWebPageEvent subclass that + * is the response to the request. + */ + +/*! + * \variable QxtWebRequestEvent::url + * Contains the request URL, possibly after rewriting by intermediate services + * such as QxtWebServiceDirectory. + */ + +/*! + * \variable QxtWebRequestEvent::originalUrl + * Contains the request URL exactly as it was sent from the web browser. + */ + +/*! + * \variable QxtWebRequestEvent::contentType + * Contains the MIME type of the request body, if present. + */ + +/*! + * \variable QxtWebRequestEvent::content + * Contains the content of the request body, if present. + */ + +/*! + * \variable QxtWebRequestEvent::cookies + * Contains all of the cookies sent by the web browser. + */ + +/*! + * \variable QxtWebRequestEvent::headers + * Contains all of the headers sent by the web browser. + * + * Note that use of these values may not be portable across session maangers. + */ + +/* +QxtWebFileUploadEvent::QxtWebFileUploadEvent(int sessionID) +: QxtWebEvent(QxtWebEvent::FileUpload, sessionID) {} +*/ + +/*! +\class QxtWebErrorEvent + +\inmodule QxtWeb + +\brief The QxtWebErrorEvent class describes an error condition to be sent to a web browser + +The QxtWebErrorEvent class contains information about an error that will be +sent to a web browser. + +QxtWebErrorEvent is a QxtWebPageEvent, so the \a dataSource may be replaced +with a custom error page. If you choose to do this, be sure to delete the +original data source automatically generated by the constructor first. +*/ + +/*! + * Constructs a QxtWebErrorEvent for the specified \a sessionID and \a requestID, + * with the provided \a statusCode and \a statusMessage. + * + * The requestID is an opaque value generated by the session manager; services + * will receive this value via QxtWebRequestEvent and must use it in every + * event that responds to that request. + */ +QxtWebErrorEvent::QxtWebErrorEvent(int sessionID, int requestID, int statusCode, QByteArray statusMessage) + : QxtWebPageEvent(sessionID, requestID, "

" + statusMessage + "

\r\n") +{ + status = statusCode; + QxtWebPageEvent::statusMessage = statusMessage; +} + +/*! +\class QxtWebPageEvent + +\inmodule QxtWeb + +\brief The QxtWebPageEvent class describes a web page or other content to be sent to a web browser + +The QxtWebPageEvent class contains information about a web page or other similar +content that will be sent to a web browser. +*/ + +/*! + * Constructs a QxtWebPageEvent for the specified \a sessionID and \a requestID that will + * use the data from \a source as the content to be sent to the web browser. + * + * The requestID is an opaque value generated by the session manager; services + * will receive this value via QxtWebRequestEvent and must use it in every + * event that responds to that request. + * + * QxtWeb takes ownership of the source and will delete it when the response + * is completed. + */ +QxtWebPageEvent::QxtWebPageEvent(int sessionID, int requestID, QSharedPointer source) + : QxtWebEvent(QxtWebEvent::Page, sessionID), dataSource(source), chunked(true), streaming(true), requestID(requestID), + status(200), statusMessage("OK"), contentType("text/html") {} + +/*! + * Constructs a QxtWebPageEvent for the specified \a sessionID and \a requestID that will + * use \a source as the content to be sent to the web browser. + * + * The requestID is an opaque value generated by the session manager; services + * will receive this value via QxtWebRequestEvent and must use it in every + * event that responds to that request. + */ +QxtWebPageEvent::QxtWebPageEvent(int sessionID, int requestID, QByteArray source) + : QxtWebEvent(QxtWebEvent::Page, sessionID), chunked(false), streaming(false), requestID(requestID), + status(200), statusMessage("OK"), contentType("text/html") +{ + QBuffer* buffer = new QBuffer; + buffer->setData(source); + buffer->open(QIODevice::ReadOnly); + dataSource = QSharedPointer( buffer ); +} + +/*! + * \internal + */ +QxtWebPageEvent::QxtWebPageEvent(QxtWebEvent::EventType typeOverride, int sessionID, int requestID, QByteArray source) + : QxtWebEvent(typeOverride, sessionID), chunked(false), streaming(false), requestID(requestID), + status(200), statusMessage("OK"), contentType("text/html") +{ + QBuffer* buffer = new QBuffer; + buffer->setData(source); + buffer->open(QIODevice::ReadOnly); + dataSource = QSharedPointer( buffer ); +} + +/*! + * Destroys the event + */ +QxtWebPageEvent::~QxtWebPageEvent() +{ + +} + +/*! + * \variable QxtWebPageEvent::dataSource + * Data will be read from this device and relayed to the web browser. + */ + +/*! + * \variable QxtWebPageEvent::chunked + * If true, and if the web browser supports "chunked" encoding, the content + * will be sent using "chunked" encoding. If false, or if the browser does not + * support this encoding (for instance, HTTP/0.9 and HTTP/1.0 user agents), + * HTTP keep-alive will be disabled. + * + * The default value is true when using the QIODevice* constructor and false + * when using the QByteArray constructor. + */ + +/*! + * \variable QxtWebPageEvent::streaming + * If true, the data source is considered to be a source of streaming data. + * The QIODevice must emit the readyRead() signal when data is available and + * must emit aboutToClose() after all data has been transferred. (This can + * be accomplished by invoking QIODevice::close() on it after all data is + * determined to have been transferred.) + * + * The default value is true when using the QIODevice* constructor and false + * when using the QByteArray constructor. If using a QIODevice that does not + * produce streaming data, such as QFile, this \a must be set to false to + * ensure correct behavior. + */ + +/*! + * \variable QxtWebPageEvent::requestID + * Contains the opaque requestID provided by QxtWebRequestEvent. + */ + +/*! + * \variable QxtWebPageEvent::status + * Contains the HTTP status code that will be sent with the response. + * + * The default value is 200 ("OK"). + */ + +/*! + * \variable QxtWebPageEvent::statusMessage + * Contains the human-readable message associated with the HTTP status code + * that will be sent with the response. + * + * The default value is "OK". + */ + +/*! + * \variable QxtWebPageEvent::contentType + * Contains the MIME type of the content being sent to the web browser. + * + * The default value is "text/html". + */ + +/*! + * \variable QxtWebPageEvent::headers + * Contains custom headers to be sent to the web browser. + * + * It is empty by default. + */ + +/*! +\class QxtWebStoreCookieEvent + +\inmodule QxtWeb + +\brief The QxtWebStoreCookieEvent class describes a cookie to be sent to a web browser + +The QxtWebStoreCookieEvent class instructs the session manager to store +a cookie on the web browser. +*/ + +/*! + * Constructs a QxtWebStoreCookieEvent for the specified \a sessionID that will + * store a cookie with the specified \a name and \a data on the web browser. + * + * If an \a expiration date is supplied, it will be passed to the browser along + * with the cookie. The browser will delete the cookie automatically after + * the specified date. If an expiration date is not supplied, the cookie will + * expire when the browser is closed. + */ +QxtWebStoreCookieEvent::QxtWebStoreCookieEvent(int sessionID, QString name, QString data, QDateTime expiration) + : QxtWebEvent(QxtWebEvent::StoreCookie, sessionID), name(name), data(data), expiration(expiration) {} + +/*! + * \variable QxtWebStoreCookieEvent::name + * Contains the name of the cookie to be stored. + */ + +/*! + * \variable QxtWebStoreCookieEvent::data + * Contains the content of the cookie to be stored. + */ + +/*! + * \variable QxtWebStoreCookieEvent::expiration + * Contains the expiration date of the cookie to be stored. If null, the + * cookie will expire when the web browser is closed. + */ + +/*! +\class QxtWebRemoveCookieEvent + +\inmodule QxtWeb + +\brief The QxtWebRemoveCookieEvent class describes a cookie to be deleted from a web browser + +The QxtWebStoreCookieEvent class instructs the session manager to remove +a cookie stored on the web browser. +*/ + +/*! + * Constructs a QxtWebRemoveCookieEvent for the specified \a sessionID that + * removed the cookie with \a name from the web browser. + */ +QxtWebRemoveCookieEvent::QxtWebRemoveCookieEvent(int sessionID, QString name) + : QxtWebEvent(QxtWebEvent::RemoveCookie, sessionID), name(name) {} + +/*! + * \variable QxtWebRemoveCookieEvent::name + * Contains the name of the cookie to be removed. + */ + +/*! +\class QxtWebRedirectEvent + +\inmodule QxtWeb + +\brief The QxtWebRedirectEvent class describes a redirect event to be sent to a web browser + +The QxtWebRedirectEvent class instructs the web browser to load a page found at +another location. + +The default status code, 302, indicates that the requested page was found at +a different location. Other useful status codes are 301, which indicates +that the web browser should always use the new URL in place of the old one, +and (in HTTP/1.1) 307, which indicates that the web browser should reissue +the same request (including POST data) to the new URL. +*/ + +/*! + * Constructs a QxtWebRedirectEvent for the specified \a sessionID and \a requestID that + * instructs the browser to move to the specified \a destination URL with \a statusCode. + */ +QxtWebRedirectEvent::QxtWebRedirectEvent(int sessionID, int requestID, const QString& destination, int statusCode) + : QxtWebPageEvent(QxtWebEvent::Redirect, sessionID, requestID, QString("Redirect: %1").arg(destination).toUtf8()), destination(destination) +{ + QxtWebPageEvent::status = statusCode; + QxtWebPageEvent::statusMessage = ("Redirect to " + destination).toUtf8(); +} + +/*! + * \variable QxtWebRedirectEvent::destination + * Contains the new location (absolute or relative) to which the browser + * should redirect. + */ diff --git a/qxtweb-standalone/qxtweb/qxtwebevent.h b/qxtweb-standalone/qxtweb/qxtwebevent.h new file mode 100644 index 000000000..1b57c535c --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebevent.h @@ -0,0 +1,153 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTWEBEVENT_H +#define QXTWEBEVENT_H + +#include +#include +#include +#include +#include +#include +#include +#include +QT_FORWARD_DECLARE_CLASS(QIODevice) +class QxtWebContent; + +class QXT_WEB_EXPORT QxtWebEvent +{ +public: + enum EventType + { + None = 0, + Request, + FileUpload, + Page, + StoreCookie, + RemoveCookie, + Redirect + }; + + QxtWebEvent(EventType type, int sessionID); + virtual ~QxtWebEvent(); + + inline EventType type() const + { + return m_type; + } + const int sessionID; + +private: + EventType m_type; +}; + +class QXT_WEB_EXPORT QxtWebRequestEvent : public QxtWebEvent +{ +public: + QxtWebRequestEvent(int sessionID, int requestID, const QUrl& url); + ~QxtWebRequestEvent(); + + const int requestID; + + QUrl url; + const QUrl originalUrl; + QString contentType; + QPointer content; + QString method; + QString remoteAddress; + + QMultiHash cookies; + QMultiHash headers; +}; + +/* TODO: refactor and implement +class QXT_WEB_EXPORT QxtWebFileUploadEvent : public QxtWebEvent { +public: + QxtWebFileUploadEvent(int sessionID); + + QString filename; + int contentLength; + QIODevice* content; +}; +*/ + +class QxtWebRedirectEvent; +class QXT_WEB_EXPORT QxtWebPageEvent : public QxtWebEvent +{ +public: + QxtWebPageEvent(int sessionID, int requestID, QSharedPointer source); + QxtWebPageEvent(int sessionID, int requestID, QByteArray source); // creates a QBuffer + virtual ~QxtWebPageEvent(); + + QSharedPointer dataSource; // data is read from this device and written to the client + bool chunked; + bool streaming; + + const int requestID; + int status; + QByteArray statusMessage; + QByteArray contentType; + + QMultiHash headers; + +private: + friend class QxtWebRedirectEvent; + QxtWebPageEvent(QxtWebEvent::EventType typeOverride, int sessionID, int requestID, QByteArray source); +}; + +class QXT_WEB_EXPORT QxtWebErrorEvent : public QxtWebPageEvent +{ +public: + QxtWebErrorEvent(int sessionID, int requestID, int status, QByteArray statusMessage); +}; + +class QXT_WEB_EXPORT QxtWebStoreCookieEvent : public QxtWebEvent +{ +public: + QxtWebStoreCookieEvent(int sessionID, QString name, QString data, QDateTime expiration = QDateTime()); + + QString name; + QString data; + QDateTime expiration; +}; + +class QXT_WEB_EXPORT QxtWebRemoveCookieEvent : public QxtWebEvent +{ +public: + QxtWebRemoveCookieEvent(int sessionID, QString name); + + QString name; +}; + +class QXT_WEB_EXPORT QxtWebRedirectEvent : public QxtWebPageEvent +{ +public: + QxtWebRedirectEvent(int sessionID, int requestID, const QString& destination, int statusCode = 302); + + QString destination; +}; + +#endif // QXTWEBEVENT_H diff --git a/qxtweb-standalone/qxtweb/qxtwebservicedirectory.cpp b/qxtweb-standalone/qxtweb/qxtwebservicedirectory.cpp new file mode 100644 index 000000000..06209b36e --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebservicedirectory.cpp @@ -0,0 +1,239 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +/*! +\class QxtWebServiceDirectory + +\inmodule QxtWeb + +\brief The QxtWebServiceDirectory class provides Path-based web service dispatcher + +QxtWebServiceDirectory allows multiple services to be associated with a single +session. Selection between services is determined by the first path component +in the URL. For example, the URL "/site/request?param=true" would relay the +URL "/request?param=true" to the service named "site". + +This class can be used recursively to declare a hierarchy of services. For +example: +\code +QxtWebServiceDirectory* top = new QxtWebServiceDirectory(sm, sm); +QxtWebServiceDirectory* service1 = new QxtWebServiceDirectory(sm, top); +QxtWebServiceDirectory* service2 = new QxtWebServiceDirectory(sm, top); +QxtWebServiceDirectory* service1a = new QxtWebServiceDirectory(sm, service1); +QxtWebServiceDirectory* service1b = new QxtWebServiceDirectory(sm, service1); +top->addService("1", service1); +top->addService("2", service2); +service1->addService("a", service1a); +service1->addService("b", service1b); +\endcode +This accepts the URLs "/1/a/", "/1/b/", and "/2/". +*/ + +#include "qxtwebservicedirectory.h" +#include "qxtwebservicedirectory_p.h" +#include "qxtwebevent.h" +#include +#include + +#ifndef QXT_DOXYGEN_RUN +QxtWebServiceDirectoryPrivate::QxtWebServiceDirectoryPrivate() : QObject(0) +{ + // initializers only +} + +void QxtWebServiceDirectoryPrivate::serviceDestroyed() +{ + QxtAbstractWebService* service = qobject_cast(sender()); + if (!service) return; // this shouldn't happen + QString path; + while (!(path = services.key(service)).isNull()) + { + services.remove(path); + } +} +#endif + +/*! + * Constructs a QxtWebServiceDirectory object with the specified session manager \a sm and \a parent. + * + * Often, the session manager will also be the parent, but this is not a requirement. + */ +QxtWebServiceDirectory::QxtWebServiceDirectory(QxtAbstractWebSessionManager* sm, QObject* parent) : QxtAbstractWebService(sm, parent) +{ + QXT_INIT_PRIVATE(QxtWebServiceDirectory); +} + +/*! + * Adds a \a service to the directory at the given \a path. + * \sa removeService(), service() + */ +void QxtWebServiceDirectory::addService(const QString& path, QxtAbstractWebService* service) +{ + if (qxt_d().services.contains(path)) + { + qWarning() << "QxtWebServiceDirectory::addService:" << path << "already registered"; + } + + qxt_d().services[path] = service; + if (qxt_d().defaultRedirect.isEmpty()) + setDefaultRedirect(path); + connect(service, SIGNAL(destroyed()), &qxt_d(), SLOT(serviceDestroyed())); +} + +/*! + * Removes the service at the given \a path. + * + * Note that the service object is not destroyed. + */ +void QxtWebServiceDirectory::removeService(const QString& path) +{ + if (!qxt_d().services.contains(path)) + { + qWarning() << "QxtWebServiceDirectory::removeService:" << path << "not registered"; + } + else + { + qxt_d().services.remove(path); + } +} + +/*! + * Returns the service at the given \a path. + */ +QxtAbstractWebService* QxtWebServiceDirectory::service(const QString& path) const +{ + if (!qxt_d().services.contains(path)) + return 0; + return qxt_d().services[path]; +} + +/*! + * \internal + * Returns the first path segment from the URL in the \a event object. + * (i.e. "a" from "/a/b/c") This also removes the path segment from the + * event object. (in the previous example, the event's URL is now "/b/c") + */ +static QString extractPathLevel(QxtWebRequestEvent* event) +{ + QString path = event->url.path(); + int pos = path.indexOf("/", 1); // the path always starts with / + if (pos == -1) + event->url.setPath(""); // cue to redirect to /service/ + else + event->url.setPath(path.mid(pos)); + return path.mid(1, pos - 1); +} + +/*! + * \reimp + */ +void QxtWebServiceDirectory::pageRequestedEvent(QxtWebRequestEvent* event) +{ + QString path = extractPathLevel(event); + if (path.isEmpty()) + { + indexRequested(event); + } + else if (!qxt_d().services.contains(path)) + { + unknownServiceRequested(event, path); + } + else if (event->url.path().isEmpty()) + { + postEvent(new QxtWebRedirectEvent(event->sessionID, event->requestID, path + '/', 307)); + } + else + { + qxt_d().services[path]->pageRequestedEvent(event); + } +} + +/* + * \reimp unimplemented + */ +/* +void QxtWebServiceDirectory::functionInvokedEvent(QxtWebRequestEvent* event) { + QString path = extractPathLevel(event); + if(path == "") { + indexRequested(event); + } else if(!qxt_d().services.contains(path)) { + unknownServiceRequested(event, path); + } else { + qxt_d().services[path]->functionInvokedEvent(event); + } +} +*/ + +/*! + * This \a event handler is called whenever the URL requests a service with \a name that has + * not been added to the directory. + * + * The default implementation returns a 404 "Service not known" error. + * Subclasses may reimplement this event handler to customize this behavior. + */ +void QxtWebServiceDirectory::unknownServiceRequested(QxtWebRequestEvent* event, const QString& name) +{ + postEvent(new QxtWebErrorEvent(event->sessionID, event->requestID, 404, ("Service "" + QString(name).replace('<', "<") + "" not known").toUtf8())); +} + +/*! + * This \a event handler is called whenever the URL does not contain a path, that + * is, the URL is "/" or empty. + * + * The default implementation redirects to the service specified by + * setDefaultRedirect(), or invokes unknownServiceRequested() if no default + * redirect has been set. + */ +void QxtWebServiceDirectory::indexRequested(QxtWebRequestEvent* event) +{ + if (defaultRedirect().isEmpty()) + { + unknownServiceRequested(event, "/"); + } + else + { + postEvent(new QxtWebRedirectEvent(event->sessionID, event->requestID, defaultRedirect() + '/', 307)); + } +} + +/*! + * Returns the path that will be used by default by the indexRequested event. + * \sa indexRequested(), setDefaultRedirect() + */ +QString QxtWebServiceDirectory::defaultRedirect() const +{ + return qxt_d().defaultRedirect; +} + +/*! + * Sets the \a path that will be used by default by the indexRequested event. + * \sa indexRequested(), defaultRedirect() + */ +void QxtWebServiceDirectory::setDefaultRedirect(const QString& path) +{ + if (!qxt_d().services.contains(path)) + qWarning() << "QxtWebServiceDirectory::setDefaultRedirect:" << path << "not registered"; + qxt_d().defaultRedirect = path; +} diff --git a/qxtweb-standalone/qxtweb/qxtwebservicedirectory.h b/qxtweb-standalone/qxtweb/qxtwebservicedirectory.h new file mode 100644 index 000000000..a0ebe71d2 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebservicedirectory.h @@ -0,0 +1,59 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTWEBSERVICEDIRECTORY_H +#define QXTWEBSERVICEDIRECTORY_H + +#include +#include +class QxtAbstractWebSessionManager; +class QxtWebEvent; + +class QxtWebServiceDirectoryPrivate; +class QXT_WEB_EXPORT QxtWebServiceDirectory : public QxtAbstractWebService +{ + Q_OBJECT +public: + explicit QxtWebServiceDirectory(QxtAbstractWebSessionManager* sm, QObject* parent = 0); + + void addService(const QString& path, QxtAbstractWebService* service); + void removeService(const QString& path); + QxtAbstractWebService* service(const QString& path) const; + + virtual void pageRequestedEvent(QxtWebRequestEvent* event); +// virtual void functionInvokedEvent(QxtWebRequestEvent* event); + + QString defaultRedirect() const; + void setDefaultRedirect(const QString& path); + +protected: + virtual void unknownServiceRequested(QxtWebRequestEvent* event, const QString& name); + virtual void indexRequested(QxtWebRequestEvent* event); + +private: + QXT_DECLARE_PRIVATE(QxtWebServiceDirectory) +}; + +#endif // QXTWEBSERVICEDIRECTORY_H diff --git a/qxtweb-standalone/qxtweb/qxtwebservicedirectory_p.h b/qxtweb-standalone/qxtweb/qxtwebservicedirectory_p.h new file mode 100644 index 000000000..cbaa7528e --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebservicedirectory_p.h @@ -0,0 +1,49 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTWEBSERVICEDIRECTORY_P_H +#define QXTWEBSERVICEDIRECTORY_P_H + +#include "qxtwebservicedirectory.h" +#include +#include + +#ifndef QXT_DOXYGEN_RUN +class QxtWebServiceDirectoryPrivate : public QObject, public QxtPrivate +{ + Q_OBJECT +public: + QXT_DECLARE_PUBLIC(QxtWebServiceDirectory) + QxtWebServiceDirectoryPrivate(); + + QHash services; + QString defaultRedirect; + +public Q_SLOTS: + void serviceDestroyed(); +}; +#endif // QXT_DOXYGEN_RUN + +#endif // QXTWEBSERVICEDIRECTORY_P_H diff --git a/qxtweb-standalone/qxtweb/qxtwebslotservice.cpp b/qxtweb-standalone/qxtweb/qxtwebslotservice.cpp new file mode 100644 index 000000000..0ae2bd9a0 --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebslotservice.cpp @@ -0,0 +1,226 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + + +/*! +\class QxtWebSlotService + +\inmodule QxtWeb + +\brief The QxtWebSlotService class provides a Slot based webservice + +A WebService that resolves the first part of the path to a slot name and passes the rest as arguments. + +\code +class MyService : public QxtWebSlotService +{ +Q_OBJECT +public slots: + void hello(QxtWebRequestEvent* event, QString a) + { + postEvent(new QxtWebPageEvent(event->sessionID, event->requestID, "<h1>"+a.toUtf8()+"</h1>)); + } +} +\endcode + + +/hello/foo
+will output
+<h1>Foo</h1>
+ + +\sa QxtAbstractWebService +*/ + +#include "qxtwebslotservice.h" +#include "qxtwebevent.h" + +/*! + Constructs a new QxtWebSlotService with \a sm and \a parent. + */ +QxtWebSlotService::QxtWebSlotService(QxtAbstractWebSessionManager* sm, QObject* parent): QxtAbstractWebService(sm, parent) +{ +} + +/*! + Returns the current absolute url of this service depending on the request \a event. + */ +QUrl QxtWebSlotService::self(QxtWebRequestEvent* event) + +{ + QStringList u = event->url.path().split('/'); + QStringList o = event->originalUrl.path().split('/'); + u.removeFirst(); + o.removeFirst(); + for (int i = 0;i < u.count();i++) + o.removeLast(); + + + QString r = "/"; + foreach(const QString& d, o) + { + r += d + '/'; + } + return r; +} + +/*! + \reimp + */ +void QxtWebSlotService::pageRequestedEvent(QxtWebRequestEvent* event) +{ + QList args = event->url.path().split('/'); + args.removeFirst(); + if (args.at(args.count() - 1).isEmpty()) + args.removeLast(); + + + ///--------------find action ------------------ + QByteArray action = "index"; + if (args.count()) + { + action = args.at(0).toUtf8(); + if (action.trimmed().isEmpty()) + action = "index"; + args.removeFirst(); + } + + + + bool ok = false; + if (args.count() > 7) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)), + Q_ARG(QString, args.at(1)), + Q_ARG(QString, args.at(2)), + Q_ARG(QString, args.at(3)), + Q_ARG(QString, args.at(4)), + Q_ARG(QString, args.at(5)), + Q_ARG(QString, args.at(6)), + Q_ARG(QString, args.at(7)) + ); + } + else if (args.count() > 6) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)), + Q_ARG(QString, args.at(1)), + Q_ARG(QString, args.at(2)), + Q_ARG(QString, args.at(3)), + Q_ARG(QString, args.at(4)), + Q_ARG(QString, args.at(5)), + Q_ARG(QString, args.at(6)) + ); + } + else if (args.count() > 5) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)), + Q_ARG(QString, args.at(1)), + Q_ARG(QString, args.at(2)), + Q_ARG(QString, args.at(3)), + Q_ARG(QString, args.at(4)), + Q_ARG(QString, args.at(5)) + ); + } + else if (args.count() > 4) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)), + Q_ARG(QString, args.at(1)), + Q_ARG(QString, args.at(2)), + Q_ARG(QString, args.at(3)), + Q_ARG(QString, args.at(4)) + ); + } + else if (args.count() > 3) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)), + Q_ARG(QString, args.at(1)), + Q_ARG(QString, args.at(2)), + Q_ARG(QString, args.at(3)) + ); + } + else if (args.count() > 2) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)), + Q_ARG(QString, args.at(1)), + Q_ARG(QString, args.at(2)) + ); + } + else if (args.count() > 1) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)), + Q_ARG(QString, args.at(1)) + ); + } + else if (args.count() > 0) + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event), + Q_ARG(QString, args.at(0)) + ); + } + else + { + ok = QMetaObject::invokeMethod(this, action, + Q_ARG(QxtWebRequestEvent*, event) + ); + } + + + if (!ok) + { + QByteArray err = "

Can not find slot

Class " + QByteArray(metaObject()->className()) + "\r{\npublic slots:\r    void " + action.replace('<', "<") + " ( QxtWebRequestEvent* event, ";
+        for (int i = 0;i < args.count();i++)
+            err += "QString arg" + QByteArray::number(i) + ", ";
+        err.chop(2);
+
+        err += " ); \r};\r
"; + + postEvent(new QxtWebErrorEvent(event->sessionID, event->requestID, 404, err)); + } + + +} + +/*! + \reimp + */ +void QxtWebSlotService::functionInvokedEvent(QxtWebRequestEvent* event) +{ + postEvent(new QxtWebErrorEvent(event->sessionID, event->requestID, 500, "

Not supported

")); +} diff --git a/qxtweb-standalone/qxtweb/qxtwebslotservice.h b/qxtweb-standalone/qxtweb/qxtwebslotservice.h new file mode 100644 index 000000000..4f917825e --- /dev/null +++ b/qxtweb-standalone/qxtweb/qxtwebslotservice.h @@ -0,0 +1,45 @@ +/**************************************************************************** + ** + ** Copyright (C) Qxt Foundation. Some rights reserved. + ** + ** This file is part of the QxtWeb module of the Qxt library. + ** + ** This library is free software; you can redistribute it and/or modify it + ** under the terms of the Common Public License, version 1.0, as published + ** by IBM, and/or under the terms of the GNU Lesser General Public License, + ** version 2.1, as published by the Free Software Foundation. + ** + ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY + ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + ** FITNESS FOR A PARTICULAR PURPOSE. + ** + ** You should have received a copy of the CPL and the LGPL along with this + ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files + ** included with the source distribution for more information. + ** If you did not receive a copy of the licenses, contact the Qxt Foundation. + ** + ** + ** + ****************************************************************************/ + +#ifndef QXTWEBSLOTSERVICE_H +#define QXTWEBSLOTSERVICE_H + +#include "qxtabstractwebservice.h" +#include + +class QXT_WEB_EXPORT QxtWebSlotService : public QxtAbstractWebService +{ + Q_OBJECT +public: + explicit QxtWebSlotService(QxtAbstractWebSessionManager* sm, QObject* parent = 0); + +protected: + QUrl self(QxtWebRequestEvent* event); + + virtual void pageRequestedEvent(QxtWebRequestEvent* event); + virtual void functionInvokedEvent(QxtWebRequestEvent* event); +}; + +#endif // QXTWEBSLOTSERVICE_H diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 000000000..88605377d --- /dev/null +++ b/resources.qrc @@ -0,0 +1,71 @@ + + +./data/images/add-friend-button-pressed.png +./data/images/add-friend-button-rest.png +./data/images/avatar-dude-plus.png +./data/images/avatar-dude.png +./data/images/back-pressed.png +./data/images/back-rest.png +./data/images/magnifying-glass.png +./data/images/no-album-art-placeholder.png +./data/images/now-playing-panel.png +./data/images/now-playing-speaker.png +./data/images/pause-pressed.png +./data/images/pause-rest.png +./data/images/play-pressed.png +./data/images/play-rest.png +./data/images/playlist-icon.png +./data/images/repeat-1-on-pressed.png +./data/images/repeat-1-on-rest.png +./data/images/repeat-all-on-pressed.png +./data/images/repeat-all-on-rest.png +./data/images/repeat-off-pressed.png +./data/images/repeat-off-rest.png +./data/images/search-box-dismiss-x.png +./data/images/search-box.png +./data/images/seek-and-volume-knob-pressed.png +./data/images/seek-and-volume-knob-rest.png +./data/images/seek-slider-bkg.png +./data/images/seek-slider-level.png +./data/images/shuffle-off-pressed.png +./data/images/shuffle-off-rest.png +./data/images/shuffle-on-pressed.png +./data/images/shuffle-on-rest.png +./data/images/skip-pressed.png +./data/images/skip-rest.png +./data/images/source-off-pressed.png +./data/images/source-off-rest.png +./data/images/source-on-pressed.png +./data/images/source-on-rest.png +./data/images/status-alert-icon.png +./data/images/status-bar-bkg.png +./data/images/status-dismiss-x.png +./data/images/user-avatar.png +./data/images/view-toggle-active-centre.png +./data/images/view-toggle-active-left.png +./data/images/view-toggle-active-right.png +./data/images/view-toggle-icon-artist-active.png +./data/images/view-toggle-icon-artist-inactive.png +./data/images/view-toggle-icon-cloud-active.png +./data/images/view-toggle-icon-cloud-inactive.png +./data/images/view-toggle-icon-list-active.png +./data/images/view-toggle-icon-list-inactive.png +./data/images/view-toggle-inactive-centre.png +./data/images/view-toggle-inactive-left.png +./data/images/view-toggle-inactive-right.png +./data/images/view-toggle-pressed-centre.png +./data/images/view-toggle-pressed-left.png +./data/images/view-toggle-pressed-right.png +./data/images/volume-icon-full.png +./data/images/volume-icon-muted.png +./data/images/volume-slider-bkg.png +./data/images/volume-slider-level.png +./data/topbar-radiobuttons.css +./data/icons/tomahawk-icon-16.png +./data/icons/tomahawk-icon-32.png +./data/icons/tomahawk-icon-64.png +./data/icons/tomahawk-icon-128.png +./data/icons/tomahawk-icon-256.png +./data/icons/tomahawk-icon-512.png + + diff --git a/rtaudio/CMakeLists.txt b/rtaudio/CMakeLists.txt new file mode 100644 index 000000000..df7cd0368 --- /dev/null +++ b/rtaudio/CMakeLists.txt @@ -0,0 +1,45 @@ +PROJECT(rtaudio) + +CMAKE_MINIMUM_REQUIRED(VERSION 2.6 FATAL_ERROR) +SET(CMAKE_VERBOSE_MAKEFILE ON) +SET(CMAKE_INSTALL_PREFIX ".") + +SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") +SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") + +#ADD_DEFINITIONS(-Wall -O2 -DNDEBUG) +#ADD_DEFINITIONS(-fPIC) + +SET(AUDIO_LIBS "") + +if(APPLE) + ADD_DEFINITIONS(-DHAVE_GETTIMEOFDAY -D__MACOSX_CORE__) +endif(APPLE) + +if(WIN32) + ADD_DEFINITIONS(-D__WINDOWS_DS__) + INCLUDE_DIRECTORIES( + ../../dx2010/include + # Just copy sal.h from VS\include to directx/include instead of this: + # "c:\\Program Files\\Microsoft Visual Studio 10.0\\VC\\include" + ) + SET(AUDIO_LIBS "dsound.dll" "winmm.dll" ) +endif(WIN32) + +if(UNIX AND NOT APPLE) + ADD_DEFINITIONS(-DHAVE_GETTIMEOFDAY -D__LINUX_ALSA__) + SET(AUDIO_LIBS "asound") +endif(UNIX AND NOT APPLE) + +if(WIN32) + ADD_LIBRARY(rtaudio SHARED RtAudio.cpp) +else() + ADD_LIBRARY(rtaudio STATIC RtAudio.cpp) +endif() + +target_link_libraries( rtaudio + ${AUDIO_LIBS} +) + +INSTALL(TARGETS rtaudio ARCHIVE DESTINATION lib) diff --git a/rtaudio/RtAudio.cpp b/rtaudio/RtAudio.cpp new file mode 100644 index 000000000..69ac86f40 --- /dev/null +++ b/rtaudio/RtAudio.cpp @@ -0,0 +1,7905 @@ +/************************************************************************/ +/*! \class RtAudio + \brief Realtime audio i/o C++ classes. + + RtAudio provides a common API (Application Programming Interface) + for realtime audio input/output across Linux (native ALSA, Jack, + and OSS), Macintosh OS X (CoreAudio and Jack), and Windows + (DirectSound and ASIO) operating systems. + + RtAudio WWW site: http://www.music.mcgill.ca/~gary/rtaudio/ + + RtAudio: realtime audio i/o C++ classes + Copyright (c) 2001-2010 Gary P. Scavone + + 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. + + Any person wishing to distribute modifications to the Software is + asked to send the modifications to the original developer so that + they can be incorporated into the canonical version. This is, + however, not a binding provision of this license. + + 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. +*/ +/************************************************************************/ + +// RtAudio: Version 4.0.7 + +#include "RtAudio.h" +#include +#include +#include +#include + +// Static variable definitions. +const unsigned int RtApi::MAX_SAMPLE_RATES = 14; +const unsigned int RtApi::SAMPLE_RATES[] = { + 4000, 5512, 8000, 9600, 11025, 16000, 22050, + 32000, 44100, 48000, 88200, 96000, 176400, 192000 +}; + +#if defined(__WINDOWS_DS__) || defined(__WINDOWS_ASIO__) + #define NULL 0 // needed for mingw build + #define MUTEX_INITIALIZE(A) InitializeCriticalSection(A) + #define MUTEX_DESTROY(A) DeleteCriticalSection(A) + #define MUTEX_LOCK(A) EnterCriticalSection(A) + #define MUTEX_UNLOCK(A) LeaveCriticalSection(A) +#elif defined(__LINUX_ALSA__) || defined(__UNIX_JACK__) || defined(__LINUX_OSS__) || defined(__MACOSX_CORE__) + // pthread API + #define MUTEX_INITIALIZE(A) pthread_mutex_init(A, NULL) + #define MUTEX_DESTROY(A) pthread_mutex_destroy(A) + #define MUTEX_LOCK(A) pthread_mutex_lock(A) + #define MUTEX_UNLOCK(A) pthread_mutex_unlock(A) +#else + #define MUTEX_INITIALIZE(A) abs(*A) // dummy definitions + #define MUTEX_DESTROY(A) abs(*A) // dummy definitions +#endif + +// *************************************************** // +// +// RtAudio definitions. +// +// *************************************************** // + +void RtAudio :: getCompiledApi( std::vector &apis ) throw() +{ + apis.clear(); + + // The order here will control the order of RtAudio's API search in + // the constructor. +#if defined(__UNIX_JACK__) + apis.push_back( UNIX_JACK ); +#endif +#if defined(__LINUX_ALSA__) + apis.push_back( LINUX_ALSA ); +#endif +#if defined(__LINUX_OSS__) + apis.push_back( LINUX_OSS ); +#endif +#if defined(__WINDOWS_ASIO__) + apis.push_back( WINDOWS_ASIO ); +#endif +#if defined(__WINDOWS_DS__) + apis.push_back( WINDOWS_DS ); +#endif +#if defined(__MACOSX_CORE__) + apis.push_back( MACOSX_CORE ); +#endif +#if defined(__RTAUDIO_DUMMY__) + apis.push_back( RTAUDIO_DUMMY ); +#endif +} + +void RtAudio :: openRtApi( RtAudio::Api api ) +{ +#if defined(__UNIX_JACK__) + if ( api == UNIX_JACK ) + rtapi_ = new RtApiJack(); +#endif +#if defined(__LINUX_ALSA__) + if ( api == LINUX_ALSA ) + rtapi_ = new RtApiAlsa(); +#endif +#if defined(__LINUX_OSS__) + if ( api == LINUX_OSS ) + rtapi_ = new RtApiOss(); +#endif +#if defined(__WINDOWS_ASIO__) + if ( api == WINDOWS_ASIO ) + rtapi_ = new RtApiAsio(); +#endif +#if defined(__WINDOWS_DS__) + if ( api == WINDOWS_DS ) + rtapi_ = new RtApiDs(); +#endif +#if defined(__MACOSX_CORE__) + if ( api == MACOSX_CORE ) + rtapi_ = new RtApiCore(); +#endif +#if defined(__RTAUDIO_DUMMY__) + if ( api == RTAUDIO_DUMMY ) + rtapi_ = new RtApiDummy(); +#endif +} + +RtAudio :: RtAudio( RtAudio::Api api ) throw() +{ + rtapi_ = 0; + + if ( api != UNSPECIFIED ) { + // Attempt to open the specified API. + openRtApi( api ); + if ( rtapi_ ) return; + + // No compiled support for specified API value. Issue a debug + // warning and continue as if no API was specified. + std::cerr << "\nRtAudio: no compiled support for specified API argument!\n" << std::endl; + } + + // Iterate through the compiled APIs and return as soon as we find + // one with at least one device or we reach the end of the list. + std::vector< RtAudio::Api > apis; + getCompiledApi( apis ); + for ( unsigned int i=0; igetDeviceCount() ) break; + } + + if ( rtapi_ ) return; + + // It should not be possible to get here because the preprocessor + // definition __RTAUDIO_DUMMY__ is automatically defined if no + // API-specific definitions are passed to the compiler. But just in + // case something weird happens, we'll print out an error message. + std::cerr << "\nRtAudio: no compiled API support found ... critical error!!\n\n"; +} + +RtAudio :: ~RtAudio() throw() +{ + delete rtapi_; +} + +void RtAudio :: openStream( RtAudio::StreamParameters *outputParameters, + RtAudio::StreamParameters *inputParameters, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, + RtAudioCallback callback, void *userData, + RtAudio::StreamOptions *options ) +{ + return rtapi_->openStream( outputParameters, inputParameters, format, + sampleRate, bufferFrames, callback, + userData, options ); +} + +// *************************************************** // +// +// Public RtApi definitions (see end of file for +// private or protected utility functions). +// +// *************************************************** // + +RtApi :: RtApi() +{ + stream_.state = STREAM_CLOSED; + stream_.mode = UNINITIALIZED; + stream_.apiHandle = 0; + stream_.userBuffer[0] = 0; + stream_.userBuffer[1] = 0; + MUTEX_INITIALIZE( &stream_.mutex ); + showWarnings_ = true; +} + +RtApi :: ~RtApi() +{ + MUTEX_DESTROY( &stream_.mutex ); +} + +void RtApi :: openStream( RtAudio::StreamParameters *oParams, + RtAudio::StreamParameters *iParams, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, + RtAudioCallback callback, void *userData, + RtAudio::StreamOptions *options ) +{ + if ( stream_.state != STREAM_CLOSED ) { + errorText_ = "RtApi::openStream: a stream is already open!"; + error( RtError::INVALID_USE ); + } + + if ( oParams && oParams->nChannels < 1 ) { + errorText_ = "RtApi::openStream: a non-NULL output StreamParameters structure cannot have an nChannels value less than one."; + error( RtError::INVALID_USE ); + } + + if ( iParams && iParams->nChannels < 1 ) { + errorText_ = "RtApi::openStream: a non-NULL input StreamParameters structure cannot have an nChannels value less than one."; + error( RtError::INVALID_USE ); + } + + if ( oParams == NULL && iParams == NULL ) { + errorText_ = "RtApi::openStream: input and output StreamParameters structures are both NULL!"; + error( RtError::INVALID_USE ); + } + + if ( formatBytes(format) == 0 ) { + errorText_ = "RtApi::openStream: 'format' parameter value is undefined."; + error( RtError::INVALID_USE ); + } + + unsigned int nDevices = getDeviceCount(); + unsigned int oChannels = 0; + if ( oParams ) { + oChannels = oParams->nChannels; + if ( oParams->deviceId >= nDevices ) { + errorText_ = "RtApi::openStream: output device parameter value is invalid."; + error( RtError::INVALID_USE ); + } + } + + unsigned int iChannels = 0; + if ( iParams ) { + iChannels = iParams->nChannels; + if ( iParams->deviceId >= nDevices ) { + errorText_ = "RtApi::openStream: input device parameter value is invalid."; + error( RtError::INVALID_USE ); + } + } + + clearStreamInfo(); + bool result; + + if ( oChannels > 0 ) { + + result = probeDeviceOpen( oParams->deviceId, OUTPUT, oChannels, oParams->firstChannel, + sampleRate, format, bufferFrames, options ); + if ( result == false ) error( RtError::SYSTEM_ERROR ); + } + + if ( iChannels > 0 ) { + + result = probeDeviceOpen( iParams->deviceId, INPUT, iChannels, iParams->firstChannel, + sampleRate, format, bufferFrames, options ); + if ( result == false ) { + if ( oChannels > 0 ) closeStream(); + error( RtError::SYSTEM_ERROR ); + } + } + + stream_.callbackInfo.callback = (void *) callback; + stream_.callbackInfo.userData = userData; + + if ( options ) options->numberOfBuffers = stream_.nBuffers; + stream_.state = STREAM_STOPPED; +} + +unsigned int RtApi :: getDefaultInputDevice( void ) +{ + // Should be implemented in subclasses if possible. + return 0; +} + +unsigned int RtApi :: getDefaultOutputDevice( void ) +{ + // Should be implemented in subclasses if possible. + return 0; +} + +void RtApi :: closeStream( void ) +{ + // MUST be implemented in subclasses! + return; +} + +bool RtApi :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + // MUST be implemented in subclasses! + return FAILURE; +} + +void RtApi :: tickStreamTime( void ) +{ + // Subclasses that do not provide their own implementation of + // getStreamTime should call this function once per buffer I/O to + // provide basic stream time support. + + stream_.streamTime += ( stream_.bufferSize * 1.0 / stream_.sampleRate ); + +#if defined( HAVE_GETTIMEOFDAY ) + gettimeofday( &stream_.lastTickTimestamp, NULL ); +#endif +} + +long RtApi :: getStreamLatency( void ) +{ + verifyStream(); + + long totalLatency = 0; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) + totalLatency = stream_.latency[0]; + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) + totalLatency += stream_.latency[1]; + + return totalLatency; +} + +double RtApi :: getStreamTime( void ) +{ + verifyStream(); + +#if defined( HAVE_GETTIMEOFDAY ) + // Return a very accurate estimate of the stream time by + // adding in the elapsed time since the last tick. + struct timeval then; + struct timeval now; + + if ( stream_.state != STREAM_RUNNING || stream_.streamTime == 0.0 ) + return stream_.streamTime; + + gettimeofday( &now, NULL ); + then = stream_.lastTickTimestamp; + return stream_.streamTime + + ((now.tv_sec + 0.000001 * now.tv_usec) - + (then.tv_sec + 0.000001 * then.tv_usec)); +#else + return stream_.streamTime; +#endif +} + +unsigned int RtApi :: getStreamSampleRate( void ) +{ + verifyStream(); + + return stream_.sampleRate; +} + + +// *************************************************** // +// +// OS/API-specific methods. +// +// *************************************************** // + +#if defined(__MACOSX_CORE__) + +// The OS X CoreAudio API is designed to use a separate callback +// procedure for each of its audio devices. A single RtAudio duplex +// stream using two different devices is supported here, though it +// cannot be guaranteed to always behave correctly because we cannot +// synchronize these two callbacks. +// +// A property listener is installed for over/underrun information. +// However, no functionality is currently provided to allow property +// listeners to trigger user handlers because it is unclear what could +// be done if a critical stream parameter (buffer size, sample rate, +// device disconnect) notification arrived. The listeners entail +// quite a bit of extra code and most likely, a user program wouldn't +// be prepared for the result anyway. However, we do provide a flag +// to the client callback function to inform of an over/underrun. +// +// The mechanism for querying and setting system parameters was +// updated (and perhaps simplified) in OS-X version 10.4. However, +// since 10.4 support is not necessarily available to all users, I've +// decided not to update the respective code at this time. Perhaps +// this will happen when Apple makes 10.4 free for everyone. :-) + +// A structure to hold various information related to the CoreAudio API +// implementation. +struct CoreHandle { + AudioDeviceID id[2]; // device ids +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + AudioDeviceIOProcID procId[2]; +#endif + UInt32 iStream[2]; // device stream index (or first if using multiple) + UInt32 nStreams[2]; // number of streams to use + bool xrun[2]; + char *deviceBuffer; + pthread_cond_t condition; + int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + + CoreHandle() + :deviceBuffer(0), drainCounter(0), internalDrain(false) { nStreams[0] = 1; nStreams[1] = 1; id[0] = 0; id[1] = 0; xrun[0] = false; xrun[1] = false; } +}; + +RtApiCore :: RtApiCore() +{ + // Nothing to do here. +} + +RtApiCore :: ~RtApiCore() +{ + // The subclass destructor gets called before the base class + // destructor, so close an existing stream before deallocating + // apiDeviceId memory. + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiCore :: getDeviceCount( void ) +{ + // Find out how many audio devices there are, if any. + UInt32 dataSize; + AudioObjectPropertyAddress propertyAddress = { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyDataSize( kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDeviceCount: OS-X error getting device info!"; + error( RtError::WARNING ); + return 0; + } + + return dataSize / sizeof( AudioDeviceID ); +} + +unsigned int RtApiCore :: getDefaultInputDevice( void ) +{ + unsigned int nDevices = getDeviceCount(); + if ( nDevices <= 1 ) return 0; + + AudioDeviceID id; + UInt32 dataSize = sizeof( AudioDeviceID ); + AudioObjectPropertyAddress property = { kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, 0, NULL, &dataSize, &id ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDefaultInputDevice: OS-X system error getting device."; + error( RtError::WARNING ); + return 0; + } + + dataSize *= nDevices; + AudioDeviceID deviceList[ nDevices ]; + property.mSelector = kAudioHardwarePropertyDevices; + result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, 0, NULL, &dataSize, (void *) &deviceList ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDefaultInputDevice: OS-X system error getting device IDs."; + error( RtError::WARNING ); + return 0; + } + + for ( unsigned int i=0; i= nDevices ) { + errorText_ = "RtApiCore::getDeviceInfo: device ID is invalid!"; + error( RtError::INVALID_USE ); + } + + AudioDeviceID deviceList[ nDevices ]; + UInt32 dataSize = sizeof( AudioDeviceID ) * nDevices; + AudioObjectPropertyAddress property = { kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, + 0, NULL, &dataSize, (void *) &deviceList ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDeviceInfo: OS-X system error getting device IDs."; + error( RtError::WARNING ); + return info; + } + + AudioDeviceID id = deviceList[ device ]; + + // Get the device name. + info.name.erase(); + CFStringRef cfname; + dataSize = sizeof( CFStringRef ); + property.mSelector = kAudioObjectPropertyManufacturer; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &cfname ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceInfo: system error (" << getErrorCode( result ) << ") getting device manufacturer."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + const char *mname = CFStringGetCStringPtr( cfname, CFStringGetSystemEncoding() ); + info.name.append( (const char *)mname, strlen(mname) ); + info.name.append( ": " ); + CFRelease( cfname ); + + property.mSelector = kAudioObjectPropertyName; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &cfname ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceInfo: system error (" << getErrorCode( result ) << ") getting device name."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + const char *name = CFStringGetCStringPtr( cfname, CFStringGetSystemEncoding() ); + info.name.append( (const char *)name, strlen(name) ); + CFRelease( cfname ); + + // Get the output stream "configuration". + AudioBufferList *bufferList = nil; + property.mSelector = kAudioDevicePropertyStreamConfiguration; + property.mScope = kAudioDevicePropertyScopeOutput; + // property.mElement = kAudioObjectPropertyElementWildcard; + dataSize = 0; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != noErr || dataSize == 0 ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting output stream configuration info for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Allocate the AudioBufferList. + bufferList = (AudioBufferList *) malloc( dataSize ); + if ( bufferList == NULL ) { + errorText_ = "RtApiCore::getDeviceInfo: memory error allocating output AudioBufferList."; + error( RtError::WARNING ); + return info; + } + + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, bufferList ); + if ( result != noErr || dataSize == 0 ) { + free( bufferList ); + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting output stream configuration for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Get output channel information. + unsigned int i, nStreams = bufferList->mNumberBuffers; + for ( i=0; imBuffers[i].mNumberChannels; + free( bufferList ); + + // Get the input stream "configuration". + property.mScope = kAudioDevicePropertyScopeInput; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != noErr || dataSize == 0 ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting input stream configuration info for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Allocate the AudioBufferList. + bufferList = (AudioBufferList *) malloc( dataSize ); + if ( bufferList == NULL ) { + errorText_ = "RtApiCore::getDeviceInfo: memory error allocating input AudioBufferList."; + error( RtError::WARNING ); + return info; + } + + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, bufferList ); + if (result != noErr || dataSize == 0) { + free( bufferList ); + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting input stream configuration for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Get input channel information. + nStreams = bufferList->mNumberBuffers; + for ( i=0; imBuffers[i].mNumberChannels; + free( bufferList ); + + // If device opens for both playback and capture, we determine the channels. + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // Probe the device sample rates. + bool isInput = false; + if ( info.outputChannels == 0 ) isInput = true; + + // Determine the supported sample rates. + property.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + if ( isInput == false ) property.mScope = kAudioDevicePropertyScopeOutput; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != kAudioHardwareNoError || dataSize == 0 ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting sample rate info."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + UInt32 nRanges = dataSize / sizeof( AudioValueRange ); + AudioValueRange rangeList[ nRanges ]; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &rangeList ); + if ( result != kAudioHardwareNoError ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting sample rates."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + Float64 minimumRate = 100000000.0, maximumRate = 0.0; + for ( UInt32 i=0; i maximumRate ) maximumRate = rangeList[i].mMaximum; + } + + info.sampleRates.clear(); + for ( unsigned int k=0; k= (unsigned int) minimumRate && SAMPLE_RATES[k] <= (unsigned int) maximumRate ) + info.sampleRates.push_back( SAMPLE_RATES[k] ); + } + + if ( info.sampleRates.size() == 0 ) { + errorStream_ << "RtApiCore::probeDeviceInfo: No supported sample rates found for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // CoreAudio always uses 32-bit floating point data for PCM streams. + // Thus, any other "physical" formats supported by the device are of + // no interest to the client. + info.nativeFormats = RTAUDIO_FLOAT32; + + if ( info.outputChannels > 0 ) + if ( getDefaultOutputDevice() == device ) info.isDefaultOutput = true; + if ( info.inputChannels > 0 ) + if ( getDefaultInputDevice() == device ) info.isDefaultInput = true; + + info.probed = true; + return info; +} + +OSStatus callbackHandler( AudioDeviceID inDevice, + const AudioTimeStamp* inNow, + const AudioBufferList* inInputData, + const AudioTimeStamp* inInputTime, + AudioBufferList* outOutputData, + const AudioTimeStamp* inOutputTime, + void* infoPointer ) +{ + CallbackInfo *info = (CallbackInfo *) infoPointer; + + RtApiCore *object = (RtApiCore *) info->object; + if ( object->callbackEvent( inDevice, inInputData, outOutputData ) == false ) + return kAudioHardwareUnspecifiedError; + else + return kAudioHardwareNoError; +} + +OSStatus deviceListener( AudioObjectID inDevice, + UInt32 nAddresses, + const AudioObjectPropertyAddress properties[], + void* handlePointer ) +{ + CoreHandle *handle = (CoreHandle *) handlePointer; + for ( UInt32 i=0; ixrun[1] = true; + else + handle->xrun[0] = true; + } + } + + return kAudioHardwareNoError; +} + +bool RtApiCore :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + // Get device ID + unsigned int nDevices = getDeviceCount(); + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiCore::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiCore::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + AudioDeviceID deviceList[ nDevices ]; + UInt32 dataSize = sizeof( AudioDeviceID ) * nDevices; + AudioObjectPropertyAddress property = { kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, + 0, NULL, &dataSize, (void *) &deviceList ); + if ( result != noErr ) { + errorText_ = "RtApiCore::probeDeviceOpen: OS-X system error getting device IDs."; + return FAILURE; + } + + AudioDeviceID id = deviceList[ device ]; + + // Setup for stream mode. + bool isInput = false; + if ( mode == INPUT ) { + isInput = true; + property.mScope = kAudioDevicePropertyScopeInput; + } + else + property.mScope = kAudioDevicePropertyScopeOutput; + + // Get the stream "configuration". + AudioBufferList *bufferList = nil; + dataSize = 0; + property.mSelector = kAudioDevicePropertyStreamConfiguration; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != noErr || dataSize == 0 ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream configuration info for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Allocate the AudioBufferList. + bufferList = (AudioBufferList *) malloc( dataSize ); + if ( bufferList == NULL ) { + errorText_ = "RtApiCore::probeDeviceOpen: memory error allocating AudioBufferList."; + return FAILURE; + } + + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, bufferList ); + if (result != noErr || dataSize == 0) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream configuration for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Search for one or more streams that contain the desired number of + // channels. CoreAudio devices can have an arbitrary number of + // streams and each stream can have an arbitrary number of channels. + // For each stream, a single buffer of interleaved samples is + // provided. RtAudio prefers the use of one stream of interleaved + // data or multiple consecutive single-channel streams. However, we + // now support multiple consecutive multi-channel streams of + // interleaved data as well. + UInt32 iStream, offsetCounter = firstChannel; + UInt32 nStreams = bufferList->mNumberBuffers; + bool monoMode = false; + bool foundStream = false; + + // First check that the device supports the requested number of + // channels. + UInt32 deviceChannels = 0; + for ( iStream=0; iStreammBuffers[iStream].mNumberChannels; + + if ( deviceChannels < ( channels + firstChannel ) ) { + free( bufferList ); + errorStream_ << "RtApiCore::probeDeviceOpen: the device (" << device << ") does not support the requested channel count."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Look for a single stream meeting our needs. + UInt32 firstStream, streamCount = 1, streamChannels = 0, channelOffset = 0; + for ( iStream=0; iStreammBuffers[iStream].mNumberChannels; + if ( streamChannels >= channels + offsetCounter ) { + firstStream = iStream; + channelOffset = offsetCounter; + foundStream = true; + break; + } + if ( streamChannels > offsetCounter ) break; + offsetCounter -= streamChannels; + } + + // If we didn't find a single stream above, then we should be able + // to meet the channel specification with multiple streams. + if ( foundStream == false ) { + monoMode = true; + offsetCounter = firstChannel; + for ( iStream=0; iStreammBuffers[iStream].mNumberChannels; + if ( streamChannels > offsetCounter ) break; + offsetCounter -= streamChannels; + } + + firstStream = iStream; + channelOffset = offsetCounter; + Int32 channelCounter = channels + offsetCounter - streamChannels; + + if ( streamChannels > 1 ) monoMode = false; + while ( channelCounter > 0 ) { + streamChannels = bufferList->mBuffers[++iStream].mNumberChannels; + if ( streamChannels > 1 ) monoMode = false; + channelCounter -= streamChannels; + streamCount++; + } + } + + free( bufferList ); + + // Determine the buffer size. + AudioValueRange bufferRange; + dataSize = sizeof( AudioValueRange ); + property.mSelector = kAudioDevicePropertyBufferFrameSizeRange; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &bufferRange ); + + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting buffer size range for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( bufferRange.mMinimum > *bufferSize ) *bufferSize = (unsigned long) bufferRange.mMinimum; + else if ( bufferRange.mMaximum < *bufferSize ) *bufferSize = (unsigned long) bufferRange.mMaximum; + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) *bufferSize = (unsigned long) bufferRange.mMinimum; + + // Set the buffer size. For multiple streams, I'm assuming we only + // need to make this setting for the master channel. + UInt32 theSize = (UInt32) *bufferSize; + dataSize = sizeof( UInt32 ); + property.mSelector = kAudioDevicePropertyBufferFrameSize; + result = AudioObjectSetPropertyData( id, &property, 0, NULL, dataSize, &theSize ); + + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting the buffer size for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // If attempting to setup a duplex stream, the bufferSize parameter + // MUST be the same in both directions! + *bufferSize = theSize; + if ( stream_.mode == OUTPUT && mode == INPUT && *bufferSize != stream_.bufferSize ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error setting buffer size for duplex stream on device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + stream_.bufferSize = *bufferSize; + stream_.nBuffers = 1; + + // Check and if necessary, change the sample rate for the device. + Float64 nominalRate; + dataSize = sizeof( Float64 ); + property.mSelector = kAudioDevicePropertyNominalSampleRate; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &nominalRate ); + + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting current sample rate."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Only change the sample rate if off by more than 1 Hz. + if ( fabs( nominalRate - (double)sampleRate ) > 1.0 ) { + nominalRate = (Float64) sampleRate; + result = AudioObjectSetPropertyData( id, &property, 0, NULL, dataSize, &nominalRate ); + + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting sample rate for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Try to set "hog" mode ... it's not clear to me this is working. + if ( options && options->flags & RTAUDIO_HOG_DEVICE ) { + pid_t hog_pid; + dataSize = sizeof( hog_pid ); + property.mSelector = kAudioDevicePropertyHogMode; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &hog_pid ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting 'hog' state!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( hog_pid != getpid() ) { + hog_pid = getpid(); + result = AudioObjectSetPropertyData( id, &property, 0, NULL, dataSize, &hog_pid ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting 'hog' state!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + } + + // Get the stream ID(s) so we can set the stream format. + AudioStreamID streamIDs[ nStreams ]; + dataSize = nStreams * sizeof( AudioStreamID ); + property.mSelector = kAudioDevicePropertyStreams; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &streamIDs ); + + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream ID(s) for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Now set the stream format for each stream. Also, check the + // physical format of the device and change that if necessary. + AudioStreamBasicDescription description; + dataSize = sizeof( AudioStreamBasicDescription ); + + bool updateFormat; + for ( UInt32 i=0; i 1.0 ) { + description.mSampleRate = (double) sampleRate; + updateFormat = true; + } + + if ( description.mFormatID != kAudioFormatLinearPCM ) { + description.mFormatID = kAudioFormatLinearPCM; + updateFormat = true; + } + + if ( updateFormat ) { + result = AudioObjectSetPropertyData( streamIDs[firstStream+i], &property, 0, NULL, dataSize, &description ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting sample rate or data format for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Now check the physical format. + property.mSelector = kAudioStreamPropertyPhysicalFormat; + result = AudioObjectGetPropertyData( streamIDs[firstStream+i], &property, 0, NULL, &dataSize, &description ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream physical format for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( description.mFormatID != kAudioFormatLinearPCM || description.mBitsPerChannel < 24 ) { + description.mFormatID = kAudioFormatLinearPCM; + AudioStreamBasicDescription testDescription = description; + unsigned long formatFlags; + + // We'll try higher bit rates first and then work our way down. + testDescription.mBitsPerChannel = 32; + testDescription.mBytesPerFrame = testDescription.mBitsPerChannel/8 * testDescription.mChannelsPerFrame; + testDescription.mBytesPerPacket = testDescription.mBytesPerFrame * testDescription.mFramesPerPacket; + formatFlags = description.mFormatFlags | kLinearPCMFormatFlagIsFloat & ~kLinearPCMFormatFlagIsSignedInteger; + testDescription.mFormatFlags = formatFlags; + result = AudioObjectSetPropertyData( streamIDs[firstStream+i], &property, 0, NULL, dataSize, &testDescription ); + if ( result == noErr ) continue; + + testDescription = description; + testDescription.mBitsPerChannel = 32; + testDescription.mBytesPerFrame = testDescription.mBitsPerChannel/8 * testDescription.mChannelsPerFrame; + testDescription.mBytesPerPacket = testDescription.mBytesPerFrame * testDescription.mFramesPerPacket; + formatFlags = (description.mFormatFlags | kLinearPCMFormatFlagIsSignedInteger) & ~kLinearPCMFormatFlagIsFloat; + testDescription.mFormatFlags = formatFlags; + result = AudioObjectSetPropertyData( streamIDs[firstStream+i], &property, 0, NULL, dataSize, &testDescription ); + if ( result == noErr ) continue; + + testDescription = description; + testDescription.mBitsPerChannel = 24; + testDescription.mBytesPerFrame = testDescription.mBitsPerChannel/8 * testDescription.mChannelsPerFrame; + testDescription.mBytesPerPacket = testDescription.mBytesPerFrame * testDescription.mFramesPerPacket; + testDescription.mFormatFlags = formatFlags; + result = AudioObjectSetPropertyData( streamIDs[firstStream+i], &property, 0, NULL, dataSize, &testDescription ); + if ( result == noErr ) continue; + + testDescription = description; + testDescription.mBitsPerChannel = 16; + testDescription.mBytesPerFrame = testDescription.mBitsPerChannel/8 * testDescription.mChannelsPerFrame; + testDescription.mBytesPerPacket = testDescription.mBytesPerFrame * testDescription.mFramesPerPacket; + testDescription.mFormatFlags = formatFlags; + result = AudioObjectSetPropertyData( streamIDs[firstStream+i], &property, 0, NULL, dataSize, &testDescription ); + if ( result == noErr ) continue; + + testDescription = description; + testDescription.mBitsPerChannel = 8; + testDescription.mBytesPerFrame = testDescription.mBitsPerChannel/8 * testDescription.mChannelsPerFrame; + testDescription.mBytesPerPacket = testDescription.mBytesPerFrame * testDescription.mFramesPerPacket; + testDescription.mFormatFlags = formatFlags; + result = AudioObjectSetPropertyData( streamIDs[firstStream+i], &property, 0, NULL, dataSize, &testDescription ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting physical data format for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + } + + + // Get the stream latency. There can be latency in both the device + // and the stream. First, attempt to get the device latency on the + // master channel or the first open channel. Errors that might + // occur here are not deemed critical. + + UInt32 latency; + dataSize = sizeof( UInt32 ); + property.mSelector = kAudioDevicePropertyLatency; + if ( AudioObjectHasProperty( id, &property ) == true ) { + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &latency ); + if ( result == kAudioHardwareNoError ) stream_.latency[ mode ] = latency; + else { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting device latency for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + } + } + + // Now try to get the stream latency. For multiple streams, I assume the + // latency is equal for each. + result = AudioObjectGetPropertyData( streamIDs[firstStream], &property, 0, NULL, &dataSize, &latency ); + if ( result == kAudioHardwareNoError ) stream_.latency[ mode ] += latency; + else { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream latency for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + } + + // Byte-swapping: According to AudioHardware.h, the stream data will + // always be presented in native-endian format, so we should never + // need to byte swap. + stream_.doByteSwap[mode] = false; + + // From the CoreAudio documentation, PCM data must be supplied as + // 32-bit floats. + stream_.userFormat = format; + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + + if ( streamCount == 1 ) + stream_.nDeviceChannels[mode] = description.mChannelsPerFrame; + else // multiple streams + stream_.nDeviceChannels[mode] = channels; + stream_.nUserChannels[mode] = channels; + stream_.channelOffset[mode] = channelOffset; // offset within a CoreAudio stream + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + stream_.deviceInterleaved[mode] = true; + if ( monoMode == true ) stream_.deviceInterleaved[mode] = false; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( streamCount == 1 ) { + if ( stream_.nUserChannels[mode] > 1 && + stream_.userInterleaved != stream_.deviceInterleaved[mode] ) + stream_.doConvertBuffer[mode] = true; + } + else if ( monoMode && stream_.userInterleaved ) + stream_.doConvertBuffer[mode] = true; + + // Allocate our CoreHandle structure for the stream. + CoreHandle *handle = 0; + if ( stream_.apiHandle == 0 ) { + try { + handle = new CoreHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiCore::probeDeviceOpen: error allocating CoreHandle memory."; + goto error; + } + + if ( pthread_cond_init( &handle->condition, NULL ) ) { + errorText_ = "RtApiCore::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + stream_.apiHandle = (void *) handle; + } + else + handle = (CoreHandle *) stream_.apiHandle; + handle->iStream[mode] = firstStream; + handle->nStreams[mode] = streamCount; + handle->id[mode] = id; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiCore::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + // If possible, we will make use of the CoreAudio stream buffers as + // "device buffers". However, we can't do this if using multiple + // streams. + if ( stream_.doConvertBuffer[mode] && handle->nStreams[mode] > 1 ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiCore::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.sampleRate = sampleRate; + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + stream_.callbackInfo.object = (void *) this; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) { + if ( streamCount > 1 ) setConvertInfo( mode, 0 ); + else setConvertInfo( mode, channelOffset ); + } + + if ( mode == INPUT && stream_.mode == OUTPUT && stream_.device[0] == device ) + // Only one callback procedure per device. + stream_.mode = DUPLEX; + else { +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + result = AudioDeviceCreateIOProcID( id, callbackHandler, (void *) &stream_.callbackInfo, &handle->procId[mode] ); +#else + // deprecated in favor of AudioDeviceCreateIOProcID() + result = AudioDeviceAddIOProc( id, callbackHandler, (void *) &stream_.callbackInfo ); +#endif + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error setting callback for device (" << device << ")."; + errorText_ = errorStream_.str(); + goto error; + } + if ( stream_.mode == OUTPUT && mode == INPUT ) + stream_.mode = DUPLEX; + else + stream_.mode = mode; + } + + // Setup the device property listener for over/underload. + property.mSelector = kAudioDeviceProcessorOverload; + result = AudioObjectAddPropertyListener( id, &property, deviceListener, (void *) handle ); + + return SUCCESS; + + error: + if ( handle ) { + pthread_cond_destroy( &handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiCore :: closeStream( void ) +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiCore::closeStream(): no open stream to close!"; + error( RtError::WARNING ); + return; + } + + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( stream_.state == STREAM_RUNNING ) + AudioDeviceStop( handle->id[0], callbackHandler ); +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + AudioDeviceDestroyIOProcID( handle->id[0], handle->procId[0] ); +#else + // deprecated in favor of AudioDeviceDestroyIOProcID() + AudioDeviceRemoveIOProc( handle->id[0], callbackHandler ); +#endif + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && stream_.device[0] != stream_.device[1] ) ) { + if ( stream_.state == STREAM_RUNNING ) + AudioDeviceStop( handle->id[1], callbackHandler ); +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + AudioDeviceDestroyIOProcID( handle->id[1], handle->procId[1] ); +#else + // deprecated in favor of AudioDeviceDestroyIOProcID() + AudioDeviceRemoveIOProc( handle->id[1], callbackHandler ); +#endif + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + // Destroy pthread condition variable. + pthread_cond_destroy( &handle->condition ); + delete handle; + stream_.apiHandle = 0; + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiCore :: startStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiCore::startStream(): the stream is already running!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + OSStatus result = noErr; + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + result = AudioDeviceStart( handle->id[0], callbackHandler ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::startStream: system error (" << getErrorCode( result ) << ") starting callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( stream_.mode == INPUT || + ( stream_.mode == DUPLEX && stream_.device[0] != stream_.device[1] ) ) { + + result = AudioDeviceStart( handle->id[1], callbackHandler ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::startStream: system error starting input callback procedure on device (" << stream_.device[1] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + handle->drainCounter = 0; + handle->internalDrain = false; + stream_.state = STREAM_RUNNING; + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result == noErr ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiCore :: stopStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiCore::stopStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + OSStatus result = noErr; + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 1; + pthread_cond_wait( &handle->condition, &stream_.mutex ); // block until signaled + } + + MUTEX_UNLOCK( &stream_.mutex ); + result = AudioDeviceStop( handle->id[0], callbackHandler ); + MUTEX_LOCK( &stream_.mutex ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::stopStream: system error (" << getErrorCode( result ) << ") stopping callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && stream_.device[0] != stream_.device[1] ) ) { + + result = AudioDeviceStop( handle->id[1], callbackHandler ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::stopStream: system error (" << getErrorCode( result ) << ") stopping input callback procedure on device (" << stream_.device[1] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + stream_.state = STREAM_STOPPED; + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result == noErr ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiCore :: abortStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiCore::abortStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + handle->drainCounter = 1; + + stopStream(); +} + +bool RtApiCore :: callbackEvent( AudioDeviceID deviceId, + const AudioBufferList *inBufferList, + const AudioBufferList *outBufferList ) +{ + if ( stream_.state == STREAM_STOPPED ) return SUCCESS; + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiCore::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtError::WARNING ); + return FAILURE; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal is finished. + if ( handle->drainCounter > 3 ) { + if ( handle->internalDrain == false ) + pthread_cond_signal( &handle->condition ); + else + stopStream(); + return SUCCESS; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return SUCCESS; + } + + AudioDeviceID outputDevice = handle->id[0]; + + // Invoke user callback to get fresh output data UNLESS we are + // draining stream or duplex mode AND the input/output devices are + // different AND this function is called for the input device. + if ( handle->drainCounter == 0 && ( stream_.mode != DUPLEX || deviceId == outputDevice ) ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + handle->drainCounter = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( handle->drainCounter == 2 ) { + MUTEX_UNLOCK( &stream_.mutex ); + abortStream(); + return SUCCESS; + } + else if ( handle->drainCounter == 1 ) + handle->internalDrain = true; + } + + if ( stream_.mode == OUTPUT || ( stream_.mode == DUPLEX && deviceId == outputDevice ) ) { + + if ( handle->drainCounter > 1 ) { // write zeros to the output stream + + if ( handle->nStreams[0] == 1 ) { + memset( outBufferList->mBuffers[handle->iStream[0]].mData, + 0, + outBufferList->mBuffers[handle->iStream[0]].mDataByteSize ); + } + else { // fill multiple streams with zeros + for ( unsigned int i=0; inStreams[0]; i++ ) { + memset( outBufferList->mBuffers[handle->iStream[0]+i].mData, + 0, + outBufferList->mBuffers[handle->iStream[0]+i].mDataByteSize ); + } + } + } + else if ( handle->nStreams[0] == 1 ) { + if ( stream_.doConvertBuffer[0] ) { // convert directly to CoreAudio stream buffer + convertBuffer( (char *) outBufferList->mBuffers[handle->iStream[0]].mData, + stream_.userBuffer[0], stream_.convertInfo[0] ); + } + else { // copy from user buffer + memcpy( outBufferList->mBuffers[handle->iStream[0]].mData, + stream_.userBuffer[0], + outBufferList->mBuffers[handle->iStream[0]].mDataByteSize ); + } + } + else { // fill multiple streams + Float32 *inBuffer = (Float32 *) stream_.userBuffer[0]; + if ( stream_.doConvertBuffer[0] ) { + convertBuffer( stream_.deviceBuffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + inBuffer = (Float32 *) stream_.deviceBuffer; + } + + if ( stream_.deviceInterleaved[0] == false ) { // mono mode + UInt32 bufferBytes = outBufferList->mBuffers[handle->iStream[0]].mDataByteSize; + for ( unsigned int i=0; imBuffers[handle->iStream[0]+i].mData, + (void *)&inBuffer[i*stream_.bufferSize], bufferBytes ); + } + } + else { // fill multiple multi-channel streams with interleaved data + UInt32 streamChannels, channelsLeft, inJump, outJump, inOffset; + Float32 *out, *in; + + bool inInterleaved = ( stream_.userInterleaved ) ? true : false; + UInt32 inChannels = stream_.nUserChannels[0]; + if ( stream_.doConvertBuffer[0] ) { + inInterleaved = true; // device buffer will always be interleaved for nStreams > 1 and not mono mode + inChannels = stream_.nDeviceChannels[0]; + } + + if ( inInterleaved ) inOffset = 1; + else inOffset = stream_.bufferSize; + + channelsLeft = inChannels; + for ( unsigned int i=0; inStreams[0]; i++ ) { + in = inBuffer; + out = (Float32 *) outBufferList->mBuffers[handle->iStream[0]+i].mData; + streamChannels = outBufferList->mBuffers[handle->iStream[0]+i].mNumberChannels; + + outJump = 0; + // Account for possible channel offset in first stream + if ( i == 0 && stream_.channelOffset[0] > 0 ) { + streamChannels -= stream_.channelOffset[0]; + outJump = stream_.channelOffset[0]; + out += outJump; + } + + // Account for possible unfilled channels at end of the last stream + if ( streamChannels > channelsLeft ) { + outJump = streamChannels - channelsLeft; + streamChannels = channelsLeft; + } + + // Determine input buffer offsets and skips + if ( inInterleaved ) { + inJump = inChannels; + in += inChannels - channelsLeft; + } + else { + inJump = 1; + in += (inChannels - channelsLeft) * inOffset; + } + + for ( unsigned int i=0; idrainCounter ) { + handle->drainCounter++; + goto unlock; + } + } + + AudioDeviceID inputDevice; + inputDevice = handle->id[1]; + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && deviceId == inputDevice ) ) { + + if ( handle->nStreams[1] == 1 ) { + if ( stream_.doConvertBuffer[1] ) { // convert directly from CoreAudio stream buffer + convertBuffer( stream_.userBuffer[1], + (char *) inBufferList->mBuffers[handle->iStream[1]].mData, + stream_.convertInfo[1] ); + } + else { // copy to user buffer + memcpy( stream_.userBuffer[1], + inBufferList->mBuffers[handle->iStream[1]].mData, + inBufferList->mBuffers[handle->iStream[1]].mDataByteSize ); + } + } + else { // read from multiple streams + Float32 *outBuffer = (Float32 *) stream_.userBuffer[1]; + if ( stream_.doConvertBuffer[1] ) outBuffer = (Float32 *) stream_.deviceBuffer; + + if ( stream_.deviceInterleaved[1] == false ) { // mono mode + UInt32 bufferBytes = inBufferList->mBuffers[handle->iStream[1]].mDataByteSize; + for ( unsigned int i=0; imBuffers[handle->iStream[1]+i].mData, bufferBytes ); + } + } + else { // read from multiple multi-channel streams + UInt32 streamChannels, channelsLeft, inJump, outJump, outOffset; + Float32 *out, *in; + + bool outInterleaved = ( stream_.userInterleaved ) ? true : false; + UInt32 outChannels = stream_.nUserChannels[1]; + if ( stream_.doConvertBuffer[1] ) { + outInterleaved = true; // device buffer will always be interleaved for nStreams > 1 and not mono mode + outChannels = stream_.nDeviceChannels[1]; + } + + if ( outInterleaved ) outOffset = 1; + else outOffset = stream_.bufferSize; + + channelsLeft = outChannels; + for ( unsigned int i=0; inStreams[1]; i++ ) { + out = outBuffer; + in = (Float32 *) inBufferList->mBuffers[handle->iStream[1]+i].mData; + streamChannels = inBufferList->mBuffers[handle->iStream[1]+i].mNumberChannels; + + inJump = 0; + // Account for possible channel offset in first stream + if ( i == 0 && stream_.channelOffset[1] > 0 ) { + streamChannels -= stream_.channelOffset[1]; + inJump = stream_.channelOffset[1]; + in += inJump; + } + + // Account for possible unread channels at end of the last stream + if ( streamChannels > channelsLeft ) { + inJump = streamChannels - channelsLeft; + streamChannels = channelsLeft; + } + + // Determine output buffer offsets and skips + if ( outInterleaved ) { + outJump = outChannels; + out += outChannels - channelsLeft; + } + else { + outJump = 1; + out += (outChannels - channelsLeft) * outOffset; + } + + for ( unsigned int i=0; i +#include +#include + +// A structure to hold various information related to the Jack API +// implementation. +struct JackHandle { + jack_client_t *client; + jack_port_t **ports[2]; + std::string deviceName[2]; + bool xrun[2]; + pthread_cond_t condition; + int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + + JackHandle() + :client(0), drainCounter(0), internalDrain(false) { ports[0] = 0; ports[1] = 0; xrun[0] = false; xrun[1] = false; } +}; + +ThreadHandle threadId; +void jackSilentError( const char * ) {}; + +RtApiJack :: RtApiJack() +{ + // Nothing to do here. +#if !defined(__RTAUDIO_DEBUG__) + // Turn off Jack's internal error reporting. + jack_set_error_function( &jackSilentError ); +#endif +} + +RtApiJack :: ~RtApiJack() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiJack :: getDeviceCount( void ) +{ + // See if we can become a jack client. + jack_options_t options = (jack_options_t) ( JackNoStartServer | JackUseExactName ); //JackNullOption; + jack_status_t *status = NULL; + jack_client_t *client = jack_client_open( "RtApiJackCount", options, status ); + if ( client == 0 ) return 0; + + const char **ports; + std::string port, previousPort; + unsigned int nChannels = 0, nDevices = 0; + ports = jack_get_ports( client, NULL, NULL, 0 ); + if ( ports ) { + // Parse the port names up to the first colon (:). + size_t iColon = 0; + do { + port = (char *) ports[ nChannels ]; + iColon = port.find(":"); + if ( iColon != std::string::npos ) { + port = port.substr( 0, iColon + 1 ); + if ( port != previousPort ) { + nDevices++; + previousPort = port; + } + } + } while ( ports[++nChannels] ); + free( ports ); + } + + jack_client_close( client ); + return nDevices; +} + +RtAudio::DeviceInfo RtApiJack :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + jack_options_t options = (jack_options_t) ( JackNoStartServer | JackUseExactName ); //JackNullOption + jack_status_t *status = NULL; + jack_client_t *client = jack_client_open( "RtApiJackInfo", options, status ); + if ( client == 0 ) { + errorText_ = "RtApiJack::getDeviceInfo: Jack server not found or connection error!"; + error( RtError::WARNING ); + return info; + } + + const char **ports; + std::string port, previousPort; + unsigned int nPorts = 0, nDevices = 0; + ports = jack_get_ports( client, NULL, NULL, 0 ); + if ( ports ) { + // Parse the port names up to the first colon (:). + size_t iColon = 0; + do { + port = (char *) ports[ nPorts ]; + iColon = port.find(":"); + if ( iColon != std::string::npos ) { + port = port.substr( 0, iColon ); + if ( port != previousPort ) { + if ( nDevices == device ) info.name = port; + nDevices++; + previousPort = port; + } + } + } while ( ports[++nPorts] ); + free( ports ); + } + + if ( device >= nDevices ) { + errorText_ = "RtApiJack::getDeviceInfo: device ID is invalid!"; + error( RtError::INVALID_USE ); + } + + // Get the current jack server sample rate. + info.sampleRates.clear(); + info.sampleRates.push_back( jack_get_sample_rate( client ) ); + + // Count the available ports containing the client name as device + // channels. Jack "input ports" equal RtAudio output channels. + unsigned int nChannels = 0; + ports = jack_get_ports( client, info.name.c_str(), NULL, JackPortIsInput ); + if ( ports ) { + while ( ports[ nChannels ] ) nChannels++; + free( ports ); + info.outputChannels = nChannels; + } + + // Jack "output ports" equal RtAudio input channels. + nChannels = 0; + ports = jack_get_ports( client, info.name.c_str(), NULL, JackPortIsOutput ); + if ( ports ) { + while ( ports[ nChannels ] ) nChannels++; + free( ports ); + info.inputChannels = nChannels; + } + + if ( info.outputChannels == 0 && info.inputChannels == 0 ) { + jack_client_close(client); + errorText_ = "RtApiJack::getDeviceInfo: error determining Jack input/output channels!"; + error( RtError::WARNING ); + return info; + } + + // If device opens for both playback and capture, we determine the channels. + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // Jack always uses 32-bit floats. + info.nativeFormats = RTAUDIO_FLOAT32; + + // Jack doesn't provide default devices so we'll use the first available one. + if ( device == 0 && info.outputChannels > 0 ) + info.isDefaultOutput = true; + if ( device == 0 && info.inputChannels > 0 ) + info.isDefaultInput = true; + + jack_client_close(client); + info.probed = true; + return info; +} + +int jackCallbackHandler( jack_nframes_t nframes, void *infoPointer ) +{ + CallbackInfo *info = (CallbackInfo *) infoPointer; + + RtApiJack *object = (RtApiJack *) info->object; + if ( object->callbackEvent( (unsigned long) nframes ) == false ) return 1; + + return 0; +} + +// This function will be called by a spawned thread when the Jack +// server signals that it is shutting down. It is necessary to handle +// it this way because the jackShutdown() function must return before +// the jack_deactivate() function (in closeStream()) will return. +extern "C" void *jackCloseStream( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiJack *object = (RtApiJack *) info->object; + + object->closeStream(); + + pthread_exit( NULL ); +} +void jackShutdown( void *infoPointer ) +{ + CallbackInfo *info = (CallbackInfo *) infoPointer; + RtApiJack *object = (RtApiJack *) info->object; + + // Check current stream state. If stopped, then we'll assume this + // was called as a result of a call to RtApiJack::stopStream (the + // deactivation of a client handle causes this function to be called). + // If not, we'll assume the Jack server is shutting down or some + // other problem occurred and we should close the stream. + if ( object->isStreamRunning() == false ) return; + + pthread_create( &threadId, NULL, jackCloseStream, info ); + std::cerr << "\nRtApiJack: the Jack server is shutting down this client ... stream stopped and closed!!\n" << std::endl; +} + +int jackXrun( void *infoPointer ) +{ + JackHandle *handle = (JackHandle *) infoPointer; + + if ( handle->ports[0] ) handle->xrun[0] = true; + if ( handle->ports[1] ) handle->xrun[1] = true; + + return 0; +} + +bool RtApiJack :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + JackHandle *handle = (JackHandle *) stream_.apiHandle; + + // Look for jack server and try to become a client (only do once per stream). + jack_client_t *client = 0; + if ( mode == OUTPUT || ( mode == INPUT && stream_.mode != OUTPUT ) ) { + jack_options_t jackoptions = (jack_options_t) ( JackNoStartServer | JackUseExactName ); //JackNullOption; + jack_status_t *status = NULL; + if ( options && !options->streamName.empty() ) + client = jack_client_open( options->streamName.c_str(), jackoptions, status ); + else + client = jack_client_open( "RtApiJack", jackoptions, status ); + if ( client == 0 ) { + errorText_ = "RtApiJack::probeDeviceOpen: Jack server not found or connection error!"; + error( RtError::WARNING ); + return FAILURE; + } + } + else { + // The handle must have been created on an earlier pass. + client = handle->client; + } + + const char **ports; + std::string port, previousPort, deviceName; + unsigned int nPorts = 0, nDevices = 0; + ports = jack_get_ports( client, NULL, NULL, 0 ); + if ( ports ) { + // Parse the port names up to the first colon (:). + size_t iColon = 0; + do { + port = (char *) ports[ nPorts ]; + iColon = port.find(":"); + if ( iColon != std::string::npos ) { + port = port.substr( 0, iColon ); + if ( port != previousPort ) { + if ( nDevices == device ) deviceName = port; + nDevices++; + previousPort = port; + } + } + } while ( ports[++nPorts] ); + free( ports ); + } + + if ( device >= nDevices ) { + errorText_ = "RtApiJack::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + // Count the available ports containing the client name as device + // channels. Jack "input ports" equal RtAudio output channels. + unsigned int nChannels = 0; + unsigned long flag = JackPortIsInput; + if ( mode == INPUT ) flag = JackPortIsOutput; + ports = jack_get_ports( client, deviceName.c_str(), NULL, flag ); + if ( ports ) { + while ( ports[ nChannels ] ) nChannels++; + free( ports ); + } + + // Compare the jack ports for specified client to the requested number of channels. + if ( nChannels < (channels + firstChannel) ) { + errorStream_ << "RtApiJack::probeDeviceOpen: requested number of channels (" << channels << ") + offset (" << firstChannel << ") not found for specified device (" << device << ":" << deviceName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check the jack server sample rate. + unsigned int jackRate = jack_get_sample_rate( client ); + if ( sampleRate != jackRate ) { + jack_client_close( client ); + errorStream_ << "RtApiJack::probeDeviceOpen: the requested sample rate (" << sampleRate << ") is different than the JACK server rate (" << jackRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.sampleRate = jackRate; + + // Get the latency of the JACK port. + ports = jack_get_ports( client, deviceName.c_str(), NULL, flag ); + if ( ports[ firstChannel ] ) + stream_.latency[mode] = jack_port_get_latency( jack_port_by_name( client, ports[ firstChannel ] ) ); + free( ports ); + + // The jack server always uses 32-bit floating-point data. + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + stream_.userFormat = format; + + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + + // Jack always uses non-interleaved buffers. + stream_.deviceInterleaved[mode] = false; + + // Jack always provides host byte-ordered data. + stream_.doByteSwap[mode] = false; + + // Get the buffer size. The buffer size and number of buffers + // (periods) is set when the jack server is started. + stream_.bufferSize = (int) jack_get_buffer_size( client ); + *bufferSize = stream_.bufferSize; + + stream_.nDeviceChannels[mode] = channels; + stream_.nUserChannels[mode] = channels; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate our JackHandle structure for the stream. + if ( handle == 0 ) { + try { + handle = new JackHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating JackHandle memory."; + goto error; + } + + if ( pthread_cond_init(&handle->condition, NULL) ) { + errorText_ = "RtApiJack::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + stream_.apiHandle = (void *) handle; + handle->client = client; + } + handle->deviceName[mode] = deviceName; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + if ( mode == OUTPUT ) + bufferBytes = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + else { // mode == INPUT + bufferBytes = stream_.nDeviceChannels[1] * formatBytes( stream_.deviceFormat[1] ); + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes(stream_.deviceFormat[0]); + if ( bufferBytes < bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + // Allocate memory for the Jack ports (channels) identifiers. + handle->ports[mode] = (jack_port_t **) malloc ( sizeof (jack_port_t *) * channels ); + if ( handle->ports[mode] == NULL ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating port memory."; + goto error; + } + + stream_.device[mode] = device; + stream_.channelOffset[mode] = firstChannel; + stream_.state = STREAM_STOPPED; + stream_.callbackInfo.object = (void *) this; + + if ( stream_.mode == OUTPUT && mode == INPUT ) + // We had already set up the stream for output. + stream_.mode = DUPLEX; + else { + stream_.mode = mode; + jack_set_process_callback( handle->client, jackCallbackHandler, (void *) &stream_.callbackInfo ); + jack_set_xrun_callback( handle->client, jackXrun, (void *) &handle ); + jack_on_shutdown( handle->client, jackShutdown, (void *) &stream_.callbackInfo ); + } + + // Register our ports. + char label[64]; + if ( mode == OUTPUT ) { + for ( unsigned int i=0; iports[0][i] = jack_port_register( handle->client, (const char *)label, + JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0 ); + } + } + else { + for ( unsigned int i=0; iports[1][i] = jack_port_register( handle->client, (const char *)label, + JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0 ); + } + } + + // Setup the buffer conversion information structure. We don't use + // buffers to do channel offsets, so we override that parameter + // here. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, 0 ); + + return SUCCESS; + + error: + if ( handle ) { + pthread_cond_destroy( &handle->condition ); + jack_client_close( handle->client ); + + if ( handle->ports[0] ) free( handle->ports[0] ); + if ( handle->ports[1] ) free( handle->ports[1] ); + + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiJack :: closeStream( void ) +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiJack::closeStream(): no open stream to close!"; + error( RtError::WARNING ); + return; + } + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + if ( handle ) { + + if ( stream_.state == STREAM_RUNNING ) + jack_deactivate( handle->client ); + + jack_client_close( handle->client ); + } + + if ( handle ) { + if ( handle->ports[0] ) free( handle->ports[0] ); + if ( handle->ports[1] ) free( handle->ports[1] ); + pthread_cond_destroy( &handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiJack :: startStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiJack::startStream(): the stream is already running!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK(&stream_.mutex); + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + int result = jack_activate( handle->client ); + if ( result ) { + errorText_ = "RtApiJack::startStream(): unable to activate JACK client!"; + goto unlock; + } + + const char **ports; + + // Get the list of available ports. + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + result = 1; + ports = jack_get_ports( handle->client, handle->deviceName[0].c_str(), NULL, JackPortIsInput); + if ( ports == NULL) { + errorText_ = "RtApiJack::startStream(): error determining available JACK input ports!"; + goto unlock; + } + + // Now make the port connections. Since RtAudio wasn't designed to + // allow the user to select particular channels of a device, we'll + // just open the first "nChannels" ports with offset. + for ( unsigned int i=0; iclient, jack_port_name( handle->ports[0][i] ), ports[ stream_.channelOffset[0] + i ] ); + if ( result ) { + free( ports ); + errorText_ = "RtApiJack::startStream(): error connecting output ports!"; + goto unlock; + } + } + free(ports); + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + result = 1; + ports = jack_get_ports( handle->client, handle->deviceName[1].c_str(), NULL, JackPortIsOutput ); + if ( ports == NULL) { + errorText_ = "RtApiJack::startStream(): error determining available JACK output ports!"; + goto unlock; + } + + // Now make the port connections. See note above. + for ( unsigned int i=0; iclient, ports[ stream_.channelOffset[1] + i ], jack_port_name( handle->ports[1][i] ) ); + if ( result ) { + free( ports ); + errorText_ = "RtApiJack::startStream(): error connecting input ports!"; + goto unlock; + } + } + free(ports); + } + + handle->drainCounter = 0; + handle->internalDrain = false; + stream_.state = STREAM_RUNNING; + + unlock: + MUTEX_UNLOCK(&stream_.mutex); + + if ( result == 0 ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiJack :: stopStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiJack::stopStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 1; + pthread_cond_wait( &handle->condition, &stream_.mutex ); // block until signaled + } + } + + jack_deactivate( handle->client ); + stream_.state = STREAM_STOPPED; + + MUTEX_UNLOCK( &stream_.mutex ); +} + +void RtApiJack :: abortStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiJack::abortStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + handle->drainCounter = 1; + + stopStream(); +} + +// This function will be called by a spawned thread when the user +// callback function signals that the stream should be stopped or +// aborted. It is necessary to handle it this way because the +// callbackEvent() function must return before the jack_deactivate() +// function will return. +extern "C" void *jackStopStream( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiJack *object = (RtApiJack *) info->object; + + object->stopStream(); + + pthread_exit( NULL ); +} + +bool RtApiJack :: callbackEvent( unsigned long nframes ) +{ + if ( stream_.state == STREAM_STOPPED ) return SUCCESS; + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiCore::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtError::WARNING ); + return FAILURE; + } + if ( stream_.bufferSize != nframes ) { + errorText_ = "RtApiCore::callbackEvent(): the JACK buffer size has changed ... cannot process!"; + error( RtError::WARNING ); + return FAILURE; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + JackHandle *handle = (JackHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal is finished. + if ( handle->drainCounter > 3 ) { + if ( handle->internalDrain == true ) { + pthread_create( &threadId, NULL, jackStopStream, info ); + } + else + pthread_cond_signal( &handle->condition ); + return SUCCESS; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return SUCCESS; + } + + // Invoke user callback first, to get fresh output data. + if ( handle->drainCounter == 0 ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + handle->drainCounter = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( handle->drainCounter == 2 ) { + MUTEX_UNLOCK( &stream_.mutex ); + ThreadHandle id; + pthread_create( &id, NULL, jackStopStream, info ); + return SUCCESS; + } + else if ( handle->drainCounter == 1 ) + handle->internalDrain = true; + } + + jack_default_audio_sample_t *jackbuffer; + unsigned long bufferBytes = nframes * sizeof( jack_default_audio_sample_t ); + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + if ( handle->drainCounter > 0 ) { // write zeros to the output stream + + for ( unsigned int i=0; iports[0][i], (jack_nframes_t) nframes ); + memset( jackbuffer, 0, bufferBytes ); + } + + } + else if ( stream_.doConvertBuffer[0] ) { + + convertBuffer( stream_.deviceBuffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + + for ( unsigned int i=0; iports[0][i], (jack_nframes_t) nframes ); + memcpy( jackbuffer, &stream_.deviceBuffer[i*bufferBytes], bufferBytes ); + } + } + else { // no buffer conversion + for ( unsigned int i=0; iports[0][i], (jack_nframes_t) nframes ); + memcpy( jackbuffer, &stream_.userBuffer[0][i*bufferBytes], bufferBytes ); + } + } + + if ( handle->drainCounter ) { + handle->drainCounter++; + goto unlock; + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + if ( stream_.doConvertBuffer[1] ) { + for ( unsigned int i=0; iports[1][i], (jack_nframes_t) nframes ); + memcpy( &stream_.deviceBuffer[i*bufferBytes], jackbuffer, bufferBytes ); + } + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + } + else { // no buffer conversion + for ( unsigned int i=0; iports[1][i], (jack_nframes_t) nframes ); + memcpy( &stream_.userBuffer[1][i*bufferBytes], jackbuffer, bufferBytes ); + } + } + } + + unlock: + MUTEX_UNLOCK(&stream_.mutex); + + RtApi::tickStreamTime(); + return SUCCESS; +} + //******************** End of __UNIX_JACK__ *********************// +#endif + +#if defined(__WINDOWS_ASIO__) // ASIO API on Windows + +// The ASIO API is designed around a callback scheme, so this +// implementation is similar to that used for OS-X CoreAudio and Linux +// Jack. The primary constraint with ASIO is that it only allows +// access to a single driver at a time. Thus, it is not possible to +// have more than one simultaneous RtAudio stream. +// +// This implementation also requires a number of external ASIO files +// and a few global variables. The ASIO callback scheme does not +// allow for the passing of user data, so we must create a global +// pointer to our callbackInfo structure. +// +// On unix systems, we make use of a pthread condition variable. +// Since there is no equivalent in Windows, I hacked something based +// on information found in +// http://www.cs.wustl.edu/~schmidt/win32-cv-1.html. + +#include "asiosys.h" +#include "asio.h" +#include "iasiothiscallresolver.h" +#include "asiodrivers.h" +#include + +AsioDrivers drivers; +ASIOCallbacks asioCallbacks; +ASIODriverInfo driverInfo; +CallbackInfo *asioCallbackInfo; +bool asioXRun; + +struct AsioHandle { + int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + ASIOBufferInfo *bufferInfos; + HANDLE condition; + + AsioHandle() + :drainCounter(0), internalDrain(false), bufferInfos(0) {} +}; + +// Function declarations (definitions at end of section) +static const char* getAsioErrorString( ASIOError result ); +void sampleRateChanged( ASIOSampleRate sRate ); +long asioMessages( long selector, long value, void* message, double* opt ); + +RtApiAsio :: RtApiAsio() +{ + // ASIO cannot run on a multi-threaded appartment. You can call + // CoInitialize beforehand, but it must be for appartment threading + // (in which case, CoInitilialize will return S_FALSE here). + coInitialized_ = false; + HRESULT hr = CoInitialize( NULL ); + if ( FAILED(hr) ) { + errorText_ = "RtApiAsio::ASIO requires a single-threaded appartment. Call CoInitializeEx(0,COINIT_APARTMENTTHREADED)"; + error( RtError::WARNING ); + } + coInitialized_ = true; + + drivers.removeCurrentDriver(); + driverInfo.asioVersion = 2; + + // See note in DirectSound implementation about GetDesktopWindow(). + driverInfo.sysRef = GetForegroundWindow(); +} + +RtApiAsio :: ~RtApiAsio() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); + if ( coInitialized_ ) CoUninitialize(); +} + +unsigned int RtApiAsio :: getDeviceCount( void ) +{ + return (unsigned int) drivers.asioGetNumDev(); +} + +RtAudio::DeviceInfo RtApiAsio :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + // Get device ID + unsigned int nDevices = getDeviceCount(); + if ( nDevices == 0 ) { + errorText_ = "RtApiAsio::getDeviceInfo: no devices found!"; + error( RtError::INVALID_USE ); + } + + if ( device >= nDevices ) { + errorText_ = "RtApiAsio::getDeviceInfo: device ID is invalid!"; + error( RtError::INVALID_USE ); + } + + // If a stream is already open, we cannot probe other devices. Thus, use the saved results. + if ( stream_.state != STREAM_CLOSED ) { + if ( device >= devices_.size() ) { + errorText_ = "RtApiAsio::getDeviceInfo: device ID was not present before stream was opened."; + error( RtError::WARNING ); + return info; + } + return devices_[ device ]; + } + + char driverName[32]; + ASIOError result = drivers.asioGetDriverName( (int) device, driverName, 32 ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::getDeviceInfo: unable to get driver name (" << getAsioErrorString( result ) << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + info.name = driverName; + + if ( !drivers.loadDriver( driverName ) ) { + errorStream_ << "RtApiAsio::getDeviceInfo: unable to load driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + result = ASIOInit( &driverInfo ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::getDeviceInfo: error (" << getAsioErrorString( result ) << ") initializing driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Determine the device channel information. + long inputChannels, outputChannels; + result = ASIOGetChannels( &inputChannels, &outputChannels ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::getDeviceInfo: error (" << getAsioErrorString( result ) << ") getting channel count (" << driverName << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + info.outputChannels = outputChannels; + info.inputChannels = inputChannels; + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // Determine the supported sample rates. + info.sampleRates.clear(); + for ( unsigned int i=0; i 0 ) + if ( getDefaultOutputDevice() == device ) info.isDefaultOutput = true; + if ( info.inputChannels > 0 ) + if ( getDefaultInputDevice() == device ) info.isDefaultInput = true; + + info.probed = true; + drivers.removeCurrentDriver(); + return info; +} + +void bufferSwitch( long index, ASIOBool processNow ) +{ + RtApiAsio *object = (RtApiAsio *) asioCallbackInfo->object; + object->callbackEvent( index ); +} + +void RtApiAsio :: saveDeviceInfo( void ) +{ + devices_.clear(); + + unsigned int nDevices = getDeviceCount(); + devices_.resize( nDevices ); + for ( unsigned int i=0; isaveDeviceInfo(); + + // Only load the driver once for duplex stream. + if ( mode != INPUT || stream_.mode != OUTPUT ) { + if ( !drivers.loadDriver( driverName ) ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: unable to load driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + result = ASIOInit( &driverInfo ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: error (" << getAsioErrorString( result ) << ") initializing driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Check the device channel count. + long inputChannels, outputChannels; + result = ASIOGetChannels( &inputChannels, &outputChannels ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: error (" << getAsioErrorString( result ) << ") getting channel count (" << driverName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( ( mode == OUTPUT && (channels+firstChannel) > (unsigned int) outputChannels) || + ( mode == INPUT && (channels+firstChannel) > (unsigned int) inputChannels) ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") does not support requested channel count (" << channels << ") + offset (" << firstChannel << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.nDeviceChannels[mode] = channels; + stream_.nUserChannels[mode] = channels; + stream_.channelOffset[mode] = firstChannel; + + // Verify the sample rate is supported. + result = ASIOCanSampleRate( (ASIOSampleRate) sampleRate ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") does not support requested sample rate (" << sampleRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Get the current sample rate + ASIOSampleRate currentRate; + result = ASIOGetSampleRate( ¤tRate ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error getting sample rate."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the sample rate only if necessary + if ( currentRate != sampleRate ) { + result = ASIOSetSampleRate( (ASIOSampleRate) sampleRate ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error setting sample rate (" << sampleRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Determine the driver data type. + ASIOChannelInfo channelInfo; + channelInfo.channel = 0; + if ( mode == OUTPUT ) channelInfo.isInput = false; + else channelInfo.isInput = true; + result = ASIOGetChannelInfo( &channelInfo ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") getting data format."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Assuming WINDOWS host is always little-endian. + stream_.doByteSwap[mode] = false; + stream_.userFormat = format; + stream_.deviceFormat[mode] = 0; + if ( channelInfo.type == ASIOSTInt16MSB || channelInfo.type == ASIOSTInt16LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + if ( channelInfo.type == ASIOSTInt16MSB ) stream_.doByteSwap[mode] = true; + } + else if ( channelInfo.type == ASIOSTInt32MSB || channelInfo.type == ASIOSTInt32LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + if ( channelInfo.type == ASIOSTInt32MSB ) stream_.doByteSwap[mode] = true; + } + else if ( channelInfo.type == ASIOSTFloat32MSB || channelInfo.type == ASIOSTFloat32LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + if ( channelInfo.type == ASIOSTFloat32MSB ) stream_.doByteSwap[mode] = true; + } + else if ( channelInfo.type == ASIOSTFloat64MSB || channelInfo.type == ASIOSTFloat64LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT64; + if ( channelInfo.type == ASIOSTFloat64MSB ) stream_.doByteSwap[mode] = true; + } + + if ( stream_.deviceFormat[mode] == 0 ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the buffer size. For a duplex stream, this will end up + // setting the buffer size based on the input constraints, which + // should be ok. + long minSize, maxSize, preferSize, granularity; + result = ASIOGetBufferSize( &minSize, &maxSize, &preferSize, &granularity ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") getting buffer size."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( *bufferSize < (unsigned int) minSize ) *bufferSize = (unsigned int) minSize; + else if ( *bufferSize > (unsigned int) maxSize ) *bufferSize = (unsigned int) maxSize; + else if ( granularity == -1 ) { + // Make sure bufferSize is a power of two. + int log2_of_min_size = 0; + int log2_of_max_size = 0; + + for ( unsigned int i = 0; i < sizeof(long) * 8; i++ ) { + if ( minSize & ((long)1 << i) ) log2_of_min_size = i; + if ( maxSize & ((long)1 << i) ) log2_of_max_size = i; + } + + long min_delta = std::abs( (long)*bufferSize - ((long)1 << log2_of_min_size) ); + int min_delta_num = log2_of_min_size; + + for (int i = log2_of_min_size + 1; i <= log2_of_max_size; i++) { + long current_delta = std::abs( (long)*bufferSize - ((long)1 << i) ); + if (current_delta < min_delta) { + min_delta = current_delta; + min_delta_num = i; + } + } + + *bufferSize = ( (unsigned int)1 << min_delta_num ); + if ( *bufferSize < (unsigned int) minSize ) *bufferSize = (unsigned int) minSize; + else if ( *bufferSize > (unsigned int) maxSize ) *bufferSize = (unsigned int) maxSize; + } + else if ( granularity != 0 ) { + // Set to an even multiple of granularity, rounding up. + *bufferSize = (*bufferSize + granularity-1) / granularity * granularity; + } + + if ( mode == INPUT && stream_.mode == OUTPUT && stream_.bufferSize != *bufferSize ) { + drivers.removeCurrentDriver(); + errorText_ = "RtApiAsio::probeDeviceOpen: input/output buffersize discrepancy!"; + return FAILURE; + } + + stream_.bufferSize = *bufferSize; + stream_.nBuffers = 2; + + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + + // ASIO always uses non-interleaved buffers. + stream_.deviceInterleaved[mode] = false; + + // Allocate, if necessary, our AsioHandle structure for the stream. + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + if ( handle == 0 ) { + try { + handle = new AsioHandle; + } + catch ( std::bad_alloc& ) { + //if ( handle == NULL ) { + drivers.removeCurrentDriver(); + errorText_ = "RtApiAsio::probeDeviceOpen: error allocating AsioHandle memory."; + return FAILURE; + } + handle->bufferInfos = 0; + + // Create a manual-reset event. + handle->condition = CreateEvent( NULL, // no security + TRUE, // manual-reset + FALSE, // non-signaled initially + NULL ); // unnamed + stream_.apiHandle = (void *) handle; + } + + // Create the ASIO internal buffers. Since RtAudio sets up input + // and output separately, we'll have to dispose of previously + // created output buffers for a duplex stream. + long inputLatency, outputLatency; + if ( mode == INPUT && stream_.mode == OUTPUT ) { + ASIODisposeBuffers(); + if ( handle->bufferInfos ) free( handle->bufferInfos ); + } + + // Allocate, initialize, and save the bufferInfos in our stream callbackInfo structure. + bool buffersAllocated = false; + unsigned int i, nChannels = stream_.nDeviceChannels[0] + stream_.nDeviceChannels[1]; + handle->bufferInfos = (ASIOBufferInfo *) malloc( nChannels * sizeof(ASIOBufferInfo) ); + if ( handle->bufferInfos == NULL ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: error allocating bufferInfo memory for driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + goto error; + } + + ASIOBufferInfo *infos; + infos = handle->bufferInfos; + for ( i=0; iisInput = ASIOFalse; + infos->channelNum = i + stream_.channelOffset[0]; + infos->buffers[0] = infos->buffers[1] = 0; + } + for ( i=0; iisInput = ASIOTrue; + infos->channelNum = i + stream_.channelOffset[1]; + infos->buffers[0] = infos->buffers[1] = 0; + } + + // Set up the ASIO callback structure and create the ASIO data buffers. + asioCallbacks.bufferSwitch = &bufferSwitch; + asioCallbacks.sampleRateDidChange = &sampleRateChanged; + asioCallbacks.asioMessage = &asioMessages; + asioCallbacks.bufferSwitchTimeInfo = NULL; + result = ASIOCreateBuffers( handle->bufferInfos, nChannels, stream_.bufferSize, &asioCallbacks ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") creating buffers."; + errorText_ = errorStream_.str(); + goto error; + } + buffersAllocated = true; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate necessary internal buffers + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiAsio::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiAsio::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.sampleRate = sampleRate; + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + asioCallbackInfo = &stream_.callbackInfo; + stream_.callbackInfo.object = (void *) this; + if ( stream_.mode == OUTPUT && mode == INPUT ) + // We had already set up an output stream. + stream_.mode = DUPLEX; + else + stream_.mode = mode; + + // Determine device latencies + result = ASIOGetLatencies( &inputLatency, &outputLatency ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") getting latency."; + errorText_ = errorStream_.str(); + error( RtError::WARNING); // warn but don't fail + } + else { + stream_.latency[0] = outputLatency; + stream_.latency[1] = inputLatency; + } + + // Setup the buffer conversion information structure. We don't use + // buffers to do channel offsets, so we override that parameter + // here. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, 0 ); + + return SUCCESS; + + error: + if ( buffersAllocated ) + ASIODisposeBuffers(); + drivers.removeCurrentDriver(); + + if ( handle ) { + CloseHandle( handle->condition ); + if ( handle->bufferInfos ) + free( handle->bufferInfos ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiAsio :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAsio::closeStream(): no open stream to close!"; + error( RtError::WARNING ); + return; + } + + if ( stream_.state == STREAM_RUNNING ) { + stream_.state = STREAM_STOPPED; + ASIOStop(); + } + ASIODisposeBuffers(); + drivers.removeCurrentDriver(); + + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + if ( handle ) { + CloseHandle( handle->condition ); + if ( handle->bufferInfos ) + free( handle->bufferInfos ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiAsio :: startStream() +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiAsio::startStream(): the stream is already running!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + ASIOError result = ASIOStart(); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::startStream: error (" << getAsioErrorString( result ) << ") starting device."; + errorText_ = errorStream_.str(); + goto unlock; + } + + handle->drainCounter = 0; + handle->internalDrain = false; + stream_.state = STREAM_RUNNING; + asioXRun = false; + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result == ASE_OK ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiAsio :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAsio::stopStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 1; + MUTEX_UNLOCK( &stream_.mutex ); + WaitForMultipleObjects( 1, &handle->condition, FALSE, INFINITE ); // block until signaled + ResetEvent( handle->condition ); + MUTEX_LOCK( &stream_.mutex ); + } + } + + ASIOError result = ASIOStop(); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::stopStream: error (" << getAsioErrorString( result ) << ") stopping device."; + errorText_ = errorStream_.str(); + } + + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result == ASE_OK ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiAsio :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAsio::abortStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + // The following lines were commented-out because some behavior was + // noted where the device buffers need to be zeroed to avoid + // continuing sound, even when the device buffers are completely + // disposed. So now, calling abort is the same as calling stop. + // AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + // handle->drainCounter = 1; + stopStream(); +} + +bool RtApiAsio :: callbackEvent( long bufferIndex ) +{ + if ( stream_.state == STREAM_STOPPED ) return SUCCESS; + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAsio::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtError::WARNING ); + return FAILURE; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal is finished. + if ( handle->drainCounter > 3 ) { + if ( handle->internalDrain == false ) + SetEvent( handle->condition ); + else + stopStream(); + return SUCCESS; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) goto unlock; + + // Invoke user callback to get fresh output data UNLESS we are + // draining stream. + if ( handle->drainCounter == 0 ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && asioXRun == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + asioXRun = false; + } + if ( stream_.mode != OUTPUT && asioXRun == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + asioXRun = false; + } + handle->drainCounter = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( handle->drainCounter == 2 ) { + MUTEX_UNLOCK( &stream_.mutex ); + abortStream(); + return SUCCESS; + } + else if ( handle->drainCounter == 1 ) + handle->internalDrain = true; + } + + unsigned int nChannels, bufferBytes, i, j; + nChannels = stream_.nDeviceChannels[0] + stream_.nDeviceChannels[1]; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + bufferBytes = stream_.bufferSize * formatBytes( stream_.deviceFormat[0] ); + + if ( handle->drainCounter > 1 ) { // write zeros to the output stream + + for ( i=0, j=0; ibufferInfos[i].isInput != ASIOTrue ) + memset( handle->bufferInfos[i].buffers[bufferIndex], 0, bufferBytes ); + } + + } + else if ( stream_.doConvertBuffer[0] ) { + + convertBuffer( stream_.deviceBuffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + if ( stream_.doByteSwap[0] ) + byteSwapBuffer( stream_.deviceBuffer, + stream_.bufferSize * stream_.nDeviceChannels[0], + stream_.deviceFormat[0] ); + + for ( i=0, j=0; ibufferInfos[i].isInput != ASIOTrue ) + memcpy( handle->bufferInfos[i].buffers[bufferIndex], + &stream_.deviceBuffer[j++*bufferBytes], bufferBytes ); + } + + } + else { + + if ( stream_.doByteSwap[0] ) + byteSwapBuffer( stream_.userBuffer[0], + stream_.bufferSize * stream_.nUserChannels[0], + stream_.userFormat ); + + for ( i=0, j=0; ibufferInfos[i].isInput != ASIOTrue ) + memcpy( handle->bufferInfos[i].buffers[bufferIndex], + &stream_.userBuffer[0][bufferBytes*j++], bufferBytes ); + } + + } + + if ( handle->drainCounter ) { + handle->drainCounter++; + goto unlock; + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + bufferBytes = stream_.bufferSize * formatBytes(stream_.deviceFormat[1]); + + if (stream_.doConvertBuffer[1]) { + + // Always interleave ASIO input data. + for ( i=0, j=0; ibufferInfos[i].isInput == ASIOTrue ) + memcpy( &stream_.deviceBuffer[j++*bufferBytes], + handle->bufferInfos[i].buffers[bufferIndex], + bufferBytes ); + } + + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( stream_.deviceBuffer, + stream_.bufferSize * stream_.nDeviceChannels[1], + stream_.deviceFormat[1] ); + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + + } + else { + for ( i=0, j=0; ibufferInfos[i].isInput == ASIOTrue ) { + memcpy( &stream_.userBuffer[1][bufferBytes*j++], + handle->bufferInfos[i].buffers[bufferIndex], + bufferBytes ); + } + } + + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( stream_.userBuffer[1], + stream_.bufferSize * stream_.nUserChannels[1], + stream_.userFormat ); + } + } + + unlock: + // The following call was suggested by Malte Clasen. While the API + // documentation indicates it should not be required, some device + // drivers apparently do not function correctly without it. + ASIOOutputReady(); + + MUTEX_UNLOCK( &stream_.mutex ); + + RtApi::tickStreamTime(); + return SUCCESS; +} + +void sampleRateChanged( ASIOSampleRate sRate ) +{ + // The ASIO documentation says that this usually only happens during + // external sync. Audio processing is not stopped by the driver, + // actual sample rate might not have even changed, maybe only the + // sample rate status of an AES/EBU or S/PDIF digital input at the + // audio device. + + RtApi *object = (RtApi *) asioCallbackInfo->object; + try { + object->stopStream(); + } + catch ( RtError &exception ) { + std::cerr << "\nRtApiAsio: sampleRateChanged() error (" << exception.getMessage() << ")!\n" << std::endl; + return; + } + + std::cerr << "\nRtApiAsio: driver reports sample rate changed to " << sRate << " ... stream stopped!!!\n" << std::endl; +} + +long asioMessages( long selector, long value, void* message, double* opt ) +{ + long ret = 0; + + switch( selector ) { + case kAsioSelectorSupported: + if ( value == kAsioResetRequest + || value == kAsioEngineVersion + || value == kAsioResyncRequest + || value == kAsioLatenciesChanged + // The following three were added for ASIO 2.0, you don't + // necessarily have to support them. + || value == kAsioSupportsTimeInfo + || value == kAsioSupportsTimeCode + || value == kAsioSupportsInputMonitor) + ret = 1L; + break; + case kAsioResetRequest: + // Defer the task and perform the reset of the driver during the + // next "safe" situation. You cannot reset the driver right now, + // as this code is called from the driver. Reset the driver is + // done by completely destruct is. I.e. ASIOStop(), + // ASIODisposeBuffers(), Destruction Afterwards you initialize the + // driver again. + std::cerr << "\nRtApiAsio: driver reset requested!!!" << std::endl; + ret = 1L; + break; + case kAsioResyncRequest: + // This informs the application that the driver encountered some + // non-fatal data loss. It is used for synchronization purposes + // of different media. Added mainly to work around the Win16Mutex + // problems in Windows 95/98 with the Windows Multimedia system, + // which could lose data because the Mutex was held too long by + // another thread. However a driver can issue it in other + // situations, too. + // std::cerr << "\nRtApiAsio: driver resync requested!!!" << std::endl; + asioXRun = true; + ret = 1L; + break; + case kAsioLatenciesChanged: + // This will inform the host application that the drivers were + // latencies changed. Beware, it this does not mean that the + // buffer sizes have changed! You might need to update internal + // delay data. + std::cerr << "\nRtApiAsio: driver latency may have changed!!!" << std::endl; + ret = 1L; + break; + case kAsioEngineVersion: + // Return the supported ASIO version of the host application. If + // a host application does not implement this selector, ASIO 1.0 + // is assumed by the driver. + ret = 2L; + break; + case kAsioSupportsTimeInfo: + // Informs the driver whether the + // asioCallbacks.bufferSwitchTimeInfo() callback is supported. + // For compatibility with ASIO 1.0 drivers the host application + // should always support the "old" bufferSwitch method, too. + ret = 0; + break; + case kAsioSupportsTimeCode: + // Informs the driver whether application is interested in time + // code info. If an application does not need to know about time + // code, the driver has less work to do. + ret = 0; + break; + } + return ret; +} + +static const char* getAsioErrorString( ASIOError result ) +{ + struct Messages + { + ASIOError value; + const char*message; + }; + + static Messages m[] = + { + { ASE_NotPresent, "Hardware input or output is not present or available." }, + { ASE_HWMalfunction, "Hardware is malfunctioning." }, + { ASE_InvalidParameter, "Invalid input parameter." }, + { ASE_InvalidMode, "Invalid mode." }, + { ASE_SPNotAdvancing, "Sample position not advancing." }, + { ASE_NoClock, "Sample clock or rate cannot be determined or is not present." }, + { ASE_NoMemory, "Not enough memory to complete the request." } + }; + + for ( unsigned int i = 0; i < sizeof(m)/sizeof(m[0]); ++i ) + if ( m[i].value == result ) return m[i].message; + + return "Unknown error."; +} +//******************** End of __WINDOWS_ASIO__ *********************// +#endif + + +#if defined(__WINDOWS_DS__) // Windows DirectSound API + +// Modified by Robin Davies, October 2005 +// - Improvements to DirectX pointer chasing. +// - Bug fix for non-power-of-two Asio granularity used by Edirol PCR-A30. +// - Auto-call CoInitialize for DSOUND and ASIO platforms. +// Various revisions for RtAudio 4.0 by Gary Scavone, April 2007 +// Changed device query structure for RtAudio 4.0.7, January 2010 + +#include +#include +#include + +#if defined(__MINGW32__) + // missing from latest mingw winapi +#define WAVE_FORMAT_96M08 0x00010000 /* 96 kHz, Mono, 8-bit */ +#define WAVE_FORMAT_96S08 0x00020000 /* 96 kHz, Stereo, 8-bit */ +#define WAVE_FORMAT_96M16 0x00040000 /* 96 kHz, Mono, 16-bit */ +#define WAVE_FORMAT_96S16 0x00080000 /* 96 kHz, Stereo, 16-bit */ +#endif + +#define MINIMUM_DEVICE_BUFFER_SIZE 32768 + +#ifdef _MSC_VER // if Microsoft Visual C++ +#pragma comment( lib, "winmm.lib" ) // then, auto-link winmm.lib. Otherwise, it has to be added manually. +#endif + +static inline DWORD dsPointerBetween( DWORD pointer, DWORD laterPointer, DWORD earlierPointer, DWORD bufferSize ) +{ + if ( pointer > bufferSize ) pointer -= bufferSize; + if ( laterPointer < earlierPointer ) laterPointer += bufferSize; + if ( pointer < earlierPointer ) pointer += bufferSize; + return pointer >= earlierPointer && pointer < laterPointer; +} + +// A structure to hold various information related to the DirectSound +// API implementation. +struct DsHandle { + unsigned int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + void *id[2]; + void *buffer[2]; + bool xrun[2]; + UINT bufferPointer[2]; + DWORD dsBufferSize[2]; + DWORD dsPointerLeadTime[2]; // the number of bytes ahead of the safe pointer to lead by. + HANDLE condition; + + DsHandle() + :drainCounter(0), internalDrain(false) { id[0] = 0; id[1] = 0; buffer[0] = 0; buffer[1] = 0; xrun[0] = false; xrun[1] = false; bufferPointer[0] = 0; bufferPointer[1] = 0; } +}; + +// Declarations for utility functions, callbacks, and structures +// specific to the DirectSound implementation. +static BOOL CALLBACK deviceQueryCallback( LPGUID lpguid, + LPCTSTR description, + LPCTSTR module, + LPVOID lpContext ); + +static const char* getErrorString( int code ); + +extern "C" unsigned __stdcall callbackHandler( void *ptr ); + +struct DsDevice { + LPGUID id[2]; + bool validId[2]; + bool found; + std::string name; + + DsDevice() + : found(false) { validId[0] = false; validId[1] = false; } +}; + +std::vector< DsDevice > dsDevices; + +RtApiDs :: RtApiDs() +{ + // Dsound will run both-threaded. If CoInitialize fails, then just + // accept whatever the mainline chose for a threading model. + coInitialized_ = false; + HRESULT hr = CoInitialize( NULL ); + if ( !FAILED( hr ) ) coInitialized_ = true; +} + +RtApiDs :: ~RtApiDs() +{ + if ( coInitialized_ ) CoUninitialize(); // balanced call. + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +// The DirectSound default output is always the first device. +unsigned int RtApiDs :: getDefaultOutputDevice( void ) +{ + return 0; +} + +// The DirectSound default input is always the first input device, +// which is the first capture device enumerated. +unsigned int RtApiDs :: getDefaultInputDevice( void ) +{ + return 0; +} + +unsigned int RtApiDs :: getDeviceCount( void ) +{ + // Set query flag for previously found devices to false, so that we + // can check for any devices that have disappeared. + for ( unsigned int i=0; i :: iterator it; + for ( it=dsDevices.begin(); it < dsDevices.end(); it++ ) + if ( it->found == false ) dsDevices.erase( it ); + + return dsDevices.size(); +} + +RtAudio::DeviceInfo RtApiDs :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + if ( dsDevices.size() == 0 ) { + // Force a query of all devices + getDeviceCount(); + if ( dsDevices.size() == 0 ) { + errorText_ = "RtApiDs::getDeviceInfo: no devices found!"; + error( RtError::INVALID_USE ); + } + } + + if ( device >= dsDevices.size() ) { + errorText_ = "RtApiDs::getDeviceInfo: device ID is invalid!"; + error( RtError::INVALID_USE ); + } + + HRESULT result; + if ( dsDevices[ device ].validId[0] == false ) goto probeInput; + + LPDIRECTSOUND output; + DSCAPS outCaps; + result = DirectSoundCreate( dsDevices[ device ].id[0], &output, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") opening output device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + goto probeInput; + } + + outCaps.dwSize = sizeof( outCaps ); + result = output->GetCaps( &outCaps ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") getting capabilities!"; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + goto probeInput; + } + + // Get output channel information. + info.outputChannels = ( outCaps.dwFlags & DSCAPS_PRIMARYSTEREO ) ? 2 : 1; + + // Get sample rate information. + info.sampleRates.clear(); + for ( unsigned int k=0; k= (unsigned int) outCaps.dwMinSecondarySampleRate && + SAMPLE_RATES[k] <= (unsigned int) outCaps.dwMaxSecondarySampleRate ) + info.sampleRates.push_back( SAMPLE_RATES[k] ); + } + + // Get format information. + if ( outCaps.dwFlags & DSCAPS_PRIMARY16BIT ) info.nativeFormats |= RTAUDIO_SINT16; + if ( outCaps.dwFlags & DSCAPS_PRIMARY8BIT ) info.nativeFormats |= RTAUDIO_SINT8; + + output->Release(); + + if ( getDefaultOutputDevice() == device ) + info.isDefaultOutput = true; + + if ( dsDevices[ device ].validId[1] == false ) { + info.name = dsDevices[ device ].name; + info.probed = true; + return info; + } + + probeInput: + + LPDIRECTSOUNDCAPTURE input; + result = DirectSoundCaptureCreate( dsDevices[ device ].id[1], &input, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") opening input device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + DSCCAPS inCaps; + inCaps.dwSize = sizeof( inCaps ); + result = input->GetCaps( &inCaps ); + if ( FAILED( result ) ) { + input->Release(); + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") getting object capabilities (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Get input channel information. + info.inputChannels = inCaps.dwChannels; + + // Get sample rate and format information. + std::vector rates; + if ( inCaps.dwChannels == 2 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_2S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_4S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_96S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_1S08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_2S08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_4S08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_96S08 ) info.nativeFormats |= RTAUDIO_SINT8; + + if ( info.nativeFormats & RTAUDIO_SINT16 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1S16 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2S16 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4S16 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96S16 ) rates.push_back( 96000 ); + } + else if ( info.nativeFormats & RTAUDIO_SINT8 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1S08 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2S08 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4S08 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96S08 ) rates.push_back( 96000 ); + } + } + else if ( inCaps.dwChannels == 1 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_2M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_4M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_96M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_1M08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_2M08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_4M08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_96M08 ) info.nativeFormats |= RTAUDIO_SINT8; + + if ( info.nativeFormats & RTAUDIO_SINT16 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1M16 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2M16 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4M16 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96M16 ) rates.push_back( 96000 ); + } + else if ( info.nativeFormats & RTAUDIO_SINT8 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1M08 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2M08 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4M08 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96M08 ) rates.push_back( 96000 ); + } + } + else info.inputChannels = 0; // technically, this would be an error + + input->Release(); + + if ( info.inputChannels == 0 ) return info; + + // Copy the supported rates to the info structure but avoid duplication. + bool found; + for ( unsigned int i=0; i 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + if ( device == 0 ) info.isDefaultInput = true; + + // Copy name and return. + info.name = dsDevices[ device ].name; + info.probed = true; + return info; +} + +bool RtApiDs :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + if ( channels + firstChannel > 2 ) { + errorText_ = "RtApiDs::probeDeviceOpen: DirectSound does not support more than 2 channels per device."; + return FAILURE; + } + + unsigned int nDevices = dsDevices.size(); + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiDs::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiDs::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + if ( mode == OUTPUT ) { + if ( dsDevices[ device ].validId[0] == false ) { + errorStream_ << "RtApiDs::probeDeviceOpen: device (" << device << ") does not support output!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + else { // mode == INPUT + if ( dsDevices[ device ].validId[1] == false ) { + errorStream_ << "RtApiDs::probeDeviceOpen: device (" << device << ") does not support input!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // According to a note in PortAudio, using GetDesktopWindow() + // instead of GetForegroundWindow() is supposed to avoid problems + // that occur when the application's window is not the foreground + // window. Also, if the application window closes before the + // DirectSound buffer, DirectSound can crash. In the past, I had + // problems when using GetDesktopWindow() but it seems fine now + // (January 2010). I'll leave it commented here. + // HWND hWnd = GetForegroundWindow(); + HWND hWnd = GetDesktopWindow(); + + // Check the numberOfBuffers parameter and limit the lowest value to + // two. This is a judgement call and a value of two is probably too + // low for capture, but it should work for playback. + int nBuffers = 0; + if ( options ) nBuffers = options->numberOfBuffers; + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) nBuffers = 2; + if ( nBuffers < 2 ) nBuffers = 3; + + // Check the lower range of the user-specified buffer size and set + // (arbitrarily) to a lower bound of 32. + if ( *bufferSize < 32 ) *bufferSize = 32; + + // Create the wave format structure. The data format setting will + // be determined later. + WAVEFORMATEX waveFormat; + ZeroMemory( &waveFormat, sizeof(WAVEFORMATEX) ); + waveFormat.wFormatTag = WAVE_FORMAT_PCM; + waveFormat.nChannels = channels + firstChannel; + waveFormat.nSamplesPerSec = (unsigned long) sampleRate; + + // Determine the device buffer size. By default, we'll use the value + // defined above (32K), but we will grow it to make allowances for + // very large software buffer sizes. + DWORD dsBufferSize = MINIMUM_DEVICE_BUFFER_SIZE;; + DWORD dsPointerLeadTime = 0; + + void *ohandle = 0, *bhandle = 0; + HRESULT result; + if ( mode == OUTPUT ) { + + LPDIRECTSOUND output; + result = DirectSoundCreate( dsDevices[ device ].id[0], &output, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") opening output device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + DSCAPS outCaps; + outCaps.dwSize = sizeof( outCaps ); + result = output->GetCaps( &outCaps ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting capabilities (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check channel information. + if ( channels + firstChannel == 2 && !( outCaps.dwFlags & DSCAPS_PRIMARYSTEREO ) ) { + errorStream_ << "RtApiDs::getDeviceInfo: the output device (" << dsDevices[ device ].name << ") does not support stereo playback."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check format information. Use 16-bit format unless not + // supported or user requests 8-bit. + if ( outCaps.dwFlags & DSCAPS_PRIMARY16BIT && + !( format == RTAUDIO_SINT8 && outCaps.dwFlags & DSCAPS_PRIMARY8BIT ) ) { + waveFormat.wBitsPerSample = 16; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + else { + waveFormat.wBitsPerSample = 8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + stream_.userFormat = format; + + // Update wave format structure and buffer information. + waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; + waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; + dsPointerLeadTime = nBuffers * (*bufferSize) * (waveFormat.wBitsPerSample / 8) * channels; + + // If the user wants an even bigger buffer, increase the device buffer size accordingly. + while ( dsPointerLeadTime * 2U > dsBufferSize ) + dsBufferSize *= 2; + + // Set cooperative level to DSSCL_EXCLUSIVE ... sound stops when window focus changes. + // result = output->SetCooperativeLevel( hWnd, DSSCL_EXCLUSIVE ); + // Set cooperative level to DSSCL_PRIORITY ... sound remains when window focus changes. + result = output->SetCooperativeLevel( hWnd, DSSCL_PRIORITY ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") setting cooperative level (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Even though we will write to the secondary buffer, we need to + // access the primary buffer to set the correct output format + // (since the default is 8-bit, 22 kHz!). Setup the DS primary + // buffer description. + DSBUFFERDESC bufferDescription; + ZeroMemory( &bufferDescription, sizeof( DSBUFFERDESC ) ); + bufferDescription.dwSize = sizeof( DSBUFFERDESC ); + bufferDescription.dwFlags = DSBCAPS_PRIMARYBUFFER; + + // Obtain the primary buffer + LPDIRECTSOUNDBUFFER buffer; + result = output->CreateSoundBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") accessing primary buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the primary DS buffer sound format. + result = buffer->SetFormat( &waveFormat ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") setting primary buffer format (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Setup the secondary DS buffer description. + ZeroMemory( &bufferDescription, sizeof( DSBUFFERDESC ) ); + bufferDescription.dwSize = sizeof( DSBUFFERDESC ); + bufferDescription.dwFlags = ( DSBCAPS_STICKYFOCUS | + DSBCAPS_GLOBALFOCUS | + DSBCAPS_GETCURRENTPOSITION2 | + DSBCAPS_LOCHARDWARE ); // Force hardware mixing + bufferDescription.dwBufferBytes = dsBufferSize; + bufferDescription.lpwfxFormat = &waveFormat; + + // Try to create the secondary DS buffer. If that doesn't work, + // try to use software mixing. Otherwise, there's a problem. + result = output->CreateSoundBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + bufferDescription.dwFlags = ( DSBCAPS_STICKYFOCUS | + DSBCAPS_GLOBALFOCUS | + DSBCAPS_GETCURRENTPOSITION2 | + DSBCAPS_LOCSOFTWARE ); // Force software mixing + result = output->CreateSoundBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") creating secondary buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Get the buffer size ... might be different from what we specified. + DSBCAPS dsbcaps; + dsbcaps.dwSize = sizeof( DSBCAPS ); + result = buffer->GetCaps( &dsbcaps ); + if ( FAILED( result ) ) { + output->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting buffer settings (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + dsBufferSize = dsbcaps.dwBufferBytes; + + // Lock the DS buffer + LPVOID audioPtr; + DWORD dataLen; + result = buffer->Lock( 0, dsBufferSize, &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + output->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") locking buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Zero the DS buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the DS buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + output->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") unlocking buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + ohandle = (void *) output; + bhandle = (void *) buffer; + } + + if ( mode == INPUT ) { + + LPDIRECTSOUNDCAPTURE input; + result = DirectSoundCaptureCreate( dsDevices[ device ].id[1], &input, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") opening input device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + DSCCAPS inCaps; + inCaps.dwSize = sizeof( inCaps ); + result = input->GetCaps( &inCaps ); + if ( FAILED( result ) ) { + input->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting input capabilities (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check channel information. + if ( inCaps.dwChannels < channels + firstChannel ) { + errorText_ = "RtApiDs::getDeviceInfo: the input device does not support requested input channels."; + return FAILURE; + } + + // Check format information. Use 16-bit format unless user + // requests 8-bit. + DWORD deviceFormats; + if ( channels + firstChannel == 2 ) { + deviceFormats = WAVE_FORMAT_1S08 | WAVE_FORMAT_2S08 | WAVE_FORMAT_4S08 | WAVE_FORMAT_96S08; + if ( format == RTAUDIO_SINT8 && inCaps.dwFormats & deviceFormats ) { + waveFormat.wBitsPerSample = 8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + else { // assume 16-bit is supported + waveFormat.wBitsPerSample = 16; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + } + else { // channel == 1 + deviceFormats = WAVE_FORMAT_1M08 | WAVE_FORMAT_2M08 | WAVE_FORMAT_4M08 | WAVE_FORMAT_96M08; + if ( format == RTAUDIO_SINT8 && inCaps.dwFormats & deviceFormats ) { + waveFormat.wBitsPerSample = 8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + else { // assume 16-bit is supported + waveFormat.wBitsPerSample = 16; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + } + stream_.userFormat = format; + + // Update wave format structure and buffer information. + waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; + waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; + dsPointerLeadTime = nBuffers * (*bufferSize) * (waveFormat.wBitsPerSample / 8) * channels; + + // If the user wants an even bigger buffer, increase the device buffer size accordingly. + while ( dsPointerLeadTime * 2U > dsBufferSize ) + dsBufferSize *= 2; + + // Setup the secondary DS buffer description. + DSCBUFFERDESC bufferDescription; + ZeroMemory( &bufferDescription, sizeof( DSCBUFFERDESC ) ); + bufferDescription.dwSize = sizeof( DSCBUFFERDESC ); + bufferDescription.dwFlags = 0; + bufferDescription.dwReserved = 0; + bufferDescription.dwBufferBytes = dsBufferSize; + bufferDescription.lpwfxFormat = &waveFormat; + + // Create the capture buffer. + LPDIRECTSOUNDCAPTUREBUFFER buffer; + result = input->CreateCaptureBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + input->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") creating input buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Get the buffer size ... might be different from what we specified. + DSCBCAPS dscbcaps; + dscbcaps.dwSize = sizeof( DSCBCAPS ); + result = buffer->GetCaps( &dscbcaps ); + if ( FAILED( result ) ) { + input->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting buffer settings (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + dsBufferSize = dscbcaps.dwBufferBytes; + + // NOTE: We could have a problem here if this is a duplex stream + // and the play and capture hardware buffer sizes are different + // (I'm actually not sure if that is a problem or not). + // Currently, we are not verifying that. + + // Lock the capture buffer + LPVOID audioPtr; + DWORD dataLen; + result = buffer->Lock( 0, dsBufferSize, &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + input->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") locking input buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Zero the buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + input->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") unlocking input buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + ohandle = (void *) input; + bhandle = (void *) buffer; + } + + // Set various stream parameters + DsHandle *handle = 0; + stream_.nDeviceChannels[mode] = channels + firstChannel; + stream_.nUserChannels[mode] = channels; + stream_.bufferSize = *bufferSize; + stream_.channelOffset[mode] = firstChannel; + stream_.deviceInterleaved[mode] = true; + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + + // Set flag for buffer conversion + stream_.doConvertBuffer[mode] = false; + if (stream_.nUserChannels[mode] != stream_.nDeviceChannels[mode]) + stream_.doConvertBuffer[mode] = true; + if (stream_.userFormat != stream_.deviceFormat[mode]) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate necessary internal buffers + long bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiDs::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= (long) bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiDs::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + // Allocate our DsHandle structures for the stream. + if ( stream_.apiHandle == 0 ) { + try { + handle = new DsHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiDs::probeDeviceOpen: error allocating AsioHandle memory."; + goto error; + } + + // Create a manual-reset event. + handle->condition = CreateEvent( NULL, // no security + TRUE, // manual-reset + FALSE, // non-signaled initially + NULL ); // unnamed + stream_.apiHandle = (void *) handle; + } + else + handle = (DsHandle *) stream_.apiHandle; + handle->id[mode] = ohandle; + handle->buffer[mode] = bhandle; + handle->dsBufferSize[mode] = dsBufferSize; + handle->dsPointerLeadTime[mode] = dsPointerLeadTime; + + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + if ( stream_.mode == OUTPUT && mode == INPUT ) + // We had already set up an output stream. + stream_.mode = DUPLEX; + else + stream_.mode = mode; + stream_.nBuffers = nBuffers; + stream_.sampleRate = sampleRate; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + // Setup the callback thread. + unsigned threadId; + stream_.callbackInfo.object = (void *) this; + stream_.callbackInfo.isRunning = true; + stream_.callbackInfo.thread = _beginthreadex( NULL, 0, &callbackHandler, + &stream_.callbackInfo, 0, &threadId ); + if ( stream_.callbackInfo.thread == 0 ) { + errorText_ = "RtApiDs::probeDeviceOpen: error creating callback thread!"; + goto error; + } + + // Boost DS thread priority + SetThreadPriority( (HANDLE) stream_.callbackInfo.thread, THREAD_PRIORITY_HIGHEST ); + return SUCCESS; + + error: + if ( handle ) { + if ( handle->buffer[0] ) { // the object pointer can be NULL and valid + LPDIRECTSOUND object = (LPDIRECTSOUND) handle->id[0]; + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + if ( buffer ) buffer->Release(); + object->Release(); + } + if ( handle->buffer[1] ) { + LPDIRECTSOUNDCAPTURE object = (LPDIRECTSOUNDCAPTURE) handle->id[1]; + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + if ( buffer ) buffer->Release(); + object->Release(); + } + CloseHandle( handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiDs :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiDs::closeStream(): no open stream to close!"; + error( RtError::WARNING ); + return; + } + + // Stop the callback thread. + stream_.callbackInfo.isRunning = false; + WaitForSingleObject( (HANDLE) stream_.callbackInfo.thread, INFINITE ); + CloseHandle( (HANDLE) stream_.callbackInfo.thread ); + + DsHandle *handle = (DsHandle *) stream_.apiHandle; + if ( handle ) { + if ( handle->buffer[0] ) { // the object pointer can be NULL and valid + LPDIRECTSOUND object = (LPDIRECTSOUND) handle->id[0]; + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + if ( buffer ) { + buffer->Stop(); + buffer->Release(); + } + object->Release(); + } + if ( handle->buffer[1] ) { + LPDIRECTSOUNDCAPTURE object = (LPDIRECTSOUNDCAPTURE) handle->id[1]; + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + if ( buffer ) { + buffer->Stop(); + buffer->Release(); + } + object->Release(); + } + CloseHandle( handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiDs :: startStream() +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiDs::startStream(): the stream is already running!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + DsHandle *handle = (DsHandle *) stream_.apiHandle; + + // Increase scheduler frequency on lesser windows (a side-effect of + // increasing timer accuracy). On greater windows (Win2K or later), + // this is already in effect. + timeBeginPeriod( 1 ); + + buffersRolling = false; + duplexPrerollBytes = 0; + + if ( stream_.mode == DUPLEX ) { + // 0.5 seconds of silence in DUPLEX mode while the devices spin up and synchronize. + duplexPrerollBytes = (int) ( 0.5 * stream_.sampleRate * formatBytes( stream_.deviceFormat[1] ) * stream_.nDeviceChannels[1] ); + } + + HRESULT result = 0; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + result = buffer->Play( 0, 0, DSBPLAY_LOOPING ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::startStream: error (" << getErrorString( result ) << ") starting output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + result = buffer->Start( DSCBSTART_LOOPING ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::startStream: error (" << getErrorString( result ) << ") starting input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + handle->drainCounter = 0; + handle->internalDrain = false; + stream_.state = STREAM_RUNNING; + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + if ( FAILED( result ) ) error( RtError::SYSTEM_ERROR ); +} + +void RtApiDs :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiDs::stopStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + HRESULT result = 0; + LPVOID audioPtr; + DWORD dataLen; + DsHandle *handle = (DsHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 1; + MUTEX_UNLOCK( &stream_.mutex ); + WaitForMultipleObjects( 1, &handle->condition, FALSE, INFINITE ); // block until signaled + ResetEvent( handle->condition ); + MUTEX_LOCK( &stream_.mutex ); + } + + // Stop the buffer and clear memory + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + result = buffer->Stop(); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") stopping output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Lock the buffer and clear it so that if we start to play again, + // we won't have old data playing. + result = buffer->Lock( 0, handle->dsBufferSize[0], &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") locking output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Zero the DS buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the DS buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") unlocking output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // If we start playing again, we must begin at beginning of buffer. + handle->bufferPointer[0] = 0; + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + audioPtr = NULL; + dataLen = 0; + + result = buffer->Stop(); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") stopping input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Lock the buffer and clear it so that if we start to play again, + // we won't have old data playing. + result = buffer->Lock( 0, handle->dsBufferSize[1], &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") locking input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Zero the DS buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the DS buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") unlocking input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // If we start recording again, we must begin at beginning of buffer. + handle->bufferPointer[1] = 0; + } + + unlock: + timeEndPeriod( 1 ); // revert to normal scheduler frequency on lesser windows. + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( FAILED( result ) ) error( RtError::SYSTEM_ERROR ); +} + +void RtApiDs :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiDs::abortStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + DsHandle *handle = (DsHandle *) stream_.apiHandle; + handle->drainCounter = 1; + + stopStream(); +} + +void RtApiDs :: callbackEvent() +{ + if ( stream_.state == STREAM_STOPPED ) { + Sleep( 50 ); // sleep 50 milliseconds + return; + } + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiDs::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtError::WARNING ); + return; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + DsHandle *handle = (DsHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal is finished. + if ( handle->drainCounter > stream_.nBuffers + 2 ) { + if ( handle->internalDrain == false ) + SetEvent( handle->condition ); + else + stopStream(); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + // Invoke user callback to get fresh output data UNLESS we are + // draining stream. + if ( handle->drainCounter == 0 ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + handle->drainCounter = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( handle->drainCounter == 2 ) { + MUTEX_UNLOCK( &stream_.mutex ); + abortStream(); + return; + } + else if ( handle->drainCounter == 1 ) + handle->internalDrain = true; + } + + HRESULT result; + DWORD currentWritePointer, safeWritePointer; + DWORD currentReadPointer, safeReadPointer; + UINT nextWritePointer; + + LPVOID buffer1 = NULL; + LPVOID buffer2 = NULL; + DWORD bufferSize1 = 0; + DWORD bufferSize2 = 0; + + char *buffer; + long bufferBytes; + + if ( buffersRolling == false ) { + if ( stream_.mode == DUPLEX ) { + //assert( handle->dsBufferSize[0] == handle->dsBufferSize[1] ); + + // It takes a while for the devices to get rolling. As a result, + // there's no guarantee that the capture and write device pointers + // will move in lockstep. Wait here for both devices to start + // rolling, and then set our buffer pointers accordingly. + // e.g. Crystal Drivers: the capture buffer starts up 5700 to 9600 + // bytes later than the write buffer. + + // Stub: a serious risk of having a pre-emptive scheduling round + // take place between the two GetCurrentPosition calls... but I'm + // really not sure how to solve the problem. Temporarily boost to + // Realtime priority, maybe; but I'm not sure what priority the + // DirectSound service threads run at. We *should* be roughly + // within a ms or so of correct. + + LPDIRECTSOUNDBUFFER dsWriteBuffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + LPDIRECTSOUNDCAPTUREBUFFER dsCaptureBuffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + + DWORD startSafeWritePointer, startSafeReadPointer; + + result = dsWriteBuffer->GetCurrentPosition( NULL, &startSafeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + result = dsCaptureBuffer->GetCurrentPosition( NULL, &startSafeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + while ( true ) { + result = dsWriteBuffer->GetCurrentPosition( NULL, &safeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + result = dsCaptureBuffer->GetCurrentPosition( NULL, &safeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + if ( safeWritePointer != startSafeWritePointer && safeReadPointer != startSafeReadPointer ) break; + Sleep( 1 ); + } + + //assert( handle->dsBufferSize[0] == handle->dsBufferSize[1] ); + + handle->bufferPointer[0] = safeWritePointer + handle->dsPointerLeadTime[0]; + if ( handle->bufferPointer[0] >= handle->dsBufferSize[0] ) handle->bufferPointer[0] -= handle->dsBufferSize[0]; + handle->bufferPointer[1] = safeReadPointer; + } + else if ( stream_.mode == OUTPUT ) { + + // Set the proper nextWritePosition after initial startup. + LPDIRECTSOUNDBUFFER dsWriteBuffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + result = dsWriteBuffer->GetCurrentPosition( ¤tWritePointer, &safeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + handle->bufferPointer[0] = safeWritePointer + handle->dsPointerLeadTime[0]; + if ( handle->bufferPointer[0] >= handle->dsBufferSize[0] ) handle->bufferPointer[0] -= handle->dsBufferSize[0]; + } + + buffersRolling = true; + } + + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + LPDIRECTSOUNDBUFFER dsBuffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + + if ( handle->drainCounter > 1 ) { // write zeros to the output stream + bufferBytes = stream_.bufferSize * stream_.nUserChannels[0]; + bufferBytes *= formatBytes( stream_.userFormat ); + memset( stream_.userBuffer[0], 0, bufferBytes ); + } + + // Setup parameters and do buffer conversion if necessary. + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + convertBuffer( buffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + bufferBytes = stream_.bufferSize * stream_.nDeviceChannels[0]; + bufferBytes *= formatBytes( stream_.deviceFormat[0] ); + } + else { + buffer = stream_.userBuffer[0]; + bufferBytes = stream_.bufferSize * stream_.nUserChannels[0]; + bufferBytes *= formatBytes( stream_.userFormat ); + } + + // No byte swapping necessary in DirectSound implementation. + + // Ahhh ... windoze. 16-bit data is signed but 8-bit data is + // unsigned. So, we need to convert our signed 8-bit data here to + // unsigned. + if ( stream_.deviceFormat[0] == RTAUDIO_SINT8 ) + for ( int i=0; idsBufferSize[0]; + nextWritePointer = handle->bufferPointer[0]; + + DWORD endWrite, leadPointer; + while ( true ) { + // Find out where the read and "safe write" pointers are. + result = dsBuffer->GetCurrentPosition( ¤tWritePointer, &safeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + + // We will copy our output buffer into the region between + // safeWritePointer and leadPointer. If leadPointer is not + // beyond the next endWrite position, wait until it is. + leadPointer = safeWritePointer + handle->dsPointerLeadTime[0]; + //std::cout << "safeWritePointer = " << safeWritePointer << ", leadPointer = " << leadPointer << ", nextWritePointer = " << nextWritePointer << std::endl; + if ( leadPointer > dsBufferSize ) leadPointer -= dsBufferSize; + if ( leadPointer < nextWritePointer ) leadPointer += dsBufferSize; // unwrap offset + endWrite = nextWritePointer + bufferBytes; + + // Check whether the entire write region is behind the play pointer. + if ( leadPointer >= endWrite ) break; + + // If we are here, then we must wait until the leadPointer advances + // beyond the end of our next write region. We use the + // Sleep() function to suspend operation until that happens. + double millis = ( endWrite - leadPointer ) * 1000.0; + millis /= ( formatBytes( stream_.deviceFormat[0]) * stream_.nDeviceChannels[0] * stream_.sampleRate); + if ( millis < 1.0 ) millis = 1.0; + Sleep( (DWORD) millis ); + } + + if ( dsPointerBetween( nextWritePointer, safeWritePointer, currentWritePointer, dsBufferSize ) + || dsPointerBetween( endWrite, safeWritePointer, currentWritePointer, dsBufferSize ) ) { + // We've strayed into the forbidden zone ... resync the read pointer. + handle->xrun[0] = true; + nextWritePointer = safeWritePointer + handle->dsPointerLeadTime[0] - bufferBytes; + if ( nextWritePointer >= dsBufferSize ) nextWritePointer -= dsBufferSize; + handle->bufferPointer[0] = nextWritePointer; + endWrite = nextWritePointer + bufferBytes; + } + + // Lock free space in the buffer + result = dsBuffer->Lock( nextWritePointer, bufferBytes, &buffer1, + &bufferSize1, &buffer2, &bufferSize2, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") locking buffer during playback!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + + // Copy our buffer into the DS buffer + CopyMemory( buffer1, buffer, bufferSize1 ); + if ( buffer2 != NULL ) CopyMemory( buffer2, buffer+bufferSize1, bufferSize2 ); + + // Update our buffer offset and unlock sound buffer + dsBuffer->Unlock( buffer1, bufferSize1, buffer2, bufferSize2 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") unlocking buffer during playback!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + nextWritePointer = ( nextWritePointer + bufferSize1 + bufferSize2 ) % dsBufferSize; + handle->bufferPointer[0] = nextWritePointer; + + if ( handle->drainCounter ) { + handle->drainCounter++; + goto unlock; + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + // Setup parameters. + if ( stream_.doConvertBuffer[1] ) { + buffer = stream_.deviceBuffer; + bufferBytes = stream_.bufferSize * stream_.nDeviceChannels[1]; + bufferBytes *= formatBytes( stream_.deviceFormat[1] ); + } + else { + buffer = stream_.userBuffer[1]; + bufferBytes = stream_.bufferSize * stream_.nUserChannels[1]; + bufferBytes *= formatBytes( stream_.userFormat ); + } + + LPDIRECTSOUNDCAPTUREBUFFER dsBuffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + long nextReadPointer = handle->bufferPointer[1]; + DWORD dsBufferSize = handle->dsBufferSize[1]; + + // Find out where the write and "safe read" pointers are. + result = dsBuffer->GetCurrentPosition( ¤tReadPointer, &safeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + + if ( safeReadPointer < (DWORD)nextReadPointer ) safeReadPointer += dsBufferSize; // unwrap offset + DWORD endRead = nextReadPointer + bufferBytes; + + // Handling depends on whether we are INPUT or DUPLEX. + // If we're in INPUT mode then waiting is a good thing. If we're in DUPLEX mode, + // then a wait here will drag the write pointers into the forbidden zone. + // + // In DUPLEX mode, rather than wait, we will back off the read pointer until + // it's in a safe position. This causes dropouts, but it seems to be the only + // practical way to sync up the read and write pointers reliably, given the + // the very complex relationship between phase and increment of the read and write + // pointers. + // + // In order to minimize audible dropouts in DUPLEX mode, we will + // provide a pre-roll period of 0.5 seconds in which we return + // zeros from the read buffer while the pointers sync up. + + if ( stream_.mode == DUPLEX ) { + if ( safeReadPointer < endRead ) { + if ( duplexPrerollBytes <= 0 ) { + // Pre-roll time over. Be more agressive. + int adjustment = endRead-safeReadPointer; + + handle->xrun[1] = true; + // Two cases: + // - large adjustments: we've probably run out of CPU cycles, so just resync exactly, + // and perform fine adjustments later. + // - small adjustments: back off by twice as much. + if ( adjustment >= 2*bufferBytes ) + nextReadPointer = safeReadPointer-2*bufferBytes; + else + nextReadPointer = safeReadPointer-bufferBytes-adjustment; + + if ( nextReadPointer < 0 ) nextReadPointer += dsBufferSize; + + } + else { + // In pre=roll time. Just do it. + nextReadPointer = safeReadPointer - bufferBytes; + while ( nextReadPointer < 0 ) nextReadPointer += dsBufferSize; + } + endRead = nextReadPointer + bufferBytes; + } + } + else { // mode == INPUT + while ( safeReadPointer < endRead ) { + // See comments for playback. + double millis = (endRead - safeReadPointer) * 1000.0; + millis /= ( formatBytes(stream_.deviceFormat[1]) * stream_.nDeviceChannels[1] * stream_.sampleRate); + if ( millis < 1.0 ) millis = 1.0; + Sleep( (DWORD) millis ); + + // Wake up and find out where we are now. + result = dsBuffer->GetCurrentPosition( ¤tReadPointer, &safeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + + if ( safeReadPointer < (DWORD)nextReadPointer ) safeReadPointer += dsBufferSize; // unwrap offset + } + } + + // Lock free space in the buffer + result = dsBuffer->Lock( nextReadPointer, bufferBytes, &buffer1, + &bufferSize1, &buffer2, &bufferSize2, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") locking capture buffer!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + + if ( duplexPrerollBytes <= 0 ) { + // Copy our buffer into the DS buffer + CopyMemory( buffer, buffer1, bufferSize1 ); + if ( buffer2 != NULL ) CopyMemory( buffer+bufferSize1, buffer2, bufferSize2 ); + } + else { + memset( buffer, 0, bufferSize1 ); + if ( buffer2 != NULL ) memset( buffer + bufferSize1, 0, bufferSize2 ); + duplexPrerollBytes -= bufferSize1 + bufferSize2; + } + + // Update our buffer offset and unlock sound buffer + nextReadPointer = ( nextReadPointer + bufferSize1 + bufferSize2 ) % dsBufferSize; + dsBuffer->Unlock( buffer1, bufferSize1, buffer2, bufferSize2 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") unlocking capture buffer!"; + errorText_ = errorStream_.str(); + error( RtError::SYSTEM_ERROR ); + } + handle->bufferPointer[1] = nextReadPointer; + + // No byte swapping necessary in DirectSound implementation. + + // If necessary, convert 8-bit data from unsigned to signed. + if ( stream_.deviceFormat[1] == RTAUDIO_SINT8 ) + for ( int j=0; jobject; + bool* isRunning = &info->isRunning; + + while ( *isRunning == true ) { + object->callbackEvent(); + } + + _endthreadex( 0 ); + return 0; +} + +#include "tchar.h" + +std::string convertTChar( LPCTSTR name ) +{ + std::string s; + +#if defined( UNICODE ) || defined( _UNICODE ) + // Yes, this conversion doesn't make sense for two-byte characters + // but RtAudio is currently written to return an std::string of + // one-byte chars for the device name. + for ( unsigned int i=0; iGetCaps( &caps ); + if ( hr == DS_OK ) { + if ( caps.dwChannels > 0 && caps.dwFormats > 0 ) + validDevice = true; + } + object->Release(); + } + else { + DSCAPS caps; + LPDIRECTSOUND object; + hr = DirectSoundCreate( lpguid, &object, NULL ); + if ( hr != DS_OK ) return TRUE; + + caps.dwSize = sizeof(caps); + hr = object->GetCaps( &caps ); + if ( hr == DS_OK ) { + if ( caps.dwFlags & DSCAPS_PRIMARYMONO || caps.dwFlags & DSCAPS_PRIMARYSTEREO ) + validDevice = true; + } + object->Release(); + } + + // If good device, then save its name and guid. + std::string name = convertTChar( description ); + if ( name == "Primary Sound Driver" || name == "Primary Sound Capture Driver" ) + name = "Default Device"; + if ( validDevice ) { + for ( unsigned int i=0; i +#include + + // A structure to hold various information related to the ALSA API + // implementation. +struct AlsaHandle { + snd_pcm_t *handles[2]; + bool synchronized; + bool xrun[2]; + pthread_cond_t runnable; + + AlsaHandle() + :synchronized(false) { xrun[0] = false; xrun[1] = false; } +}; + +extern "C" void *alsaCallbackHandler( void * ptr ); + +RtApiAlsa :: RtApiAlsa() +{ + // Nothing to do here. +} + +RtApiAlsa :: ~RtApiAlsa() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiAlsa :: getDeviceCount( void ) +{ + unsigned nDevices = 0; + int result, subdevice, card; + char name[64]; + snd_ctl_t *handle; + + // Count cards and devices + card = -1; + snd_card_next( &card ); + while ( card >= 0 ) { + sprintf( name, "hw:%d", card ); + result = snd_ctl_open( &handle, name, 0 ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceCount: control open, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + goto nextcard; + } + subdevice = -1; + while( 1 ) { + result = snd_ctl_pcm_next_device( handle, &subdevice ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceCount: control next device, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + break; + } + if ( subdevice < 0 ) + break; + nDevices++; + } + nextcard: + snd_ctl_close( handle ); + snd_card_next( &card ); + } + + return nDevices; +} + +RtAudio::DeviceInfo RtApiAlsa :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + unsigned nDevices = 0; + int result, subdevice, card; + char name[64]; + snd_ctl_t *chandle; + + // Count cards and devices + card = -1; + snd_card_next( &card ); + while ( card >= 0 ) { + sprintf( name, "hw:%d", card ); + result = snd_ctl_open( &chandle, name, SND_CTL_NONBLOCK ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: control open, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + goto nextcard; + } + subdevice = -1; + while( 1 ) { + result = snd_ctl_pcm_next_device( chandle, &subdevice ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: control next device, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + break; + } + if ( subdevice < 0 ) break; + if ( nDevices == device ) { + sprintf( name, "hw:%d,%d", card, subdevice ); + goto foundDevice; + } + nDevices++; + } + nextcard: + snd_ctl_close( chandle ); + snd_card_next( &card ); + } + + if ( nDevices == 0 ) { + errorText_ = "RtApiAlsa::getDeviceInfo: no devices found!"; + error( RtError::INVALID_USE ); + } + + if ( device >= nDevices ) { + errorText_ = "RtApiAlsa::getDeviceInfo: device ID is invalid!"; + error( RtError::INVALID_USE ); + } + + foundDevice: + + // If a stream is already open, we cannot probe the stream devices. + // Thus, use the saved results. + if ( stream_.state != STREAM_CLOSED && + ( stream_.device[0] == device || stream_.device[1] == device ) ) { + if ( device >= devices_.size() ) { + errorText_ = "RtApiAlsa::getDeviceInfo: device ID was not present before stream was opened."; + error( RtError::WARNING ); + return info; + } + return devices_[ device ]; + } + + int openMode = SND_PCM_ASYNC; + snd_pcm_stream_t stream; + snd_pcm_info_t *pcminfo; + snd_pcm_info_alloca( &pcminfo ); + snd_pcm_t *phandle; + snd_pcm_hw_params_t *params; + snd_pcm_hw_params_alloca( ¶ms ); + + // First try for playback + stream = SND_PCM_STREAM_PLAYBACK; + snd_pcm_info_set_device( pcminfo, subdevice ); + snd_pcm_info_set_subdevice( pcminfo, 0 ); + snd_pcm_info_set_stream( pcminfo, stream ); + + result = snd_ctl_pcm_info( chandle, pcminfo ); + if ( result < 0 ) { + // Device probably doesn't support playback. + goto captureProbe; + } + + result = snd_pcm_open( &phandle, name, stream, openMode | SND_PCM_NONBLOCK ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_open error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + goto captureProbe; + } + + // The device is open ... fill the parameter structure. + result = snd_pcm_hw_params_any( phandle, params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_hw_params error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + goto captureProbe; + } + + // Get output channel information. + unsigned int value; + result = snd_pcm_hw_params_get_channels_max( params, &value ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: error getting device (" << name << ") output channels, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + goto captureProbe; + } + info.outputChannels = value; + snd_pcm_close( phandle ); + + captureProbe: + // Now try for capture + stream = SND_PCM_STREAM_CAPTURE; + snd_pcm_info_set_stream( pcminfo, stream ); + + result = snd_ctl_pcm_info( chandle, pcminfo ); + snd_ctl_close( chandle ); + if ( result < 0 ) { + // Device probably doesn't support capture. + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + + result = snd_pcm_open( &phandle, name, stream, openMode | SND_PCM_NONBLOCK); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_open error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + + // The device is open ... fill the parameter structure. + result = snd_pcm_hw_params_any( phandle, params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_hw_params error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + + result = snd_pcm_hw_params_get_channels_max( params, &value ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: error getting device (" << name << ") input channels, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + info.inputChannels = value; + snd_pcm_close( phandle ); + + // If device opens for both playback and capture, we determine the channels. + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // ALSA doesn't provide default devices so we'll use the first available one. + if ( device == 0 && info.outputChannels > 0 ) + info.isDefaultOutput = true; + if ( device == 0 && info.inputChannels > 0 ) + info.isDefaultInput = true; + + probeParameters: + // At this point, we just need to figure out the supported data + // formats and sample rates. We'll proceed by opening the device in + // the direction with the maximum number of channels, or playback if + // they are equal. This might limit our sample rate options, but so + // be it. + + if ( info.outputChannels >= info.inputChannels ) + stream = SND_PCM_STREAM_PLAYBACK; + else + stream = SND_PCM_STREAM_CAPTURE; + snd_pcm_info_set_stream( pcminfo, stream ); + + result = snd_pcm_open( &phandle, name, stream, openMode | SND_PCM_NONBLOCK); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_open error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // The device is open ... fill the parameter structure. + result = snd_pcm_hw_params_any( phandle, params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_hw_params error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Test our discrete set of sample rate values. + info.sampleRates.clear(); + for ( unsigned int i=0; i= 0 ) + sprintf( name, "hw:%s,%d", cardname, subdevice ); + info.name = name; + + // That's all ... close the device and return + snd_pcm_close( phandle ); + info.probed = true; + return info; +} + +void RtApiAlsa :: saveDeviceInfo( void ) +{ + devices_.clear(); + + unsigned int nDevices = getDeviceCount(); + devices_.resize( nDevices ); + for ( unsigned int i=0; i= 0 ) { + sprintf( name, "hw:%d", card ); + result = snd_ctl_open( &chandle, name, SND_CTL_NONBLOCK ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::probeDeviceOpen: control open, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + subdevice = -1; + while( 1 ) { + result = snd_ctl_pcm_next_device( chandle, &subdevice ); + if ( result < 0 ) break; + if ( subdevice < 0 ) break; + if ( nDevices == device ) { + sprintf( name, "hw:%d,%d", card, subdevice ); + snd_ctl_close( chandle ); + goto foundDevice; + } + nDevices++; + } + snd_ctl_close( chandle ); + snd_card_next( &card ); + } + + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiAlsa::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiAlsa::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + foundDevice: + + // The getDeviceInfo() function will not work for a device that is + // already open. Thus, we'll probe the system before opening a + // stream and save the results for use by getDeviceInfo(). + if ( mode == OUTPUT || ( mode == INPUT && stream_.mode != OUTPUT ) ) // only do once + this->saveDeviceInfo(); + + snd_pcm_stream_t stream; + if ( mode == OUTPUT ) + stream = SND_PCM_STREAM_PLAYBACK; + else + stream = SND_PCM_STREAM_CAPTURE; + + snd_pcm_t *phandle; + int openMode = SND_PCM_ASYNC; + result = snd_pcm_open( &phandle, name, stream, openMode ); + if ( result < 0 ) { + if ( mode == OUTPUT ) + errorStream_ << "RtApiAlsa::probeDeviceOpen: pcm device (" << name << ") won't open for output."; + else + errorStream_ << "RtApiAlsa::probeDeviceOpen: pcm device (" << name << ") won't open for input."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Fill the parameter structure. + snd_pcm_hw_params_t *hw_params; + snd_pcm_hw_params_alloca( &hw_params ); + result = snd_pcm_hw_params_any( phandle, hw_params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error getting pcm device (" << name << ") parameters, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + +#if defined(__RTAUDIO_DEBUG__) + fprintf( stderr, "\nRtApiAlsa: dump hardware params just after device open:\n\n" ); + snd_pcm_hw_params_dump( hw_params, out ); +#endif + + // Set access ... check user preference. + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) { + stream_.userInterleaved = false; + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_NONINTERLEAVED ); + if ( result < 0 ) { + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED ); + stream_.deviceInterleaved[mode] = true; + } + else + stream_.deviceInterleaved[mode] = false; + } + else { + stream_.userInterleaved = true; + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED ); + if ( result < 0 ) { + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_NONINTERLEAVED ); + stream_.deviceInterleaved[mode] = false; + } + else + stream_.deviceInterleaved[mode] = true; + } + + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting pcm device (" << name << ") access, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine how to set the device format. + stream_.userFormat = format; + snd_pcm_format_t deviceFormat = SND_PCM_FORMAT_UNKNOWN; + + if ( format == RTAUDIO_SINT8 ) + deviceFormat = SND_PCM_FORMAT_S8; + else if ( format == RTAUDIO_SINT16 ) + deviceFormat = SND_PCM_FORMAT_S16; + else if ( format == RTAUDIO_SINT24 ) + deviceFormat = SND_PCM_FORMAT_S24; + else if ( format == RTAUDIO_SINT32 ) + deviceFormat = SND_PCM_FORMAT_S32; + else if ( format == RTAUDIO_FLOAT32 ) + deviceFormat = SND_PCM_FORMAT_FLOAT; + else if ( format == RTAUDIO_FLOAT64 ) + deviceFormat = SND_PCM_FORMAT_FLOAT64; + + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat) == 0) { + stream_.deviceFormat[mode] = format; + goto setFormat; + } + + // The user requested format is not natively supported by the device. + deviceFormat = SND_PCM_FORMAT_FLOAT64; + if ( snd_pcm_hw_params_test_format( phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT64; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_FLOAT; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S32; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S24; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S16; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S8; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + goto setFormat; + } + + // If we get here, no supported format was found. + errorStream_ << "RtApiAlsa::probeDeviceOpen: pcm device " << device << " data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + return FAILURE; + + setFormat: + result = snd_pcm_hw_params_set_format( phandle, hw_params, deviceFormat ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting pcm device (" << name << ") data format, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine whether byte-swaping is necessary. + stream_.doByteSwap[mode] = false; + if ( deviceFormat != SND_PCM_FORMAT_S8 ) { + result = snd_pcm_format_cpu_endian( deviceFormat ); + if ( result == 0 ) + stream_.doByteSwap[mode] = true; + else if (result < 0) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error getting pcm device (" << name << ") endian-ness, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Set the sample rate. + result = snd_pcm_hw_params_set_rate_near( phandle, hw_params, (unsigned int*) &sampleRate, 0 ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting sample rate on device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine the number of channels for this device. We support a possible + // minimum device channel number > than the value requested by the user. + stream_.nUserChannels[mode] = channels; + unsigned int value; + result = snd_pcm_hw_params_get_channels_max( hw_params, &value ); + unsigned int deviceChannels = value; + if ( result < 0 || deviceChannels < channels + firstChannel ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: requested channel parameters not supported by device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + result = snd_pcm_hw_params_get_channels_min( hw_params, &value ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error getting minimum channels for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + deviceChannels = value; + if ( deviceChannels < channels + firstChannel ) deviceChannels = channels + firstChannel; + stream_.nDeviceChannels[mode] = deviceChannels; + + // Set the device channels. + result = snd_pcm_hw_params_set_channels( phandle, hw_params, deviceChannels ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting channels for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the buffer number, which in ALSA is referred to as the "period". + int totalSize, dir = 0; + unsigned int periods = 0; + if ( options ) periods = options->numberOfBuffers; + totalSize = *bufferSize * periods; + + // Set the buffer (or period) size. + snd_pcm_uframes_t periodSize = *bufferSize; + result = snd_pcm_hw_params_set_period_size_near( phandle, hw_params, &periodSize, &dir ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting period size for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + *bufferSize = periodSize; + + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) periods = 2; + else periods = totalSize / *bufferSize; + // Even though the hardware might allow 1 buffer, it won't work reliably. + if ( periods < 2 ) periods = 2; + result = snd_pcm_hw_params_set_periods_near( phandle, hw_params, &periods, &dir ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting periods for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // If attempting to setup a duplex stream, the bufferSize parameter + // MUST be the same in both directions! + if ( stream_.mode == OUTPUT && mode == INPUT && *bufferSize != stream_.bufferSize ) { + errorStream_ << "RtApiAlsa::probeDeviceOpen: system error setting buffer size for duplex stream on device (" << name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + stream_.bufferSize = *bufferSize; + + // Install the hardware configuration + result = snd_pcm_hw_params( phandle, hw_params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error installing hardware configuration on device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + +#if defined(__RTAUDIO_DEBUG__) + fprintf(stderr, "\nRtApiAlsa: dump hardware params after installation:\n\n"); + snd_pcm_hw_params_dump( hw_params, out ); +#endif + + // Set the software configuration to fill buffers with zeros and prevent device stopping on xruns. + snd_pcm_sw_params_t *sw_params = NULL; + snd_pcm_sw_params_alloca( &sw_params ); + snd_pcm_sw_params_current( phandle, sw_params ); + snd_pcm_sw_params_set_start_threshold( phandle, sw_params, *bufferSize ); + snd_pcm_sw_params_set_stop_threshold( phandle, sw_params, ULONG_MAX ); + snd_pcm_sw_params_set_silence_threshold( phandle, sw_params, 0 ); + + // The following two settings were suggested by Theo Veenker + //snd_pcm_sw_params_set_avail_min( phandle, sw_params, *bufferSize ); + //snd_pcm_sw_params_set_xfer_align( phandle, sw_params, 1 ); + + // here are two options for a fix + //snd_pcm_sw_params_set_silence_size( phandle, sw_params, ULONG_MAX ); + snd_pcm_uframes_t val; + snd_pcm_sw_params_get_boundary( sw_params, &val ); + snd_pcm_sw_params_set_silence_size( phandle, sw_params, val ); + + result = snd_pcm_sw_params( phandle, sw_params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error installing software configuration on device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + +#if defined(__RTAUDIO_DEBUG__) + fprintf(stderr, "\nRtApiAlsa: dump software params after installation:\n\n"); + snd_pcm_sw_params_dump( sw_params, out ); +#endif + + // Set flags for buffer conversion + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate the ApiHandle if necessary and then save. + AlsaHandle *apiInfo = 0; + if ( stream_.apiHandle == 0 ) { + try { + apiInfo = (AlsaHandle *) new AlsaHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error allocating AlsaHandle memory."; + goto error; + } + + if ( pthread_cond_init( &apiInfo->runnable, NULL ) ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + + stream_.apiHandle = (void *) apiInfo; + apiInfo->handles[0] = 0; + apiInfo->handles[1] = 0; + } + else { + apiInfo = (AlsaHandle *) stream_.apiHandle; + } + apiInfo->handles[mode] = phandle; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.sampleRate = sampleRate; + stream_.nBuffers = periods; + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + // Setup thread if necessary. + if ( stream_.mode == OUTPUT && mode == INPUT ) { + // We had already set up an output stream. + stream_.mode = DUPLEX; + // Link the streams if possible. + apiInfo->synchronized = false; + if ( snd_pcm_link( apiInfo->handles[0], apiInfo->handles[1] ) == 0 ) + apiInfo->synchronized = true; + else { + errorText_ = "RtApiAlsa::probeDeviceOpen: unable to synchronize input and output devices."; + error( RtError::WARNING ); + } + } + else { + stream_.mode = mode; + + // Setup callback thread. + stream_.callbackInfo.object = (void *) this; + + // Set the thread attributes for joinable and realtime scheduling + // priority (optional). The higher priority will only take affect + // if the program is run as root or suid. Note, under Linux + // processes with CAP_SYS_NICE privilege, a user can change + // scheduling policy and priority (thus need not be root). See + // POSIX "capabilities". + pthread_attr_t attr; + pthread_attr_init( &attr ); + pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE ); +#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread) + if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) { + struct sched_param param; + int priority = options->priority; + int min = sched_get_priority_min( SCHED_RR ); + int max = sched_get_priority_max( SCHED_RR ); + if ( priority < min ) priority = min; + else if ( priority > max ) priority = max; + param.sched_priority = priority; + pthread_attr_setschedparam( &attr, ¶m ); + pthread_attr_setschedpolicy( &attr, SCHED_RR ); + } + else + pthread_attr_setschedpolicy( &attr, SCHED_OTHER ); +#else + pthread_attr_setschedpolicy( &attr, SCHED_OTHER ); +#endif + + stream_.callbackInfo.isRunning = true; + result = pthread_create( &stream_.callbackInfo.thread, &attr, alsaCallbackHandler, &stream_.callbackInfo ); + pthread_attr_destroy( &attr ); + if ( result ) { + stream_.callbackInfo.isRunning = false; + errorText_ = "RtApiAlsa::error creating callback thread!"; + goto error; + } + } + + return SUCCESS; + + error: + if ( apiInfo ) { + pthread_cond_destroy( &apiInfo->runnable ); + if ( apiInfo->handles[0] ) snd_pcm_close( apiInfo->handles[0] ); + if ( apiInfo->handles[1] ) snd_pcm_close( apiInfo->handles[1] ); + delete apiInfo; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiAlsa :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAlsa::closeStream(): no open stream to close!"; + error( RtError::WARNING ); + return; + } + + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + stream_.callbackInfo.isRunning = false; + MUTEX_LOCK( &stream_.mutex ); + if ( stream_.state == STREAM_STOPPED ) + pthread_cond_signal( &apiInfo->runnable ); + MUTEX_UNLOCK( &stream_.mutex ); + pthread_join( stream_.callbackInfo.thread, NULL ); + + if ( stream_.state == STREAM_RUNNING ) { + stream_.state = STREAM_STOPPED; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) + snd_pcm_drop( apiInfo->handles[0] ); + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) + snd_pcm_drop( apiInfo->handles[1] ); + } + + if ( apiInfo ) { + pthread_cond_destroy( &apiInfo->runnable ); + if ( apiInfo->handles[0] ) snd_pcm_close( apiInfo->handles[0] ); + if ( apiInfo->handles[1] ) snd_pcm_close( apiInfo->handles[1] ); + delete apiInfo; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiAlsa :: startStream() +{ + // This method calls snd_pcm_prepare if the device isn't already in that state. + + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiAlsa::startStream(): the stream is already running!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + int result = 0; + snd_pcm_state_t state; + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + snd_pcm_t **handle = (snd_pcm_t **) apiInfo->handles; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + state = snd_pcm_state( handle[0] ); + if ( state != SND_PCM_STATE_PREPARED ) { + result = snd_pcm_prepare( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::startStream: error preparing output pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + } + + if ( ( stream_.mode == INPUT || stream_.mode == DUPLEX ) && !apiInfo->synchronized ) { + state = snd_pcm_state( handle[1] ); + if ( state != SND_PCM_STATE_PREPARED ) { + result = snd_pcm_prepare( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::startStream: error preparing input pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + } + + stream_.state = STREAM_RUNNING; + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + pthread_cond_signal( &apiInfo->runnable ); + + if ( result >= 0 ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiAlsa :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAlsa::stopStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + int result = 0; + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + snd_pcm_t **handle = (snd_pcm_t **) apiInfo->handles; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( apiInfo->synchronized ) + result = snd_pcm_drop( handle[0] ); + else + result = snd_pcm_drain( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::stopStream: error draining output pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( ( stream_.mode == INPUT || stream_.mode == DUPLEX ) && !apiInfo->synchronized ) { + result = snd_pcm_drop( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::stopStream: error stopping input pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result >= 0 ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiAlsa :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAlsa::abortStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + int result = 0; + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + snd_pcm_t **handle = (snd_pcm_t **) apiInfo->handles; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + result = snd_pcm_drop( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::abortStream: error aborting output pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( ( stream_.mode == INPUT || stream_.mode == DUPLEX ) && !apiInfo->synchronized ) { + result = snd_pcm_drop( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::abortStream: error aborting input pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result >= 0 ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiAlsa :: callbackEvent() +{ + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_LOCK( &stream_.mutex ); + pthread_cond_wait( &apiInfo->runnable, &stream_.mutex ); + if ( stream_.state != STREAM_RUNNING ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + MUTEX_UNLOCK( &stream_.mutex ); + } + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAlsa::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtError::WARNING ); + return; + } + + int doStopStream = 0; + RtAudioCallback callback = (RtAudioCallback) stream_.callbackInfo.callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && apiInfo->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + apiInfo->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && apiInfo->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + apiInfo->xrun[1] = false; + } + doStopStream = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, stream_.callbackInfo.userData ); + + if ( doStopStream == 2 ) { + abortStream(); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) goto unlock; + + int result; + char *buffer; + int channels; + snd_pcm_t **handle; + snd_pcm_sframes_t frames; + RtAudioFormat format; + handle = (snd_pcm_t **) apiInfo->handles; + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + // Setup parameters. + if ( stream_.doConvertBuffer[1] ) { + buffer = stream_.deviceBuffer; + channels = stream_.nDeviceChannels[1]; + format = stream_.deviceFormat[1]; + } + else { + buffer = stream_.userBuffer[1]; + channels = stream_.nUserChannels[1]; + format = stream_.userFormat; + } + + // Read samples from device in interleaved/non-interleaved format. + if ( stream_.deviceInterleaved[1] ) + result = snd_pcm_readi( handle[1], buffer, stream_.bufferSize ); + else { + void *bufs[channels]; + size_t offset = stream_.bufferSize * formatBytes( format ); + for ( int i=0; ixrun[1] = true; + result = snd_pcm_prepare( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::callbackEvent: error preparing device after overrun, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: error, current state is " << snd_pcm_state_name( state ) << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: audio read error, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + error( RtError::WARNING ); + goto tryOutput; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( buffer, stream_.bufferSize * channels, format ); + + // Do buffer conversion if necessary. + if ( stream_.doConvertBuffer[1] ) + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + + // Check stream latency + result = snd_pcm_delay( handle[1], &frames ); + if ( result == 0 && frames > 0 ) stream_.latency[1] = frames; + } + + tryOutput: + + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + // Setup parameters and do buffer conversion if necessary. + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + convertBuffer( buffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + channels = stream_.nDeviceChannels[0]; + format = stream_.deviceFormat[0]; + } + else { + buffer = stream_.userBuffer[0]; + channels = stream_.nUserChannels[0]; + format = stream_.userFormat; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[0] ) + byteSwapBuffer(buffer, stream_.bufferSize * channels, format); + + // Write samples to device in interleaved/non-interleaved format. + if ( stream_.deviceInterleaved[0] ) + result = snd_pcm_writei( handle[0], buffer, stream_.bufferSize ); + else { + void *bufs[channels]; + size_t offset = stream_.bufferSize * formatBytes( format ); + for ( int i=0; ixrun[0] = true; + result = snd_pcm_prepare( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::callbackEvent: error preparing device after underrun, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: error, current state is " << snd_pcm_state_name( state ) << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: audio write error, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + error( RtError::WARNING ); + goto unlock; + } + + // Check stream latency + result = snd_pcm_delay( handle[0], &frames ); + if ( result == 0 && frames > 0 ) stream_.latency[0] = frames; + } + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + RtApi::tickStreamTime(); + if ( doStopStream == 1 ) this->stopStream(); +} + +extern "C" void *alsaCallbackHandler( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiAlsa *object = (RtApiAlsa *) info->object; + bool *isRunning = &info->isRunning; + + while ( *isRunning == true ) { + pthread_testcancel(); + object->callbackEvent(); + } + + pthread_exit( NULL ); +} + +//******************** End of __LINUX_ALSA__ *********************// +#endif + + +#if defined(__LINUX_OSS__) + +#include +#include +#include +#include +#include "soundcard.h" +#include +#include + +extern "C" void *ossCallbackHandler(void * ptr); + +// A structure to hold various information related to the OSS API +// implementation. +struct OssHandle { + int id[2]; // device ids + bool xrun[2]; + bool triggered; + pthread_cond_t runnable; + + OssHandle() + :triggered(false) { id[0] = 0; id[1] = 0; xrun[0] = false; xrun[1] = false; } +}; + +RtApiOss :: RtApiOss() +{ + // Nothing to do here. +} + +RtApiOss :: ~RtApiOss() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiOss :: getDeviceCount( void ) +{ + int mixerfd = open( "/dev/mixer", O_RDWR, 0 ); + if ( mixerfd == -1 ) { + errorText_ = "RtApiOss::getDeviceCount: error opening '/dev/mixer'."; + error( RtError::WARNING ); + return 0; + } + + oss_sysinfo sysinfo; + if ( ioctl( mixerfd, SNDCTL_SYSINFO, &sysinfo ) == -1 ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceCount: error getting sysinfo, OSS version >= 4.0 is required."; + error( RtError::WARNING ); + return 0; + } + + close( mixerfd ); + return sysinfo.numaudios; +} + +RtAudio::DeviceInfo RtApiOss :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + int mixerfd = open( "/dev/mixer", O_RDWR, 0 ); + if ( mixerfd == -1 ) { + errorText_ = "RtApiOss::getDeviceInfo: error opening '/dev/mixer'."; + error( RtError::WARNING ); + return info; + } + + oss_sysinfo sysinfo; + int result = ioctl( mixerfd, SNDCTL_SYSINFO, &sysinfo ); + if ( result == -1 ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceInfo: error getting sysinfo, OSS version >= 4.0 is required."; + error( RtError::WARNING ); + return info; + } + + unsigned nDevices = sysinfo.numaudios; + if ( nDevices == 0 ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceInfo: no devices found!"; + error( RtError::INVALID_USE ); + } + + if ( device >= nDevices ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceInfo: device ID is invalid!"; + error( RtError::INVALID_USE ); + } + + oss_audioinfo ainfo; + ainfo.dev = device; + result = ioctl( mixerfd, SNDCTL_AUDIOINFO, &ainfo ); + close( mixerfd ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::getDeviceInfo: error getting device (" << ainfo.name << ") info."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Probe channels + if ( ainfo.caps & PCM_CAP_OUTPUT ) info.outputChannels = ainfo.max_channels; + if ( ainfo.caps & PCM_CAP_INPUT ) info.inputChannels = ainfo.max_channels; + if ( ainfo.caps & PCM_CAP_DUPLEX ) { + if ( info.outputChannels > 0 && info.inputChannels > 0 && ainfo.caps & PCM_CAP_DUPLEX ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + } + + // Probe data formats ... do for input + unsigned long mask = ainfo.iformats; + if ( mask & AFMT_S16_LE || mask & AFMT_S16_BE ) + info.nativeFormats |= RTAUDIO_SINT16; + if ( mask & AFMT_S8 ) + info.nativeFormats |= RTAUDIO_SINT8; + if ( mask & AFMT_S32_LE || mask & AFMT_S32_BE ) + info.nativeFormats |= RTAUDIO_SINT32; + if ( mask & AFMT_FLOAT ) + info.nativeFormats |= RTAUDIO_FLOAT32; + if ( mask & AFMT_S24_LE || mask & AFMT_S24_BE ) + info.nativeFormats |= RTAUDIO_SINT24; + + // Check that we have at least one supported format + if ( info.nativeFormats == 0 ) { + errorStream_ << "RtApiOss::getDeviceInfo: device (" << ainfo.name << ") data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + return info; + } + + // Probe the supported sample rates. + info.sampleRates.clear(); + if ( ainfo.nrates ) { + for ( unsigned int i=0; i= (int) SAMPLE_RATES[k] ) + info.sampleRates.push_back( SAMPLE_RATES[k] ); + } + } + + if ( info.sampleRates.size() == 0 ) { + errorStream_ << "RtApiOss::getDeviceInfo: no supported sample rates found for device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + error( RtError::WARNING ); + } + else { + info.probed = true; + info.name = ainfo.name; + } + + return info; +} + + +bool RtApiOss :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + int mixerfd = open( "/dev/mixer", O_RDWR, 0 ); + if ( mixerfd == -1 ) { + errorText_ = "RtApiOss::probeDeviceOpen: error opening '/dev/mixer'."; + return FAILURE; + } + + oss_sysinfo sysinfo; + int result = ioctl( mixerfd, SNDCTL_SYSINFO, &sysinfo ); + if ( result == -1 ) { + close( mixerfd ); + errorText_ = "RtApiOss::probeDeviceOpen: error getting sysinfo, OSS version >= 4.0 is required."; + return FAILURE; + } + + unsigned nDevices = sysinfo.numaudios; + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + close( mixerfd ); + errorText_ = "RtApiOss::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + close( mixerfd ); + errorText_ = "RtApiOss::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + oss_audioinfo ainfo; + ainfo.dev = device; + result = ioctl( mixerfd, SNDCTL_AUDIOINFO, &ainfo ); + close( mixerfd ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::getDeviceInfo: error getting device (" << ainfo.name << ") info."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check if device supports input or output + if ( ( mode == OUTPUT && !( ainfo.caps & PCM_CAP_OUTPUT ) ) || + ( mode == INPUT && !( ainfo.caps & PCM_CAP_INPUT ) ) ) { + if ( mode == OUTPUT ) + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support output."; + else + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support input."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + int flags = 0; + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( mode == OUTPUT ) + flags |= O_WRONLY; + else { // mode == INPUT + if (stream_.mode == OUTPUT && stream_.device[0] == device) { + // We just set the same device for playback ... close and reopen for duplex (OSS only). + close( handle->id[0] ); + handle->id[0] = 0; + if ( !( ainfo.caps & PCM_CAP_DUPLEX ) ) { + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support duplex mode."; + errorText_ = errorStream_.str(); + return FAILURE; + } + // Check that the number previously set channels is the same. + if ( stream_.nUserChannels[0] != channels ) { + errorStream_ << "RtApiOss::probeDeviceOpen: input/output channels must be equal for OSS duplex device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + flags |= O_RDWR; + } + else + flags |= O_RDONLY; + } + + // Set exclusive access if specified. + if ( options && options->flags & RTAUDIO_HOG_DEVICE ) flags |= O_EXCL; + + // Try to open the device. + int fd; + fd = open( ainfo.devnode, flags, 0 ); + if ( fd == -1 ) { + if ( errno == EBUSY ) + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") is busy."; + else + errorStream_ << "RtApiOss::probeDeviceOpen: error opening device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // For duplex operation, specifically set this mode (this doesn't seem to work). + /* + if ( flags | O_RDWR ) { + result = ioctl( fd, SNDCTL_DSP_SETDUPLEX, NULL ); + if ( result == -1) { + errorStream_ << "RtApiOss::probeDeviceOpen: error setting duplex mode for device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + */ + + // Check the device channel support. + stream_.nUserChannels[mode] = channels; + if ( ainfo.max_channels < (int)(channels + firstChannel) ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: the device (" << ainfo.name << ") does not support requested channel parameters."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the number of channels. + int deviceChannels = channels + firstChannel; + result = ioctl( fd, SNDCTL_DSP_CHANNELS, &deviceChannels ); + if ( result == -1 || deviceChannels < (int)(channels + firstChannel) ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting channel parameters on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.nDeviceChannels[mode] = deviceChannels; + + // Get the data format mask + int mask; + result = ioctl( fd, SNDCTL_DSP_GETFMTS, &mask ); + if ( result == -1 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error getting device (" << ainfo.name << ") data formats."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine how to set the device format. + stream_.userFormat = format; + int deviceFormat = -1; + stream_.doByteSwap[mode] = false; + if ( format == RTAUDIO_SINT8 ) { + if ( mask & AFMT_S8 ) { + deviceFormat = AFMT_S8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + } + else if ( format == RTAUDIO_SINT16 ) { + if ( mask & AFMT_S16_NE ) { + deviceFormat = AFMT_S16_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + else if ( mask & AFMT_S16_OE ) { + deviceFormat = AFMT_S16_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + stream_.doByteSwap[mode] = true; + } + } + else if ( format == RTAUDIO_SINT24 ) { + if ( mask & AFMT_S24_NE ) { + deviceFormat = AFMT_S24_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + } + else if ( mask & AFMT_S24_OE ) { + deviceFormat = AFMT_S24_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + stream_.doByteSwap[mode] = true; + } + } + else if ( format == RTAUDIO_SINT32 ) { + if ( mask & AFMT_S32_NE ) { + deviceFormat = AFMT_S32_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + } + else if ( mask & AFMT_S32_OE ) { + deviceFormat = AFMT_S32_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + stream_.doByteSwap[mode] = true; + } + } + + if ( deviceFormat == -1 ) { + // The user requested format is not natively supported by the device. + if ( mask & AFMT_S16_NE ) { + deviceFormat = AFMT_S16_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + else if ( mask & AFMT_S32_NE ) { + deviceFormat = AFMT_S32_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + } + else if ( mask & AFMT_S24_NE ) { + deviceFormat = AFMT_S24_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + } + else if ( mask & AFMT_S16_OE ) { + deviceFormat = AFMT_S16_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + stream_.doByteSwap[mode] = true; + } + else if ( mask & AFMT_S32_OE ) { + deviceFormat = AFMT_S32_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + stream_.doByteSwap[mode] = true; + } + else if ( mask & AFMT_S24_OE ) { + deviceFormat = AFMT_S24_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + stream_.doByteSwap[mode] = true; + } + else if ( mask & AFMT_S8) { + deviceFormat = AFMT_S8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + } + + if ( stream_.deviceFormat[mode] == 0 ) { + // This really shouldn't happen ... + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the data format. + int temp = deviceFormat; + result = ioctl( fd, SNDCTL_DSP_SETFMT, &deviceFormat ); + if ( result == -1 || deviceFormat != temp ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting data format on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Attempt to set the buffer size. According to OSS, the minimum + // number of buffers is two. The supposed minimum buffer size is 16 + // bytes, so that will be our lower bound. The argument to this + // call is in the form 0xMMMMSSSS (hex), where the buffer size (in + // bytes) is given as 2^SSSS and the number of buffers as 2^MMMM. + // We'll check the actual value used near the end of the setup + // procedure. + int ossBufferBytes = *bufferSize * formatBytes( stream_.deviceFormat[mode] ) * deviceChannels; + if ( ossBufferBytes < 16 ) ossBufferBytes = 16; + int buffers = 0; + if ( options ) buffers = options->numberOfBuffers; + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) buffers = 2; + if ( buffers < 2 ) buffers = 3; + temp = ((int) buffers << 16) + (int)( log10( (double)ossBufferBytes ) / log10( 2.0 ) ); + result = ioctl( fd, SNDCTL_DSP_SETFRAGMENT, &temp ); + if ( result == -1 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting buffer size on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.nBuffers = buffers; + + // Save buffer size (in sample frames). + *bufferSize = ossBufferBytes / ( formatBytes(stream_.deviceFormat[mode]) * deviceChannels ); + stream_.bufferSize = *bufferSize; + + // Set the sample rate. + int srate = sampleRate; + result = ioctl( fd, SNDCTL_DSP_SPEED, &srate ); + if ( result == -1 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting sample rate (" << sampleRate << ") on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Verify the sample rate setup worked. + if ( abs( srate - sampleRate ) > 100 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support sample rate (" << sampleRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.sampleRate = sampleRate; + + if ( mode == INPUT && stream_.mode == OUTPUT && stream_.device[0] == device) { + // We're doing duplex setup here. + stream_.deviceFormat[0] = stream_.deviceFormat[1]; + stream_.nDeviceChannels[0] = deviceChannels; + } + + // Set interleaving parameters. + stream_.userInterleaved = true; + stream_.deviceInterleaved[mode] = true; + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) + stream_.userInterleaved = false; + + // Set flags for buffer conversion + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate the stream handles if necessary and then save. + if ( stream_.apiHandle == 0 ) { + try { + handle = new OssHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiOss::probeDeviceOpen: error allocating OssHandle memory."; + goto error; + } + + if ( pthread_cond_init( &handle->runnable, NULL ) ) { + errorText_ = "RtApiOss::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + + stream_.apiHandle = (void *) handle; + } + else { + handle = (OssHandle *) stream_.apiHandle; + } + handle->id[mode] = fd; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiOss::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiOss::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + // Setup thread if necessary. + if ( stream_.mode == OUTPUT && mode == INPUT ) { + // We had already set up an output stream. + stream_.mode = DUPLEX; + if ( stream_.device[0] == device ) handle->id[0] = fd; + } + else { + stream_.mode = mode; + + // Setup callback thread. + stream_.callbackInfo.object = (void *) this; + + // Set the thread attributes for joinable and realtime scheduling + // priority. The higher priority will only take affect if the + // program is run as root or suid. + pthread_attr_t attr; + pthread_attr_init( &attr ); + pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE ); +#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread) + if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) { + struct sched_param param; + int priority = options->priority; + int min = sched_get_priority_min( SCHED_RR ); + int max = sched_get_priority_max( SCHED_RR ); + if ( priority < min ) priority = min; + else if ( priority > max ) priority = max; + param.sched_priority = priority; + pthread_attr_setschedparam( &attr, ¶m ); + pthread_attr_setschedpolicy( &attr, SCHED_RR ); + } + else + pthread_attr_setschedpolicy( &attr, SCHED_OTHER ); +#else + pthread_attr_setschedpolicy( &attr, SCHED_OTHER ); +#endif + + stream_.callbackInfo.isRunning = true; + result = pthread_create( &stream_.callbackInfo.thread, &attr, ossCallbackHandler, &stream_.callbackInfo ); + pthread_attr_destroy( &attr ); + if ( result ) { + stream_.callbackInfo.isRunning = false; + errorText_ = "RtApiOss::error creating callback thread!"; + goto error; + } + } + + return SUCCESS; + + error: + if ( handle ) { + pthread_cond_destroy( &handle->runnable ); + if ( handle->id[0] ) close( handle->id[0] ); + if ( handle->id[1] ) close( handle->id[1] ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiOss :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiOss::closeStream(): no open stream to close!"; + error( RtError::WARNING ); + return; + } + + OssHandle *handle = (OssHandle *) stream_.apiHandle; + stream_.callbackInfo.isRunning = false; + MUTEX_LOCK( &stream_.mutex ); + if ( stream_.state == STREAM_STOPPED ) + pthread_cond_signal( &handle->runnable ); + MUTEX_UNLOCK( &stream_.mutex ); + pthread_join( stream_.callbackInfo.thread, NULL ); + + if ( stream_.state == STREAM_RUNNING ) { + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) + ioctl( handle->id[0], SNDCTL_DSP_HALT, 0 ); + else + ioctl( handle->id[1], SNDCTL_DSP_HALT, 0 ); + stream_.state = STREAM_STOPPED; + } + + if ( handle ) { + pthread_cond_destroy( &handle->runnable ); + if ( handle->id[0] ) close( handle->id[0] ); + if ( handle->id[1] ) close( handle->id[1] ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiOss :: startStream() +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiOss::startStream(): the stream is already running!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + stream_.state = STREAM_RUNNING; + + // No need to do anything else here ... OSS automatically starts + // when fed samples. + + MUTEX_UNLOCK( &stream_.mutex ); + + OssHandle *handle = (OssHandle *) stream_.apiHandle; + pthread_cond_signal( &handle->runnable ); +} + +void RtApiOss :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiOss::stopStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + int result = 0; + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + // Flush the output with zeros a few times. + char *buffer; + int samples; + RtAudioFormat format; + + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + samples = stream_.bufferSize * stream_.nDeviceChannels[0]; + format = stream_.deviceFormat[0]; + } + else { + buffer = stream_.userBuffer[0]; + samples = stream_.bufferSize * stream_.nUserChannels[0]; + format = stream_.userFormat; + } + + memset( buffer, 0, samples * formatBytes(format) ); + for ( unsigned int i=0; iid[0], buffer, samples * formatBytes(format) ); + if ( result == -1 ) { + errorText_ = "RtApiOss::stopStream: audio write error."; + error( RtError::WARNING ); + } + } + + result = ioctl( handle->id[0], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::stopStream: system error stopping callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + handle->triggered = false; + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && handle->id[0] != handle->id[1] ) ) { + result = ioctl( handle->id[1], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::stopStream: system error stopping input callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result != -1 ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiOss :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiOss::abortStream(): the stream is already stopped!"; + error( RtError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + int result = 0; + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + result = ioctl( handle->id[0], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::abortStream: system error stopping callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + handle->triggered = false; + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && handle->id[0] != handle->id[1] ) ) { + result = ioctl( handle->id[1], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::abortStream: system error stopping input callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result != -1 ) return; + error( RtError::SYSTEM_ERROR ); +} + +void RtApiOss :: callbackEvent() +{ + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_LOCK( &stream_.mutex ); + pthread_cond_wait( &handle->runnable, &stream_.mutex ); + if ( stream_.state != STREAM_RUNNING ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + MUTEX_UNLOCK( &stream_.mutex ); + } + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiOss::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtError::WARNING ); + return; + } + + // Invoke user callback to get fresh output data. + int doStopStream = 0; + RtAudioCallback callback = (RtAudioCallback) stream_.callbackInfo.callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + doStopStream = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, stream_.callbackInfo.userData ); + if ( doStopStream == 2 ) { + this->abortStream(); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) goto unlock; + + int result; + char *buffer; + int samples; + RtAudioFormat format; + + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + // Setup parameters and do buffer conversion if necessary. + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + convertBuffer( buffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + samples = stream_.bufferSize * stream_.nDeviceChannels[0]; + format = stream_.deviceFormat[0]; + } + else { + buffer = stream_.userBuffer[0]; + samples = stream_.bufferSize * stream_.nUserChannels[0]; + format = stream_.userFormat; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[0] ) + byteSwapBuffer( buffer, samples, format ); + + if ( stream_.mode == DUPLEX && handle->triggered == false ) { + int trig = 0; + ioctl( handle->id[0], SNDCTL_DSP_SETTRIGGER, &trig ); + result = write( handle->id[0], buffer, samples * formatBytes(format) ); + trig = PCM_ENABLE_INPUT|PCM_ENABLE_OUTPUT; + ioctl( handle->id[0], SNDCTL_DSP_SETTRIGGER, &trig ); + handle->triggered = true; + } + else + // Write samples to device. + result = write( handle->id[0], buffer, samples * formatBytes(format) ); + + if ( result == -1 ) { + // We'll assume this is an underrun, though there isn't a + // specific means for determining that. + handle->xrun[0] = true; + errorText_ = "RtApiOss::callbackEvent: audio write error."; + error( RtError::WARNING ); + // Continue on to input section. + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + // Setup parameters. + if ( stream_.doConvertBuffer[1] ) { + buffer = stream_.deviceBuffer; + samples = stream_.bufferSize * stream_.nDeviceChannels[1]; + format = stream_.deviceFormat[1]; + } + else { + buffer = stream_.userBuffer[1]; + samples = stream_.bufferSize * stream_.nUserChannels[1]; + format = stream_.userFormat; + } + + // Read samples from device. + result = read( handle->id[1], buffer, samples * formatBytes(format) ); + + if ( result == -1 ) { + // We'll assume this is an overrun, though there isn't a + // specific means for determining that. + handle->xrun[1] = true; + errorText_ = "RtApiOss::callbackEvent: audio read error."; + error( RtError::WARNING ); + goto unlock; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( buffer, samples, format ); + + // Do buffer conversion if necessary. + if ( stream_.doConvertBuffer[1] ) + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + } + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + RtApi::tickStreamTime(); + if ( doStopStream == 1 ) this->stopStream(); +} + +extern "C" void *ossCallbackHandler( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiOss *object = (RtApiOss *) info->object; + bool *isRunning = &info->isRunning; + + while ( *isRunning == true ) { + pthread_testcancel(); + object->callbackEvent(); + } + + pthread_exit( NULL ); +} + +//******************** End of __LINUX_OSS__ *********************// +#endif + + +// *************************************************** // +// +// Protected common (OS-independent) RtAudio methods. +// +// *************************************************** // + +// This method can be modified to control the behavior of error +// message printing. +void RtApi :: error( RtError::Type type ) +{ + errorStream_.str(""); // clear the ostringstream + if ( type == RtError::WARNING && showWarnings_ == true ) + std::cerr << '\n' << errorText_ << "\n\n"; + else + throw( RtError( errorText_, type ) ); +} + +void RtApi :: verifyStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApi:: a stream is not open!"; + error( RtError::INVALID_USE ); + } +} + +void RtApi :: clearStreamInfo() +{ + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; + stream_.sampleRate = 0; + stream_.bufferSize = 0; + stream_.nBuffers = 0; + stream_.userFormat = 0; + stream_.userInterleaved = true; + stream_.streamTime = 0.0; + stream_.apiHandle = 0; + stream_.deviceBuffer = 0; + stream_.callbackInfo.callback = 0; + stream_.callbackInfo.userData = 0; + stream_.callbackInfo.isRunning = false; + for ( int i=0; i<2; i++ ) { + stream_.device[i] = 11111; + stream_.doConvertBuffer[i] = false; + stream_.deviceInterleaved[i] = true; + stream_.doByteSwap[i] = false; + stream_.nUserChannels[i] = 0; + stream_.nDeviceChannels[i] = 0; + stream_.channelOffset[i] = 0; + stream_.deviceFormat[i] = 0; + stream_.latency[i] = 0; + stream_.userBuffer[i] = 0; + stream_.convertInfo[i].channels = 0; + stream_.convertInfo[i].inJump = 0; + stream_.convertInfo[i].outJump = 0; + stream_.convertInfo[i].inFormat = 0; + stream_.convertInfo[i].outFormat = 0; + stream_.convertInfo[i].inOffset.clear(); + stream_.convertInfo[i].outOffset.clear(); + } +} + +unsigned int RtApi :: formatBytes( RtAudioFormat format ) +{ + if ( format == RTAUDIO_SINT16 ) + return 2; + else if ( format == RTAUDIO_SINT24 || format == RTAUDIO_SINT32 || + format == RTAUDIO_FLOAT32 ) + return 4; + else if ( format == RTAUDIO_FLOAT64 ) + return 8; + else if ( format == RTAUDIO_SINT8 ) + return 1; + + errorText_ = "RtApi::formatBytes: undefined format."; + error( RtError::WARNING ); + + return 0; +} + +void RtApi :: setConvertInfo( StreamMode mode, unsigned int firstChannel ) +{ + if ( mode == INPUT ) { // convert device to user buffer + stream_.convertInfo[mode].inJump = stream_.nDeviceChannels[1]; + stream_.convertInfo[mode].outJump = stream_.nUserChannels[1]; + stream_.convertInfo[mode].inFormat = stream_.deviceFormat[1]; + stream_.convertInfo[mode].outFormat = stream_.userFormat; + } + else { // convert user to device buffer + stream_.convertInfo[mode].inJump = stream_.nUserChannels[0]; + stream_.convertInfo[mode].outJump = stream_.nDeviceChannels[0]; + stream_.convertInfo[mode].inFormat = stream_.userFormat; + stream_.convertInfo[mode].outFormat = stream_.deviceFormat[0]; + } + + if ( stream_.convertInfo[mode].inJump < stream_.convertInfo[mode].outJump ) + stream_.convertInfo[mode].channels = stream_.convertInfo[mode].inJump; + else + stream_.convertInfo[mode].channels = stream_.convertInfo[mode].outJump; + + // Set up the interleave/deinterleave offsets. + if ( stream_.deviceInterleaved[mode] != stream_.userInterleaved ) { + if ( ( mode == OUTPUT && stream_.deviceInterleaved[mode] ) || + ( mode == INPUT && stream_.userInterleaved ) ) { + for ( int k=0; k 0 ) { + if ( stream_.deviceInterleaved[mode] ) { + if ( mode == OUTPUT ) { + for ( int k=0; k>= 8; + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_FLOAT32) { + Float32 *in = (Float32 *)inBuffer; + for (unsigned int i=0; i> 8) & 0x0000ffff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_SINT32) { + Int32 *in = (Int32 *)inBuffer; + for (unsigned int i=0; i> 16) & 0x0000ffff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_FLOAT32) { + Float32 *in = (Float32 *)inBuffer; + for (unsigned int i=0; i> 8) & 0x00ff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_SINT24) { + Int32 *in = (Int32 *)inBuffer; + for (unsigned int i=0; i> 16) & 0x000000ff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_SINT32) { + Int32 *in = (Int32 *)inBuffer; + for (unsigned int i=0; i> 24) & 0x000000ff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_FLOAT32) { + Float32 *in = (Float32 *)inBuffer; + for (unsigned int i=0; i>8) | (x<<8); } + //static inline uint32_t bswap_32(uint32_t x) { return (bswap_16(x&0xffff)<<16) | (bswap_16(x>>16)); } + //static inline uint64_t bswap_64(uint64_t x) { return (((unsigned long long)bswap_32(x&0xffffffffull))<<32) | (bswap_32(x>>32)); } + +void RtApi :: byteSwapBuffer( char *buffer, unsigned int samples, RtAudioFormat format ) +{ + register char val; + register char *ptr; + + ptr = buffer; + if ( format == RTAUDIO_SINT16 ) { + for ( unsigned int i=0; i +#include +#include "RtError.h" + +/*! \typedef typedef unsigned long RtAudioFormat; + \brief RtAudio data format type. + + Support for signed integers and floats. Audio data fed to/from an + RtAudio stream is assumed to ALWAYS be in host byte order. The + internal routines will automatically take care of any necessary + byte-swapping between the host format and the soundcard. Thus, + endian-ness is not a concern in the following format definitions. + + - \e RTAUDIO_SINT8: 8-bit signed integer. + - \e RTAUDIO_SINT16: 16-bit signed integer. + - \e RTAUDIO_SINT24: Upper 3 bytes of 32-bit signed integer. + - \e RTAUDIO_SINT32: 32-bit signed integer. + - \e RTAUDIO_FLOAT32: Normalized between plus/minus 1.0. + - \e RTAUDIO_FLOAT64: Normalized between plus/minus 1.0. +*/ +typedef unsigned long RtAudioFormat; +static const RtAudioFormat RTAUDIO_SINT8 = 0x1; // 8-bit signed integer. +static const RtAudioFormat RTAUDIO_SINT16 = 0x2; // 16-bit signed integer. +static const RtAudioFormat RTAUDIO_SINT24 = 0x4; // Lower 3 bytes of 32-bit signed integer. +static const RtAudioFormat RTAUDIO_SINT32 = 0x8; // 32-bit signed integer. +static const RtAudioFormat RTAUDIO_FLOAT32 = 0x10; // Normalized between plus/minus 1.0. +static const RtAudioFormat RTAUDIO_FLOAT64 = 0x20; // Normalized between plus/minus 1.0. + +/*! \typedef typedef unsigned long RtAudioStreamFlags; + \brief RtAudio stream option flags. + + The following flags can be OR'ed together to allow a client to + make changes to the default stream behavior: + + - \e RTAUDIO_NONINTERLEAVED: Use non-interleaved buffers (default = interleaved). + - \e RTAUDIO_MINIMIZE_LATENCY: Attempt to set stream parameters for lowest possible latency. + - \e RTAUDIO_HOG_DEVICE: Attempt grab device for exclusive use. + + By default, RtAudio streams pass and receive audio data from the + client in an interleaved format. By passing the + RTAUDIO_NONINTERLEAVED flag to the openStream() function, audio + data will instead be presented in non-interleaved buffers. In + this case, each buffer argument in the RtAudioCallback function + will point to a single array of data, with \c nFrames samples for + each channel concatenated back-to-back. For example, the first + sample of data for the second channel would be located at index \c + nFrames (assuming the \c buffer pointer was recast to the correct + data type for the stream). + + Certain audio APIs offer a number of parameters that influence the + I/O latency of a stream. By default, RtAudio will attempt to set + these parameters internally for robust (glitch-free) performance + (though some APIs, like Windows Direct Sound, make this difficult). + By passing the RTAUDIO_MINIMIZE_LATENCY flag to the openStream() + function, internal stream settings will be influenced in an attempt + to minimize stream latency, though possibly at the expense of stream + performance. + + If the RTAUDIO_HOG_DEVICE flag is set, RtAudio will attempt to + open the input and/or output stream device(s) for exclusive use. + Note that this is not possible with all supported audio APIs. + + If the RTAUDIO_SCHEDULE_REALTIME flag is set, RtAudio will attempt + to select realtime scheduling (round-robin) for the callback thread. +*/ +typedef unsigned int RtAudioStreamFlags; +static const RtAudioStreamFlags RTAUDIO_NONINTERLEAVED = 0x1; // Use non-interleaved buffers (default = interleaved). +static const RtAudioStreamFlags RTAUDIO_MINIMIZE_LATENCY = 0x2; // Attempt to set stream parameters for lowest possible latency. +static const RtAudioStreamFlags RTAUDIO_HOG_DEVICE = 0x4; // Attempt grab device and prevent use by others. +static const RtAudioStreamFlags RTAUDIO_SCHEDULE_REALTIME = 0x8; // Try to select realtime scheduling for callback thread. + +/*! \typedef typedef unsigned long RtAudioStreamStatus; + \brief RtAudio stream status (over- or underflow) flags. + + Notification of a stream over- or underflow is indicated by a + non-zero stream \c status argument in the RtAudioCallback function. + The stream status can be one of the following two options, + depending on whether the stream is open for output and/or input: + + - \e RTAUDIO_INPUT_OVERFLOW: Input data was discarded because of an overflow condition at the driver. + - \e RTAUDIO_OUTPUT_UNDERFLOW: The output buffer ran low, likely producing a break in the output sound. +*/ +typedef unsigned int RtAudioStreamStatus; +static const RtAudioStreamStatus RTAUDIO_INPUT_OVERFLOW = 0x1; // Input data was discarded because of an overflow condition at the driver. +static const RtAudioStreamStatus RTAUDIO_OUTPUT_UNDERFLOW = 0x2; // The output buffer ran low, likely causing a gap in the output sound. + +//! RtAudio callback function prototype. +/*! + All RtAudio clients must create a function of type RtAudioCallback + to read and/or write data from/to the audio stream. When the + underlying audio system is ready for new input or output data, this + function will be invoked. + + \param outputBuffer For output (or duplex) streams, the client + should write \c nFrames of audio sample frames into this + buffer. This argument should be recast to the datatype + specified when the stream was opened. For input-only + streams, this argument will be NULL. + + \param inputBuffer For input (or duplex) streams, this buffer will + hold \c nFrames of input audio sample frames. This + argument should be recast to the datatype specified when the + stream was opened. For output-only streams, this argument + will be NULL. + + \param nFrames The number of sample frames of input or output + data in the buffers. The actual buffer size in bytes is + dependent on the data type and number of channels in use. + + \param streamTime The number of seconds that have elapsed since the + stream was started. + + \param status If non-zero, this argument indicates a data overflow + or underflow condition for the stream. The particular + condition can be determined by comparison with the + RtAudioStreamStatus flags. + + \param userData A pointer to optional data provided by the client + when opening the stream (default = NULL). + + To continue normal stream operation, the RtAudioCallback function + should return a value of zero. To stop the stream and drain the + output buffer, the function should return a value of one. To abort + the stream immediately, the client should return a value of two. + */ +typedef int (*RtAudioCallback)( void *outputBuffer, void *inputBuffer, + unsigned int nFrames, + double streamTime, + RtAudioStreamStatus status, + void *userData ); + + +// **************************************************************** // +// +// RtAudio class declaration. +// +// RtAudio is a "controller" used to select an available audio i/o +// interface. It presents a common API for the user to call but all +// functionality is implemented by the class RtApi and its +// subclasses. RtAudio creates an instance of an RtApi subclass +// based on the user's API choice. If no choice is made, RtAudio +// attempts to make a "logical" API selection. +// +// **************************************************************** // + +class RtApi; + +class RtAudio +{ + public: + + //! Audio API specifier arguments. + enum Api { + UNSPECIFIED, /*!< Search for a working compiled API. */ + LINUX_ALSA, /*!< The Advanced Linux Sound Architecture API. */ + LINUX_OSS, /*!< The Linux Open Sound System API. */ + UNIX_JACK, /*!< The Jack Low-Latency Audio Server API. */ + MACOSX_CORE, /*!< Macintosh OS-X Core Audio API. */ + WINDOWS_ASIO, /*!< The Steinberg Audio Stream I/O API. */ + WINDOWS_DS, /*!< The Microsoft Direct Sound API. */ + RTAUDIO_DUMMY /*!< A compilable but non-functional API. */ + }; + + //! The public device information structure for returning queried values. + struct DeviceInfo { + bool probed; /*!< true if the device capabilities were successfully probed. */ + std::string name; /*!< Character string device identifier. */ + unsigned int outputChannels; /*!< Maximum output channels supported by device. */ + unsigned int inputChannels; /*!< Maximum input channels supported by device. */ + unsigned int duplexChannels; /*!< Maximum simultaneous input/output channels supported by device. */ + bool isDefaultOutput; /*!< true if this is the default output device. */ + bool isDefaultInput; /*!< true if this is the default input device. */ + std::vector sampleRates; /*!< Supported sample rates (queried from list of standard rates). */ + RtAudioFormat nativeFormats; /*!< Bit mask of supported data formats. */ + + // Default constructor. + DeviceInfo() + :probed(false), outputChannels(0), inputChannels(0), duplexChannels(0), + isDefaultOutput(false), isDefaultInput(false), nativeFormats(0) {} + }; + + //! The structure for specifying input or ouput stream parameters. + struct StreamParameters { + unsigned int deviceId; /*!< Device index (0 to getDeviceCount() - 1). */ + unsigned int nChannels; /*!< Number of channels. */ + unsigned int firstChannel; /*!< First channel index on device (default = 0). */ + + // Default constructor. + StreamParameters() + : deviceId(0), nChannels(0), firstChannel(0) {} + }; + + //! The structure for specifying stream options. + /*! + The following flags can be OR'ed together to allow a client to + make changes to the default stream behavior: + + - \e RTAUDIO_NONINTERLEAVED: Use non-interleaved buffers (default = interleaved). + - \e RTAUDIO_MINIMIZE_LATENCY: Attempt to set stream parameters for lowest possible latency. + - \e RTAUDIO_HOG_DEVICE: Attempt grab device for exclusive use. + - \e RTAUDIO_SCHEDULE_REALTIME: Attempt to select realtime scheduling for callback thread. + + By default, RtAudio streams pass and receive audio data from the + client in an interleaved format. By passing the + RTAUDIO_NONINTERLEAVED flag to the openStream() function, audio + data will instead be presented in non-interleaved buffers. In + this case, each buffer argument in the RtAudioCallback function + will point to a single array of data, with \c nFrames samples for + each channel concatenated back-to-back. For example, the first + sample of data for the second channel would be located at index \c + nFrames (assuming the \c buffer pointer was recast to the correct + data type for the stream). + + Certain audio APIs offer a number of parameters that influence the + I/O latency of a stream. By default, RtAudio will attempt to set + these parameters internally for robust (glitch-free) performance + (though some APIs, like Windows Direct Sound, make this difficult). + By passing the RTAUDIO_MINIMIZE_LATENCY flag to the openStream() + function, internal stream settings will be influenced in an attempt + to minimize stream latency, though possibly at the expense of stream + performance. + + If the RTAUDIO_HOG_DEVICE flag is set, RtAudio will attempt to + open the input and/or output stream device(s) for exclusive use. + Note that this is not possible with all supported audio APIs. + + If the RTAUDIO_SCHEDULE_REALTIME flag is set, RtAudio will attempt + to select realtime scheduling (round-robin) for the callback thread. + The \c priority parameter will only be used if the RTAUDIO_SCHEDULE_REALTIME + flag is set. It defines the thread's realtime priority. + + The \c numberOfBuffers parameter can be used to control stream + latency in the Windows DirectSound, Linux OSS, and Linux Alsa APIs + only. A value of two is usually the smallest allowed. Larger + numbers can potentially result in more robust stream performance, + though likely at the cost of stream latency. The value set by the + user is replaced during execution of the RtAudio::openStream() + function by the value actually used by the system. + + The \c streamName parameter can be used to set the client name + when using the Jack API. By default, the client name is set to + RtApiJack. However, if you wish to create multiple instances of + RtAudio with Jack, each instance must have a unique client name. + */ + struct StreamOptions { + RtAudioStreamFlags flags; /*!< A bit-mask of stream flags (RTAUDIO_NONINTERLEAVED, RTAUDIO_MINIMIZE_LATENCY, RTAUDIO_HOG_DEVICE). */ + unsigned int numberOfBuffers; /*!< Number of stream buffers. */ + std::string streamName; /*!< A stream name (currently used only in Jack). */ + int priority; /*!< Scheduling priority of callback thread (only used with flag RTAUDIO_SCHEDULE_REALTIME). */ + + // Default constructor. + StreamOptions() + : flags(0), numberOfBuffers(0), priority(0) {} + }; + + //! A static function to determine the available compiled audio APIs. + /*! + The values returned in the std::vector can be compared against + the enumerated list values. Note that there can be more than one + API compiled for certain operating systems. + */ + static void getCompiledApi( std::vector &apis ) throw(); + + //! The class constructor. + /*! + The constructor performs minor initialization tasks. No exceptions + can be thrown. + + If no API argument is specified and multiple API support has been + compiled, the default order of use is JACK, ALSA, OSS (Linux + systems) and ASIO, DS (Windows systems). + */ + RtAudio( RtAudio::Api api=UNSPECIFIED ) throw(); + + //! The destructor. + /*! + If a stream is running or open, it will be stopped and closed + automatically. + */ + ~RtAudio() throw(); + + //! Returns the audio API specifier for the current instance of RtAudio. + RtAudio::Api getCurrentApi( void ) throw(); + + //! A public function that queries for the number of audio devices available. + /*! + This function performs a system query of available devices each time it + is called, thus supporting devices connected \e after instantiation. If + a system error occurs during processing, a warning will be issued. + */ + unsigned int getDeviceCount( void ) throw(); + + //! Return an RtAudio::DeviceInfo structure for a specified device number. + /*! + + Any device integer between 0 and getDeviceCount() - 1 is valid. + If an invalid argument is provided, an RtError (type = INVALID_USE) + will be thrown. If a device is busy or otherwise unavailable, the + structure member "probed" will have a value of "false" and all + other members are undefined. If the specified device is the + current default input or output device, the corresponding + "isDefault" member will have a value of "true". + */ + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + + //! A function that returns the index of the default output device. + /*! + If the underlying audio API does not provide a "default + device", or if no devices are available, the return value will be + 0. Note that this is a valid device identifier and it is the + client's responsibility to verify that a device is available + before attempting to open a stream. + */ + unsigned int getDefaultOutputDevice( void ) throw(); + + //! A function that returns the index of the default input device. + /*! + If the underlying audio API does not provide a "default + device", or if no devices are available, the return value will be + 0. Note that this is a valid device identifier and it is the + client's responsibility to verify that a device is available + before attempting to open a stream. + */ + unsigned int getDefaultInputDevice( void ) throw(); + + //! A public function for opening a stream with the specified parameters. + /*! + An RtError (type = SYSTEM_ERROR) is thrown if a stream cannot be + opened with the specified parameters or an error occurs during + processing. An RtError (type = INVALID_USE) is thrown if any + invalid device ID or channel number parameters are specified. + + \param outputParameters Specifies output stream parameters to use + when opening a stream, including a device ID, number of channels, + and starting channel number. For input-only streams, this + argument should be NULL. The device ID is an index value between + 0 and getDeviceCount() - 1. + \param inputParameters Specifies input stream parameters to use + when opening a stream, including a device ID, number of channels, + and starting channel number. For output-only streams, this + argument should be NULL. The device ID is an index value between + 0 and getDeviceCount() - 1. + \param format An RtAudioFormat specifying the desired sample data format. + \param sampleRate The desired sample rate (sample frames per second). + \param *bufferFrames A pointer to a value indicating the desired + internal buffer size in sample frames. The actual value + used by the device is returned via the same pointer. A + value of zero can be specified, in which case the lowest + allowable value is determined. + \param callback A client-defined function that will be invoked + when input data is available and/or output data is needed. + \param userData An optional pointer to data that can be accessed + from within the callback function. + \param options An optional pointer to a structure containing various + global stream options, including a list of OR'ed RtAudioStreamFlags + and a suggested number of stream buffers that can be used to + control stream latency. More buffers typically result in more + robust performance, though at a cost of greater latency. If a + value of zero is specified, a system-specific median value is + chosen. If the RTAUDIO_MINIMIZE_LATENCY flag bit is set, the + lowest allowable value is used. The actual value used is + returned via the structure argument. The parameter is API dependent. + */ + void openStream( RtAudio::StreamParameters *outputParameters, + RtAudio::StreamParameters *inputParameters, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, RtAudioCallback callback, + void *userData = NULL, RtAudio::StreamOptions *options = NULL ); + + //! A function that closes a stream and frees any associated stream memory. + /*! + If a stream is not open, this function issues a warning and + returns (no exception is thrown). + */ + void closeStream( void ) throw(); + + //! A function that starts a stream. + /*! + An RtError (type = SYSTEM_ERROR) is thrown if an error occurs + during processing. An RtError (type = INVALID_USE) is thrown if a + stream is not open. A warning is issued if the stream is already + running. + */ + void startStream( void ); + + //! Stop a stream, allowing any samples remaining in the output queue to be played. + /*! + An RtError (type = SYSTEM_ERROR) is thrown if an error occurs + during processing. An RtError (type = INVALID_USE) is thrown if a + stream is not open. A warning is issued if the stream is already + stopped. + */ + void stopStream( void ); + + //! Stop a stream, discarding any samples remaining in the input/output queue. + /*! + An RtError (type = SYSTEM_ERROR) is thrown if an error occurs + during processing. An RtError (type = INVALID_USE) is thrown if a + stream is not open. A warning is issued if the stream is already + stopped. + */ + void abortStream( void ); + + //! Returns true if a stream is open and false if not. + bool isStreamOpen( void ) const throw(); + + //! Returns true if the stream is running and false if it is stopped or not open. + bool isStreamRunning( void ) const throw(); + + //! Returns the number of elapsed seconds since the stream was started. + /*! + If a stream is not open, an RtError (type = INVALID_USE) will be thrown. + */ + double getStreamTime( void ); + + //! Returns the internal stream latency in sample frames. + /*! + The stream latency refers to delay in audio input and/or output + caused by internal buffering by the audio system and/or hardware. + For duplex streams, the returned value will represent the sum of + the input and output latencies. If a stream is not open, an + RtError (type = INVALID_USE) will be thrown. If the API does not + report latency, the return value will be zero. + */ + long getStreamLatency( void ); + + //! Returns actual sample rate in use by the stream. + /*! + On some systems, the sample rate used may be slightly different + than that specified in the stream parameters. If a stream is not + open, an RtError (type = INVALID_USE) will be thrown. + */ + unsigned int getStreamSampleRate( void ); + + //! Specify whether warning messages should be printed to stderr. + void showWarnings( bool value = true ) throw(); + + protected: + + void openRtApi( RtAudio::Api api ); + RtApi *rtapi_; +}; + +// Operating system dependent thread functionality. +#if defined(__WINDOWS_DS__) || defined(__WINDOWS_ASIO__) + #include + #include + + typedef unsigned long ThreadHandle; + typedef CRITICAL_SECTION StreamMutex; + +#elif defined(__LINUX_ALSA__) || defined(__UNIX_JACK__) || defined(__LINUX_OSS__) || defined(__MACOSX_CORE__) + // Using pthread library for various flavors of unix. + #include + + typedef pthread_t ThreadHandle; + typedef pthread_mutex_t StreamMutex; + +#else // Setup for "dummy" behavior + + #define __RTAUDIO_DUMMY__ + typedef int ThreadHandle; + typedef int StreamMutex; + +#endif + +// This global structure type is used to pass callback information +// between the private RtAudio stream structure and global callback +// handling functions. +struct CallbackInfo { + void *object; // Used as a "this" pointer. + ThreadHandle thread; + void *callback; + void *userData; + void *apiInfo; // void pointer for API specific callback information + bool isRunning; + + // Default constructor. + CallbackInfo() + :object(0), callback(0), userData(0), apiInfo(0), isRunning(false) {} +}; + +// **************************************************************** // +// +// RtApi class declaration. +// +// Subclasses of RtApi contain all API- and OS-specific code necessary +// to fully implement the RtAudio API. +// +// Note that RtApi is an abstract base class and cannot be +// explicitly instantiated. The class RtAudio will create an +// instance of an RtApi subclass (RtApiOss, RtApiAlsa, +// RtApiJack, RtApiCore, RtApiAl, RtApiDs, or RtApiAsio). +// +// **************************************************************** // + +#if defined( HAVE_GETTIMEOFDAY ) + #include +#endif + +#include + +class RtApi +{ +public: + + RtApi(); + virtual ~RtApi(); + virtual RtAudio::Api getCurrentApi( void ) = 0; + virtual unsigned int getDeviceCount( void ) = 0; + virtual RtAudio::DeviceInfo getDeviceInfo( unsigned int device ) = 0; + virtual unsigned int getDefaultInputDevice( void ); + virtual unsigned int getDefaultOutputDevice( void ); + void openStream( RtAudio::StreamParameters *outputParameters, + RtAudio::StreamParameters *inputParameters, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, RtAudioCallback callback, + void *userData, RtAudio::StreamOptions *options ); + virtual void closeStream( void ); + virtual void startStream( void ) = 0; + virtual void stopStream( void ) = 0; + virtual void abortStream( void ) = 0; + long getStreamLatency( void ); + unsigned int getStreamSampleRate( void ); + virtual double getStreamTime( void ); + bool isStreamOpen( void ) const { return stream_.state != STREAM_CLOSED; }; + bool isStreamRunning( void ) const { return stream_.state == STREAM_RUNNING; }; + void showWarnings( bool value ) { showWarnings_ = value; }; + + +protected: + + static const unsigned int MAX_SAMPLE_RATES; + static const unsigned int SAMPLE_RATES[]; + + enum { FAILURE, SUCCESS }; + + enum StreamState { + STREAM_STOPPED, + STREAM_RUNNING, + STREAM_CLOSED = -50 + }; + + enum StreamMode { + OUTPUT, + INPUT, + DUPLEX, + UNINITIALIZED = -75 + }; + + // A protected structure used for buffer conversion. + struct ConvertInfo { + int channels; + int inJump, outJump; + RtAudioFormat inFormat, outFormat; + std::vector inOffset; + std::vector outOffset; + }; + + // A protected structure for audio streams. + struct RtApiStream { + unsigned int device[2]; // Playback and record, respectively. + void *apiHandle; // void pointer for API specific stream handle information + StreamMode mode; // OUTPUT, INPUT, or DUPLEX. + StreamState state; // STOPPED, RUNNING, or CLOSED + char *userBuffer[2]; // Playback and record, respectively. + char *deviceBuffer; + bool doConvertBuffer[2]; // Playback and record, respectively. + bool userInterleaved; + bool deviceInterleaved[2]; // Playback and record, respectively. + bool doByteSwap[2]; // Playback and record, respectively. + unsigned int sampleRate; + unsigned int bufferSize; + unsigned int nBuffers; + unsigned int nUserChannels[2]; // Playback and record, respectively. + unsigned int nDeviceChannels[2]; // Playback and record channels, respectively. + unsigned int channelOffset[2]; // Playback and record, respectively. + unsigned long latency[2]; // Playback and record, respectively. + RtAudioFormat userFormat; + RtAudioFormat deviceFormat[2]; // Playback and record, respectively. + StreamMutex mutex; + CallbackInfo callbackInfo; + ConvertInfo convertInfo[2]; + double streamTime; // Number of elapsed seconds since the stream started. + +#if defined(HAVE_GETTIMEOFDAY) + struct timeval lastTickTimestamp; +#endif + + RtApiStream() + :apiHandle(0), deviceBuffer(0) { device[0] = 11111; device[1] = 11111; } + }; + + typedef signed short Int16; + typedef signed int Int32; + typedef float Float32; + typedef double Float64; + + std::ostringstream errorStream_; + std::string errorText_; + bool showWarnings_; + RtApiStream stream_; + + /*! + Protected, api-specific method that attempts to open a device + with the given parameters. This function MUST be implemented by + all subclasses. If an error is encountered during the probe, a + "warning" message is reported and FAILURE is returned. A + successful probe is indicated by a return value of SUCCESS. + */ + virtual bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); + + //! A protected function used to increment the stream time. + void tickStreamTime( void ); + + //! Protected common method to clear an RtApiStream structure. + void clearStreamInfo(); + + /*! + Protected common method that throws an RtError (type = + INVALID_USE) if a stream is not open. + */ + void verifyStream( void ); + + //! Protected common error method to allow global control over error handling. + void error( RtError::Type type ); + + /*! + Protected method used to perform format, channel number, and/or interleaving + conversions between the user and device buffers. + */ + void convertBuffer( char *outBuffer, char *inBuffer, ConvertInfo &info ); + + //! Protected common method used to perform byte-swapping on buffers. + void byteSwapBuffer( char *buffer, unsigned int samples, RtAudioFormat format ); + + //! Protected common method that returns the number of bytes for a given format. + unsigned int formatBytes( RtAudioFormat format ); + + //! Protected common method that sets up the parameters for buffer conversion. + void setConvertInfo( StreamMode mode, unsigned int firstChannel ); +}; + +// **************************************************************** // +// +// Inline RtAudio definitions. +// +// **************************************************************** // + +inline RtAudio::Api RtAudio :: getCurrentApi( void ) throw() { return rtapi_->getCurrentApi(); } +inline unsigned int RtAudio :: getDeviceCount( void ) throw() { return rtapi_->getDeviceCount(); } +inline RtAudio::DeviceInfo RtAudio :: getDeviceInfo( unsigned int device ) { return rtapi_->getDeviceInfo( device ); } +inline unsigned int RtAudio :: getDefaultInputDevice( void ) throw() { return rtapi_->getDefaultInputDevice(); } +inline unsigned int RtAudio :: getDefaultOutputDevice( void ) throw() { return rtapi_->getDefaultOutputDevice(); } +inline void RtAudio :: closeStream( void ) throw() { return rtapi_->closeStream(); } +inline void RtAudio :: startStream( void ) { return rtapi_->startStream(); } +inline void RtAudio :: stopStream( void ) { return rtapi_->stopStream(); } +inline void RtAudio :: abortStream( void ) { return rtapi_->abortStream(); } +inline bool RtAudio :: isStreamOpen( void ) const throw() { return rtapi_->isStreamOpen(); } +inline bool RtAudio :: isStreamRunning( void ) const throw() { return rtapi_->isStreamRunning(); } +inline long RtAudio :: getStreamLatency( void ) { return rtapi_->getStreamLatency(); } +inline unsigned int RtAudio :: getStreamSampleRate( void ) { return rtapi_->getStreamSampleRate(); }; +inline double RtAudio :: getStreamTime( void ) { return rtapi_->getStreamTime(); } +inline void RtAudio :: showWarnings( bool value ) throw() { rtapi_->showWarnings( value ); } + +// RtApi Subclass prototypes. + +#if defined(__MACOSX_CORE__) + +#include + +class RtApiCore: public RtApi +{ +public: + + RtApiCore(); + ~RtApiCore(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::MACOSX_CORE; }; + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + unsigned int getDefaultOutputDevice( void ); + unsigned int getDefaultInputDevice( void ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + bool callbackEvent( AudioDeviceID deviceId, + const AudioBufferList *inBufferList, + const AudioBufferList *outBufferList ); + + private: + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); + static const char* getErrorCode( OSStatus code ); +}; + +#endif + +#if defined(__UNIX_JACK__) + +class RtApiJack: public RtApi +{ +public: + + RtApiJack(); + ~RtApiJack(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::UNIX_JACK; }; + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + bool callbackEvent( unsigned long nframes ); + + private: + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__WINDOWS_ASIO__) + +class RtApiAsio: public RtApi +{ +public: + + RtApiAsio(); + ~RtApiAsio(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::WINDOWS_ASIO; }; + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + bool callbackEvent( long bufferIndex ); + + private: + + std::vector devices_; + void saveDeviceInfo( void ); + bool coInitialized_; + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__WINDOWS_DS__) + +class RtApiDs: public RtApi +{ +public: + + RtApiDs(); + ~RtApiDs(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::WINDOWS_DS; }; + unsigned int getDeviceCount( void ); + unsigned int getDefaultOutputDevice( void ); + unsigned int getDefaultInputDevice( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + void callbackEvent( void ); + + private: + + bool coInitialized_; + bool buffersRolling; + long duplexPrerollBytes; + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__LINUX_ALSA__) + +class RtApiAlsa: public RtApi +{ +public: + + RtApiAlsa(); + ~RtApiAlsa(); + RtAudio::Api getCurrentApi() { return RtAudio::LINUX_ALSA; }; + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + void callbackEvent( void ); + + private: + + std::vector devices_; + void saveDeviceInfo( void ); + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__LINUX_OSS__) + +class RtApiOss: public RtApi +{ +public: + + RtApiOss(); + ~RtApiOss(); + RtAudio::Api getCurrentApi() { return RtAudio::LINUX_OSS; }; + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + void callbackEvent( void ); + + private: + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__RTAUDIO_DUMMY__) + +class RtApiDummy: public RtApi +{ +public: + + RtApiDummy() { errorText_ = "RtApiDummy: This class provides no functionality."; error( RtError::WARNING ); }; + RtAudio::Api getCurrentApi( void ) { return RtAudio::RTAUDIO_DUMMY; }; + unsigned int getDeviceCount( void ) { return 0; }; + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ) { RtAudio::DeviceInfo info; return info; }; + void closeStream( void ) {}; + void startStream( void ) {}; + void stopStream( void ) {}; + void abortStream( void ) {}; + + private: + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) { return false; }; +}; + +#endif + +#endif + +// Indentation settings for Vim and Emacs +// +// Local Variables: +// c-basic-offset: 2 +// indent-tabs-mode: nil +// End: +// +// vim: et sts=2 sw=2 diff --git a/rtaudio/RtError.h b/rtaudio/RtError.h new file mode 100644 index 000000000..ac5c41498 --- /dev/null +++ b/rtaudio/RtError.h @@ -0,0 +1,60 @@ +/************************************************************************/ +/*! \class RtError + \brief Exception handling class for RtAudio & RtMidi. + + The RtError class is quite simple but it does allow errors to be + "caught" by RtError::Type. See the RtAudio and RtMidi + documentation to know which methods can throw an RtError. + +*/ +/************************************************************************/ + +#ifndef RTERROR_H +#define RTERROR_H + +#include +#include +#include + +class RtError : public std::exception +{ + public: + //! Defined RtError types. + enum Type { + WARNING, /*!< A non-critical error. */ + DEBUG_WARNING, /*!< A non-critical error which might be useful for debugging. */ + UNSPECIFIED, /*!< The default, unspecified error type. */ + NO_DEVICES_FOUND, /*!< No devices found on system. */ + INVALID_DEVICE, /*!< An invalid device ID was specified. */ + MEMORY_ERROR, /*!< An error occured during memory allocation. */ + INVALID_PARAMETER, /*!< An invalid parameter was specified to a function. */ + INVALID_USE, /*!< The function was called incorrectly. */ + DRIVER_ERROR, /*!< A system driver error occured. */ + SYSTEM_ERROR, /*!< A system error occured. */ + THREAD_ERROR /*!< A thread error occured. */ + }; + + //! The constructor. + RtError( const std::string& message, Type type = RtError::UNSPECIFIED ) throw() : message_(message), type_(type) {} + + //! The destructor. + virtual ~RtError( void ) throw() {} + + //! Prints thrown error message to stderr. + virtual void printMessage( void ) throw() { std::cerr << '\n' << message_ << "\n\n"; } + + //! Returns the thrown error message type. + virtual const Type& getType(void) throw() { return type_; } + + //! Returns the thrown error message string. + virtual const std::string& getMessage(void) throw() { return message_; } + + //! Returns the thrown error message as a c-style string. + virtual const char* what( void ) const throw() { return message_.c_str(); } + + protected: + std::string message_; + Type type_; +}; + +#endif diff --git a/rtaudio/readme b/rtaudio/readme new file mode 100644 index 000000000..291a3fb9c --- /dev/null +++ b/rtaudio/readme @@ -0,0 +1,61 @@ +RtAudio - a set of C++ classes that provide a common API for realtime audio input/output across Linux (native ALSA, JACK, and OSS), Macintosh OS X (CoreAudio and JACK), and Windows (DirectSound and ASIO) operating systems. + +By Gary P. Scavone, 2001-2010. + +This distribution of RtAudio contains the following: + +doc: RtAudio documentation (see doc/html/index.html) +tests: example RtAudio programs +asio: header and source files necessary for ASIO compilation +tests/Windows: Visual C++ .net test program workspace and projects + +OVERVIEW: + +RtAudio is a set of C++ classes that provides a common API (Application Programming Interface) for realtime audio input/output across Linux (native ALSA, JACK, and OSS), Macintosh OS X, SGI, and Windows (DirectSound and ASIO) operating systems. RtAudio significantly simplifies the process of interacting with computer audio hardware. It was designed with the following objectives: + + - object-oriented C++ design + - simple, common API across all supported platforms + - only one source and two header files for easy inclusion in programming projects + - allow simultaneous multi-api support + - support dynamic connection of devices + - provide extensive audio device parameter control + - allow audio device capability probing + - automatic internal conversion for data format, channel number compensation, (de)interleaving, and byte-swapping + +RtAudio incorporates the concept of audio streams, which represent audio output (playback) and/or input (recording). Available audio devices and their capabilities can be enumerated and then specified when opening a stream. Where applicable, multiple API support can be compiled and a particular API specified when creating an RtAudio instance. See the \ref apinotes section for information specific to each of the supported audio APIs. + +FURTHER READING: + +For complete documentation on RtAudio, see the doc directory of the distribution or surf to http://www.music.mcgill.ca/~gary/rtaudio/. + + +LEGAL AND ETHICAL: + +The RtAudio license is similar to the MIT License. + + RtAudio: a set of realtime audio i/o C++ classes + Copyright (c) 2001-2010 Gary P. Scavone + + 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. + + Any person wishing to distribute modifications to the Software is + asked to send the modifications to the original developer so that + they can be incorporated into the canonical version. This is, + however, not a binding provision of this license. + + 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. diff --git a/src/CMakeLists.linux.txt b/src/CMakeLists.linux.txt new file mode 100644 index 000000000..f1f07659f --- /dev/null +++ b/src/CMakeLists.linux.txt @@ -0,0 +1,10 @@ +IF( "${gui}" STREQUAL "no" ) +ELSE() + SET( OS_SPECIFIC_LINK_LIBRARIES + ${OS_SPECIFIC_LINK_LIBRARIES} + alsaplayback + gnutls + ) +ENDIF() + +#include( "CPack.txt" ) diff --git a/src/CMakeLists.osx.txt b/src/CMakeLists.osx.txt new file mode 100644 index 000000000..f12f7a6ca --- /dev/null +++ b/src/CMakeLists.osx.txt @@ -0,0 +1,21 @@ +IF( "${gui}" STREQUAL "no" ) +ELSE() + SET( tomahawkSourcesGui ${tomahawkSourcesGui} + audio/rtaudiooutput.cpp + ) + + SET( tomahawkHeadersGui ${tomahawkHeadersGui} + audio/rtaudiooutput.h + ) + + FIND_LIBRARY( COREAUDIO_LIBRARY CoreAudio ) + FIND_LIBRARY( COREFOUNDATION_LIBRARY CoreFoundation ) + MARK_AS_ADVANCED( COREAUDIO_LIBRARY COREFOUNDATION_LIBRARY ) + + SET( OS_SPECIFIC_LINK_LIBRARIES + ${OS_SPECIFIC_LINK_LIBRARIES} + ${COREAUDIO_LIBRARY} + ${COREFOUNDATION_LIBRARY} + rtaudio + ) +ENDIF() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 000000000..2d62a40ec --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,298 @@ +PROJECT( tomahawk ) +CMAKE_MINIMUM_REQUIRED( VERSION 2.8 ) + +IF( "${gui}" STREQUAL "no" ) + SET( QT_DONT_USE_QTGUI TRUE ) +ENDIF() +SET( QT_USE_QTSQL TRUE ) +SET( QT_USE_QTNETWORK TRUE ) +SET( QT_USE_QTXML TRUE ) +INCLUDE( ${QT_USE_FILE} ) + +SET( CMAKE_BUILD_TYPE "debugfull" ) +SET( CMAKE_VERBOSE_MAKEFILE ON ) +SET( CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" ) +SET( CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" ) +SET( CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" ) +SET( TOMAHAWK_INC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../include/" ) + +# build plugins +# use glob, but hardcoded list for now: +#FILE( GLOB plugindirs "src/plugins/*" ) +#FOREACH( moddir ${plugindirs} ) +# MESSAGE( status "Building plugin: ${moddir}" ) +# ADD_SUBDIRECTORY( ${moddir} ) +#ENDFOREACH( moddir ) + +SET( tomahawkSources ${tomahawkSources} + pipeline.cpp + playlist.cpp + pluginapi.cpp + query.cpp + result.cpp + source.cpp + sourcelist.cpp + + utils/tomahawkutils.cpp + jabber/jabber_p.cpp + + bufferiodevice.cpp + connection.cpp + msgprocessor.cpp + controlconnection.cpp + collection.cpp + filetransferconnection.cpp + dbsyncconnection.cpp + musicscanner.cpp + remotecollection.cpp + servent.cpp + scriptresolver.cpp + + database/fuzzyindex.cpp + database/database.cpp + database/databaseworker.cpp + database/databaseimpl.cpp + database/databaseresolver.cpp + database/databasecommand.cpp + database/databasecommandloggable.cpp + database/databasecommand_resolve.cpp + database/databasecommand_alltracks.cpp + database/databasecommand_addfiles.cpp + database/databasecommand_dirmtimes.cpp + database/databasecommand_loadfile.cpp + database/databasecommand_addsource.cpp + database/databasecommand_sourceoffline.cpp + database/databasecommand_collectionstats.cpp + database/databasecommand_loadplaylistentries.cpp + database/databasecommand_modifyplaylist.cpp + database/databasecommand_setplaylistrevision.cpp + database/databasecommand_loadallplaylists.cpp + database/databasecommand_createplaylist.cpp + database/databasecommand_deleteplaylist.cpp + database/databasecommand_loadops.cpp + database/databasecommand_updatesearchindex.cpp + database/databasecollection.cpp + + web/api_v1.cpp + + widgetdragfilter.cpp + tomahawksettings.cpp + tomahawkapp.cpp + main.cpp +) + +SET( tomahawkSourcesGui ${tomahawkSourcesGui} + xspfloader.cpp + +# utils/modeltest.cpp + utils/animatedrowremover.cpp + utils/imagebutton.cpp + utils/progresstreeview.cpp + + audio/madtranscode.cpp + audio/audioengine.cpp + + playlist/playlistproxymodel.cpp + playlist/playlistmodel.cpp + playlist/playlistmodelworker.cpp + playlist/playlistitem.cpp + playlist/playlistitemdelegate.cpp + playlist/playlistview.cpp + + sourcetree/sourcesmodel.cpp + sourcetree/sourcetreeitem.cpp + sourcetree/sourcetreeitemwidget.cpp + sourcetree/sourcetreeview.cpp + + topbar/topbar.cpp + topbar/clearbutton.cpp + topbar/searchlineedit.cpp + topbar/lineedit.cpp + topbar/searchbutton.cpp + + tomahawkwindow.cpp + audiocontrols.cpp + settingsdialog.cpp + proxystyle.cpp +) + +SET( tomahawkHeaders ${tomahawkHeaders} + "${TOMAHAWK_INC_DIR}/tomahawk/tomahawkapp.h" + + "${TOMAHAWK_INC_DIR}/tomahawk/collection.h" + "${TOMAHAWK_INC_DIR}/tomahawk/pipeline.h" + "${TOMAHAWK_INC_DIR}/tomahawk/pluginapi.h" + "${TOMAHAWK_INC_DIR}/tomahawk/query.h" + "${TOMAHAWK_INC_DIR}/tomahawk/resolver.h" + "${TOMAHAWK_INC_DIR}/tomahawk/result.h" + "${TOMAHAWK_INC_DIR}/tomahawk/source.h" + "${TOMAHAWK_INC_DIR}/tomahawk/sourcelist.h" + + "${TOMAHAWK_INC_DIR}/tomahawk/artist.h" + "${TOMAHAWK_INC_DIR}/tomahawk/album.h" + "${TOMAHAWK_INC_DIR}/tomahawk/track.h" + "${TOMAHAWK_INC_DIR}/tomahawk/playlist.h" + + "${TOMAHAWK_INC_DIR}/tomahawk/functimeout.h" +# "${TOMAHAWK_INC_DIR}/tomahawk/tomahawkplugin.h" + + database/fuzzyindex.h + database/database.h + database/databaseworker.h + database/databaseimpl.h + database/databaseresolver.h + database/databasecommand.h + database/databasecommandloggable.h + database/databasecommand_resolve.h + database/databasecommand_alltracks.h + database/databasecommand_addfiles.h + database/databasecommand_dirmtimes.h + database/databasecommand_loadfile.h + database/databasecommand_addsource.h + database/databasecommand_sourceoffline.h + database/databasecommand_collectionstats.h + database/databasecommand_loadplaylistentries.h + database/databasecommand_modifyplaylist.h + database/databasecommand_setplaylistrevision.h + database/databasecommand_loadallplaylists.h + database/databasecommand_createplaylist.h + database/databasecommand_deleteplaylist.h + database/databasecommand_loadops.h + database/databasecommand_updatesearchindex.h + database/databasecollection.h + + jabber/jabber.h + jabber/jabber_p.h + + bufferiodevice.h + connection.h + msgprocessor.h + controlconnection.h + filetransferconnection.h + dbsyncconnection.h + musicscanner.h + tomahawkzeroconf.h + remotecollection.h + servent.h + scriptresolver.h + tomahawksettings.h + widgetdragfilter.h + + web/api_v1.h +) + +SET( tomahawkHeadersGui ${tomahawkHeadersGui} + xspfloader.h + +# utils/modeltest.h + utils/animatedcounterlabel.h + utils/animatedrowremover.h + utils/imagebutton.h + utils/progresstreeview.h + + audio/transcodeinterface.h + audio/madtranscode.h + audio/audioengine.h + + playlist/playlistproxymodel.h + playlist/playlistmodel.h + playlist/playlistmodelworker.h + playlist/playlistitem.h + playlist/playlistitemdelegate.h + playlist/playlistview.h + + sourcetree/sourcesmodel.h + sourcetree/sourcetreeitem.h + sourcetree/sourcetreeitemwidget.h + sourcetree/sourcetreeview.h + + topbar/topbar.h + topbar/clearbutton.h + topbar/searchlineedit.h + topbar/lineedit.h + topbar/lineedit_p.h + topbar/searchbutton.h + + tomahawkwindow.h + audiocontrols.h + settingsdialog.h +) + +SET( tomahawkUI ${tomahawkUI} + tomahawkwindow.ui + settingsdialog.ui + + audiocontrols.ui + sourcetree/sourcetreeitemwidget.ui + topbar/topbar.ui +) + +INCLUDE_DIRECTORIES( + . + ${TOMAHAWK_INC_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + + audio + database + playlist + sourcetree + topbar + utils + + ../rtaudio + ../alsa-playback + ../libportfwd/include + ../qxtweb-standalone/qxtweb/ + + /usr/include/taglib + /usr/local/include/taglib + /usr/local/include +) + +SET( OS_SPECIFIC_LINK_LIBRARIES "" ) + +IF( WIN32 ) + INCLUDE( "CMakeLists.win32.txt" ) +ENDIF( WIN32 ) +IF( UNIX ) + INCLUDE( "CMakeLists.unix.txt" ) +ENDIF( UNIX ) +IF( APPLE ) + INCLUDE( "CMakeLists.osx.txt" ) +ENDIF( APPLE ) +IF( UNIX AND NOT APPLE ) + INCLUDE( "CMakeLists.linux.txt" ) +ENDIF( UNIX AND NOT APPLE ) + +qt4_add_resources( RC_SRCS "../resources.qrc" ) +qt4_wrap_cpp( tomahawkMoc ${tomahawkHeaders} ) + +SET( final_src ${final_src} ${tomahawkMoc} ${tomahawkSources} ${tomahawkHeaders} ) + +IF( "${gui}" STREQUAL "no" ) +ELSE() + qt4_wrap_ui( tomahawkUI_H ${tomahawkUI} ) + qt4_wrap_cpp( tomahawkMocGui ${tomahawkHeadersGui} ) + SET( final_src ${final_src} ${tomahawkUI_H} ${tomahawkMocGui} ${tomahawkSourcesGui} ${RC_SRCS} ) +ENDIF() + +IF( UNIX AND NOT APPLE ) + ADD_EXECUTABLE( tomahawk ${final_src} ) +ENDIF( UNIX AND NOT APPLE ) +IF( APPLE ) + ADD_EXECUTABLE( tomahawk MACOSX_BUNDLE ${final_src} ) +ENDIF( APPLE ) +IF( WIN32 ) + ADD_EXECUTABLE( tomahawk ${final_src} ) +ENDIF( WIN32 ) + +MESSAGE( STATUS "OS_SPECIFIC_LINK_LIBRARIES: ${OS_SPECIFIC_LINK_LIBRARIES}" ) + +TARGET_LINK_LIBRARIES( tomahawk + ${QT_LIBRARIES} + ${MAC_EXTRA_LIBS} + ${OS_SPECIFIC_LINK_LIBRARIES} + portfwd +) + +INCLUDE( "CPack.txt" ) diff --git a/src/CMakeLists.unix.txt b/src/CMakeLists.unix.txt new file mode 100644 index 000000000..097a08b3c --- /dev/null +++ b/src/CMakeLists.unix.txt @@ -0,0 +1,28 @@ +ADD_DEFINITIONS( -ggdb ) +ADD_DEFINITIONS( -Wall ) +ADD_DEFINITIONS( -g ) +ADD_DEFINITIONS( -fno-operator-names ) +ADD_DEFINITIONS( -fPIC ) + +SET( GLOOX_LIBS ${GLOOX_LIBS} resolv gloox.a ) + +SET( OS_SPECIFIC_LINK_LIBRARIES + ${LIBLASTFM_LIBRARY} + ${GLOOX_LIBS} + qxtweb-standalone + qjson + tag +) + +IF( "${gui}" STREQUAL "no" ) +ELSE() + SET( OS_SPECIFIC_LINK_LIBRARIES + ${OS_SPECIFIC_LINK_LIBRARIES} + mad + vorbisfile + ogg + ) + + SET( tomahawkSourcesGui ${tomahawkSourcesGui} audio/vorbistranscode.cpp scrobbler.cpp ) + SET( tomahawkHeadersGui ${tomahawkHeadersGui} audio/vorbistranscode.h scrobbler.h ) +ENDIF() \ No newline at end of file diff --git a/src/CMakeLists.win32.txt b/src/CMakeLists.win32.txt new file mode 100644 index 000000000..50d80e0f7 --- /dev/null +++ b/src/CMakeLists.win32.txt @@ -0,0 +1,51 @@ +ADD_DEFINITIONS( /DNOMINMAX ) +ADD_DEFINITIONS( /DWIN32_LEAN_AND_MEAN ) +ADD_DEFINITIONS( -static-libgcc ) +ADD_DEFINITIONS( -DNO_LIBLASTFM ) +ADD_DEFINITIONS( -DNO_OGG ) + +# Add manual locations to stuff: +INCLUDE_DIRECTORIES( + ../../libmad-0.15.1b + ../../boost_1_43_0 + ../../gloox-1.0 + ../../qjson + ../../liblastfm/_include + ../../taglib-1.6.3/ + ../../taglib-1.6.3/build + ../../taglib-1.6.3/taglib + ../../taglib-1.6.3/taglib/toolkit +) + +SET( OS_SPECIFIC_LINK_LIBRARIES + "${CMAKE_CURRENT_SOURCE_DIR}/../../gloox-1.0/src/.libs/libgloox.a" + "${CMAKE_CURRENT_SOURCE_DIR}/../../qjson/build/lib/libqjson.dll.a" + "${CMAKE_CURRENT_SOURCE_DIR}/../../taglib-1.6.3/build/taglib/libtag.dll" + "${CMAKE_CURRENT_SOURCE_DIR}/../../zlib-1.2.3/lib/libz.a" + "secur32.dll" + "Crypt32.dll" + "ws2_32.dll" + "dnsapi.dll" + "${CMAKE_CURRENT_SOURCE_DIR}/../qxtweb-standalone/libqxtweb-standalone.dll" +) + +SET( OS_SPECIFIC_LINK_LIBRARIES + ${OS_SPECIFIC_LINK_LIBRARIES} + "${CMAKE_CURRENT_SOURCE_DIR}/../admin/win/tomahawk.res" + ) + +IF( "${gui}" STREQUAL "no" ) +ELSE() + SET( tomahawkSourcesGui ${tomahawkSourcesGui} + audio/rtaudiooutput.cpp + ) + SET( tomahawkHeadersGui ${tomahawkHeadersGui} audio/rtaudiooutput.h ) + + SET( OS_SPECIFIC_LINK_LIBRARIES + ${OS_SPECIFIC_LINK_LIBRARIES} + "dsound.dll" + "winmm.dll" + "${CMAKE_CURRENT_SOURCE_DIR}/../rtaudio/librtaudio.dll" + "${CMAKE_CURRENT_SOURCE_DIR}/../admin/win/dlls/libmad.dll" + ) +ENDIF() \ No newline at end of file diff --git a/src/CPack.txt b/src/CPack.txt new file mode 100644 index 000000000..fac0a04c5 --- /dev/null +++ b/src/CPack.txt @@ -0,0 +1,70 @@ +# cd build ; cmake -DCPACK_GENERATOR=DEB .. ; make package + +INCLUDE(InstallRequiredSystemLibraries) + +set(CPACK_PACKAGING_INSTALL_PREFIX /usr/local) + +SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Tomahawk desktop player") +SET(CPACK_PACKAGE_NAME "tomahawk") +SET(CPACK_PACKAGE_VENDOR "tomahawk.org") +SET(CPACK_PACKAGE_CONTACT "Richard Jones") +SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "rj@tomahawk.org") + +SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../README") +SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE.txt") +SET(CPACK_PACKAGE_VERSION_MAJOR "0") +SET(CPACK_PACKAGE_VERSION_MINOR "1") +SET(CPACK_PACKAGE_VERSION_PATCH "3") +SET(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") + +#SET(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "i386") # Default: Output of dpkg --print-architecture or i386 +# Copied from generator script, needs to be set for inclusion into filename of package: +# $ dpkg --print-architecture +FIND_PROGRAM(DPKG_CMD dpkg) +IF(NOT DPKG_CMD) + MESSAGE(STATUS "Can not find dpkg in your path, default to i386.") + SET(CPACK_DEBIAN_PACKAGE_ARCHITECTURE i386) +ELSE(NOT DPKG_CMD) + EXECUTE_PROCESS(COMMAND "${DPKG_CMD}" --print-architecture + OUTPUT_VARIABLE CPACK_DEBIAN_PACKAGE_ARCHITECTURE + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +ENDIF(NOT DPKG_CMD) + +EXECUTE_PROCESS(COMMAND "date" "+%s" + OUTPUT_VARIABLE TIMEMARK + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + +# eg: tomahawk-i386-1.0.0 +SET(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}-${CPACK_PACKAGE_VERSION}_${TIMEMARK}") + +SET(CPACK_PACKAGE_INSTALL_DIRECTORY "CMake ${CMake_VERSION_MAJOR}.${CMake_VERSION_MINOR}") + +IF(WIN32 AND NOT UNIX) + ### +ELSE(WIN32 AND NOT UNIX) +# SET(CPACK_STRIP_FILES "tomahawk") +# SET(CPACK_SOURCE_STRIP_FILES "") +ENDIF(WIN32 AND NOT UNIX) + +# Nsis only? SET(CPACK_PACKAGE_EXECUTABLES "tomahawk" "tomahawk") + +#gnutls is in here because gloox needs it, and we link statically to gloox: +SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libqtgui4 (>=4:4.6.2-0ubuntu5), libtag1c2a (>=1.6.2-0ubuntu1), liblastfm-dev (>=0.4.0~git20090710-1), libqt4-sql-sqlite (>=4:4.6.2-0ubuntu5), libvorbis0a (>=1.2.3-3ubuntu1), libmad0 (>=0.15.1b-4ubuntu1), libasound2 (>=1.0.22-0ubuntu7), zlib1g (>=1:1.2.3.3.dfsg-15ubuntu1), libqjson-dev (>=0.7.1-1), libgnutls26 (>= 2.7.14-0)") + +#SET(CPACK_DEBIAN_PACKAGE_SECTION "music") + +INSTALL( + TARGETS tomahawk DESTINATION bin + RUNTIME DESTINATION bin +# LIBRARY DESTINATION lib +# ARCHIVE DESTINATION lib + ) + +INSTALL( + PROGRAMS ${CMAKE_BINARY_DIR}/tomahawk + DESTINATION bin + ) + +INCLUDE(CPack) diff --git a/src/audio/audioengine.cpp b/src/audio/audioengine.cpp new file mode 100644 index 000000000..10d5d91b2 --- /dev/null +++ b/src/audio/audioengine.cpp @@ -0,0 +1,387 @@ +#include "audioengine.h" + +#include +#include + +#include + +#include "madtranscode.h" +#ifndef NO_OGG +#include "vorbistranscode.h" +#endif + + +AudioEngine::AudioEngine() + : QThread() + , m_playlist( 0 ) + , m_i( 0 ) +{ + qDebug() << "Init AudioEngine"; + moveToThread( this ); + +#ifdef Q_WS_X11 + m_audio = new AlsaPlayback(); +#else + m_audio = new RTAudioOutput(); +#endif + connect( m_audio, SIGNAL( timeElapsed( unsigned int ) ), SLOT( timerTriggered( unsigned int ) ), Qt::DirectConnection ); + + start(); +} + + +AudioEngine::~AudioEngine() +{ + qDebug() << Q_FUNC_INFO << "waiting for event loop to finish.."; + quit(); + wait( 1000 ); + qDebug() << Q_FUNC_INFO << "ok"; + + m_input.clear(); + delete m_audio; +} + + +void +AudioEngine::play() +{ + qDebug() << Q_FUNC_INFO; + + if ( m_audio->isPaused() ) + { + QMutexLocker lock( &m_mutex ); + m_audio->resume(); + emit resumed(); + } + else + loadNextTrack(); +} + + +void +AudioEngine::pause() +{ + qDebug() << Q_FUNC_INFO; + QMutexLocker lock( &m_mutex ); + + m_audio->pause(); + emit paused(); +} + + +void +AudioEngine::stop() +{ + qDebug() << Q_FUNC_INFO; + QMutexLocker lock( &m_mutex ); + + if ( !m_input.isNull() ) + { + m_input->close(); + m_input.clear(); + } + + if ( !m_transcode.isNull() ) + m_transcode->clearBuffers(); + + m_audio->stopPlayback(); + + emit stopped(); +} + + +void +AudioEngine::previous() +{ + qDebug() << Q_FUNC_INFO; + loadPreviousTrack(); +} + + +void +AudioEngine::next() +{ + qDebug() << Q_FUNC_INFO; + loadNextTrack(); +} + + +void +AudioEngine::setVolume( int percentage ) +{ + //qDebug() << Q_FUNC_INFO; + + if ( percentage > 100 ) + percentage = 100; + if ( percentage < 0 ) + percentage = 0; + + m_audio->setVolume( percentage ); + emit volumeChanged( percentage ); +} + + +void +AudioEngine::onTrackAboutToClose() +{ + qDebug() << Q_FUNC_INFO; + // the only way the iodev we are reading from closes itself, is if + // there was a failure, usually network went away. + // but we might as well play the remaining data we received + // stop(); +} + + +bool +AudioEngine::loadTrack( PlaylistItem* item ) +{ + qDebug() << Q_FUNC_INFO << thread() << item; + bool err = false; + + // in a separate scope due to the QMutexLocker! + { + QMutexLocker lock( &m_mutex ); + QSharedPointer io; + + if ( !item ) + err = true; + else + { + m_currentTrack = item->query()->results().at( 0 ); + io = TomahawkApp::instance()->getIODeviceForUrl( m_currentTrack ); + + if ( !io || io.isNull() ) + { + qDebug() << "Error getting iodevice for item"; + err = true; + } + else + connect( io.data(), SIGNAL( aboutToClose() ), SLOT( onTrackAboutToClose() ), Qt::DirectConnection ); + } + + if ( !err ) + { + qDebug() << "Starting new song from url:" << m_currentTrack->url(); + emit loading( m_currentTrack ); + + if ( !m_input.isNull() ) + { + m_input->close(); + m_input.clear(); + } + + if ( !m_transcode.isNull() ) + { + m_transcode->clearBuffers(); + m_transcode.clear(); + } + + if ( m_currentTrack->mimetype() == "audio/mpeg" ) + { + m_transcode = QSharedPointer(new MADTranscode()); + } + #ifndef NO_OGG + else if ( m_currentTrack->mimetype() == "application/ogg" ) + { + m_transcode = QSharedPointer(new VorbisTranscode()); + } + #endif + else + qDebug() << "Could NOT find suitable transcoder! Stopping audio."; + + if ( !m_transcode.isNull() ) + { + connect( m_transcode.data(), SIGNAL( streamInitialized( long, int ) ), SLOT( setStreamData( long, int ) ), Qt::DirectConnection ); + m_input = io; + + m_audio->clearBuffers(); + if ( m_audio->isPaused() ) + m_audio->resume(); + } + } + } + + if ( err ) + { + stop(); + return false; + } + + // needs to be out of the mutexlocker scope + if ( m_transcode.isNull() ) + { + stop(); + emit error( AudioEngine::DecodeError ); + } + + return !m_transcode.isNull(); +} + + +void +AudioEngine::loadPreviousTrack() +{ + qDebug() << Q_FUNC_INFO; + + if ( !m_playlist ) + { + stop(); + return; + } + + loadTrack( m_playlist->previousItem() ); +} + + +void +AudioEngine::loadNextTrack() +{ + qDebug() << Q_FUNC_INFO; + + if ( !m_playlist ) + { + stop(); + return; + } + + loadTrack( m_playlist->nextItem() ); +} + + +void +AudioEngine::playItem( PlaylistModelInterface* model, PlaylistItem* item ) +{ + qDebug() << Q_FUNC_INFO; + m_playlist = model; + loadTrack( item ); +} + + +void +AudioEngine::onPlaylistActivated( PlaylistModelInterface* model ) +{ + qDebug() << Q_FUNC_INFO; + m_playlist = model; +} + + +void +AudioEngine::setStreamData( long sampleRate, int channels ) +{ + qDebug() << Q_FUNC_INFO << sampleRate << channels << thread(); + m_audio->initAudio( sampleRate, channels ); + if ( m_audio->startPlayback() ) + { + emit started( m_currentTrack ); + } + else + { + qDebug() << "Can't open device for audio output!"; + stop(); + emit error( AudioEngine::AudioDeviceError ); + } + + qDebug() << Q_FUNC_INFO << sampleRate << channels << "done"; +} + + +void +AudioEngine::timerTriggered( unsigned int seconds ) +{ + emit timerSeconds( seconds ); + + if ( m_currentTrack->duration() == 0 ) + { + emit timerPercentage( 0 ); + } + else + { + emit timerPercentage( (unsigned int)( seconds / m_currentTrack->duration() ) ); + } +} + + +void +AudioEngine::run() +{ + QTimer::singleShot( 0, this, SLOT( engineLoop() ) ); + exec(); + qDebug() << "AudioEngine event loop stopped"; +} + + +void +AudioEngine::engineLoop() +{ + qDebug() << "AudioEngine thread:" << this->thread(); + loop(); +} + + +void +AudioEngine::loop() +{ + m_i++; + //if( m_i % 500 == 0 ) qDebug() << Q_FUNC_INFO << thread(); + + { + QMutexLocker lock( &m_mutex ); + +/* if ( m_i % 200 == 0 ) + { + if ( !m_input.isNull() ) + qDebug() << "Outer audio loop" << m_input->bytesAvailable() << m_audio->needData(); + }*/ + + if ( m_i % 10 == 0 && m_audio->isPlaying() ) + m_audio->triggerTimers(); + + if( !m_transcode.isNull() && + !m_input.isNull() && + m_input->bytesAvailable() && + m_audio->needData() && + !m_audio->isPaused() ) + { + //if ( m_i % 50 == 0 ) + // qDebug() << "Inner audio loop"; + + if ( m_transcode->needData() > 0 ) + { + QByteArray encdata = m_input->read( 8192 ); //FIXME CONSTANT VALUE + m_transcode->processData( encdata, m_input->atEnd() ); + } + + if ( m_transcode->haveData() ) + { + QByteArray rawdata = m_transcode->data(); + m_audio->processData( rawdata ); + } + + QTimer::singleShot( 0, this, SLOT( loop() ) ); + return; + } + } + + unsigned int nextdelay = 50; + // are we cleanly at the end of a track, and ready for the next one? + if ( !m_input.isNull() && + m_input->atEnd() && + m_input->isOpen() && + !m_input->bytesAvailable() && + !m_audio->haveData() && + !m_audio->isPaused() ) + { + qDebug() << "Starting next track then"; + next(); + // will need data immediately: + nextdelay = 0; + } + else if ( !m_input.isNull() && !m_input->isOpen() ) + { + qDebug() << "AudioEngine IODev closed. errorString:" << m_input->errorString(); + next(); + nextdelay = 0; + } + + QTimer::singleShot( nextdelay, this, SLOT( loop() ) ); +} diff --git a/src/audio/audioengine.h b/src/audio/audioengine.h new file mode 100644 index 000000000..461a860db --- /dev/null +++ b/src/audio/audioengine.h @@ -0,0 +1,94 @@ +#ifndef AUDIOENGINE_H +#define AUDIOENGINE_H + +#include +#include +#include + +#include "tomahawk/playlistmodelinterface.h" +#include "tomahawk/result.h" +#include "tomahawk/typedefs.h" +#include "playlistmodel.h" +#include "playlistitem.h" + +#include "rtaudiooutput.h" +#include "alsaplayback.h" +#include "transcodeinterface.h" + +#define AUDIO_VOLUME_STEP 5 + +class AudioEngine : public QThread +{ +Q_OBJECT + +public: + enum AudioErrorCode { StreamReadError, AudioDeviceError, DecodeError }; + + explicit AudioEngine(); + ~AudioEngine(); + + unsigned int volume() { if ( m_audio ) return m_audio->volume() * 100.0; else return 0; }; // in percent + +signals: + void loading( const Tomahawk::result_ptr& track ); + void started( const Tomahawk::result_ptr& track ); + void stopped(); + void paused(); + void resumed(); + + void volumeChanged( int volume /* in percent */ ); + + void timerSeconds( unsigned int secondsElapsed ); + void timerPercentage( unsigned int percentage ); + + void error( AudioErrorCode errorCode ); + +public slots: + void play(); + void pause(); + void stop(); + + void previous(); + void next(); + + void setVolume( int percentage ); + void lowerVolume() { setVolume( volume() - AUDIO_VOLUME_STEP ); } + void raiseVolume() { setVolume( volume() + AUDIO_VOLUME_STEP ); } + void onVolumeChanged( float volume ) { emit volumeChanged( volume * 100 ); } + + void playItem( PlaylistModelInterface* model, PlaylistItem* item ); + void onPlaylistActivated( PlaylistModelInterface* model ); + + void onTrackAboutToClose(); + +private slots: + bool loadTrack( PlaylistItem* item ); + void loadPreviousTrack(); + void loadNextTrack(); + + void setStreamData( long sampleRate, int channels ); + void timerTriggered( unsigned int seconds ); + + void engineLoop(); + void loop(); + +private: + void run(); + + QSharedPointer m_input; + QSharedPointer m_transcode; + +#ifdef Q_WS_X11 + AlsaPlayback* m_audio; +#else + RTAudioOutput* m_audio; +#endif + + Tomahawk::result_ptr m_currentTrack; + PlaylistModelInterface* m_playlist; + QMutex m_mutex; + + int m_i; +}; + +#endif // AUDIOENGINE_H diff --git a/src/audio/madtranscode.cpp b/src/audio/madtranscode.cpp new file mode 100644 index 000000000..08e7e6b9c --- /dev/null +++ b/src/audio/madtranscode.cpp @@ -0,0 +1,225 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2007 by * + * Christian Muehlhaeuser, Last.fm Ltd * + * Erik Jaelevik, Last.fm Ltd * + * * + * 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 Steet, Fifth Floor, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "madtranscode.h" + +#include + +typedef struct audio_dither +{ + mad_fixed_t error[3]; + mad_fixed_t random; +} audio_dither; + + +/* fast 32-bit pseudo-random number generator */ +/* code from madplay */ +static inline unsigned long prng( unsigned long state ) +{ + return (state * 0x0019660dL + 0x3c6ef35fL) & 0xffffffffL; +} + + +/* dithers 24-bit output to 16 bits instead of simple rounding */ +/* code from madplay */ +static inline signed int dither( mad_fixed_t sample, audio_dither *dither ) +{ + unsigned int scalebits; + mad_fixed_t output, mask, random; + + enum + { + MIN = -MAD_F_ONE, + MAX = MAD_F_ONE - 1 + }; + + /* noise shape */ + sample += dither->error[0] - dither->error[1] + dither->error[2]; + + dither->error[2] = dither->error[1]; + dither->error[1] = dither->error[0] / 2; + + /* bias */ + output = sample + (1L << (MAD_F_FRACBITS + 1 - 16 - 1)); + + scalebits = MAD_F_FRACBITS + 1 - 16; + mask = (1L << scalebits) - 1; + + /* dither */ + random = prng(dither->random); + output += (random & mask) - (dither->random & mask); + + dither->random = random; + + /* clip */ + /* TODO: better clipping function */ + if (sample >= MAD_F_ONE) + sample = MAD_F_ONE - 1; + else if (sample < -MAD_F_ONE) + sample = -MAD_F_ONE; + if (output >= MAD_F_ONE) + output = MAD_F_ONE - 1; + else if (output < -MAD_F_ONE) + output = -MAD_F_ONE; + + /* quantize */ + output &= ~mask; + + /* error feedback */ + dither->error[0] = sample - output; + + /* scale */ + return output >> scalebits; +} + + +MADTranscode::MADTranscode() : + m_decodedBufferCapacity( 32 * 1024 ), + m_mpegInitialised( false ) +{ + qDebug() << "Initialising MAD Transcoding"; + + mad_stream_init( &stream ); + mad_frame_init( &frame ); + mad_synth_init( &synth ); + timer = mad_timer_zero; + last_timer = mad_timer_zero; +} + + +MADTranscode::~MADTranscode() +{ + qDebug() << Q_FUNC_INFO; + + mad_synth_finish( &synth ); + mad_frame_finish( &frame ); + mad_stream_finish( &stream ); +} + + +void +MADTranscode::processData( const QByteArray &buffer, bool finish ) +{ + static audio_dither left_dither, right_dither; + + int err = 0; + m_encodedBuffer.append( buffer ); + + while ( err == 0 && ( m_encodedBuffer.count() >= MP3_BUFFER || finish ) ) + { + mad_stream_buffer( &stream, (const unsigned char*)m_encodedBuffer.data(), m_encodedBuffer.count() ); + err = mad_frame_decode( &frame, &stream ); + + if ( stream.next_frame != 0 ) + { + size_t r = stream.next_frame - stream.buffer; + m_encodedBuffer.remove( 0, r ); + } + + if ( err ) + { +// if ( stream.error != MAD_ERROR_LOSTSYNC ) +// qDebug() << "libmad error:" << mad_stream_errorstr( &stream ); + + if ( !MAD_RECOVERABLE( stream.error ) ) + return; + + err = 0; + } + else + { + mad_timer_add( &timer, frame.header.duration ); + mad_synth_frame( &synth, &frame ); + + if ( !m_mpegInitialised ) + { + long sampleRate = synth.pcm.samplerate; + int channels = synth.pcm.channels; + + qDebug() << "madTranscode( Samplerate:" << sampleRate << "- Channels:" << channels << ")"; + + m_mpegInitialised = true; + emit streamInitialized( sampleRate, channels > 0 ? channels : 2 ); + } + + for ( int i = 0; i < synth.pcm.length; i++ ) + { + union PCMDATA + { + short i; + unsigned char b[2]; + } pcmData; + + pcmData.i = dither( synth.pcm.samples[0][i], &left_dither ); + m_decodedBuffer.append( pcmData.b[0] ); + m_decodedBuffer.append( pcmData.b[1] ); + + if ( synth.pcm.channels == 2 ) + { + pcmData.i = dither( synth.pcm.samples[1][i], &right_dither ); + m_decodedBuffer.append( pcmData.b[0] ); + m_decodedBuffer.append( pcmData.b[1] ); + } + } + + if ( timer.seconds != last_timer.seconds ) + emit timeChanged( timer.seconds ); + + last_timer = timer; + } + } +} + + +void +MADTranscode::onSeek( int seconds ) +{ + mad_timer_t t; + t.seconds = seconds; + t.fraction = 0; + + timer = mad_timer_zero; + mad_timer_add( &timer, t ); + + m_encodedBuffer.clear(); + m_decodedBuffer.clear(); +} + + +void +MADTranscode::clearBuffers() +{ + mad_synth_finish( &synth ); + mad_frame_finish( &frame ); + mad_stream_finish( &stream ); + + m_mpegInitialised = false; + timer = mad_timer_zero; + last_timer = mad_timer_zero; + + m_encodedBuffer.clear(); + m_decodedBuffer.clear(); + + mad_stream_init( &stream ); + mad_frame_init( &frame ); + mad_synth_init( &synth ); +} + diff --git a/src/audio/madtranscode.h b/src/audio/madtranscode.h new file mode 100644 index 000000000..218529b2b --- /dev/null +++ b/src/audio/madtranscode.h @@ -0,0 +1,80 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2007 by * + * Christian Muehlhaeuser, Last.fm Ltd * + * * + * 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 Steet, Fifth Floor, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +/*! \class MadTranscode + \brief Transcoding plugin for MP3 streams, using libmad. +*/ + +#ifndef MADTRANSCODE_H +#define MADTRANSCODE_H + +#include "transcodeinterface.h" + +#include "mad.h" + +#include +#include +#include +#include + +#define MP3_BUFFER 32768 + +class MADTranscode : public TranscodeInterface +{ + Q_OBJECT + + public: + MADTranscode(); + virtual ~MADTranscode(); + + const QStringList supportedTypes() const { QStringList l; l << "application/x-mp3" << "mp3"; return l; } + + int needData() { return MP3_BUFFER - m_encodedBuffer.count(); } + bool haveData() { return !m_decodedBuffer.isEmpty(); } + + QByteArray data() { QByteArray b = m_decodedBuffer; m_decodedBuffer.clear(); return b; } + + virtual void setBufferCapacity( int bytes ) { m_decodedBufferCapacity = bytes; } + int bufferSize() { return m_decodedBuffer.size(); } + + public slots: + virtual void clearBuffers(); + virtual void onSeek( int seconds ); + virtual void processData( const QByteArray& data, bool finish ); + + signals: + void streamInitialized( long sampleRate, int channels ); + void timeChanged( int seconds ); + + private: + QByteArray m_encodedBuffer; + QByteArray m_decodedBuffer; + int m_decodedBufferCapacity; + + bool m_mpegInitialised; + struct mad_decoder decoder; + struct mad_stream stream; + struct mad_frame frame; + struct mad_synth synth; + mad_timer_t timer; + mad_timer_t last_timer; +}; + +#endif diff --git a/src/audio/rtaudiooutput.cpp b/src/audio/rtaudiooutput.cpp new file mode 100644 index 000000000..6d043ebbe --- /dev/null +++ b/src/audio/rtaudiooutput.cpp @@ -0,0 +1,262 @@ +#include +#include +#include +#include + +#include "rtaudiooutput.h" + +#define BUFFER_SIZE 512 + +int +audioCallback( void *outputBuffer, void *inputBuffer, unsigned int bufferSize, double streamTime, RtAudioStreamStatus status, void* data_src ) +{ + RTAudioOutput* parent = (RTAudioOutput*)data_src; + QMutexLocker locker( parent->mutex() ); + + char* buffer = (char*)outputBuffer; + + if ( !buffer || bufferSize != BUFFER_SIZE ) + return 0; + + int bufs = bufferSize * 2 * parent->sourceChannels(); + memset( buffer, 0, bufs ); + + if ( parent->buffer()->size() >= bufs && !parent->isPaused() ) + { + // Apply volume scaling + for ( int i = 0; i < bufs / 2; i++ ) + { + union PCMDATA + { + short i; + unsigned char b[2]; + } pcmData; + + pcmData.b[0] = parent->buffer()->at( i * 2 ); + pcmData.b[1] = parent->buffer()->at( i * 2 + 1 ); + + float pcmValue = (float)pcmData.i * parent->volume(); + pcmData.i = (short)pcmValue; + + buffer[i * 2] = pcmData.b[0]; + buffer[i * 2 + 1] = pcmData.b[1]; + } + + parent->m_pcmCounter += bufs; + parent->buffer()->remove( 0, bufs ); + } + + return 0; +} + + +RTAudioOutput::RTAudioOutput() : + m_pcmCounter( 0 ), + m_audio( new RtAudio() ), + m_bufferEmpty( true ), + m_volume( 0.75 ), + m_paused( false ), + m_playing( false ), + m_bps( -1 ) +{ + qDebug() << Q_FUNC_INFO << m_audio->getCurrentApi(); + devices(); +} + + +RTAudioOutput::~RTAudioOutput() +{ + qDebug() << Q_FUNC_INFO; +} + + +QStringList +RTAudioOutput::soundSystems() +{ + QStringList l; + + #ifdef WIN32 + l << "DirectSound"; + #endif + + #ifdef Q_WS_X11 + l << "Alsa"; + #endif + + #ifdef Q_WS_MAC + l << "CoreAudio"; + #endif + + return l; +} + + +QStringList +RTAudioOutput::devices() +{ + qDebug() << Q_FUNC_INFO; + QStringList l; + + try + { + qDebug() << "Device nums:" << m_audio->getDeviceCount(); + + for ( unsigned int i = 0; i < m_audio->getDeviceCount(); i++ ) + { + RtAudio::DeviceInfo info; + info = m_audio->getDeviceInfo( i ); + qDebug() << "Device found:" << i << QString::fromStdString( info.name ) << info.outputChannels << info.duplexChannels << info.isDefaultOutput; + + if ( info.outputChannels > 0 ) + l << QString::fromStdString( info.name ); // FIXME make it utf8 compatible + } + } + catch ( RtError &error ) + { + } + + return l; +} + + +bool +RTAudioOutput::startPlayback() +{ + qDebug () << Q_FUNC_INFO; + + if ( m_audio->isStreamOpen() ) + { + m_audio->startStream(); + m_playing = true; + } + + return m_audio->isStreamOpen(); +} + + +void +RTAudioOutput::stopPlayback() +{ + qDebug() << Q_FUNC_INFO; + QMutexLocker locker( &m_mutex ); + + delete m_audio; // FIXME + m_audio = new RtAudio(); + m_buffer.clear(); + m_paused = false; + m_playing = false; + m_bps = -1; + m_pcmCounter = 0; +} + + +void +RTAudioOutput::initAudio( long sampleRate, int channels ) +{ + qDebug() << Q_FUNC_INFO << sampleRate << channels; + QMutexLocker locker( &m_mutex ); + try + { + delete m_audio; + m_audio = new RtAudio(); + m_bps = sampleRate * channels * 2; + m_pcmCounter = 0; + + RtAudio::StreamParameters parameters; + parameters.deviceId = m_audio->getDefaultOutputDevice(); + parameters.nChannels = channels; + parameters.firstChannel = 0; + unsigned int bufferFrames = BUFFER_SIZE; + + RtAudio::StreamOptions options; + options.numberOfBuffers = 32; + //options.flags = RTAUDIO_SCHEDULE_REALTIME; + + m_sourceChannels = channels; + m_buffer.clear(); + +/* if ( m_audio->isStreamRunning() ) + m_audio->abortStream(); + + if ( m_audio->isStreamOpen() ) + m_audio->closeStream();*/ + + m_audio->openStream( ¶meters, NULL, RTAUDIO_SINT16, sampleRate, &bufferFrames, &audioCallback, this, &options ); + } + catch ( RtError &error ) + { + qDebug() << "Starting stream failed. RtAudio error type: " << error.getType(); + } +} + + +bool +RTAudioOutput::needData() +{ + if ( m_buffer.isEmpty() && !m_bufferEmpty ) + { + m_bufferEmpty = true; + emit bufferEmpty(); + } + + return ( m_buffer.size() < 65535 ); // FIXME constant value +} + + +void +RTAudioOutput::processData( const QByteArray &buffer ) +{ + QMutexLocker locker( &m_mutex ); + + m_buffer.append( buffer ); + if ( m_bufferEmpty && !buffer.isEmpty() ) + { + m_bufferEmpty = false; + emit bufferFull(); + } +} + + +void +RTAudioOutput::clearBuffers() +{ + qDebug() << Q_FUNC_INFO; + QMutexLocker locker( &m_mutex ); + + m_buffer.clear(); + m_bufferEmpty = true; + emit bufferEmpty(); +} + + +int +RTAudioOutput::internalSoundCardID( int settingsID ) +{ + if ( settingsID < 0 ) + settingsID = 0; + + try + { + int card = 0; + + for ( unsigned int i = 1; i <= m_audio->getDeviceCount(); i++ ) + { + RtAudio::DeviceInfo info; + info = m_audio->getDeviceInfo( i ); + if ( info.outputChannels > 0 ) + { + if ( card++ == settingsID ) + return i; + } + } + } + catch ( RtError &error ) + { + } + + #ifdef Q_WS_MAC + return 3; // FIXME? + #endif + return 1; +} + diff --git a/src/audio/rtaudiooutput.h b/src/audio/rtaudiooutput.h new file mode 100644 index 000000000..1b4089fe3 --- /dev/null +++ b/src/audio/rtaudiooutput.h @@ -0,0 +1,71 @@ +#ifndef RTAUDIOPLAYBACK_H +#define RTAUDIOPLAYBACK_H + +#include "RtAudio.h" + +#include +#include + +class RTAudioOutput : public QObject +{ + Q_OBJECT + + public: + RTAudioOutput(); + ~RTAudioOutput(); + + void initAudio( long sampleRate, int channels ); + + float volume() { return m_volume; } + bool isPaused() { return m_paused; } + virtual bool isPlaying() { return m_playing; } + + bool haveData() { return m_buffer.length() > 0; } + bool needData(); + void processData( const QByteArray &buffer ); + + QStringList soundSystems(); + QStringList devices(); + int sourceChannels() { return m_sourceChannels; } + + QMutex* mutex() { return &m_mutex; } + QByteArray* buffer() { return &m_buffer; } + + int m_pcmCounter; + + public slots: + void clearBuffers(); + + bool startPlayback(); + void stopPlayback(); + + void pause() { m_paused = true; } + void resume() { m_paused = false; } + + void setVolume( int volume ) { m_volume = ((float)(volume)) / (float)100.0; emit volumeChanged( m_volume ); } + virtual void triggerTimers() { if ( m_bps > 0 ) emit timeElapsed( m_pcmCounter / m_bps ); else emit timeElapsed( 0 ); } + + signals: + void bufferEmpty(); + void bufferFull(); + + void volumeChanged( float volume ); + void timeElapsed( unsigned int seconds ); + + private: + RtAudio *m_audio; + bool m_bufferEmpty; + + float m_volume; + QByteArray m_buffer; + QMutex m_mutex; + + int m_sourceChannels; + bool m_paused; + bool m_playing; + int m_bps; + + int internalSoundCardID( int settingsID ); +}; + +#endif diff --git a/src/audio/transcodeinterface.h b/src/audio/transcodeinterface.h new file mode 100644 index 000000000..9b42d83af --- /dev/null +++ b/src/audio/transcodeinterface.h @@ -0,0 +1,32 @@ +#ifndef TRANSCODEINTERFACE_H +#define TRANSCODEINTERFACE_H + +#include +#include +#include +#include + +class TranscodeInterface : public QObject +{ + Q_OBJECT + + public: + virtual ~TranscodeInterface() {} + + virtual const QStringList supportedTypes() const = 0; + + virtual int needData() = 0; + virtual bool haveData() = 0; + + virtual QByteArray data() = 0; + +// virtual void setBufferCapacity( int bytes ) = 0; +// virtual int bufferSize() = 0; + + public slots: + virtual void clearBuffers() = 0; + virtual void onSeek( int seconds ) = 0; + virtual void processData( const QByteArray& data, bool finish ) = 0; +}; + +#endif diff --git a/src/audio/vorbistranscode.cpp b/src/audio/vorbistranscode.cpp new file mode 100644 index 000000000..d7a6bf233 --- /dev/null +++ b/src/audio/vorbistranscode.cpp @@ -0,0 +1,140 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2006 by * + * Christian Muehlhaeuser, Last.fm Ltd * + * * + * 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 Steet, Fifth Floor, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "vorbistranscode.h" + + +size_t +vorbis_read( void* data_ptr, size_t byteSize, size_t sizeToRead, void* data_src ) +{ + VorbisTranscode* parent = (VorbisTranscode*)data_src; + QMutexLocker locker( parent->mutex() ); + + int r = byteSize * sizeToRead; + if ( r > parent->buffer()->size() ) + r = parent->buffer()->size(); + + memcpy( data_ptr, (char*)parent->buffer()->data(), r ); + parent->buffer()->remove( 0, r ); + + return r; +} + + +int +vorbis_seek( void* data_src, ogg_int64_t offset, int origin ) +{ + return -1; +} + + +int +vorbis_close( void* data_src ) +{ + // done ;-) + return 0; +} + + +long +vorbis_tell( void* data_src ) +{ + return -1; +} + + +VorbisTranscode::VorbisTranscode() + : m_vorbisInit( false ) +{ + qDebug() << Q_FUNC_INFO; +} + + +VorbisTranscode::~VorbisTranscode() +{ + qDebug() << Q_FUNC_INFO; +} + + +void +VorbisTranscode::onSeek( int seconds ) +{ + QMutexLocker locker( &m_mutex ); + + m_buffer.clear(); + m_outBuffer.clear(); +} + + +void +VorbisTranscode::clearBuffers() +{ + QMutexLocker locker( &m_mutex ); + + m_vorbisInit = false; + m_buffer.clear(); + m_outBuffer.clear(); +} + + +void +VorbisTranscode::processData( const QByteArray& data, bool ) +{ + m_mutex.lock(); + m_buffer.append( data ); + m_mutex.unlock(); + + if ( !m_vorbisInit && m_buffer.size() >= OGG_BUFFER ) + { + ov_callbacks oggCallbacks; + + oggCallbacks.read_func = vorbis_read; + oggCallbacks.close_func = vorbis_close; + oggCallbacks.seek_func = vorbis_seek; + oggCallbacks.tell_func = vorbis_tell; + + ov_open_callbacks( this, &m_vorbisFile, 0, 0, oggCallbacks ); + m_vorbisInit = true; + + // Try to determine samplerate + vorbis_info* vi = ov_info( &m_vorbisFile, -1 ); + qDebug() << "vorbisTranscode( Samplerate:" << vi->rate << "Channels:" << vi->channels << ")"; + + emit streamInitialized( vi->rate, vi->channels ); + } + + long result = 1; + int currentSection = 0; + + while ( m_buffer.size() >= OGG_BUFFER && result > 0 ) + { + char tempBuffer[16384]; + result = ov_read( &m_vorbisFile, tempBuffer, sizeof( tempBuffer ), 0, 2, 1, ¤tSection ); + + if ( result > 0 ) + { + for ( int i = 0; i < ( result / 2 ); i++ ) + { + m_outBuffer.append( tempBuffer[i * 2] ); + m_outBuffer.append( tempBuffer[i * 2 + 1] ); + } + } + } +} diff --git a/src/audio/vorbistranscode.h b/src/audio/vorbistranscode.h new file mode 100644 index 000000000..076e04bc5 --- /dev/null +++ b/src/audio/vorbistranscode.h @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright (C) 2005 - 2006 by * + * Christian Muehlhaeuser, Last.fm Ltd * + * * + * 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 Steet, Fifth Floor, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +/*! \class VorbisTranscode + \brief Transcoding plugin for OGG/Vorbis streams. +*/ + +#ifndef VORBIS_TRANSCODE_H +#define VORBIS_TRANSCODE_H + +#include "transcodeinterface.h" + +#include +#include + +#include +#include +#include +#include + +// Must not be smaller than 8500 bytes! +#define OGG_BUFFER 8500 + +class VorbisTranscode : public TranscodeInterface +{ + Q_OBJECT + + public: + VorbisTranscode(); + ~VorbisTranscode(); + + const QStringList supportedTypes() const { QStringList l; l << "application/ogg" << "ogg"; return l; } + + int needData() { return OGG_BUFFER - m_buffer.count(); } + bool haveData() { return !m_outBuffer.isEmpty(); } + + QByteArray data() { QByteArray b = m_outBuffer; m_outBuffer.clear(); return b; } + + QMutex* mutex() { return &m_mutex; } + QByteArray* buffer() { return &m_buffer; } + + public slots: + void clearBuffers(); + void onSeek( int seconds ); + void processData( const QByteArray& data, bool finish ); + + signals: + void streamInitialized( long sampleRate, int channels ); + void timeChanged( int seconds ); + + private: + QByteArray m_outBuffer; + + QMutex m_mutex; + QByteArray m_buffer; + + OggVorbis_File m_vorbisFile; + bool m_vorbisInit; +}; + +#endif diff --git a/src/audiocontrols.cpp b/src/audiocontrols.cpp new file mode 100644 index 000000000..0831ad4dc --- /dev/null +++ b/src/audiocontrols.cpp @@ -0,0 +1,405 @@ +#include "audiocontrols.h" +#include "ui_audiocontrols.h" + +#include + +#include "tomahawk/tomahawkapp.h" +#include "utils/tomahawkutils.h" + +#include "audioengine.h" +#include "imagebutton.h" +#include "playlistproxymodel.h" +#include "playlistview.h" + +#define LASTFM_DEFAULT_COVER "http://cdn.last.fm/flatness/catalogue/noimage" + + +AudioControls::AudioControls( QWidget* parent ) + : QWidget( parent ) + , ui( new Ui::AudioControls ) + , m_repeatMode( PlaylistModelInterface::NoRepeat ) + , m_shuffled( false ) +{ + ui->setupUi( this ); + + ui->buttonAreaLayout->setSpacing( 2 ); + ui->trackLabelLayout->setSpacing( 3 ); + + QFont font( ui->artistTrackLabel->font() ); + font.setPixelSize( 12 ); + + ui->artistTrackLabel->setFont( font ); + ui->albumLabel->setFont( font ); + ui->timeLabel->setFont( font ); + ui->timeLeftLabel->setFont( font ); + + font.setPixelSize( 9 ); + ui->ownerLabel->setFont( font ); + + ui->prevButton->setPixmap( RESPATH "images/back-rest.png" ); + ui->prevButton->setPixmap( RESPATH "images/back-pressed.png", QIcon::Off, QIcon::Active ); + ui->playPauseButton->setPixmap( RESPATH "images/play-rest.png" ); + ui->playPauseButton->setPixmap( RESPATH "images/play-pressed.png", QIcon::Off, QIcon::Active ); + ui->pauseButton->setPixmap( RESPATH "images/pause-rest.png" ); + ui->pauseButton->setPixmap( RESPATH "images/pause-pressed.png", QIcon::Off, QIcon::Active ); + ui->nextButton->setPixmap( RESPATH "images/skip-rest.png" ); + ui->nextButton->setPixmap( RESPATH "images/skip-pressed.png", QIcon::Off, QIcon::Active ); + ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-rest.png" ); + ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-pressed.png", QIcon::Off, QIcon::Active ); + ui->repeatButton->setPixmap( RESPATH "images/repeat-off-rest.png" ); + ui->repeatButton->setPixmap( RESPATH "images/repeat-off-pressed.png", QIcon::Off, QIcon::Active ); + ui->volumeLowButton->setPixmap( RESPATH "images/volume-icon-muted.png" ); + ui->volumeHighButton->setPixmap( RESPATH "images/volume-icon-full.png" ); + + ui->ownerLabel->setForegroundRole( QPalette::Dark ); + ui->metadataArea->setStyleSheet( "QWidget#metadataArea {\nborder-width: 4px;\nborder-image: url(" RESPATH "images/now-playing-panel.png) 4 4 4 4 stretch stretch; }" ); + + ui->seekSlider->setFixedHeight( 20 ); + ui->seekSlider->setEnabled( false ); + ui->seekSlider->setStyleSheet( "QSlider::groove::horizontal {" + "margin: 5px; border-width: 3px;" + "border-image: url(" RESPATH "images/seek-slider-bkg.png) 3 3 3 3 stretch stretch;" + "}" + + "QSlider::handle::horizontal {" + "margin-left: 5px; margin-right: -5px; " + "width: 0px;" + + //"margin-bottom: -1px; margin-top: -1px; " + //"height: 17px; width: 6px; " + //"background-image: url(" RESPATH "images/seek-and-volume-knob-rest.png);" + //"background-repeat: no-repeat;" + "}" + + "QSlider::sub-page:horizontal {" + "margin: 5px; border-width: 3px;" + "border-image: url(" RESPATH "images/seek-slider-level.png) 3 3 3 3 stretch stretch;" + "}" + ); + + ui->volumeSlider->setFixedHeight( 20 ); + ui->volumeSlider->setRange( 0, 100 ); + ui->volumeSlider->setValue( APP->audioEngine()->volume() ); + ui->volumeSlider->setStyleSheet( "QSlider::groove::horizontal {" + "margin: 5px; border-width: 3px;" + "border-image: url(" RESPATH "images/volume-slider-bkg.png) 3 3 3 3 stretch stretch;}" + + "QSlider::sub-page:horizontal {" + "margin: 5px; border-width: 3px;" + "border-image: url(" RESPATH "images/seek-slider-level.png) 3 3 3 3 stretch stretch;" + "}" + + "QSlider::handle::horizontal {" + "margin-left: 0px; margin-right: 0px;" + "margin-bottom: -1px; margin-top: -1px; " + "height: 17px; width: 6px; " + "background-image: url(" RESPATH "images/seek-and-volume-knob-rest.png);" + "background-repeat: no-repeat;" + "}" + + ); + +/* m_playAction = new QAction( this ); + m_pauseAction = new QAction( this ); + m_prevAction = new QAction( this ); + m_nextAction = new QAction( this ); + + connect( m_playAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( play() ) ); + connect( m_pauseAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( pause() ) ); + connect( m_prevAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( previous() ) ); + connect( m_nextAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( next() ) ); */ + + connect( ui->volumeSlider, SIGNAL( valueChanged( int ) ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( setVolume( int ) ) ); + connect( ui->prevButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( previous() ) ); + connect( ui->playPauseButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( play() ) ); + connect( ui->pauseButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( pause() ) ); + connect( ui->nextButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( next() ) ); + connect( ui->volumeLowButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( lowerVolume() ) ); + connect( ui->volumeHighButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( raiseVolume() ) ); + + connect( ui->repeatButton, SIGNAL( clicked() ), SLOT( onRepeatClicked() ) ); + connect( ui->shuffleButton, SIGNAL( clicked() ), SLOT( onShuffleClicked() ) ); + + // + connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( loading( const Tomahawk::result_ptr& ) ), SLOT( onPlaybackLoading( const Tomahawk::result_ptr& ) ) ); + connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( started( const Tomahawk::result_ptr& ) ), SLOT( onPlaybackStarted( const Tomahawk::result_ptr& ) ) ); + connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( paused() ), SLOT( onPlaybackPaused() ) ); + connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( resumed() ), SLOT( onPlaybackResumed() ) ); + connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( stopped() ), SLOT( onPlaybackStopped() ) ); + connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( timerSeconds( unsigned int ) ), SLOT( onPlaybackTimer( unsigned int ) ) ); + connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( volumeChanged( int ) ), SLOT( onVolumeChanged( int ) ) ); + + m_defaultCover = QPixmap( RESPATH "images/no-album-art-placeholder.png" ) + .scaled( ui->coverImage->size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation ); + + onPlaybackStopped(); // initial state +} + + +AudioControls::~AudioControls() +{ + delete ui; +} + + +void +AudioControls::changeEvent( QEvent* e ) +{ + QWidget::changeEvent( e ); + switch ( e->type() ) + { + case QEvent::LanguageChange: +// ui->retranslateUi( this ); + break; + + default: + break; + } +} + + +void +AudioControls::onVolumeChanged( int volume ) +{ + ui->volumeSlider->blockSignals( true ); + ui->volumeSlider->setValue( volume ); + ui->volumeSlider->blockSignals( false ); +} + + +void +AudioControls::onCoverArtDownloaded() +{ + if ( m_currentTrack.isNull() ) + return; + + QNetworkReply* reply = qobject_cast( sender() ); + QUrl redir = reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).toUrl(); + if ( redir.isEmpty() ) + { + const QByteArray ba = reply->readAll(); + if ( ba.length() ) + { + QPixmap pm; + pm.loadFromData( ba ); + + if ( pm.isNull() || reply->url().toString().startsWith( LASTFM_DEFAULT_COVER ) ) + ui->coverImage->setPixmap( m_defaultCover ); + else + ui->coverImage->setPixmap( pm.scaled( ui->coverImage->size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); + } + } + else + { +// qDebug() << "Following redirect to" << redir.toString(); + QNetworkRequest req( redir ); + QNetworkReply* reply = APP->nam()->get( req ); + connect( reply, SIGNAL( finished() ), SLOT( onCoverArtDownloaded() ) ); + } + + reply->deleteLater(); +} + + +void +AudioControls::onPlaybackStarted( const Tomahawk::result_ptr& result ) +{ + qDebug() << Q_FUNC_INFO; + + onPlaybackLoading( result ); + + QString imgurl = "http://ws.audioscrobbler.com/2.0/?method=album.imageredirect&artist=%1&album=%2&size=medium&api_key=7a90f6672a04b809ee309af169f34b8b"; + QNetworkRequest req( imgurl.arg( result->artist() ).arg( result->album() ) ); + QNetworkReply* reply = APP->nam()->get( req ); + connect( reply, SIGNAL( finished() ), SLOT( onCoverArtDownloaded() ) ); +} + + +void +AudioControls::onPlaybackLoading( const Tomahawk::result_ptr& result ) +{ + qDebug() << Q_FUNC_INFO; + + m_currentTrack = result; + + ui->artistTrackLabel->setText( QString( "%1 - %2" ).arg( result->artist() ).arg( result->track() ) ); + ui->albumLabel->setText( result->album() ); + ui->ownerLabel->setText( result->collection()->source()->friendlyName() ); + ui->coverImage->setPixmap( m_defaultCover ); + + if ( ui->timeLabel->text().isEmpty() ) + ui->timeLabel->setText( "00:00" ); + + if ( ui->timeLeftLabel->text().isEmpty() ) + ui->timeLeftLabel->setText( "-" + TomahawkUtils::timeToString( result->duration() ) ); + + ui->seekSlider->setRange( 0, m_currentTrack->duration() ); + ui->seekSlider->setVisible( true ); + +/* m_playAction->setEnabled( false ); + m_pauseAction->setEnabled( true ); */ + + ui->pauseButton->setEnabled( true ); + ui->pauseButton->setVisible( true ); + ui->playPauseButton->setVisible( false ); + ui->playPauseButton->setEnabled( false ); +} + + +void +AudioControls::onPlaybackPaused() +{ +/* m_pauseAction->setEnabled( false ); + m_playAction->setEnabled( true ); */ + + ui->pauseButton->setVisible( false ); + ui->pauseButton->setEnabled( false ); + ui->playPauseButton->setEnabled( true ); + ui->playPauseButton->setVisible( true ); +} + + +void +AudioControls::onPlaybackResumed() +{ +/* m_playAction->setEnabled( false ); + m_pauseAction->setEnabled( true ); */ + + ui->pauseButton->setVisible( true ); + ui->pauseButton->setEnabled( true ); + ui->playPauseButton->setVisible( false ); + ui->playPauseButton->setEnabled( false ); +} + + +void +AudioControls::onPlaybackStopped() +{ + m_currentTrack.clear(); + + ui->artistTrackLabel->setText( "" ); + ui->albumLabel->setText( "" ); + ui->ownerLabel->setText( "" ); + ui->timeLabel->setText( "" ); + ui->timeLeftLabel->setText( "" ); + ui->coverImage->setPixmap( QPixmap() ); + ui->seekSlider->setVisible( false ); + + ui->pauseButton->setVisible( false ); + ui->pauseButton->setEnabled( false ); + ui->playPauseButton->setEnabled( true ); + ui->playPauseButton->setVisible( true ); + +/* m_pauseAction->setEnabled( false ); + m_playAction->setEnabled( true ); */ +} + + +void +AudioControls::onPlaybackTimer( unsigned int seconds ) +{ + if ( m_currentTrack.isNull() ) + return; + + ui->timeLabel->setText( TomahawkUtils::timeToString( seconds ) ); + ui->timeLeftLabel->setText( "-" + TomahawkUtils::timeToString( m_currentTrack->duration() - seconds ) ); + ui->seekSlider->setValue( seconds ); +} + + +void +AudioControls::onRepeatModeChanged( PlaylistModelInterface::RepeatMode mode ) +{ + m_repeatMode = mode; + + switch ( m_repeatMode ) + { + case PlaylistModelInterface::NoRepeat: + { + // switch to RepeatOne + ui->repeatButton->setPixmap( RESPATH "images/repeat-off-rest.png" ); + ui->repeatButton->setPixmap( RESPATH "images/repeat-off-pressed.png", QIcon::Off, QIcon::Active ); + } + break; + + case PlaylistModelInterface::RepeatOne: + { + // switch to RepeatAll + ui->repeatButton->setPixmap( RESPATH "images/repeat-1-on-rest.png" ); + ui->repeatButton->setPixmap( RESPATH "images/repeat-1-on-pressed.png", QIcon::Off, QIcon::Active ); + } + break; + + case PlaylistModelInterface::RepeatAll: + { + // switch to NoRepeat + ui->repeatButton->setPixmap( RESPATH "images/repeat-all-on-rest.png" ); + ui->repeatButton->setPixmap( RESPATH "images/repeat-all-on-pressed.png", QIcon::Off, QIcon::Active ); + } + break; + + default: + break; + } +} + + +void +AudioControls::onRepeatClicked() +{ + switch ( m_repeatMode ) + { + case PlaylistModelInterface::NoRepeat: + { + // switch to RepeatOne + APP->playlistView()->model()->setRepeatMode( PlaylistModelInterface::RepeatOne ); + } + break; + + case PlaylistModelInterface::RepeatOne: + { + // switch to RepeatAll + APP->playlistView()->model()->setRepeatMode( PlaylistModelInterface::RepeatAll ); + } + break; + + case PlaylistModelInterface::RepeatAll: + { + // switch to NoRepeat + APP->playlistView()->model()->setRepeatMode( PlaylistModelInterface::NoRepeat ); + } + break; + + default: + break; + } +} + + +void +AudioControls::onShuffleModeChanged( bool enabled ) +{ + m_shuffled = enabled; + + if ( m_shuffled ) + { + ui->shuffleButton->setPixmap( RESPATH "images/shuffle-on-rest.png" ); + ui->shuffleButton->setPixmap( RESPATH "images/shuffle-on-pressed.png", QIcon::Off, QIcon::Active ); + + ui->repeatButton->setEnabled( false ); + } + else + { + ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-rest.png" ); + ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-pressed.png", QIcon::Off, QIcon::Active ); + + ui->repeatButton->setEnabled( true ); + } +} + + +void +AudioControls::onShuffleClicked() +{ + APP->playlistView()->model()->setShuffled( m_shuffled ^ true ); +} diff --git a/src/audiocontrols.h b/src/audiocontrols.h new file mode 100644 index 000000000..503878c8a --- /dev/null +++ b/src/audiocontrols.h @@ -0,0 +1,58 @@ +#ifndef AUDIOCONTROLS_H +#define AUDIOCONTROLS_H + +#include + +#include "tomahawk/playlistmodelinterface.h" +#include "tomahawk/result.h" + +namespace Ui +{ + class AudioControls; +} + +class AudioControls : public QWidget +{ +Q_OBJECT + +public: + AudioControls( QWidget* parent = 0 ); + ~AudioControls(); + +public slots: + void onRepeatModeChanged( PlaylistModelInterface::RepeatMode mode ); + void onShuffleModeChanged( bool enabled ); + +protected: + void changeEvent( QEvent* e ); + +private slots: + void onPlaybackStarted( const Tomahawk::result_ptr& result ); + void onPlaybackLoading( const Tomahawk::result_ptr& result ); + void onPlaybackPaused(); + void onPlaybackResumed(); + void onPlaybackStopped(); + + void onPlaybackTimer( unsigned int seconds ); + void onVolumeChanged( int volume ); + + void onRepeatClicked(); + void onShuffleClicked(); + void onCoverArtDownloaded(); + +private: + Ui::AudioControls *ui; + + QAction* m_playAction; + QAction* m_pauseAction; + QAction* m_prevAction; + QAction* m_nextAction; + + QPixmap m_defaultCover; + + Tomahawk::result_ptr m_currentTrack; + PlaylistModelInterface::RepeatMode m_repeatMode; + bool m_shuffled; +}; + +#endif // AUDIOCONTROLS_H diff --git a/src/audiocontrols.ui b/src/audiocontrols.ui new file mode 100644 index 000000000..215c52714 --- /dev/null +++ b/src/audiocontrols.ui @@ -0,0 +1,458 @@ + + + AudioControls + + + + 0 + 0 + 706 + 70 + + + + + 0 + 0 + + + + + 0 + 70 + + + + + 16777215 + 70 + + + + Form + + + + 1 + + + 0 + + + 1 + + + 0 + + + + + + 0 + 0 + + + + + 254 + 0 + + + + + 254 + 16777215 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Prev + + + + + + + Play + + + + + + + Pause + + + + + + + Next + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 16777215 + 66 + + + + + + + + 12 + + + 1 + + + 8 + + + 1 + + + + + + 0 + 0 + + + + + 49 + 48 + + + + + 49 + 48 + + + + Cover + + + false + + + Qt::AlignCenter + + + 0 + + + -1 + + + + + + + 8 + + + + + + 0 + + + 6 + + + 4 + + + 0 + + + + + + + Artist + + + + + + + Album + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 7 + + + + Owner + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + 0 + + + + + + + + + + + 0 + + + 0 + + + 6 + + + 0 + + + + + Time + + + + + + + Qt::Horizontal + + + + + + + Time Left + + + + + + + + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 200 + 66 + + + + + 2 + + + 1 + + + 2 + + + 1 + + + + + + 2 + + + 2 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Shuffle + + + + + + + + 0 + 0 + + + + Repeat + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 2 + + + 2 + + + + + + 0 + 0 + + + + Low + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + High + + + + + + + + + + + + + + ImageButton + QPushButton +
imagebutton.h
+
+
+ + +
diff --git a/src/bufferiodevice.cpp b/src/bufferiodevice.cpp new file mode 100644 index 000000000..17c4fbf30 --- /dev/null +++ b/src/bufferiodevice.cpp @@ -0,0 +1,108 @@ +#include +#include "bufferiodevice.h" + +BufferIODevice::BufferIODevice( unsigned int size, QObject *parent ) : + QIODevice( parent ), + m_size(size), + m_received(0) +{ +} + + +bool +BufferIODevice::open( OpenMode mode ) +{ + QMutexLocker lock( &m_mut ); + + qDebug() << Q_FUNC_INFO; + QIODevice::open( QIODevice::ReadWrite ); // FIXME? + return true; +} + + +void +BufferIODevice::close() +{ + QMutexLocker lock( &m_mut ); + + qDebug() << Q_FUNC_INFO; + QIODevice::close(); + // TODO ? +} + + +void +BufferIODevice::inputComplete( const QString& errmsg ) +{ + qDebug() << Q_FUNC_INFO; + setErrorString( errmsg ); + emit readChannelFinished(); +} + +void +BufferIODevice::addData( QByteArray ba ) +{ + writeData( ba.data(), ba.length() ); +} + + +qint64 +BufferIODevice::bytesAvailable() const +{ + QMutexLocker lock( &m_mut ); + return m_buffer.length(); +} + + +qint64 +BufferIODevice::readData( char * data, qint64 maxSize ) +{ + // qDebug() << Q_FUNC_INFO << maxSize; + + QMutexLocker lock( &m_mut ); +// qDebug() << "readData begins, bufersize:" << m_buffer.length(); + + qint64 size = maxSize; + if ( m_buffer.length() < maxSize ) + size = m_buffer.length(); + + memcpy( data, m_buffer.data(), size ); + m_buffer.remove( 0, size ); + +// qDebug() << "readData ends, bufersize:" << m_buffer.length(); + return size; +} + + +qint64 BufferIODevice::writeData( const char * data, qint64 maxSize ) +{ + { + QMutexLocker lock( &m_mut ); + m_buffer.append( data, maxSize ); + m_received += maxSize; + } + + emit bytesWritten( maxSize ); + emit readyRead(); + return maxSize; +} + + +qint64 BufferIODevice::size() const +{ + return m_size; +} + +bool BufferIODevice::atEnd() const +{ + QMutexLocker lock( &m_mut ); + return m_size == m_received && + m_buffer.length() == 0; +} + +void +BufferIODevice::clear() +{ + QMutexLocker lock( &m_mut ); + m_buffer.clear(); +} diff --git a/src/bufferiodevice.h b/src/bufferiodevice.h new file mode 100644 index 000000000..d54e98c9e --- /dev/null +++ b/src/bufferiodevice.h @@ -0,0 +1,39 @@ +#ifndef BUFFERIODEVICE_H +#define BUFFERIODEVICE_H + +#include +#include +#include + +class BufferIODevice : public QIODevice +{ +Q_OBJECT +public: + explicit BufferIODevice( unsigned int size = 0, QObject *parent = 0 ); + + virtual bool open( OpenMode mode ); + virtual void close(); + + virtual qint64 bytesAvailable() const; + virtual qint64 size() const; + virtual bool atEnd() const; + + void addData( QByteArray ba ); + void clear(); + + bool isOpen() const { qDebug() << "isOpen"; return true; } + OpenMode openMode() const { qDebug() << "openMode"; return QIODevice::ReadWrite; } + + void inputComplete( const QString& errmsg = "" ); + +protected: + virtual qint64 readData( char * data, qint64 maxSize ); + virtual qint64 writeData( const char * data, qint64 maxSize ); + +private: + QByteArray m_buffer; + mutable QMutex m_mut; //const methods need to lock + unsigned int m_size, m_received; +}; + +#endif // BUFFERIODEVICE_H diff --git a/src/collection.cpp b/src/collection.cpp new file mode 100644 index 000000000..7a271b189 --- /dev/null +++ b/src/collection.cpp @@ -0,0 +1,118 @@ +#include "tomahawk/collection.h" + +#include +#include + +#include "tomahawk/playlist.h" + +using namespace Tomahawk; + + +Collection::Collection( const source_ptr& source, const QString& name, QObject* parent ) + : QObject( parent ) + , m_name( name ) + , m_loaded( false ) + , m_lastmodified( 0 ) + , m_source( source ) +{ +// qDebug() << Q_FUNC_INFO; +} + + +Collection::~Collection() +{ + qDebug() << Q_FUNC_INFO; +} + + +void +Collection::invokeSlotTracks( QObject* obj, const char* slotname, + const QList& val, + collection_ptr collection ) +{ + qDebug() << Q_FUNC_INFO << obj << slotname; + QMetaObject::invokeMethod( obj, slotname, Qt::QueuedConnection, + Q_ARG( QList, val ), + Q_ARG( Tomahawk::collection_ptr, collection ) ); +} + + +QString +Collection::name() const +{ + return m_name; +} + + +void +Collection::addPlaylist( const Tomahawk::playlist_ptr& p ) +{ + qDebug() << Q_FUNC_INFO; + QList toadd; + toadd << p; + m_playlists.append( toadd ); + + qDebug() << Q_FUNC_INFO << "Collection name" << name() + << "from source id" << source()->id() + << "numplaylists:" << m_playlists.length(); + emit playlistsAdded( toadd ); +} + + +void +Collection::deletePlaylist( const Tomahawk::playlist_ptr& p ) +{ + qDebug() << Q_FUNC_INFO; + QList todelete; + todelete << p; + m_playlists.removeAll( p ); + + qDebug() << Q_FUNC_INFO << "Collection name" << name() + << "from source id" << source()->id() + << "numplaylists:" << m_playlists.length(); + emit playlistsDeleted( todelete ); +} + + +void +Collection::loadTracks( QObject* obj, const char* slotname ) +{ + if ( !obj ) + obj = this; + + boost::function< void( const QList&, Tomahawk::collection_ptr )> cb = + boost::bind( &Collection::invokeSlotTracks, this, obj, slotname, _1, _2 ); + loadAllTracks( cb ); +} + + +playlist_ptr +Collection::playlist( const QString& guid ) +{ + foreach( const playlist_ptr& pp, m_playlists ) + { + if( pp->guid() == guid ) + return pp; + } + + return playlist_ptr(); +} + + +bool +Collection::trackSorter( const QVariant& left, const QVariant& right ) +{ + int art = left.toMap().value( "artist" ).toString() + .localeAwareCompare( right.toMap().value( "artist" ).toString() ); + + if ( art == 0 ) + { + int trk = left.toMap().value( "track" ).toString() + .localeAwareCompare( right.toMap().value( "track" ).toString() ); + return trk < 0; + } + else + { + return art < 0; + } +} diff --git a/src/connection.cpp b/src/connection.cpp new file mode 100644 index 000000000..13fb28d9e --- /dev/null +++ b/src/connection.cpp @@ -0,0 +1,445 @@ +#include "connection.h" + +#include +#include + +#include "servent.h" + +#define PROTOVER "2" // must match remote peer, or we can't talk. + + +Connection::Connection( Servent* parent ) + : QObject() + , m_sock( 0 ) + , m_peerport( 0 ) + , m_servent( parent ) + , m_ready( false ) + , m_onceonly( true ) + , m_do_shutdown( false ) + , m_actually_shutting_down( false ) + , m_peer_disconnected( false ) + , m_tx_bytes( 0 ) + , m_tx_bytes_requested( 0 ) + , m_rx_bytes( 0 ) + , m_id( "Connection()" ) + , m_statstimer( 0 ) + , m_stats_tx_bytes_per_sec( 0 ) + , m_stats_rx_bytes_per_sec( 0 ) + , m_rx_bytes_last( 0 ) + , m_tx_bytes_last( 0 ) +{ + moveToThread( m_servent->thread() ); + qDebug() << "CTOR Connection (super)" << thread(); + + connect( &m_msgprocessor_out, SIGNAL( ready( msg_ptr ) ), + SLOT( sendMsg_now( msg_ptr ) ), Qt::QueuedConnection ); + + connect( &m_msgprocessor_in, SIGNAL( ready( msg_ptr ) ), + SLOT( handleMsg( msg_ptr ) ), Qt::QueuedConnection ); + + connect( &m_msgprocessor_in, SIGNAL( empty() ), + SLOT( handleIncomingQueueEmpty() ), Qt::QueuedConnection ); +} + + +Connection::~Connection() +{ + qDebug() << "DTOR connection (super)" << id() << thread(); + if( !m_sock.isNull() ) + { + qDebug() << "deleteLatering sock" << m_sock; + m_sock->deleteLater(); + } + else + { + qDebug() << "no valid sock to delete"; + } + delete m_statstimer; +} + + +void +Connection::handleIncomingQueueEmpty() +{ + //qDebug() << Q_FUNC_INFO << "bavail" << m_sock->bytesAvailable() + // << "isopen" << m_sock->isOpen() + // << "m_peer_disconnected" << m_peer_disconnected + // << "bytes rx" << bytesReceived(); + + if( m_sock->bytesAvailable() == 0 && m_peer_disconnected ) + { + qDebug() << "No more data to read, peer disconnected. shutting down connection." + << "bytesavail" << m_sock->bytesAvailable() + << "bytesrx" << m_rx_bytes; + shutdown(); + } +} + + +// convenience: +void +Connection::setFirstMessage( const QVariant& m ) +{ + QJson::Serializer ser; + const QByteArray ba = ser.serialize( m ); + //qDebug() << "first msg json len:" << ba.length(); + setFirstMessage( Msg::factory( ba, Msg::JSON ) ); +} + + +void +Connection::setFirstMessage( msg_ptr m ) +{ + m_firstmsg = m; + //qDebug() << id() << " first msg set to " << QString::fromAscii(m_firstmsg->payload()) + // << "msg len:" << m_firstmsg->length() ; +} + + +void +Connection::shutdown( bool waitUntilSentAll ) +{ + qDebug() << Q_FUNC_INFO << waitUntilSentAll; + if(m_do_shutdown) + { + //qDebug() << id() << " already shutting down"; + return; + } + + m_do_shutdown = true; + if( !waitUntilSentAll ) + { + qDebug() << "Shutting down immediately " << id(); + actualShutdown(); + } + else + { + qDebug() << "Shutting down after transfer complete " << id() + << "Actual/Desired" << m_tx_bytes << m_tx_bytes_requested; + + bytesWritten( 0 ); // trigger shutdown if we've already sent everything + // otherwise the bytesWritten slot will call actualShutdown() + // once all enqueued data has been properly written to the socket + } +} + + +void +Connection::actualShutdown() +{ + qDebug() << Q_FUNC_INFO; + if( m_actually_shutting_down ) + { + qDebug() << "(already actually shutting down)"; + return; + } + m_actually_shutting_down = true; + + if( !m_sock.isNull() && m_sock->isOpen() ) + { + m_sock->disconnectFromHost(); + } + + qDebug() << "EMITTING finished()"; + emit finished(); +} + + +void +Connection::markAsFailed() +{ + qDebug() << "Connection" << id() << "FAILED ***************" << thread(); + emit failed(); + shutdown(); +} + + +void +Connection::start( QTcpSocket* sock ) +{ + Q_ASSERT( m_sock.isNull() ); + Q_ASSERT( sock ); + Q_ASSERT( sock->isValid() ); + + m_sock = sock; + + if( m_name.isEmpty() ) + { + m_name = QString( "peer[%1]" ).arg( m_sock->peerAddress().toString() ); + } + + QTimer::singleShot( 0, this, SLOT( doSetup() ) ); +} + + +void +Connection::authCheckTimeout() +{ + if( m_ready ) + return; + + qDebug() << "Closing connection, not authed in time."; + shutdown(); +} + + +void +Connection::doSetup() +{ + qDebug() << Q_FUNC_INFO << thread(); + /* + New connections can be created from other thread contexts, such as + when AudioEngine calls getIODevice.. - we need to ensure that connections + and their associated sockets are running in the same thread as the servent. + + HINT: export QT_FATAL_WARNINGS=1 helps to catch these kind of errors. + */ + if( QThread::currentThread() != m_servent->thread() ) + { + // Connections should always be in the same thread as the servent. + qDebug() << "Fixing thead affinity..."; + moveToThread( m_servent->thread() ); + qDebug() << Q_FUNC_INFO << thread(); + } + + //stats timer calculates BW used by this connection + m_statstimer = new QTimer; + m_statstimer->moveToThread( this->thread() ); + m_statstimer->setInterval(1000); + connect( m_statstimer, SIGNAL( timeout() ), SLOT( calcStats() ) ); + m_statstimer->start(); + m_statstimer_mark.start(); + + m_sock->moveToThread( thread() ); + + qsrand( QTime( 0, 0, 0 ).secsTo( QTime::currentTime() ) ); + + connect( m_sock.data(), SIGNAL(bytesWritten(qint64)), + SLOT(bytesWritten(qint64)), Qt::QueuedConnection); + + connect( m_sock.data(), SIGNAL(disconnected()), + SLOT(socketDisconnected()), Qt::QueuedConnection); + + connect( m_sock.data(), SIGNAL(error(QAbstractSocket::SocketError)), + SLOT(socketDisconnectedError(QAbstractSocket::SocketError)), Qt::QueuedConnection ); + + connect( m_sock.data(), SIGNAL(readyRead()), + SLOT(readyRead()), Qt::QueuedConnection); + + // if connection not authed/setup fast enough, kill it: + QTimer::singleShot( AUTH_TIMEOUT, this, SLOT( authCheckTimeout() ) ); + + if( outbound() ) + { + Q_ASSERT( !m_firstmsg.isNull() ); + sendMsg( m_firstmsg ); + } + else + { + sendMsg( Msg::factory( PROTOVER, Msg::SETUP ) ); + } + + // call readyRead incase we missed the signal in between the servent disconnecting and us + // connecting to the signal - won't do anything if there are no bytesAvailable anyway. + readyRead(); +} + + +void +Connection::socketDisconnected() +{ + qDebug() << "SOCKET DISCONNECTED" << this->name() + << "shutdown will happen after incoming queue empties." + << "bytesavail:" << m_sock->bytesAvailable() + << "bytesRecvd" << bytesReceived(); + + m_peer_disconnected = true; + + emit socketClosed(); + + if( m_msgprocessor_in.length() == 0 && m_sock->bytesAvailable() == 0 ) + { + handleIncomingQueueEmpty(); + } +} + +void +Connection::socketDisconnectedError(QAbstractSocket::SocketError e) +{ + qDebug() << "SOCKET ERROR CODE" << e << this->name() << " CALLING Connection::shutdown(false)"; + m_peer_disconnected = true; + emit socketErrored(e); + shutdown(false); +} + + +QString +Connection::id() const +{ + return m_id; +} + + +void +Connection::setId( const QString& id ) +{ + m_id = id; +} + + +void +Connection::readyRead() +{ + //qDebug() << "readyRead, m_bs:" << m_bs << "bytesavail:" << m_sock->bytesAvailable(); + if( m_msg.isNull() ) + { + if( m_sock->bytesAvailable() < Msg::headerSize() ) + return; + + char msgheader[ Msg::headerSize() ]; + if( m_sock->read( (char*) &msgheader, Msg::headerSize() ) != Msg::headerSize() ) + { + qDebug() << "Failed reading msg header"; + this->markAsFailed(); + return; + } + + m_msg = Msg::begin( (char*) &msgheader ); + m_rx_bytes += Msg::headerSize(); + } + + if( m_sock->bytesAvailable() < m_msg->length() ) + return; + + QByteArray ba = m_sock->read( m_msg->length() ); + if( ba.length() != (qint32)m_msg->length() ) + { + qDebug() << "Failed to read full msg payload"; + this->markAsFailed(); + return; + } + m_msg->fill( ba ); + m_rx_bytes += ba.length(); + + handleReadMsg(); // process m_msg and clear() it + + // since there is no explicit threading, use the event loop to schedule this: + if( m_sock->bytesAvailable() ) + { + QTimer::singleShot( 0, this, SLOT( readyRead() ) ); + } +} + + +void +Connection::handleReadMsg() +{ + if( outbound() == false && + m_msg->is( Msg::SETUP ) && + m_msg->payload() == "ok" ) + { + m_ready = true; + qDebug() << "Connection" << id() << "READY"; + setup(); + emit ready(); + } + else if( !m_ready && + outbound() && + m_msg->is( Msg::SETUP ) ) + { + if( m_msg->payload() == PROTOVER ) + { + sendMsg( Msg::factory( "ok", Msg::SETUP ) ); + m_ready = true; + qDebug() << "Connection" << id() << "READY"; + setup(); + emit ready(); + } + else + { + sendMsg( Msg::factory( "{\"method\":\"protovercheckfail\"}", Msg::JSON | Msg::SETUP ) ); + shutdown( true ); + } + } + else + { + m_msgprocessor_in.append( m_msg ); + } + + m_msg.clear(); +} + + +void +Connection::sendMsg( QVariant j ) +{ + if( m_do_shutdown ) + return; + + QJson::Serializer serializer; + const QByteArray payload = serializer.serialize( j ); + sendMsg( Msg::factory( payload, Msg::JSON ) ); +} + + +void +Connection::sendMsg( msg_ptr msg ) +{ + if( m_do_shutdown ) + { + qDebug() << Q_FUNC_INFO << "SHUTTING DOWN, NOT SENDING msg flags:" + << (int)msg->flags() << "length:" << msg->length(); + return; + } + + m_tx_bytes_requested += msg->length() + Msg::headerSize(); + m_msgprocessor_out.append( msg ); +} + + +void +Connection::sendMsg_now( msg_ptr msg ) +{ + //qDebug() << Q_FUNC_INFO << thread() << QThread::currentThread(); + Q_ASSERT( QThread::currentThread() == thread() ); + Q_ASSERT( this->isRunning() ); + + if( m_sock.isNull() || !m_sock->isOpen() || !m_sock->isWritable() ) + { + qDebug() << "***** Socket problem, whilst in sendMsg(). Cleaning up. *****"; + shutdown( true ); + return; + } + + if( ! msg->write( m_sock.data() ) ) + { + //qDebug() << "Error writing to socket in sendMsg() *************"; + shutdown( false ); + return; + } +} + + +void +Connection::bytesWritten( qint64 i ) +{ + m_tx_bytes += i; + // if we are waiting to shutdown, and have sent all queued data, do actual shutdown: + if( m_do_shutdown && m_tx_bytes == m_tx_bytes_requested ) + actualShutdown(); +} + + +void +Connection::calcStats() +{ + int elapsed = m_statstimer_mark.restart(); // ms since last calc + + m_stats_tx_bytes_per_sec = (float)1000 * ( (m_tx_bytes - m_tx_bytes_last) / (float)elapsed ); + m_stats_rx_bytes_per_sec = (float)1000 * ( (m_rx_bytes - m_rx_bytes_last) / (float)elapsed ); + + m_rx_bytes_last = m_rx_bytes; + m_tx_bytes_last = m_tx_bytes; + + emit statsTick( m_stats_tx_bytes_per_sec, m_stats_rx_bytes_per_sec ); +} diff --git a/src/connection.h b/src/connection.h new file mode 100644 index 000000000..a771c6eba --- /dev/null +++ b/src/connection.h @@ -0,0 +1,129 @@ +#ifndef CONNECTION_H +#define CONNECTION_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "msg.h" +#include "msgprocessor.h" + +class Servent; + +class Connection : public QObject +{ +Q_OBJECT + +public: + + Connection( Servent* parent ); + virtual ~Connection(); + virtual Connection* clone() = 0; + + QString id() const; + void setId( const QString& ); + + void setFirstMessage( const QVariant& m ); + void setFirstMessage( msg_ptr m ); + msg_ptr firstMessage() const { return m_firstmsg; }; + + const QPointer& socket() { return m_sock; }; + + void setOutbound( bool o ) { m_outbound = o; }; + bool outbound() const { return m_outbound; } + + Servent* servent() { return m_servent; }; + + // get public port of remote peer: + int peerPort() { return m_peerport; }; + void setPeerPort( int p ) { m_peerport = p; }; + + void markAsFailed(); + + void setName( const QString& n ) { m_name = n; }; + QString name() const { return m_name; }; + + void setOnceOnly( bool b ) { m_onceonly = b; }; + bool onceOnly() const { return m_onceonly; }; + + bool isReady() const { return m_ready; } ; + bool isRunning() const { return m_sock != 0; } + + qint64 bytesSent() const { return m_tx_bytes; } + qint64 bytesReceived() const { return m_rx_bytes; } + + void setMsgProcessorModeOut( quint32 m ) { m_msgprocessor_out.setMode(m); } + void setMsgProcessorModeIn( quint32 m ) { m_msgprocessor_in.setMode(m); } + +signals: + void ready(); + void failed(); + void finished(); + void statsTick( qint64 tx_bytes_sec, qint64 rx_bytes_sec ); + void socketClosed(); + void socketErrored(QAbstractSocket::SocketError); + +protected: + virtual void setup() = 0; + +protected slots: + virtual void handleMsg( msg_ptr msg ) = 0; + +public slots: + virtual void start( QTcpSocket* sock ); + void sendMsg( QVariant ); + void sendMsg( msg_ptr ); + + void shutdown( bool waitUntilSentAll = false ); + +private slots: + void handleIncomingQueueEmpty(); + void sendMsg_now( msg_ptr ); + void socketDisconnected(); + void socketDisconnectedError(QAbstractSocket::SocketError); + void readyRead(); + void doSetup(); + void authCheckTimeout(); + void bytesWritten( qint64 ); + void calcStats(); + +protected: + QPointer m_sock; + int m_peerport; + msg_ptr m_msg; + QJson::Parser parser; + Servent* m_servent; + bool m_outbound, m_ready, m_onceonly; + msg_ptr m_firstmsg; + QString m_name; + +private: + void handleReadMsg(); + void actualShutdown(); + bool m_do_shutdown, m_actually_shutting_down, m_peer_disconnected; + qint64 m_tx_bytes, m_tx_bytes_requested; + qint64 m_rx_bytes; + QString m_id; + + QTimer* m_statstimer; + QTime m_statstimer_mark; + qint64 m_stats_tx_bytes_per_sec, m_stats_rx_bytes_per_sec; + qint64 m_rx_bytes_last, m_tx_bytes_last; + + MsgProcessor m_msgprocessor_in, m_msgprocessor_out; +}; + +#endif // CONNECTION_H diff --git a/src/controlconnection.cpp b/src/controlconnection.cpp new file mode 100644 index 000000000..e851bfa9d --- /dev/null +++ b/src/controlconnection.cpp @@ -0,0 +1,209 @@ +#include "controlconnection.h" + +#include "tomahawk/tomahawkapp.h" +#include "remotecollection.h" +#include "filetransferconnection.h" +#include "database.h" +#include "databasecommand_collectionstats.h" +#include "dbsyncconnection.h" + +using namespace Tomahawk; + + +ControlConnection::ControlConnection( Servent* parent ) + : Connection( parent ) + , m_dbsyncconn( 0 ) + , m_registered( false ) +{ + qDebug() << "CTOR controlconnection"; + setId("ControlConnection()"); + + // auto delete when connection closes: + connect( this, SIGNAL( finished() ), SLOT( deleteLater() ) ); + + this->setMsgProcessorModeIn( MsgProcessor::UNCOMPRESS_ALL | MsgProcessor::PARSE_JSON ); + this->setMsgProcessorModeOut( MsgProcessor::COMPRESS_IF_LARGE ); +} + + +ControlConnection::~ControlConnection() +{ + qDebug() << "DTOR controlconnection"; + m_servent->unregisterControlConnection(this); + if( m_dbsyncconn ) m_dbsyncconn->deleteLater(); +} + + +Connection* +ControlConnection::clone() +{ + ControlConnection * clone = new ControlConnection(servent()); + clone->setOnceOnly(onceOnly()); + clone->setName(name()); + return clone; +} + + +void +ControlConnection::setup() +{ + qDebug() << Q_FUNC_INFO << id() << name(); + // setup source and remote collection for this peer + m_source = source_ptr( new Source( id(), this ) ); + + if( Servent::isIPWhitelisted( m_sock->peerAddress() ) ) + { + // FIXME TODO blocking DNS lookup if LAN, slow/fails on windows? + QHostInfo i = QHostInfo::fromName( m_sock->peerAddress().toString() ); + if( i.hostName().length() ) + { + m_source->setFriendlyName( i.hostName() ); + } + } + else + { + m_source->setFriendlyName( QString( "%1" ).arg( name() ) ); + } + + // delay setting up collection/etc until source is synced. + // we need it DB synced so it has an ID + exists in DB. + connect( m_source.data(), SIGNAL( syncedWithDatabase() ), + SLOT( registerSource() ), Qt::QueuedConnection ); + + m_source->doDBSync(); +} + + +// source was synced to DB, set it up properly: +void +ControlConnection::registerSource() +{ + qDebug() << Q_FUNC_INFO; + Source * source = (Source*) sender(); + Q_ASSERT( source == m_source.data() ); + // .. but we'll use the shared pointer we've already made: + + collection_ptr coll( new RemoteCollection( m_source ) ); + m_source->addCollection( coll ); + TomahawkApp::instance()->sourcelist().add( m_source ); + + m_registered = true; + setupDbSyncConnection(); + m_servent->registerControlConnection(this); +} + + +void +ControlConnection::setupDbSyncConnection( bool ondemand ) +{ + if( m_dbsyncconn != NULL || ! m_registered ) + return; + + qDebug() << Q_FUNC_INFO << ondemand << m_source->id(); + Q_ASSERT( m_source->id() > 0 ); + + if( ! m_dbconnkey.isEmpty() ) + { + qDebug() << "Connecting to DBSync offer from peer..."; + m_dbsyncconn = new DBSyncConnection( m_servent, m_source ); + + connect( m_dbsyncconn, SIGNAL( finished() ), + m_dbsyncconn, SLOT( deleteLater() ) ); + + connect( m_dbsyncconn, SIGNAL( destroyed( QObject* ) ), + SLOT( dbSyncConnFinished( QObject* ) ), Qt::DirectConnection ); + + m_servent->createParallelConnection( this, m_dbsyncconn, m_dbconnkey ); + m_dbconnkey.clear(); + } + else if( !outbound() || ondemand ) // only one end makes the offer + { + qDebug() << "Offering a DBSync key to peer..."; + m_dbsyncconn = new DBSyncConnection( m_servent, m_source ); + + connect( m_dbsyncconn, SIGNAL( finished() ), + m_dbsyncconn, SLOT( deleteLater()) ); + + connect( m_dbsyncconn, SIGNAL( destroyed(QObject* ) ), + SLOT( dbSyncConnFinished( QObject* ) ), Qt::DirectConnection ); + + QString key = uuid(); + m_servent->registerOffer( key, m_dbsyncconn ); + QVariantMap m; + m.insert( "method", "dbsync-offer" ); + m.insert( "key", key ); + sendMsg( m ); + } +} + + +void +ControlConnection::dbSyncConnFinished( QObject* c ) +{ + qDebug() << Q_FUNC_INFO << "DBSync connection closed (for now)"; + if( (DBSyncConnection*)c == m_dbsyncconn ) + { + //qDebug() << "Setting m_dbsyncconn to NULL"; + m_dbsyncconn = NULL; + } +} + + +DBSyncConnection* +ControlConnection::dbSyncConnection() +{ + qDebug() << Q_FUNC_INFO; + if( m_dbsyncconn == NULL ) + setupDbSyncConnection( true ); + + return m_dbsyncconn; +} + + +void +ControlConnection::handleMsg( msg_ptr msg ) +{ + // if small and not compresed, print it out for debug + if( msg->length() < 1024 && !msg->is( Msg::COMPRESSED ) ) + { + qDebug() << id() << "got msg:" << QString::fromAscii( msg->payload() ); + } + + // All control connection msgs are JSON + if( !msg->is( Msg::JSON ) ) + { + Q_ASSERT( msg->is( Msg::JSON ) ); + markAsFailed(); + return; + } + + QVariantMap m = msg->json().toMap(); + if( !m.isEmpty() ) + { + if( m.value("conntype").toString() == "request-offer" ) + { + QString theirkey = m["key"].toString(); + QString ourkey = m["offer"].toString(); + servent()->reverseOfferRequest( this, ourkey, theirkey ); + } + else if( m.value( "method" ).toString() == "dbsync-offer" ) + { + m_dbconnkey = m.value( "key" ).toString() ; + setupDbSyncConnection(); + } + else if( m.value( "method" ) == "protovercheckfail" ) + { + qDebug() << "*** Remote peer protocol version mismatch, connection closed"; + shutdown( true ); + return; + } + else + { + qDebug() << id() << "Unhandled msg:" << QString::fromAscii( msg->payload() ); + } + + return; + } + + qDebug() << id() << "Invalid msg:" << QString::fromAscii(msg->payload()); +} diff --git a/src/controlconnection.h b/src/controlconnection.h new file mode 100644 index 000000000..2280cbce2 --- /dev/null +++ b/src/controlconnection.h @@ -0,0 +1,51 @@ +/* + One ControlConnection always remains open to each peer. + + They arrange connections/reverse connections, inform us + when the peer goes offline, and own+setup DBSyncConnections. + +*/ +#ifndef CONTROLCONNECTION_H +#define CONTROLCONNECTION_H + +#include "connection.h" +#include "servent.h" +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" + +class FileTransferSession; + +class ControlConnection : public Connection +{ +Q_OBJECT + +public: + explicit ControlConnection( Servent* parent = 0 ); + ~ControlConnection(); + Connection* clone(); + + DBSyncConnection* dbSyncConnection(); + +protected: + virtual void setup(); + +protected slots: + virtual void handleMsg( msg_ptr msg ); + +signals: + +private slots: + void dbSyncConnFinished( QObject* c ); + void registerSource(); + +private: + void setupDbSyncConnection( bool ondemand = false ); + + Tomahawk::source_ptr m_source; + DBSyncConnection* m_dbsyncconn; + + QString m_dbconnkey; + bool m_registered; +}; + +#endif // CONTROLCONNECTION_H diff --git a/src/database/README.txt b/src/database/README.txt new file mode 100644 index 000000000..8bbfd8a26 --- /dev/null +++ b/src/database/README.txt @@ -0,0 +1,22 @@ +To query or modify the database you must use a DatabaseCommand. +The DatabaseCommand objects are processed sequentially and asynchronously +by the DatabaseWorker. + +This means you need to dispatch the cmd, and connect to a finished signal. +There are no blocking DB calls in the application code, except in the +exec() method of a DatabaseCommand object. + +If you inherit DatabaseCommandLoggable, the command is serialized into the +oplog, so that peers can replay it against their cache of your database. + +For example, if you dispatch an addTracks DBCmd, after scanning a new album, +this will be serialized, and peers will replay it so that their cached version +of your collection is kept up to date. + +DBCmds have GUIDs, and are ordered by the 'id' in the oplog table. + +The last DBCmd GUID applied to your cache of a source's collection is stored +in the source table (the lastop field). + +The DBSyncConnection will ask for all ops newer than that GUID, and replay. + diff --git a/src/database/database.cpp b/src/database/database.cpp new file mode 100644 index 000000000..21110b542 --- /dev/null +++ b/src/database/database.cpp @@ -0,0 +1,50 @@ +#include "database.h" + + +Database::Database( const QString& dbname, QObject* parent ) + : QObject( parent ) + , m_impl( new DatabaseImpl( dbname, this ) ) + , m_workerRO( new DatabaseWorker( m_impl, this, false ) ) + , m_workerRW( new DatabaseWorker( m_impl, this, true ) ) +{ + m_workerRO->start(); + m_workerRW->start(); +} + + +Database::~Database() +{ + qDebug() << Q_FUNC_INFO; + + delete m_workerRW; + delete m_workerRO; + delete m_impl; +} + +void +Database::loadIndex() +{ + m_impl->loadIndex(); +} + +void +Database::enqueue( QSharedPointer lc ) +{ + if( lc->doesMutates() ) + { + //qDebug() << Q_FUNC_INFO << "RW" << lc->commandname(); + emit newJobRO( lc ); + } + else + { + //qDebug() << Q_FUNC_INFO << "RO" << lc->commandname(); + emit newJobRW( lc ); + } +} + + +const QString& +Database::dbid() const +{ + return m_impl->dbid(); +} diff --git a/src/database/database.h b/src/database/database.h new file mode 100644 index 000000000..4b388f223 --- /dev/null +++ b/src/database/database.h @@ -0,0 +1,48 @@ +#ifndef DATABASE_H +#define DATABASE_H + +#include +#include + +#include "databaseimpl.h" +#include "databasecommand.h" +#include "databaseworker.h" + +/* + This class is really a firewall/pimpl - the public functions of LibraryImpl + are the ones that operate on the database, without any locks. + + HOWEVER, we're using the command pattern to serialize access to the database + and provide an async api. You create a DatabaseCommand object, and add it to + the queue of work. There is a single thread responsible for exec'ing all + the commands, so sqlite only does one thing at a time. + + Update: 1 thread for mutates, one for readonly queries. +*/ +class Database : public QObject +{ +Q_OBJECT +public: + explicit Database( const QString& dbname, QObject* parent = 0 ); + ~Database(); + + const QString& dbid() const; + const bool indexReady() const { return m_indexReady; } + + void loadIndex(); + +signals: + void indexReady(); // search index + void newJobRO( QSharedPointer ); + void newJobRW( QSharedPointer ); + +public slots: + void enqueue( QSharedPointer lc ); + +private: + DatabaseImpl* m_impl; + DatabaseWorker *m_workerRO, *m_workerRW; + bool m_indexReady; +}; + +#endif // DATABASE_H diff --git a/src/database/databasecollection.cpp b/src/database/databasecollection.cpp new file mode 100644 index 000000000..ae4a2b58b --- /dev/null +++ b/src/database/databasecollection.cpp @@ -0,0 +1,75 @@ +#include "databasecollection.h" + +#include "tomahawk/tomahawkapp.h" +#include "database.h" +#include "databasecommand_alltracks.h" +#include "databasecommand_addfiles.h" +#include "databasecommand_loadallplaylists.h" + +using namespace Tomahawk; + + +DatabaseCollection::DatabaseCollection( const source_ptr& src, QObject* parent ) + : Collection( src, QString( "dbcollection:%1" ).arg( src->userName() ), parent ) +{ +} + + +void +DatabaseCollection::loadPlaylists() +{ + qDebug() << Q_FUNC_INFO; + // load our playlists + DatabaseCommand_LoadAllPlaylists* cmd = new DatabaseCommand_LoadAllPlaylists( source() ); + connect( cmd, SIGNAL( done( const QList& ) ), + SLOT( setPlaylists( const QList& ) ) ); + + TomahawkApp::instance()->database()->enqueue( + QSharedPointer( cmd ) + ); +} + + +void +DatabaseCollection::loadAllTracks( boost::function&, collection_ptr )> callback ) +{ + qDebug() << Q_FUNC_INFO << source()->userName(); + m_callback = callback; + DatabaseCommand_AllTracks* cmd = new DatabaseCommand_AllTracks( source() ); + connect( cmd, SIGNAL( done( const QList& ) ), + SLOT( callCallback( const QList& ) ) ); + + TomahawkApp::instance()->database()->enqueue( + QSharedPointer( cmd ) + ); +} + + +void +DatabaseCollection::addTracks( const QList &newitems ) +{ + qDebug() << Q_FUNC_INFO << newitems.length(); + DatabaseCommand_AddFiles* cmd = new DatabaseCommand_AddFiles( newitems, source() ); + TomahawkApp::instance()->database()->enqueue( + QSharedPointer( cmd ) + ); +} + + +void +DatabaseCollection::removeTracks( const QList &olditems ) +{ + // FIXME + Q_ASSERT( false ); + + // TODO RemoveTracks cmd, probably builds a temp table of all the URLs in + // olditems, then joins on that to batch-delete. +} + + +void +DatabaseCollection::callCallback( const QList& res ) +{ + qDebug() << Q_FUNC_INFO << res.length() << this->source()->collection().data(); + m_callback( res, this->source()->collection() ); +} diff --git a/src/database/databasecollection.h b/src/database/databasecollection.h new file mode 100644 index 000000000..405f83107 --- /dev/null +++ b/src/database/databasecollection.h @@ -0,0 +1,31 @@ +#ifndef DATABASECOLLECTION_H +#define DATABASECOLLECTION_H + +#include "tomahawk/collection.h" +#include "tomahawk/typedefs.h" + +class DatabaseCollection : public Tomahawk::Collection +{ +Q_OBJECT + +public: + explicit DatabaseCollection( const Tomahawk::source_ptr& source, QObject* parent = 0 ); + ~DatabaseCollection() + { + qDebug() << Q_FUNC_INFO; + } + + virtual void loadAllTracks( boost::function&, Tomahawk::collection_ptr )> callback ); + virtual void loadPlaylists(); + +public slots: + virtual void addTracks( const QList &newitems ); + virtual void removeTracks( const QList &olditems ); + + void callCallback( const QList& res ); + +private: + boost::function&, Tomahawk::collection_ptr )> m_callback; +}; + +#endif // DATABASECOLLECTION_H diff --git a/src/database/databasecommand.cpp b/src/database/databasecommand.cpp new file mode 100644 index 000000000..f4cdfa658 --- /dev/null +++ b/src/database/databasecommand.cpp @@ -0,0 +1,83 @@ +#include "databasecommand.h" + +#include + +#include "databasecommand_addfiles.h" +#include "databasecommand_createplaylist.h" +#include "databasecommand_deleteplaylist.h" +#include "databasecommand_setplaylistrevision.h" + + +DatabaseCommand::DatabaseCommand( QObject* parent ) + : QObject( parent ) + , m_state( PENDING ) +{ + //qDebug() << Q_FUNC_INFO; +} + + +DatabaseCommand::DatabaseCommand( const source_ptr& src, QObject* parent ) + : QObject( parent ) + , m_state( PENDING ) + , m_source( src ) +{ + //qDebug() << Q_FUNC_INFO; +} + + +DatabaseCommand::~DatabaseCommand() +{ + //qDebug() << Q_FUNC_INFO; +} + + +void +DatabaseCommand::_exec( DatabaseImpl* lib ) +{ + //qDebug() << "RUNNING" << thread(); + m_state = RUNNING; + emit running(); + exec( lib ); + m_state=FINISHED; + //qDebug() << "FINISHED" << thread(); +} + + +DatabaseCommand* +DatabaseCommand::factory( const QVariant& op, const source_ptr& source ) +{ + const QString name = op.toMap().value( "command" ).toString(); + + if( name == "addfiles" ) + { + DatabaseCommand_AddFiles * cmd = new DatabaseCommand_AddFiles; + cmd->setSource( source ); + QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd ); + return cmd; + } + else if( name == "createplaylist" ) + { + DatabaseCommand_CreatePlaylist * cmd = new DatabaseCommand_CreatePlaylist; + cmd->setSource( source ); + QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd ); + return cmd; + } + else if( name == "deleteplaylist" ) + { + DatabaseCommand_DeletePlaylist * cmd = new DatabaseCommand_DeletePlaylist; + cmd->setSource( source ); + QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd ); + return cmd; + } + else if( name == "setplaylistrevision" ) + { + DatabaseCommand_SetPlaylistRevision * cmd = new DatabaseCommand_SetPlaylistRevision; + cmd->setSource( source ); + QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd ); + return cmd; + } + + qDebug() << "ERRROR in" << Q_FUNC_INFO; + Q_ASSERT( false ); + return NULL; +} diff --git a/src/database/databasecommand.h b/src/database/databasecommand.h new file mode 100644 index 000000000..9aa0fd020 --- /dev/null +++ b/src/database/databasecommand.h @@ -0,0 +1,82 @@ +#ifndef DATABASECOMMAND_H +#define DATABASECOMMAND_H + +#include +#include +#include +#include + +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" +#include "database/op.h" + +class DatabaseImpl; + +class DatabaseCommand : public QObject +{ +Q_OBJECT +Q_PROPERTY( QString guid READ guid WRITE setGuid ) + +public: + enum State { + PENDING = 0, + RUNNING = 1, + FINISHED = 2 + }; + + explicit DatabaseCommand( QObject* parent = 0 ); + explicit DatabaseCommand( const Tomahawk::source_ptr& src, QObject* parent = 0 ); + + DatabaseCommand( const DatabaseCommand &other ) + { + } + + virtual ~DatabaseCommand(); + + virtual QString commandname() const { return "DatabaseCommand"; } + virtual bool doesMutates() const { return true; } + State state() const { return m_state; } + + // if i make this pure virtual, i get compile errors in qmetatype.h. + // we need Q_DECLARE_METATYPE to use in queued sig/slot connections. + virtual void exec( DatabaseImpl* lib ) { Q_ASSERT( false ); } + + void _exec( DatabaseImpl* lib ); + + // stuff to do once transaction applied ok. + // Don't change the database from in here, duh. + void postCommit() { postCommitHook(); emit committed(); } + virtual void postCommitHook(){}; + + void setSource( const Tomahawk::source_ptr& s ) { m_source = s; } + const Tomahawk::source_ptr& source() const { return m_source; } + + virtual bool loggable() const { return false; } + + QString guid() const + { + if( m_guid.isEmpty() ) + m_guid = uuid(); + + return m_guid; + } + void setGuid( const QString& g ) { m_guid = g; } + + void emitFinished() { emit finished(); } + + static DatabaseCommand* factory( const QVariant& op, const Tomahawk::source_ptr& source ); + +signals: + void running(); + void finished(); + void committed(); + +private: + State m_state; + Tomahawk::source_ptr m_source; + mutable QString m_guid; +}; + +Q_DECLARE_METATYPE( DatabaseCommand ) + +#endif // DATABASECOMMAND_H diff --git a/src/database/databasecommand_addfiles.cpp b/src/database/databasecommand_addfiles.cpp new file mode 100644 index 000000000..227067f69 --- /dev/null +++ b/src/database/databasecommand_addfiles.cpp @@ -0,0 +1,174 @@ +#include "databasecommand_addfiles.h" + +#include + +#include "tomahawk/collection.h" +#include "tomahawk/tomahawkapp.h" +#include "database.h" +#include "databasecommand_collectionstats.h" +#include "databaseimpl.h" +#include "controlconnection.h" + +using namespace Tomahawk; + + +// remove file paths when making oplog/for network transmission +QVariantList +DatabaseCommand_AddFiles::files() const +{ + QVariantList list; + foreach( const QVariant& v, m_files ) + { + // replace url with the id, we don't leak file paths over the network. + QVariantMap m = v.toMap(); + m.remove( "url" ); + m.insert( "url", QString::number( m.value( "id" ).toInt() ) ); + list.append( m ); + } + return list; +} + + +// After changing a collection, we need to tell other bits of the system: +void +DatabaseCommand_AddFiles::postCommitHook() +{ + qDebug() << Q_FUNC_INFO; + + // make the collection object emit its tracksAdded signal, so the + // collection browser will update/fade in etc. + Collection* coll = source()->collection().data(); + + connect( this, SIGNAL( notify( const QList&, Tomahawk::collection_ptr ) ), + coll, SIGNAL( tracksAdded( const QList&, Tomahawk::collection_ptr ) ), + Qt::QueuedConnection ); + // do it like this so it gets called in the right thread: + emit notify( m_files, source()->collection() ); + + // also re-calc the collection stats, to updates the "X tracks" in the sidebar etc: + DatabaseCommand_CollectionStats* cmd = new DatabaseCommand_CollectionStats( source() ); + connect( cmd, SIGNAL( done( const QVariantMap& ) ), + source().data(), SLOT( setStats( const QVariantMap& ) ), Qt::QueuedConnection ); + APP->database()->enqueue( QSharedPointer( cmd ) ); + + if( source()->isLocal() ) + APP->servent().triggerDBSync(); +} + + +void +DatabaseCommand_AddFiles::exec( DatabaseImpl* dbi ) +{ + qDebug() << Q_FUNC_INFO; + Q_ASSERT( !source().isNull() ); + + TomahawkSqlQuery query_file = dbi->newquery(); + TomahawkSqlQuery query_filejoin = dbi->newquery(); + TomahawkSqlQuery query_file_del = dbi->newquery(); + + query_file.prepare( "INSERT INTO file(source, url, size, mtime, md5, mimetype, duration, bitrate) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)" ); + query_filejoin.prepare( "INSERT INTO file_join(file, artist ,album, track, albumpos) " + "VALUES (?,?,?,?,?)" ); + query_file_del.prepare( QString( "DELETE FROM file WHERE source %1 AND url = ?" ) + .arg( source()->isLocal() ? "IS NULL" : QString( "= %1" ).arg( source()->id() ) + ) ); + + int maxart, maxalb, maxtrk; // store max id, so we can index new ones after + maxart = maxalb = maxtrk = 0; + + int added = 0; + QVariant srcid = source()->isLocal() ? + QVariant( QVariant::Int ) : source()->id(); + + qDebug() << "Adding" << m_files.length() << "files to db for source" << srcid; + + QList::iterator it; + for( it = m_files.begin(); it != m_files.end(); ++it ) + { + QVariant& v = *it; + QVariantMap m = v.toMap(); + + QString url = m.value( "url" ).toString(); + int mtime = m.value( "lastmodified" ).toInt(); + int size = m.value( "size" ).toInt(); + QString hash = m.value( "hash" ).toString(); + QString mimetype = m.value( "mimetype" ).toString(); + int duration = m.value( "duration" ).toInt(); + int bitrate = m.value( "bitrate" ).toInt(); + QString artist = m.value( "artist" ).toString(); + QString album = m.value( "album" ).toString(); + QString track = m.value( "track" ).toString(); + int albumpos = m.value( "albumpos" ).toInt(); + + int fileid = 0; + query_file_del.bindValue( 0, url ); + query_file_del.exec(); + + query_file.bindValue( 0, srcid ); + query_file.bindValue( 1, url ); + query_file.bindValue( 2, size ); + query_file.bindValue( 3, mtime ); + query_file.bindValue( 4, hash ); + query_file.bindValue( 5, mimetype ); + query_file.bindValue( 6, duration ); + query_file.bindValue( 7, bitrate ); + if( !query_file.exec() ) + { + qDebug() << "Failed to insert to file:" + << query_file.lastError().databaseText() + << query_file.lastError().driverText() + << query_file.boundValues(); + continue; + } + else + { + if( added % 100 == 0 ) qDebug() << "Inserted" << added; + } + // get internal IDs for art/alb/trk + fileid = query_file.lastInsertId().toInt(); + + // insert the new fileid, set the url for our use: + m.insert( "id", fileid ); + if( !source()->isLocal() ) m["url"] = QString( "servent://%1\t%2" ) + .arg( source()->userName() ) + .arg( fileid ); + v = m; + + bool isnew; + int artid = dbi->artistId( artist, isnew ); + if( artid < 1 ) continue; + if( isnew && maxart == 0 ) maxart = artid; + + int trkid = dbi->trackId( artid, track, isnew ); + if( trkid < 1 ) continue; + if( isnew && maxtrk == 0 ) maxtrk = trkid; + + int albid = dbi->albumId( artid, album, isnew ); + if( albid > 0 && isnew && maxalb == 0 ) maxalb = albid; + + // Now add the association + query_filejoin.bindValue( 0, fileid ); + query_filejoin.bindValue( 1, artid ); + query_filejoin.bindValue( 2, albid > 0 ? albid : QVariant( QVariant::Int ) ); + query_filejoin.bindValue( 3, trkid ); + query_filejoin.bindValue( 4, albumpos ); + if( !query_filejoin.exec() ) + { + qDebug() << "Error inserting into file_join table"; + continue; + } + added++; + } + qDebug() << "Inserted" << added; + + // TODO building the index could be a separate job, outside this transaction + if(maxart) dbi->updateSearchIndex( "artist", maxart ); + if(maxalb) dbi->updateSearchIndex( "album", maxalb ); + if(maxtrk) dbi->updateSearchIndex( "track", maxtrk ); + + qDebug() << "Committing" << added << "tracks..."; + qDebug() << "Done."; + emit done( m_files, source()->collection() ); +} + diff --git a/src/database/databasecommand_addfiles.h b/src/database/databasecommand_addfiles.h new file mode 100644 index 000000000..bfd6a368e --- /dev/null +++ b/src/database/databasecommand_addfiles.h @@ -0,0 +1,43 @@ +#ifndef DATABASECOMMAND_ADDFILES_H +#define DATABASECOMMAND_ADDFILES_H + +#include +#include + +#include "database/databasecommandloggable.h" +#include "tomahawk/typedefs.h" + +class DatabaseCommand_AddFiles : public DatabaseCommandLoggable +{ +Q_OBJECT +Q_PROPERTY( QVariantList files READ files WRITE setFiles ) + +public: + explicit DatabaseCommand_AddFiles( QObject* parent = 0 ) + : DatabaseCommandLoggable( parent ) + {} + + explicit DatabaseCommand_AddFiles( const QList& files, const Tomahawk::source_ptr& source, QObject* parent = 0 ) + : DatabaseCommandLoggable( parent ), m_files( files ) + { + setSource( source ); + } + + virtual QString commandname() const { return "addfiles"; } + + virtual void exec( DatabaseImpl* ); + virtual bool doesMutates() const { return true; } + virtual void postCommitHook(); + + QVariantList files() const; + void setFiles( const QVariantList& f ) { m_files = f; } + +signals: + void done( const QList&, Tomahawk::collection_ptr ); + void notify( const QList&, Tomahawk::collection_ptr ); + +private: + QVariantList m_files; +}; + +#endif // DATABASECOMMAND_ADDFILES_H diff --git a/src/database/databasecommand_addsource.cpp b/src/database/databasecommand_addsource.cpp new file mode 100644 index 000000000..155b4c55c --- /dev/null +++ b/src/database/databasecommand_addsource.cpp @@ -0,0 +1,45 @@ +#include +#include "databasecommand_addsource.h" +#include "databaseimpl.h" + + +DatabaseCommand_addSource::DatabaseCommand_addSource( const QString& username, const QString& fname, QObject* parent ) + : DatabaseCommand( parent ) + , m_username( username ) + , m_fname( fname ) +{ +} + + +void +DatabaseCommand_addSource::exec( DatabaseImpl* dbi ) +{ + TomahawkSqlQuery query = dbi->newquery(); + query.prepare( "SELECT id, friendlyname FROM source WHERE name = ?" ); + query.addBindValue( m_username ); + query.exec(); + + if ( query.next() ) + { + unsigned int id = query.value( 0 ).toInt(); + QString fname = query.value( 1 ).toString(); + query.prepare( "UPDATE source SET isonline = 'true', friendlyname = ? WHERE id = ?" ); + query.addBindValue( m_fname ); + query.addBindValue( id ); + query.exec(); + emit done( id, fname ); + return; + } + + query.prepare( "INSERT INTO source(name, friendlyname, isonline) VALUES(?,?,?)" ); + query.addBindValue( m_username ); + query.addBindValue( m_fname ); + query.addBindValue( true ); + bool ok = query.exec(); + Q_ASSERT( ok ); + + unsigned int id = query.lastInsertId().toUInt(); + qDebug() << "Inserted new source to DB, id:" << id << " friendlyname" << m_username; + + emit done( id, m_fname ); +} diff --git a/src/database/databasecommand_addsource.h b/src/database/databasecommand_addsource.h new file mode 100644 index 000000000..a45fcfa4e --- /dev/null +++ b/src/database/databasecommand_addsource.h @@ -0,0 +1,25 @@ +#ifndef DATABASECOMMAND_ADDSOURCE_H +#define DATABASECOMMAND_ADDSOURCE_H + +#include +#include + +#include "databasecommand.h" + +class DatabaseCommand_addSource : public DatabaseCommand +{ +Q_OBJECT + +public: + explicit DatabaseCommand_addSource( const QString& username, const QString& fname, QObject* parent = 0 ); + virtual void exec( DatabaseImpl* lib ); + virtual bool doesMutates() const { return true; } + virtual QString commandname() const { return "addsource"; } +signals: + void done( unsigned int, const QString& ); + +private: + QString m_username, m_fname; +}; + +#endif // DATABASECOMMAND_ADDSOURCE_H diff --git a/src/database/databasecommand_alltracks.cpp b/src/database/databasecommand_alltracks.cpp new file mode 100644 index 000000000..3ec365b6d --- /dev/null +++ b/src/database/databasecommand_alltracks.cpp @@ -0,0 +1,58 @@ +#include "databasecommand_alltracks.h" + +#include + +#include "databaseimpl.h" + + +void +DatabaseCommand_AllTracks::exec( DatabaseImpl* dbi ) +{ + Q_ASSERT( !m_source.isNull() ); + + TomahawkSqlQuery query = dbi->newquery(); + QList tracks; + QString sql = QString( + "SELECT file.id, artist.name, album.name, track.name, file.size, " + " file.duration, file.bitrate, file.url, file.source, file.mtime, file.mimetype, file_join.albumpos " + "FROM file, artist, track, file_join " + "LEFT OUTER JOIN album " + "ON file_join.album = album.id " + "WHERE file.id = file_join.file " + "AND file_join.artist = artist.id " + "AND file_join.track = track.id " + "AND file.source %1 " + ).arg( m_source->isLocal() ? "IS NULL" : QString( "= %1" ).arg(m_source->id() ) ); + //qDebug() << sql; + + query.prepare( sql ); + if( !query.exec() ) + { + qDebug() << "ERROR: " << dbi->database().lastError().databaseText() << dbi->database().lastError().driverText(); + } + + while( query.next() ) + { + QVariantMap t; + QString url; + url = query.value( 7 ).toString(); + if( m_source->isLocal() ) + t["url"] = url; + else + t["url"] = QString( "servent://%1\t%2" ).arg( m_source->userName() ).arg( url ); + + t["id"] = QString( "%1" ).arg( query.value( 0 ).toInt() ); + t["artist"] = query.value( 1 ).toString(); + t["album"] = query.value( 2 ).toString(); + t["track"] = query.value( 3 ).toString(); + t["size"] = query.value( 4 ).toInt(); + t["duration"] = query.value( 5 ).toInt(); + t["bitrate"] = query.value( 6 ).toInt(); + t["lastmodified"] = query.value( 9 ).toInt(); + t["mimetype"] = query.value( 10 ).toString(); + t["albumpos"] = query.value( 11 ).toUInt(); + tracks.append( t ); + } + qDebug() << Q_FUNC_INFO << tracks.length(); + emit done( tracks ); +} diff --git a/src/database/databasecommand_alltracks.h b/src/database/databasecommand_alltracks.h new file mode 100644 index 000000000..743ba6a41 --- /dev/null +++ b/src/database/databasecommand_alltracks.h @@ -0,0 +1,31 @@ +#ifndef DATABASECOMMAND_ALLTRACKS_H +#define DATABASECOMMAND_ALLTRACKS_H + +#include +#include + +#include "databasecommand.h" +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" + +class DatabaseCommand_AllTracks : public DatabaseCommand +{ +Q_OBJECT +public: + explicit DatabaseCommand_AllTracks( const Tomahawk::source_ptr& source, QObject* parent = 0 ) + : DatabaseCommand( parent ), m_source( source ) + {} + + virtual void exec( DatabaseImpl* ); + + virtual bool doesMutates() const { return false; } + virtual QString commandname() const { return "alltracks"; } + +signals: + void done( const QList& ); + +private: + Tomahawk::source_ptr m_source; +}; + +#endif // DATABASECOMMAND_ALLTRACKS_H diff --git a/src/database/databasecommand_collectionstats.cpp b/src/database/databasecommand_collectionstats.cpp new file mode 100644 index 000000000..4d2ce5ccf --- /dev/null +++ b/src/database/databasecommand_collectionstats.cpp @@ -0,0 +1,57 @@ +#include "databasecommand_collectionstats.h" + +#include "databaseimpl.h" + +using namespace Tomahawk; + + +DatabaseCommand_CollectionStats::DatabaseCommand_CollectionStats( const source_ptr& source, QObject* parent ) + : DatabaseCommand( source, parent ) +{ +} + + +void +DatabaseCommand_CollectionStats::exec( DatabaseImpl* dbi ) +{ + //qDebug() << Q_FUNC_INFO; + Q_ASSERT( !source().isNull() ); + TomahawkSqlQuery query = dbi->newquery(); + + Q_ASSERT( source()->isLocal() || source()->id() >= 1 ); + + if( source()->isLocal() ) + { + query.exec("SELECT count(*), max(mtime), (SELECT guid FROM oplog WHERE source IS NULL ORDER BY id DESC LIMIT 1) " + "FROM file " + "WHERE source IS NULL"); + } + else + { + query.prepare("SELECT count(*), max(mtime), " + " (SELECT lastop FROM source WHERE id = ?) " + "FROM file " + "WHERE source = ?" + ); + query.addBindValue( source()->id() ); + query.addBindValue( source()->id() ); + } + if( !query.exec() ) + { + qDebug() << "Failed to get collection stats:" << query.boundValues(); + throw "failed to get collection stats"; + } + + QVariantMap m; + if( query.next() ) + { + m.insert( "numfiles", query.value( 0 ).toInt() ); + m.insert( "lastmodified", query.value( 1 ).toInt() ); + m.insert( "lastop", query.value( 2 ).toString() ); + } + + //qDebug() << "Loaded collection stats for" + // << (source()->isLocal() ? "LOCAL" : source()->username()) + // << m; + emit done( m ); +} diff --git a/src/database/databasecommand_collectionstats.h b/src/database/databasecommand_collectionstats.h new file mode 100644 index 000000000..50dcee941 --- /dev/null +++ b/src/database/databasecommand_collectionstats.h @@ -0,0 +1,24 @@ +#ifndef DATABASECOMMAND_COLLECTIONSTATS_H +#define DATABASECOMMAND_COLLECTIONSTATS_H + +#include + +#include "databasecommand.h" +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" + +class DatabaseCommand_CollectionStats : public DatabaseCommand +{ +Q_OBJECT + +public: + explicit DatabaseCommand_CollectionStats( const Tomahawk::source_ptr& source, QObject* parent = 0 ); + virtual void exec( DatabaseImpl* lib ); + virtual bool doesMutates() const { return false; } + virtual QString commandname() const { return "collectionstats"; } + +signals: + void done( const QVariantMap& ); +}; + +#endif // DATABASECOMMAND_COLLECTIONSTATS_H diff --git a/src/database/databasecommand_createplaylist.cpp b/src/database/databasecommand_createplaylist.cpp new file mode 100644 index 000000000..960110faa --- /dev/null +++ b/src/database/databasecommand_createplaylist.cpp @@ -0,0 +1,71 @@ +#include "databasecommand_createplaylist.h" + +#include + +#include "tomahawk/tomahawkapp.h" + +using namespace Tomahawk; + + +DatabaseCommand_CreatePlaylist::DatabaseCommand_CreatePlaylist( QObject* parent ) + : DatabaseCommandLoggable( parent ) + , m_report( true ) +{ + qDebug() << Q_FUNC_INFO << "def"; +} + +DatabaseCommand_CreatePlaylist::DatabaseCommand_CreatePlaylist( const source_ptr& author, + const playlist_ptr& playlist ) + : DatabaseCommandLoggable( author ) + , m_playlist( playlist ) + , m_report( false ) //this ctor used when creating locally, reporting done elsewhere +{ + qDebug() << Q_FUNC_INFO; +} + + +void +DatabaseCommand_CreatePlaylist::exec( DatabaseImpl* lib ) +{ + qDebug() << Q_FUNC_INFO; + + TomahawkSqlQuery cre = lib->newquery(); + cre.prepare( "INSERT INTO playlist( guid, source, shared, title, info, creator, lastmodified) " + "VALUES( :guid, :source, :shared, :title, :info, :creator, :lastmodified )" ); + Q_ASSERT( !m_playlist.isNull() ); + Q_ASSERT( !source().isNull() ); + cre.bindValue( ":guid", m_playlist->guid() ); + cre.bindValue( ":source", source()->isLocal() ? QVariant(QVariant::Int) : source()->id() ); + cre.bindValue( ":shared", m_playlist->shared() ); + cre.bindValue( ":title", m_playlist->title() ); + cre.bindValue( ":info", m_playlist->info() ); + cre.bindValue( ":creator", m_playlist->creator() ); + cre.bindValue( ":lastmodified", m_playlist->lastmodified() ); + + qDebug() << "CREATE PLAYLIST:" << cre.boundValues(); + + bool ok = cre.exec(); + if( !ok ) + { + qDebug() << cre.lastError().databaseText() + << cre.lastError().driverText() + << cre.executedQuery() + << cre.boundValues(); + Q_ASSERT( ok ); + } +} + + +void +DatabaseCommand_CreatePlaylist::postCommitHook() +{ + qDebug() << Q_FUNC_INFO; + if( m_report == false ) + return; + + qDebug() << Q_FUNC_INFO << "..reporting.."; + m_playlist->reportCreated( m_playlist ); + + if( source()->isLocal() ) + APP->servent().triggerDBSync(); +} diff --git a/src/database/databasecommand_createplaylist.h b/src/database/databasecommand_createplaylist.h new file mode 100644 index 000000000..bd259eece --- /dev/null +++ b/src/database/databasecommand_createplaylist.h @@ -0,0 +1,44 @@ +#ifndef DATABASECOMMAND_CREATEPLAYLIST_H +#define DATABASECOMMAND_CREATEPLAYLIST_H + +#include "databaseimpl.h" +#include "databasecommandloggable.h" +#include "tomahawk/playlist.h" +#include "tomahawk/typedefs.h" + +class DatabaseCommand_CreatePlaylist : public DatabaseCommandLoggable +{ +Q_OBJECT +Q_PROPERTY( QVariant playlist READ playlistV WRITE setPlaylistV ) + +public: + explicit DatabaseCommand_CreatePlaylist( QObject* parent = 0 ); + explicit DatabaseCommand_CreatePlaylist( const Tomahawk::source_ptr& author, const Tomahawk::playlist_ptr& playlist ); + + QString commandname() const { return "createplaylist"; } + + virtual void exec( DatabaseImpl* lib ); + virtual void postCommitHook(); + virtual bool doesMutates() const { return true; } + + QVariant playlistV() const + { + return QJson::QObjectHelper::qobject2qvariant( (QObject*)m_playlist.data() ); + } + + void setPlaylistV( const QVariant& v ) + { + qDebug() << "***********" << Q_FUNC_INFO << v; + using namespace Tomahawk; + + Playlist* p = new Playlist( source() ); + QJson::QObjectHelper::qvariant2qobject( v.toMap(), p ); + m_playlist = playlist_ptr( p ); + } + +private: + Tomahawk::playlist_ptr m_playlist; + bool m_report; // call Playlist::reportCreated? +}; + +#endif // DATABASECOMMAND_CREATEPLAYLIST_H diff --git a/src/database/databasecommand_deleteplaylist.cpp b/src/database/databasecommand_deleteplaylist.cpp new file mode 100644 index 000000000..5261c4362 --- /dev/null +++ b/src/database/databasecommand_deleteplaylist.cpp @@ -0,0 +1,53 @@ +#include "databasecommand_deleteplaylist.h" + +#include + +#include "tomahawk/tomahawkapp.h" + +using namespace Tomahawk; + + +DatabaseCommand_DeletePlaylist::DatabaseCommand_DeletePlaylist( const source_ptr& source, const QString& playlistguid ) + : DatabaseCommandLoggable( source ) +{ + setPlaylistguid( playlistguid ); +} + + +void +DatabaseCommand_DeletePlaylist::exec( DatabaseImpl* lib ) +{ + qDebug() << Q_FUNC_INFO; + + TomahawkSqlQuery cre = lib->newquery(); + + QString sql = QString( "DELETE FROM playlist WHERE guid = :id AND source %1" ) + .arg( source()->isLocal() ? "IS NULL" : QString("= %1").arg( source()->id() ) ); + cre.prepare( sql ); + cre.bindValue( ":id", m_playlistguid ); + + bool ok = cre.exec(); + if( !ok ) + { + qDebug() << cre.lastError().databaseText() + << cre.lastError().driverText() + << cre.executedQuery() + << cre.boundValues(); + Q_ASSERT( ok ); + } +} + + +void +DatabaseCommand_DeletePlaylist::postCommitHook() +{ + qDebug() << Q_FUNC_INFO << "..reporting.."; + + playlist_ptr playlist = source()->collection()->playlist( m_playlistguid ); + Q_ASSERT( !playlist.isNull() ); + + playlist->reportDeleted( playlist ); + + if( source()->isLocal() ) + APP->servent().triggerDBSync(); +} diff --git a/src/database/databasecommand_deleteplaylist.h b/src/database/databasecommand_deleteplaylist.h new file mode 100644 index 000000000..84d069750 --- /dev/null +++ b/src/database/databasecommand_deleteplaylist.h @@ -0,0 +1,34 @@ +#ifndef DATABASECOMMAND_DELETEPLAYLIST_H +#define DATABASECOMMAND_DELETEPLAYLIST_H + +#include "databaseimpl.h" +#include "databasecommandloggable.h" +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" + +class DatabaseCommand_DeletePlaylist : public DatabaseCommandLoggable +{ +Q_OBJECT +Q_PROPERTY( QString playlistguid READ playlistguid WRITE setPlaylistguid ) + +public: + explicit DatabaseCommand_DeletePlaylist( QObject* parent = 0 ) + : DatabaseCommandLoggable( parent ) + {} + + explicit DatabaseCommand_DeletePlaylist( const Tomahawk::source_ptr& source, const QString& playlistguid ); + + QString commandname() const { return "deleteplaylist"; } + + virtual void exec( DatabaseImpl* lib ); + virtual void postCommitHook(); + virtual bool doesMutates() const { return true; } + + QString playlistguid() const { return m_playlistguid; } + void setPlaylistguid( const QString& s ) { m_playlistguid = s; } + +private: + QString m_playlistguid; +}; + +#endif // DATABASECOMMAND_DELETEPLAYLIST_H diff --git a/src/database/databasecommand_dirmtimes.cpp b/src/database/databasecommand_dirmtimes.cpp new file mode 100644 index 000000000..d8c11c799 --- /dev/null +++ b/src/database/databasecommand_dirmtimes.cpp @@ -0,0 +1,57 @@ +#include "databasecommand_dirmtimes.h" + +#include + +#include "databaseimpl.h" + + +void +DatabaseCommand_DirMtimes::exec( DatabaseImpl* dbi ) +{ + if( m_update ) + execUpdate( dbi ); + else + execSelect( dbi ); +} + + +void +DatabaseCommand_DirMtimes::execSelect( DatabaseImpl* dbi ) +{ + QMap mtimes; + TomahawkSqlQuery query = dbi->newquery(); + if( m_prefix.isEmpty() ) + query.exec( "SELECT name, mtime FROM dirs_scanned" ); + else + { + query.prepare( QString( "SELECT name, mtime " + "FROM dirs_scanned " + "WHERE name LIKE '%1%'" ).arg(m_prefix.replace( '\'',"''" ) ) ); + query.exec(); + } + while( query.next() ) + { + mtimes.insert( query.value( 0 ).toString(), query.value( 1 ).toUInt() ); + } + + emit done( mtimes ); +} + + +void +DatabaseCommand_DirMtimes::execUpdate( DatabaseImpl* dbi ) +{ + qDebug() << "Saving mtimes..."; + TomahawkSqlQuery query = dbi->newquery(); + query.exec( "DELETE FROM dirs_scanned" ); + query.prepare( "INSERT INTO dirs_scanned(name, mtime) VALUES(?,?)" ); + + foreach( const QString& k, m_tosave.keys() ) + { + query.bindValue( 0, k ); + query.bindValue( 1, m_tosave.value( k ) ); + query.exec(); + } + + qDebug() << "Saved mtimes for" << m_tosave.size() << "dirs."; +} diff --git a/src/database/databasecommand_dirmtimes.h b/src/database/databasecommand_dirmtimes.h new file mode 100644 index 000000000..bc9ba4bef --- /dev/null +++ b/src/database/databasecommand_dirmtimes.h @@ -0,0 +1,42 @@ +#ifndef DATABASECOMMAND_DIRMTIMES_H +#define DATABASECOMMAND_DIRMTIMES_H + +#include +#include +#include + +#include "databasecommand.h" + +// Not loggable, mtimes only used to speed up our local scanner. + +class DatabaseCommand_DirMtimes : public DatabaseCommand +{ +Q_OBJECT + +public: + explicit DatabaseCommand_DirMtimes( const QString& prefix = "", QObject* parent = 0 ) + : DatabaseCommand( parent ), m_prefix( prefix ), m_update( false ) + {} + + explicit DatabaseCommand_DirMtimes( QMap tosave, QObject* parent = 0 ) + : DatabaseCommand( parent ), m_update( true ), m_tosave( tosave ) + {} + + virtual void exec( DatabaseImpl* ); + virtual bool doesMutates() const { return m_update; } + virtual QString commandname() const { return "dirmtimes"; } + +signals: + void done( const QMap& ); + +public slots: + +private: + void execSelect( DatabaseImpl* dbi ); + void execUpdate( DatabaseImpl* dbi ); + QString m_prefix; + bool m_update; + QMap m_tosave; +}; + +#endif // DATABASECOMMAND_DIRMTIMES_H diff --git a/src/database/databasecommand_importplaylist.cpp b/src/database/databasecommand_importplaylist.cpp new file mode 100644 index 000000000..b0e84c07d --- /dev/null +++ b/src/database/databasecommand_importplaylist.cpp @@ -0,0 +1,40 @@ +#include "databasecommand_importplaylist.h" + +#include + +#include "tomahawk/query.h" +#include "tomahawk/playlist.h" +#include "databaseimpl.h" + +void DatabaseCommand_ImportPlaylist::exec(DatabaseImpl * dbi) +{ + /* + qDebug() << "Importing playlist of" << m_playlist->length() << "tracks"; + TomahawkSqlQuery query = dbi->newquery(); + query.prepare("INSERT INTO playlist(title, info, creator, lastmodified) " + "VALUES(?,?,?,?)"); + query.addBindValue(m_playlist->title()); + query.addBindValue(m_playlist->info()); + query.addBindValue(m_playlist->creator()); + query.addBindValue(m_playlist->lastmodified()); + query.exec(); + int pid = query.lastInsertId().toInt(); + int pos = 0; + query.prepare("INSERT INTO playlist_tracks( " + "playlist, position, trackname, albumname, artistname) " + "VALUES (?,?,?,?,?)"); + + for(int k = 0; k < m_playlist->length(); k++) + { + pos++; + query.addBindValue(pid); + query.addBindValue(pos); + query.addBindValue(m_playlist->at(k)->artist()); + query.addBindValue(m_playlist->at(k)->album()); + query.addBindValue(m_playlist->at(k)->track()); + query.exec(); + } + emit done(pid); + */ +} + diff --git a/src/database/databasecommand_importplaylist.h b/src/database/databasecommand_importplaylist.h new file mode 100644 index 000000000..19832a4d2 --- /dev/null +++ b/src/database/databasecommand_importplaylist.h @@ -0,0 +1,29 @@ +#ifndef DATABASECOMMAND_IMPORTPLAYLIST_H +#define DATABASECOMMAND_IMPORTPLAYLIST_H +#include +#include +#include "databasecommand.h" +#include "tomahawk/source.h" + +class Playlist; + +class DatabaseCommand_ImportPlaylist : public DatabaseCommand +{ + Q_OBJECT +public: + explicit DatabaseCommand_ImportPlaylist(Playlist * p, QObject *parent = 0) + : DatabaseCommand(parent), m_playlist(p) + {} + + virtual void exec(DatabaseImpl *); + virtual bool doesMutates() const { return true; } + virtual QString commandname() const { return "importplaylist"; } + +signals: + void done(int id); + +private: + Playlist * m_playlist; +}; + +#endif // DATABASECOMMAND_ADDFILES_H diff --git a/src/database/databasecommand_loadallplaylists.cpp b/src/database/databasecommand_loadallplaylists.cpp new file mode 100644 index 000000000..1822532d1 --- /dev/null +++ b/src/database/databasecommand_loadallplaylists.cpp @@ -0,0 +1,38 @@ +#include "databasecommand_loadallplaylists.h" + +#include + +#include "tomahawk/playlist.h" +#include "databaseimpl.h" + +using namespace Tomahawk; + + +void DatabaseCommand_LoadAllPlaylists::exec( DatabaseImpl* dbi ) +{ + TomahawkSqlQuery query = dbi->newquery(); + + query.exec( QString( "SELECT guid, title, info, creator, lastmodified, shared, currentrevision " + "FROM playlist WHERE source %1" ) + .arg( source()->isLocal() ? "IS NULL" : + QString( "=%1" ).arg( source()->id() ) + ) ); + + QList plists; + while ( query.next() ) + { + playlist_ptr p( new Playlist( source(), //src + query.value(6).toString(), //current rev + query.value(1).toString(), //title + query.value(2).toString(), //info + query.value(3).toString(), //creator + query.value(5).toBool(), //shared + query.value(4).toInt(), //lastmod + query.value(0).toString() //GUID + ) ); + plists.append( p ); + } + + emit done( plists ); +} + diff --git a/src/database/databasecommand_loadallplaylists.h b/src/database/databasecommand_loadallplaylists.h new file mode 100644 index 000000000..371651eea --- /dev/null +++ b/src/database/databasecommand_loadallplaylists.h @@ -0,0 +1,27 @@ +#ifndef DATABASECOMMAND_IMPORTALLPLAYLIST_H +#define DATABASECOMMAND_IMPORTALLPLAYLIST_H + +#include +#include + +#include "databasecommand.h" +#include "tomahawk/typedefs.h" + +class DatabaseCommand_LoadAllPlaylists : public DatabaseCommand +{ +Q_OBJECT + +public: + explicit DatabaseCommand_LoadAllPlaylists( const Tomahawk::source_ptr& s, QObject* parent = 0 ) + : DatabaseCommand( s, parent ) + {} + + virtual void exec( DatabaseImpl* ); + virtual bool doesMutates() const { return false; } + virtual QString commandname() const { return "loadallplaylists"; } + +signals: + void done( const QList& playlists ); +}; + +#endif // DATABASECOMMAND_ADDFILES_H diff --git a/src/database/databasecommand_loadfile.cpp b/src/database/databasecommand_loadfile.cpp new file mode 100644 index 000000000..8a5cfe5c5 --- /dev/null +++ b/src/database/databasecommand_loadfile.cpp @@ -0,0 +1,29 @@ +#include "databasecommand_loadfile.h" + +#include "databaseimpl.h" + + +DatabaseCommand_LoadFile::DatabaseCommand_LoadFile( const QString& id, QObject* parent ) + : DatabaseCommand( parent ) + , m_id( id ) +{ +} + + +void +DatabaseCommand_LoadFile::exec(DatabaseImpl* dbi) +{ + QVariantMap r; + // file ids internally are really ints, at least for now: + bool ok; + do + { + unsigned int fid = m_id.toInt( &ok ); + if( !ok ) + break; + + r = dbi->file( fid ); + } while( false ); + + emit result( r ); +} diff --git a/src/database/databasecommand_loadfile.h b/src/database/databasecommand_loadfile.h new file mode 100644 index 000000000..aca186109 --- /dev/null +++ b/src/database/databasecommand_loadfile.h @@ -0,0 +1,27 @@ +#ifndef DATABASECOMMAND_LOADFILE_H +#define DATABASECOMMAND_LOADFILE_H + +#include +#include +#include + +#include "databasecommand.h" + +class DatabaseCommand_LoadFile : public DatabaseCommand +{ +Q_OBJECT + +public: + explicit DatabaseCommand_LoadFile( const QString& id, QObject* parent = 0 ); + virtual void exec( DatabaseImpl* ); + virtual bool doesMutates() const { return false; } + virtual QString commandname() const { return "loadfile"; } + +signals: + void result( QVariantMap ); + +private: + QString m_id; +}; + +#endif // DATABASECOMMAND_LOADFILE_H diff --git a/src/database/databasecommand_loadops.cpp b/src/database/databasecommand_loadops.cpp new file mode 100644 index 000000000..1ecd69815 --- /dev/null +++ b/src/database/databasecommand_loadops.cpp @@ -0,0 +1,37 @@ +#include "databasecommand_loadops.h" + + +void +DatabaseCommand_loadOps::exec( DatabaseImpl* dbi ) +{ + QList< dbop_ptr > ops; + + TomahawkSqlQuery query = dbi->newquery(); + query.prepare( QString( + "SELECT guid, command, json, compressed " + "FROM oplog " + "WHERE source %1 " + "AND id > coalesce((SELECT id FROM oplog WHERE guid = ?),0) " + "ORDER BY id ASC" + ).arg( source()->isLocal() ? "IS NULL" : QString("= %1").arg(source()->id()) ) + ); + query.addBindValue( m_since ); + if( !query.exec() ) + { + Q_ASSERT(0); + } + + while( query.next() ) + { + dbop_ptr op( new DBOp ); + op->guid = query.value( 0 ).toString(); + op->command = query.value( 1 ).toString(); + op->payload = query.value( 2 ).toByteArray(); + op->compressed = query.value( 3 ).toBool(); + ops << op; + } + + qDebug() << "Loaded" << ops.length() << "ops from db"; + + emit done( m_since, ops ); +} diff --git a/src/database/databasecommand_loadops.h b/src/database/databasecommand_loadops.h new file mode 100644 index 000000000..f6e11b09a --- /dev/null +++ b/src/database/databasecommand_loadops.h @@ -0,0 +1,28 @@ +#ifndef DATABASECOMMAND_LOADOPS_H +#define DATABASECOMMAND_LOADOPS_H + +#include "tomahawk/typedefs.h" +#include "databasecommand.h" +#include "databaseimpl.h" +#include "op.h" + +class DatabaseCommand_loadOps : public DatabaseCommand +{ +Q_OBJECT +public: + explicit DatabaseCommand_loadOps( const Tomahawk::source_ptr& src, QString since, QObject* parent = 0 ) + : DatabaseCommand( src ), m_since( since ) + {} + + virtual void exec( DatabaseImpl* db ); + virtual bool doesMutates() const { return false; } + virtual QString commandname() const { return "loadops"; } + +signals: + void done( QString lastguid, QList< dbop_ptr > ops ); + +private: + QString m_since; // guid to load from +}; + +#endif // DATABASECOMMAND_LOADOPS_H diff --git a/src/database/databasecommand_loadplaylistentries.cpp b/src/database/databasecommand_loadplaylistentries.cpp new file mode 100644 index 000000000..44787fb85 --- /dev/null +++ b/src/database/databasecommand_loadplaylistentries.cpp @@ -0,0 +1,106 @@ +#include "databasecommand_loadplaylistentries.h" + +#include + +#include "databaseimpl.h" + +using namespace Tomahawk; + + +void +DatabaseCommand_LoadPlaylistEntries::exec( DatabaseImpl* dbi ) +{ + qDebug() << "Loading playlist entries for revision" << m_guid; + + TomahawkSqlQuery query_entries = dbi->newquery(); + query_entries.prepare("SELECT entries, playlist, author, timestamp, previous_revision " + "FROM playlist_revision " + "WHERE guid = :guid"); + query_entries.bindValue( ":guid", m_guid ); + + bool aok = query_entries.exec(); + Q_ASSERT( aok ); + + QStringList guids; + QMap< QString, plentry_ptr > entrymap; + bool islatest = true; + QStringList oldentries; + QString prevrev; + QJson::Parser parser; bool ok; + + if( query_entries.next() ) + { + // entries should be a list of strings: + QVariant v = parser.parse( query_entries.value(0).toByteArray(), &ok ); + Q_ASSERT( ok && v.type() == QVariant::List ); //TODO + guids = v.toStringList(); +// qDebug() << "Entries:" << guids; + + QString inclause = QString("('%1')").arg(guids.join("', '")); + + TomahawkSqlQuery query = dbi->newquery(); + QString sql = QString("SELECT guid, trackname, artistname, albumname, annotation, " + "duration, addedon, addedby, result_hint " + "FROM playlist_item " + "WHERE guid IN %1").arg( inclause ); + //qDebug() << sql; + + bool xok = query.exec( sql ); + Q_ASSERT( xok ); + + while( query.next() ) + { + plentry_ptr e( new PlaylistEntry ); + e->setGuid( query.value( 0 ).toString() ); + e->setAnnotation( query.value( 4 ).toString() ); + e->setDuration( query.value( 5 ).toUInt() ); + e->setLastmodified( 0 ); // TODO e->lastmodified = query.value(6).toInt(); + e->setResulthint( query.value( 8 ).toString() ); + + QVariantMap m; + m.insert( "artist", query.value( 2 ).toString() ); + m.insert( "album", query.value( 3 ).toString() ); + m.insert( "track", query.value( 1 ).toString() ); + m.insert( "qid", uuid() ); + + Tomahawk::query_ptr q( new Tomahawk::Query( m ) ); + e->setQuery( q ); + + entrymap.insert( e->guid(), e ); + } + + prevrev = query_entries.value( 4 ).toString(); + } + else + { + qDebug() << "Playlist has no current revision data"; + } + + if( prevrev.length() ) + { + TomahawkSqlQuery query_entries_old = dbi->newquery(); + query_entries_old.prepare( "SELECT entries, " + "(SELECT currentrevision = ? FROM playlist WHERE guid = ?) " + "FROM playlist_revision " + "WHERE guid = ?" ); + query_entries_old.addBindValue( m_guid ); + query_entries_old.addBindValue( query_entries.value( 1 ).toString() ); + query_entries_old.addBindValue( prevrev ); + bool ex = query_entries_old.exec(); + Q_ASSERT( ex ); + + if( !query_entries_old.next() ) + { + Q_ASSERT( false ); + } + + QVariant v = parser.parse( query_entries_old.value( 0 ).toByteArray(), &ok ); + Q_ASSERT( ok && v.type() == QVariant::List ); //TODO + oldentries = v.toStringList(); + islatest = query_entries_old.value( 1 ).toBool(); + } + + qDebug() << Q_FUNC_INFO << "entrymap:" << entrymap; + + emit done( m_guid, guids, oldentries, islatest, entrymap, true ); +} diff --git a/src/database/databasecommand_loadplaylistentries.h b/src/database/databasecommand_loadplaylistentries.h new file mode 100644 index 000000000..d9333e9eb --- /dev/null +++ b/src/database/databasecommand_loadplaylistentries.h @@ -0,0 +1,35 @@ +#ifndef DATABASECOMMAND_LOADPLAYLIST_H +#define DATABASECOMMAND_LOADPLAYLIST_H + +#include +#include + +#include "databasecommand.h" +#include "tomahawk/playlist.h" + +class DatabaseCommand_LoadPlaylistEntries : public DatabaseCommand +{ +Q_OBJECT + +public: + explicit DatabaseCommand_LoadPlaylistEntries( QString revision_guid, QObject* parent = 0 ) + : DatabaseCommand( parent ), m_guid( revision_guid ) + {} + + virtual void exec( DatabaseImpl* ); + virtual bool doesMutates() const { return false; } + virtual QString commandname() const { return "loadplaylistentries"; } + +signals: + void done( const QString& rev, + const QList& orderedguid, + const QList& oldorderedguid, + bool islatest, + const QMap< QString, Tomahawk::plentry_ptr >& added, + bool applied ); + +private: + QString m_guid; +}; + +#endif diff --git a/src/database/databasecommand_modifyplaylist.cpp b/src/database/databasecommand_modifyplaylist.cpp new file mode 100644 index 000000000..20e956a4c --- /dev/null +++ b/src/database/databasecommand_modifyplaylist.cpp @@ -0,0 +1,17 @@ +#include "databasecommand_modifyplaylist.h" + +using namespace Tomahawk; + + +DatabaseCommand_ModifyPlaylist::DatabaseCommand_ModifyPlaylist( Playlist* playlist, QList< plentry_ptr > entries, Mode mode ) + : DatabaseCommand() + , m_playlist( playlist ) + , m_entries( entries ) + , m_mode( mode ) +{ +} + + +void DatabaseCommand_ModifyPlaylist::exec( DatabaseImpl* lib ) +{ +} diff --git a/src/database/databasecommand_modifyplaylist.h b/src/database/databasecommand_modifyplaylist.h new file mode 100644 index 000000000..0ac5c3d57 --- /dev/null +++ b/src/database/databasecommand_modifyplaylist.h @@ -0,0 +1,39 @@ +#ifndef DATABASECOMMAND_MODIFYPLAYLIST_H +#define DATABASECOMMAND_MODIFYPLAYLIST_H + +#include +#include + +#include "databasecommand.h" +#include "tomahawk/source.h" +#include "tomahawk/playlist.h" + +class DatabaseCommand_ModifyPlaylist : public DatabaseCommand +{ +Q_OBJECT +Q_PROPERTY( int mode READ mode WRITE setMode ) + +public: + enum Mode + { + ADD = 1, + REMOVE = 2, + UPDATE = 3 + }; + + explicit DatabaseCommand_ModifyPlaylist( Tomahawk::Playlist* playlist, QList< Tomahawk::plentry_ptr > entries, Mode mode ); + + virtual bool doesMutates() const { return true; } + + virtual void exec( DatabaseImpl* lib ); + + int mode() const { return m_mode; } + void setMode( int m ) { m_mode = (Mode)m; } + +private: + Tomahawk::Playlist* m_playlist; + QList< Tomahawk::plentry_ptr > m_entries; + Mode m_mode; +}; + +#endif // DATABASECOMMAND_MODIFYPLAYLIST_H diff --git a/src/database/databasecommand_resolve.cpp b/src/database/databasecommand_resolve.cpp new file mode 100644 index 000000000..715d4ef82 --- /dev/null +++ b/src/database/databasecommand_resolve.cpp @@ -0,0 +1,202 @@ +#include "databasecommand_resolve.h" + +#include "tomahawk/tomahawkapp.h" + +#define MINSCORE 0.5 + +using namespace Tomahawk; + + +DatabaseCommand_Resolve::DatabaseCommand_Resolve( QVariant v, bool searchlocal ) + : DatabaseCommand() + , m_v( v ) + , m_searchlocal( searchlocal ) +{ +} + + +void +DatabaseCommand_Resolve::exec( DatabaseImpl* lib ) +{ + QTime timer; + + const Tomahawk::QID qid = m_v.toMap().value("qid").toString(); + const QString artistname = m_v.toMap().value("artist").toString(); + const QString albumname = m_v.toMap().value("album").toString(); + const QString trackname = m_v.toMap().value("track").toString(); + + //qDebug() << Q_FUNC_INFO << artistname << trackname; + + /* + Resolving is a 2 stage process. + 1) find list of trk/art/alb IDs that are reasonable matches to the metadata given + 2) find files in database by permitted sources and calculate score, ignoring + results that are less than MINSCORE + */ + + typedef QPair scorepair_t; + + // STEP 1 + timer.start(); + QList< int > artists = lib->searchTable( "artist", artistname, 10 ); + QList< int > tracks = lib->searchTable( "track", trackname, 10 ); + QList< int > albums = lib->searchTable( "album", albumname, 10 ); + + //qDebug() << "art" << artists.size() << "trk" << tracks.size(); + //qDebug() << "searchTable calls duration:" << timer.elapsed(); + + if( artists.length() == 0 || tracks.length() == 0 ) + { + //qDebug() << "No candidates found in first pass, aborting resolve" << artistname << trackname; + return; + } + + // STEP 2 + + TomahawkSqlQuery files_query = lib->newquery(); + + QStringList artsl, trksl; + foreach( int i, artists ) artsl.append( QString::number(i) ); + foreach( int i, tracks ) trksl.append( QString::number(i) ); + + QString sql = QString("SELECT " + "url, mtime, size, md5, mimetype, duration, bitrate, file_join.artist, file_join.album, file_join.track, " + "artist.name as artname, " + "album.name as albname, " + "track.name as trkname, " + "file.source, " + "file_join.albumpos " + "FROM file, file_join, artist, track " + "LEFT JOIN album ON album.id = file_join.album " + "WHERE " + "artist.id = file_join.artist AND " + "track.id = file_join.track AND " + "file.source %1 AND " + "file.id = file_join.file AND " + "file_join.artist IN (%2) AND " + "file_join.track IN (%3) " + "ORDER by file_join.artist,file_join.track" + ).arg( m_searchlocal ? "IS NULL" : " IN (SELECT id FROM source WHERE isonline = 'true') " ) + .arg( artsl.join(",") ) + .arg( trksl.join(",") ); + + timer.start(); + + files_query.prepare( sql ); + + bool ok = files_query.exec(); + Q_ASSERT( ok ); + if(!ok) throw "Error"; + + //qDebug() << "SQL exec() duration, ms, " << timer.elapsed() + // << "numresults" << files_query.numRowsAffected(); + //qDebug() << sql; + + QList res; + + while( files_query.next() ) + { + QVariantMap m; + + m["mtime"] = files_query.value(1).toString(); + m["size"] = files_query.value(2).toInt(); + m["hash"] = files_query.value(3).toString(); + m["mimetype"] = files_query.value(4).toString(); + m["duration"] = files_query.value(5).toInt(); + m["bitrate"] = files_query.value(6).toInt(); + m["artist"] = files_query.value(10).toString(); + m["album"] = files_query.value(11).toString(); + m["track"] = files_query.value(12).toString(); + m["srcid"] = files_query.value(13).toInt(); + m["albumpos"] = files_query.value(14).toUInt(); + m["sid"] = uuid(); + + collection_ptr coll; + + const QString url_str = files_query.value( 0 ).toString(); + if( m_searchlocal ) + { + coll = APP->sourcelist().getLocal()->collection(); + m["url"] = url_str; + m["source"] = "Local Database"; // TODO + } + else + { + source_ptr s = APP->sourcelist().lookup( files_query.value( 13 ).toUInt() ); + if( s.isNull() ) + { + //qDebug() << "Skipping result for offline sourceid:" << files_query.value(13).toUInt(); + // will happen for valid sources which are offline (and thus not in the sourcelist) + return; + } + + coll = s->collection(); + m.insert( "url", QString( "servent://%1\t%2" ) + .arg( s->userName() ) + .arg( url_str ) ); + m.insert( "source", s->friendlyName() ); + } + + //int artid = files_query.value( 7 ).toInt(); + //int albid = files_query.value( 8 ).toInt(); + //int trkid = files_query.value( 9 ).toInt(); + + timer.start(); + float score = how_similar( m_v.toMap(), m ); + //qDebug() << "Score calc:" << timer.elapsed(); + + m["score"] = score; + + //qDebug() << "RESULT" << score << m; + + if( score < MINSCORE ) continue; + + res << Tomahawk::result_ptr( new Tomahawk::Result( m, coll ) ); + } + + // return results, if any found + + if( res.length() > 0 ) + { + emit results( qid, res ); + } +} + + +// TODO make clever (ft. featuring live (stuff) etc) +float +DatabaseCommand_Resolve::how_similar( const QVariantMap& q, const QVariantMap& r ) +{ + // query values + const QString qArtistname = DatabaseImpl::sortname( q.value("artist").toString() ); + const QString qAlbumname = DatabaseImpl::sortname( q.value("album").toString() ); + const QString qTrackname = DatabaseImpl::sortname( q.value("track").toString() ); + + // result values + const QString rArtistname = DatabaseImpl::sortname( r.value("artist").toString() ); + const QString rAlbumname = DatabaseImpl::sortname( r.value("album").toString() ); + const QString rTrackname = DatabaseImpl::sortname( r.value("track").toString() ); + + // normal edit distance + int artdist = levenshtein( qArtistname, rArtistname ); + int albdist = levenshtein( qAlbumname, rAlbumname ); + int trkdist = levenshtein( qTrackname, rTrackname ); + + // max length of name + int mlart = qMax( qArtistname.length(), rArtistname.length() ); + int mlalb = qMax( qAlbumname.length(), rAlbumname.length() ); + int mltrk = qMax( qTrackname.length(), rTrackname.length() ); + + // distance scores + float dcart = (float)( mlart - artdist ) / mlart; + float dcalb = (float)( mlalb - albdist ) / mlalb; + float dctrk = (float)( mltrk - trkdist ) / mltrk; + + // don't penalize for missing album name + if( qAlbumname.length() == 0 ) dcalb = 1.0; + + // weighted, so album match is worth less than track title + float combined = ( dcart*4 + dcalb + dctrk*5 ) / 10; + + return combined; +} diff --git a/src/database/databasecommand_resolve.h b/src/database/databasecommand_resolve.h new file mode 100644 index 000000000..f519e87c3 --- /dev/null +++ b/src/database/databasecommand_resolve.h @@ -0,0 +1,103 @@ +#ifndef DATABASECOMMAND_RESOLVE_H +#define DATABASECOMMAND_RESOLVE_H +#include "databasecommand.h" +#include "databaseimpl.h" +#include "tomahawk/result.h" +#include + +class DatabaseCommand_Resolve : public DatabaseCommand +{ +Q_OBJECT +public: + //explicit DatabaseCommand_Resolve(QObject *parent = 0); + explicit DatabaseCommand_Resolve( QVariant v, bool searchlocal ); + + virtual QString commandname() const { return "dbresolve"; } + virtual bool doesMutates() const { return false; } + + virtual void exec(DatabaseImpl *lib); + + +signals: + + void results( Tomahawk::QID qid, QList results ); + +public slots: + +private: + QVariant m_v; + bool m_searchlocal; + + float how_similar( const QVariantMap& q, const QVariantMap& r ); + + static int levenshtein(const QString& source, const QString& target) + { + // Step 1 + const int n = source.length(); + const int m = target.length(); + if (n == 0) { + return m; + } + if (m == 0) { + return n; + } + // Good form to declare a TYPEDEF + typedef QVector< QVector > Tmatrix; + Tmatrix matrix; + matrix.resize( n+1 ); + + // Size the vectors in the 2.nd dimension. Unfortunately C++ doesn't + // allow for allocation on declaration of 2.nd dimension of vec of vec + for (int i = 0; i <= n; i++) { + QVector tmp; + tmp.resize( m+1 ); + matrix.insert( i, tmp ); + } + // Step 2 + for (int i = 0; i <= n; i++) { + matrix[i][0]=i; + } + for (int j = 0; j <= m; j++) { + matrix[0][j]=j; + } + // Step 3 + for (int i = 1; i <= n; i++) { + const QChar s_i = source[i-1]; + // Step 4 + for (int j = 1; j <= m; j++) { + const QChar t_j = target[j-1]; + // Step 5 + int cost; + if (s_i == t_j) { + cost = 0; + } + else { + cost = 1; + } + // Step 6 + const int above = matrix[i-1][j]; + const int left = matrix[i][j-1]; + const int diag = matrix[i-1][j-1]; + //int cell = min( above + 1, min(left + 1, diag + cost)); + int cell = (((left+1)>(diag+cost))?diag+cost:left+1); + if(above+1 < cell) cell = above+1; + // Step 6A: Cover transposition, in addition to deletion, + // insertion and substitution. This step is taken from: + // Berghel, Hal ; Roach, David : "An Extension of Ukkonen's + // Enhanced Dynamic Programming ASM Algorithm" + // (http://www.acm.org/~hlb/publications/asm/asm.html) + if (i>2 && j>2) { + int trans=matrix[i-2][j-2]+1; + if (source[i-2]!=t_j) trans++; + if (s_i!=target[j-2]) trans++; + if (cell>trans) cell=trans; + } + matrix[i][j]=cell; + } + } + // Step 7 + return matrix[n][m]; + }; +}; + +#endif // DATABASECOMMAND_RESOLVE_H diff --git a/src/database/databasecommand_setplaylistrevision.cpp b/src/database/databasecommand_setplaylistrevision.cpp new file mode 100644 index 000000000..3184ca60e --- /dev/null +++ b/src/database/databasecommand_setplaylistrevision.cpp @@ -0,0 +1,183 @@ +#include "databasecommand_setplaylistrevision.h" + +#include + +#include "tomahawksqlquery.h" +#include "tomahawk/tomahawkapp.h" + + +DatabaseCommand_SetPlaylistRevision::DatabaseCommand_SetPlaylistRevision( + const source_ptr& s, + QString playlistguid, + QString newrev, + QString oldrev, + QStringList orderedguids, + QList addedentries ) + : DatabaseCommandLoggable( s ) + , m_newrev( newrev ) + , m_oldrev( oldrev ) + , m_addedentries( addedentries ) + , m_applied( false ) +{ + setPlaylistguid( playlistguid ); + + QVariantList tmp; + foreach( const QString& s, orderedguids ) + tmp << s; + + setOrderedguids( tmp ); +} + + +void +DatabaseCommand_SetPlaylistRevision::postCommitHook() +{ + qDebug() << Q_FUNC_INFO; + + QStringList orderedentriesguids; + foreach( const QVariant& v, m_orderedguids ) + orderedentriesguids << v.toString(); + + // private, but we are a friend. will recall itself in its own thread: + playlist_ptr playlist = source()->collection()->playlist( m_playlistguid ); + + if ( playlist.isNull() ) + { + qDebug() << m_playlistguid; + Q_ASSERT( !playlist.isNull() ); + return; + } + + playlist->setRevision( m_newrev, + orderedentriesguids, + m_previous_rev_orderedguids, + true, // this *is* the newest revision so far + m_addedmap, + m_applied ); + + if( source()->isLocal() ) + APP->servent().triggerDBSync(); +} + + +void +DatabaseCommand_SetPlaylistRevision::exec( DatabaseImpl* lib ) +{ + using namespace Tomahawk; + + QString currentrevision; + + // get the current revision for this playlist + // this also serves to check the playlist exists. + TomahawkSqlQuery chkq = lib->newquery(); + chkq.prepare("SELECT currentrevision FROM playlist WHERE guid = ?"); + chkq.addBindValue( m_playlistguid ); + if( chkq.exec() && chkq.next() ) + { + currentrevision = chkq.value( 0 ).toString(); + //qDebug() << Q_FUNC_INFO << "pl guid" << m_playlistguid << " curr rev" << currentrevision; + } + else + { + throw "No such playlist, WTF?"; + return; + } + + QVariantList vlist = m_orderedguids; + + QJson::Serializer ser; + const QByteArray entries = ser.serialize( vlist ); + + // add any new items: + TomahawkSqlQuery adde = lib->newquery(); + + QString sql = "INSERT INTO playlist_item( guid, playlist, trackname, artistname, albumname, " + "annotation, duration, addedon, addedby, result_hint ) " + "VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )"; + adde.prepare( sql ); + + qDebug() << "Num new playlist_items to add:" << m_addedentries.length(); + foreach( const plentry_ptr& e, m_addedentries ) + { + m_addedmap.insert( e->guid(), e ); // needed in postcommithook + + adde.bindValue( 0, e->guid() ); + adde.bindValue( 1, m_playlistguid ); + adde.bindValue( 2, e->query()->track() ); + adde.bindValue( 3, e->query()->artist() ); + adde.bindValue( 4, e->query()->album() ); + adde.bindValue( 5, e->annotation() ); + adde.bindValue( 6, (int) e->duration() ); + adde.bindValue( 7, e->lastmodified() ); + adde.bindValue( 8, source()->isLocal() ? QVariant(QVariant::Int) : source()->id() ); + adde.bindValue( 9, "" ); + bool ok = adde.exec(); + if( !ok ) + { + qDebug() << adde.lastError().databaseText() << adde.lastError().driverText() << "\n" + << sql << endl + << adde.boundValues().size() ; + int i = 0; + foreach(QVariant param, adde.boundValues()) qDebug() << i++ << param; + Q_ASSERT( ok ); + } + + } + + // add the new revision: + //qDebug() << "Adding new playlist revision, guid:" << m_newrev + // << entries; + TomahawkSqlQuery query = lib->newquery(); + sql = "INSERT INTO playlist_revision(guid, playlist, entries, author, timestamp, previous_revision) " + "VALUES(?, ?, ?, ?, ?, ?)"; + + query.prepare( sql ); + query.addBindValue( m_newrev ); + query.addBindValue( m_playlistguid ); + query.addBindValue( entries ); + query.addBindValue( source()->isLocal() ? QVariant(QVariant::Int) : source()->id() ); + query.addBindValue( 0 ); //ts + query.addBindValue( m_oldrev.isEmpty() ? QVariant(QVariant::String) : m_oldrev ); + + //qDebug() << sql << "\n" << query.boundValues(); + + bool ok = query.exec(); + Q_ASSERT( ok ); + + qDebug() << "Currentrevision:" << currentrevision << "oldrev:" << m_oldrev; + // if optimistic locking is ok, update current revision to this new one + if( currentrevision == m_oldrev ) + { + TomahawkSqlQuery query2 = lib->newquery(); + qDebug() << "updating current revision, optimistic locking ok"; + query2.prepare("UPDATE playlist SET currentrevision = ? WHERE guid = ?"); + query2.bindValue( 0, m_newrev ); + query2.bindValue( 1, m_playlistguid ); + bool uok = query2.exec(); + Q_ASSERT( uok ); + m_applied = true; + + + // load previous revision entries, which we need to pass on + // so the change can be diffed + TomahawkSqlQuery query_entries = lib->newquery(); + query_entries.prepare("SELECT entries, playlist, author, timestamp, previous_revision " + "FROM playlist_revision " + "WHERE guid = :guid"); + query_entries.bindValue( ":guid", m_oldrev ); + query_entries.exec(); + if( query_entries.next() ) + { + // entries should be a list of strings: + QJson::Parser parser; + QVariant v = parser.parse( query_entries.value(0).toByteArray(), &ok ); + Q_ASSERT( ok && v.type() == QVariant::List ); //TODO + m_previous_rev_orderedguids = v.toStringList(); + } + } + else + { + qDebug() << "Not updating current revision, optimistic locking fail"; + } + +} diff --git a/src/database/databasecommand_setplaylistrevision.h b/src/database/databasecommand_setplaylistrevision.h new file mode 100644 index 000000000..43da1e2fe --- /dev/null +++ b/src/database/databasecommand_setplaylistrevision.h @@ -0,0 +1,82 @@ +#ifndef DATABASECOMMAND_SETPLAYLISTREVISION_H +#define DATABASECOMMAND_SETPLAYLISTREVISION_H + +#include "databasecommandloggable.h" +#include "databaseimpl.h" +#include "tomahawk/collection.h" +#include "tomahawk/playlist.h" + +using namespace Tomahawk; + +class DatabaseCommand_SetPlaylistRevision : public DatabaseCommandLoggable +{ +Q_OBJECT +Q_PROPERTY( QString playlistguid READ playlistguid WRITE setPlaylistguid ) +Q_PROPERTY( QString newrev READ newrev WRITE setNewrev ) +Q_PROPERTY( QString oldrev READ oldrev WRITE setOldrev ) +Q_PROPERTY( QVariantList orderedguids READ orderedguids WRITE setOrderedguids ) +Q_PROPERTY( QVariantList addedentries READ addedentriesV WRITE setAddedentriesV ) + +public: + explicit DatabaseCommand_SetPlaylistRevision( QObject* parent = 0 ) + : DatabaseCommandLoggable( parent ) + , m_applied( false ) + {} + + explicit DatabaseCommand_SetPlaylistRevision( const source_ptr& s, + QString playlistguid, + QString newrev, + QString oldrev, + QStringList orderedguids, + QList addedentries ); + + QString commandname() const { return "setplaylistrevision"; } + + virtual void exec( DatabaseImpl* lib ); + virtual void postCommitHook(); + virtual bool doesMutates() const { return true; } + + void setAddedentriesV( const QVariantList& vlist ) + { + m_addedentries.clear(); + foreach( const QVariant& v, vlist ) + { + PlaylistEntry * pep = new PlaylistEntry; + QJson::QObjectHelper::qvariant2qobject( v.toMap(), pep ); + m_addedentries << plentry_ptr(pep); + } + } + + QVariantList addedentriesV() const + { + QVariantList vlist; + foreach( const plentry_ptr& pe, m_addedentries ) + { + QVariant v = QJson::QObjectHelper::qobject2qvariant( pe.data() ); + vlist << v; + } + return vlist; + } + + void setPlaylistguid( const QString& s ) { m_playlistguid = s; } + + void setNewrev( const QString& s ) { m_newrev = s; } + void setOldrev( const QString& s ) { m_oldrev = s; } + QString newrev() const { return m_newrev; } + QString oldrev() const { return m_oldrev; } + QString playlistguid() const { return m_playlistguid; } + + void setOrderedguids( const QVariantList& l ) { m_orderedguids = l; } + QVariantList orderedguids() const { return m_orderedguids; } + +private: + QString m_playlistguid; + QString m_newrev, m_oldrev; + QVariantList m_orderedguids; + QStringList m_previous_rev_orderedguids; + QList m_addedentries; + bool m_applied; + QMap m_addedmap; +}; + +#endif // DATABASECOMMAND_SETPLAYLISTREVISION_H diff --git a/src/database/databasecommand_sourceoffline.cpp b/src/database/databasecommand_sourceoffline.cpp new file mode 100644 index 000000000..e31e7861e --- /dev/null +++ b/src/database/databasecommand_sourceoffline.cpp @@ -0,0 +1,16 @@ +#include "databasecommand_sourceoffline.h" + + +DatabaseCommand_SourceOffline::DatabaseCommand_SourceOffline( int id ) + : DatabaseCommand() + , m_id( id ) +{ +} + + +void DatabaseCommand_SourceOffline::exec( DatabaseImpl* lib ) +{ + TomahawkSqlQuery q = lib->newquery(); + q.exec( QString( "UPDATE source SET isonline = 'false' WHERE id = %1" ) + .arg( m_id ) ); +} diff --git a/src/database/databasecommand_sourceoffline.h b/src/database/databasecommand_sourceoffline.h new file mode 100644 index 000000000..c5988d378 --- /dev/null +++ b/src/database/databasecommand_sourceoffline.h @@ -0,0 +1,20 @@ +#ifndef DATABASECOMMAND_SOURCEOFFLINE_H +#define DATABASECOMMAND_SOURCEOFFLINE_H + +#include "databasecommand.h" +#include "databaseimpl.h" + +class DatabaseCommand_SourceOffline : public DatabaseCommand +{ +Q_OBJECT + +public: + explicit DatabaseCommand_SourceOffline( int id ); + bool doesMutates() const { return true; } + void exec( DatabaseImpl* lib ); + +private: + int m_id; +}; + +#endif // DATABASECOMMAND_SOURCEOFFLINE_H diff --git a/src/database/databasecommand_updatesearchindex.cpp b/src/database/databasecommand_updatesearchindex.cpp new file mode 100644 index 000000000..88570f2da --- /dev/null +++ b/src/database/databasecommand_updatesearchindex.cpp @@ -0,0 +1,114 @@ +#include "databasecommand_updatesearchindex.h" +DatabaseCommand_UpdateSearchIndex::DatabaseCommand_UpdateSearchIndex( const QString& t, int p ) + : DatabaseCommand() + , table( t ) + , pkey( p ) +{ + if( table != "artist" && table != "track" && table != "album" ) + { + Q_ASSERT(false); + return; + } +} + +void DatabaseCommand_UpdateSearchIndex::exec(DatabaseImpl *db) +{ + qDebug() << Q_FUNC_INFO; + + if( table != "artist" && table != "track" && table != "album" ) + { + Q_ASSERT(false); + return; + } + + // if pkey is 0, consult DB to see what needs indexing + if( pkey == 0 ) + { + TomahawkSqlQuery q = db->newquery(); + q.exec( QString("SELECT coalesce(max(id),0) from %1_search_index").arg(table) ); + q.next(); + pkey = 1 + q.value(0).toInt(); + qDebug() << "updateSearchIndex" << table << "consulted DB, starting at" << pkey; + } + + TomahawkSqlQuery query = db->newquery(); + qDebug() << "Building index for" << table << ">= id" << pkey; + QString searchtable( table + "_search_index" ); + query.exec(QString( "SELECT id, sortname FROM %1 WHERE id >= %2" ).arg( table ).arg(pkey ) ); + + TomahawkSqlQuery upq = db->newquery(); + TomahawkSqlQuery inq = db->newquery(); + inq.prepare( "INSERT INTO "+ searchtable +" (ngram, id, num) VALUES (?,?,?)" ); + upq.prepare( "UPDATE "+ searchtable +" SET num=num+? WHERE ngram=? AND id=?" ); + + int num_names = 0; + int num_ngrams = 0; + int id; + QString name; + QMap ngrammap; + + // this is the new ngram map we build up, to be merged into the + // main one in FuzzyIndex: + QHash< QString, QMap > idx; + + while( query.next() ) + { + id = query.value( 0 ).toInt(); + name = query.value( 1 ).toString(); + num_names++; + inq.bindValue( 1, id ); // set id + upq.bindValue( 2, id ); // set id + ngrammap = DatabaseImpl::ngrams( name ); + QMapIterator i( ngrammap ); + + while ( i.hasNext() ) + { + i.next(); + num_ngrams++; + upq.bindValue( 0, i.value() ); //num + upq.bindValue( 1, i.key() ); // ngram + upq.exec(); + + if( upq.numRowsAffected() == 0 ) + { + inq.bindValue( 0, i.key() ); //ngram + inq.bindValue( 2, i.value() ); //num + inq.exec(); + if( inq.numRowsAffected() == 0 ) + { + qDebug() << "Error updating search index:" << id << name; + continue; + } + } + + // update ngram cache: + QMapIterator iter( ngrammap ); + while ( iter.hasNext() ) + { + iter.next(); + if( idx.contains( iter.key() ) ) + { + idx[ iter.key() ][ id ] += iter.value(); + } + else + { + QMap tmp; + tmp.insert( id, iter.value() ); + idx.insert( iter.key(), tmp ); + } + } + + + } + } + + // merge in our ngrams into the main index + QMetaObject::invokeMethod( &(db->m_fuzzyIndex), + "mergeIndex", + Qt::QueuedConnection, + Q_ARG( QString, table ), + QGenericArgument( "QHash< QString, QMap >", &idx ) + ); + + qDebug() << "Finished indexing" << num_names <<" names," << num_ngrams << "ngrams."; +} diff --git a/src/database/databasecommand_updatesearchindex.h b/src/database/databasecommand_updatesearchindex.h new file mode 100644 index 000000000..6195218ad --- /dev/null +++ b/src/database/databasecommand_updatesearchindex.h @@ -0,0 +1,27 @@ +#ifndef DATABASECOMMAND_UPDATESEARCHINDEX_H +#define DATABASECOMMAND_UPDATESEARCHINDEX_H +#include "databasecommand.h" +#include "databaseimpl.h" + +class DatabaseCommand_UpdateSearchIndex : public DatabaseCommand +{ +Q_OBJECT +public: + explicit DatabaseCommand_UpdateSearchIndex(const QString& table, int pkey); + + virtual QString commandname() const { return "updatesearchindex"; } + virtual bool doesMutates() const { return true; } + virtual void exec(DatabaseImpl* db); + +signals: + void indexUpdated(); + +public slots: + +private: + QString table; + int pkey; + +}; + +#endif // DATABASECOMMAND_UPDATESEARCHINDEX_H diff --git a/src/database/databasecommandloggable.cpp b/src/database/databasecommandloggable.cpp new file mode 100644 index 000000000..293b51667 --- /dev/null +++ b/src/database/databasecommandloggable.cpp @@ -0,0 +1,33 @@ +#include "databasecommandloggable.h" + +#include + +#include "database/databasecommand_addfiles.h" +#include "database/databasecommand_setplaylistrevision.h" + + +DatabaseCommandLoggable* +DatabaseCommandLoggable::factory( QVariantMap c ) +{ + const QString name = c.value( "command" ).toString(); + //TODO dynamic class loading, factory blah + + if( name == "addfiles" ) + { + DatabaseCommand_AddFiles* cmd = new DatabaseCommand_AddFiles; + QJson::QObjectHelper::qvariant2qobject( c, cmd ); + return cmd; + } + else if( name == "setplaylistrevision" ) + { + DatabaseCommand_SetPlaylistRevision* cmd = new DatabaseCommand_SetPlaylistRevision; + QJson::QObjectHelper::qvariant2qobject( c, cmd ); + return cmd; + } + else + { + qDebug() << "Unhandled command name"; + Q_ASSERT( false ); + return 0; + } +} diff --git a/src/database/databasecommandloggable.h b/src/database/databasecommandloggable.h new file mode 100644 index 000000000..5bffff2e1 --- /dev/null +++ b/src/database/databasecommandloggable.h @@ -0,0 +1,29 @@ +#ifndef DATABASECOMMANDLOGGABLE_H +#define DATABASECOMMANDLOGGABLE_H + +#include "database/databasecommand.h" + +/// A Database Command that will be added to the oplog and sent over the network +/// so peers can sync up and changes to our collection in their cached copy. +class DatabaseCommandLoggable : public DatabaseCommand +{ +Q_OBJECT +Q_PROPERTY(QString command READ commandname) + +public: + + explicit DatabaseCommandLoggable( QObject* parent = 0 ) + : DatabaseCommand( parent ) + {} + + explicit DatabaseCommandLoggable( const Tomahawk::source_ptr& s, QObject* parent = 0 ) + : DatabaseCommand( s, parent ) + {} + + virtual bool loggable() const { return true; } + + static DatabaseCommandLoggable* factory( QVariantMap c ); + +}; + +#endif // DATABASECOMMANDLOGGABLE_H diff --git a/src/database/databaseimpl.cpp b/src/database/databaseimpl.cpp new file mode 100644 index 000000000..d1ca9ad42 --- /dev/null +++ b/src/database/databaseimpl.cpp @@ -0,0 +1,455 @@ +#include "databaseimpl.h" + +#include +#include +#include +#include +#include "database.h" +#include "tomahawk/tomahawkapp.h" +#include "databasecommand_updatesearchindex.h" + +/* !!!! You need to manually generate schema.sql.h when the schema changes: + cd src/database + ./gen_schema.h.sh ./schema.sql tomahawk > schema.sql.h +*/ +#include "schema.sql.h" + +#define CURRENT_SCHEMA_VERSION 14 + + +DatabaseImpl::DatabaseImpl( const QString& dbname, Database* parent ) + : QObject( (QObject*) parent ) + , m_lastartid( 0 ) + , m_lastalbid( 0 ) + , m_lasttrkid( 0 ) + , m_fuzzyIndex( *this ) +{ + connect( this, SIGNAL(indexReady()), parent, SIGNAL(indexReady()) ); + + db = QSqlDatabase::addDatabase( "QSQLITE", "tomahawk" ); + db.setDatabaseName( dbname ); + if ( !db.open() ) + { + qDebug() << "FAILED TO OPEN DB"; + throw "failed to open db"; // TODO + } + + QSqlQuery qry = QSqlQuery( db ); + query = newquery(); + + qry.exec( "SELECT v FROM settings WHERE k='schema_version'" ); + if ( qry.next() ) + { + int v = qry.value( 0 ).toInt(); + qDebug() << "Current schema is" << v << this->thread(); + if ( v != CURRENT_SCHEMA_VERSION ) + { + + QString newname = QString("%1.v%2").arg(dbname).arg(v); + qDebug() << endl << "****************************" << endl; + qDebug() << "Schema version too old: " << v << ". Current version is:" << CURRENT_SCHEMA_VERSION; + qDebug() << "Moving" << dbname << newname; + qDebug() << endl << "****************************" << endl; + + qry.clear(); + query.clear(); + qry.finish(); + query.finish(); + + db.close(); + db.removeDatabase( "tomahawk" ); + + if( QFile::rename( dbname, newname ) ) + { + db = QSqlDatabase::addDatabase( "QSQLITE", "tomahawk" ); + db.setDatabaseName( dbname ); + if( !db.open() ) throw "db moving failed"; + updateSchema( v ); + } + else + { + Q_ASSERT(0); + QTimer::singleShot( 0, APP, SLOT( quit() ) ); + return; + } + } + } else { + updateSchema( 0 ); + } + + query.exec( "SELECT v FROM settings WHERE k='dbid'" ); + if( query.next() ) + { + m_dbid = query.value( 0 ).toString(); + } + else + { + m_dbid = uuid(); + query.exec( QString( "INSERT INTO settings(k,v) VALUES('dbid','%1')" ).arg( m_dbid ) ); + } + qDebug() << "Database ID:" << m_dbid; + + // make sqlite behave how we want: + query.exec( "PRAGMA synchronous = ON" ); + query.exec( "PRAGMA foreign_keys = ON" ); + //query.exec( "PRAGMA temp_store = MEMORY" ); + + // in case of unclean shutdown last time: + query.exec( "UPDATE source SET isonline = 'false'" ); +} + + +DatabaseImpl::~DatabaseImpl() +{ + m_indexThread.quit(); + m_indexThread.wait(5000); +} + +void +DatabaseImpl::loadIndex() +{ + // load ngram index in the background + m_fuzzyIndex.moveToThread( &m_indexThread ); + connect( &m_indexThread, SIGNAL(started()), &m_fuzzyIndex, SLOT(loadNgramIndex()) ); + connect( &m_fuzzyIndex, SIGNAL(indexReady()), this, SIGNAL(indexReady()) ); + m_indexThread.start(); +} + +void +DatabaseImpl::updateSearchIndex( const QString& table, int pkey ) +{ + DatabaseCommand* cmd = new DatabaseCommand_UpdateSearchIndex(table, pkey); + APP->database()->enqueue( QSharedPointer( cmd ) ); +} + + +bool +DatabaseImpl::updateSchema( int currentver ) +{ + qDebug() << "Create tables... old version is" << currentver; + QString sql( get_tomahawk_sql() ); + QStringList statements = sql.split( ";", QString::SkipEmptyParts ); + db.transaction(); + + foreach( const QString& sl, statements ) + { + QString s( sl.trimmed() ); + if( s.length() == 0 ) + continue; + + qDebug() << "Executing:" << s; + query.exec( s ); + } + + db.commit(); + return true; +} + + +QVariantMap +DatabaseImpl::file( int fid ) +{ + QVariantMap m; + query.exec( QString( "SELECT url, mtime, size, md5, mimetype, duration, bitrate, " + "file_join.artist, file_join.album, file_join.track, " + "(select name from artist where id = file_join.artist) as artname, " + "(select name from album where id = file_join.album) as albname, " + "(select name from track where id = file_join.track) as trkname " + "FROM file, file_join " + "WHERE file.id = file_join.file AND file.id = %1" ) + .arg( fid ) ); + + if( query.next() ) + { + m["url"] = query.value( 0 ).toString(); + m["mtime"] = query.value( 1 ).toString(); + m["size"] = query.value( 2 ).toInt(); + m["hash"] = query.value( 3 ).toString(); + m["mimetype"] = query.value( 4 ).toString(); + m["duration"] = query.value( 5 ).toInt(); + m["bitrate"] = query.value( 6 ).toInt(); + m["artist"] = query.value( 10 ).toString(); + m["album"] = query.value( 11 ).toString(); + m["track"] = query.value( 12 ).toString(); + } + + //qDebug() << m; + return m; +} + + +int +DatabaseImpl::artistId( const QString& name_orig, bool& isnew ) +{ + isnew = false; + if( m_lastart == name_orig ) + return m_lastartid; + + int id = 0; + QString sortname = DatabaseImpl::sortname( name_orig ); + + query.prepare( "SELECT id FROM artist WHERE sortname = ?" ); + query.addBindValue( sortname ); + query.exec(); + if( query.next() ) + { + id = query.value( 0 ).toInt(); + } + if( id ) + { + m_lastart = name_orig; + m_lastartid = id; + return id; + } + + // not found, insert it. + query.prepare( "INSERT INTO artist(id,name,sortname) VALUES(NULL,?,?)" ); + query.addBindValue( name_orig ); + query.addBindValue( sortname ); + if( !query.exec() ) + { + qDebug() << "Failed to insert artist:" << name_orig; + return 0; + } + + id = query.lastInsertId().toInt(); + isnew = true; + m_lastart = name_orig; + m_lastartid = id; + return id; +} + + +int +DatabaseImpl::trackId( int artistid, const QString& name_orig, bool& isnew ) +{ + isnew = false; + int id = 0; + QString sortname = DatabaseImpl::sortname( name_orig ); + //if( ( id = m_artistcache[sortname] ) ) return id; + + query.prepare( "SELECT id FROM track WHERE artist = ? AND sortname = ?" ); + query.addBindValue( artistid ); + query.addBindValue( sortname ); + query.exec(); + + if( query.next() ) + { + id = query.value( 0 ).toInt(); + } + if( id ) + { + //m_trackcache[sortname]=id; + return id; + } + + // not found, insert it. + query.prepare( "INSERT INTO track(id,artist,name,sortname) VALUES(NULL,?,?,?)" ); + query.addBindValue( artistid ); + query.addBindValue( name_orig ); + query.addBindValue( sortname ); + if( !query.exec() ) + { + qDebug() << "Failed to insert track:" << name_orig ; + return 0; + } + + id = query.lastInsertId().toInt(); + //m_trackcache[sortname]=id; + isnew = true; + return id; +} + + +int +DatabaseImpl::albumId( int artistid, const QString& name_orig, bool& isnew ) +{ + isnew = false; + if( name_orig.isEmpty() ) + { + //qDebug() << Q_FUNC_INFO << "empty album name"; + return 0; + } + + if( m_lastartid == artistid && m_lastalb == name_orig ) + return m_lastalbid; + + int id = 0; + QString sortname = DatabaseImpl::sortname( name_orig ); + //if( ( id = m_albumcache[sortname] ) ) return id; + + query.prepare( "SELECT id FROM album WHERE artist = ? AND sortname = ?" ); + query.addBindValue( artistid ); + query.addBindValue( sortname ); + query.exec(); + if( query.next() ) + { + id = query.value( 0 ).toInt(); + } + if( id ) + { + m_lastalb = name_orig; + m_lastalbid = id; + return id; + } + + // not found, insert it. + query.prepare( "INSERT INTO album(id,artist,name,sortname) VALUES(NULL,?,?,?)" ); + query.addBindValue( artistid ); + query.addBindValue( name_orig ); + query.addBindValue( sortname ); + if( !query.exec() ) + { + qDebug() << "Failed to insert album: " << name_orig ; + return 0; + } + + id = query.lastInsertId().toInt(); + //m_albumcache[sortname]=id; + isnew = true; + m_lastalb = name_orig; + m_lastalbid = id; + return id; +} + + +QList< int > +DatabaseImpl::searchTable( const QString& table, const QString& name_orig, uint limit ) +{ + QList< int > results; + if( table != "artist" && table != "track" && table != "album" ) + return results; + + QString name = sortname( name_orig ); + + // first check for exact matches: + query.prepare( QString( "SELECT id FROM %1 WHERE sortname = ?" ).arg( table ) ); + query.addBindValue( name ); + bool exactok = query.exec(); + Q_ASSERT( exactok ); + + while( query.next() ) + results.append( query.value( 0 ).toInt() ); + + // ngram stuff only works on tracks over a certain length atm: + if( name_orig.length() > 3 ) + { + // consult ngram index to find candidates: + QMap< int, float > resultsmap = m_fuzzyIndex.search( table, name ); + + //qDebug() << "results map for" << table << resultsmap.size(); + QList< QPair > resultslist; + foreach( int i, resultsmap.keys() ) + { + resultslist << QPair( i, (float)resultsmap.value( i ) ); + } + qSort( resultslist.begin(), resultslist.end(), DatabaseImpl::scorepairSorter ); + + for( int k = 0; k < resultslist.size() && k < (int)limit; ++k ) + { + results << resultslist.at( k ).first; + } + } + + return results; +} + + +QList< int > +DatabaseImpl::getTrackFids( int tid ) +{ + QList< int > ret; + query.exec( QString( "SELECT file.id FROM file, file_join " + "WHERE file_join.file=file.id " + "AND file_join.track = %1 ").arg( tid ) ); + query.exec(); + + while( query.next() ) + ret.append( query.value( 0 ).toInt() ); + + return ret; +} + + +QMap< QString, int > +DatabaseImpl::ngrams( const QString& str_orig ) +{ + static QMap< QString, QMap > memo; + if( memo.contains( str_orig ) ) + return memo.value( str_orig ); + + int n = 3; + QMap ret; + QString str( " " + DatabaseImpl::sortname( str_orig ) + " " ); + int num = str.length(); + QString ngram; + + for( int j = 0; j < num - ( n - 1 ); j++ ) + { + ngram = str.mid( j, n ); + Q_ASSERT( ngram.length() == n ); + + if( ret.contains( ngram ) ) + ret[ngram]++; + else + ret[ngram] = 1; + } + + memo.insert( str_orig, ret ); + return ret; +} + + +QString +DatabaseImpl::sortname( const QString& str ) +{ + return str.toLower().trimmed().replace( QRegExp("[\\s]{2,}"), " " ); +} + + +QVariantMap +DatabaseImpl::artist( int id ) +{ + query.exec( QString( "SELECT id, name, sortname FROM artist WHERE id = %1" ).arg( id ) ); + QVariantMap m; + if( !query.next() ) + return m; + + m["id"] = query.value( 0 ); + m["name"] = query.value( 1 ); + m["sortname"] = query.value( 2 ); + return m; +} + + +QVariantMap +DatabaseImpl::track( int id ) +{ + query.exec( QString( "SELECT id, artist, name, sortname FROM track WHERE id = %1" ).arg( id ) ); + QVariantMap m; + if( !query.next() ) + return m; + + m["id"] = query.value( 0 ); + m["artist"] = query.value( 1 ); + m["name"] = query.value( 2 ); + m["sortname"] = query.value( 3 ); + return m; +} + + +QVariantMap +DatabaseImpl::album( int id ) +{ + query.exec( QString( "SELECT id, artist, name, sortname FROM album WHERE id = %1" ).arg( id ) ); + QVariantMap m; + if( !query.next() ) + return m; + + m["id"] = query.value( 0 ); + m["artist"] = query.value( 1 ); + m["name"] = query.value( 2 ); + m["sortname"] = query.value( 3 ); + return m; +} + diff --git a/src/database/databaseimpl.h b/src/database/databaseimpl.h new file mode 100644 index 000000000..d0eb40ad9 --- /dev/null +++ b/src/database/databaseimpl.h @@ -0,0 +1,79 @@ +#ifndef DATABASEIMPL_H +#define DATABASEIMPL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tomahawksqlquery.h" +#include "fuzzyindex.h" + +class Database; +class DatabaseImpl : public QObject +{ +Q_OBJECT +friend class FuzzyIndex; +friend class DatabaseCommand_UpdateSearchIndex; +public: + DatabaseImpl( const QString& dbname, Database* parent = 0 ); + ~DatabaseImpl(); + + TomahawkSqlQuery newquery() { return TomahawkSqlQuery( db ); } + QSqlDatabase& database() { return db; } + + int artistId( const QString& name_orig, bool& isnew ); + int trackId( int artistid, const QString& name_orig, bool& isnew ); + int albumId( int artistid, const QString& name_orig, bool& isnew ); + + QList< int > searchTable( const QString& table, const QString& name_orig, uint limit = 10 ); + QList< int > getTrackFids( int tid ); + + static QMap ngrams( const QString& str_orig ); + static QString sortname( const QString& str ); + + QVariantMap artist( int id ); + QVariantMap album( int id ); + QVariantMap track( int id ); + QVariantMap file( int fid ); + + static bool scorepairSorter( const QPair& left, const QPair& right ) + { + return left.second > right.second; + } + + // indexes entries from "table" where id >= pkey + void updateSearchIndex( const QString& table, int pkey ); + + const QString& dbid() const { return m_dbid; } + + void loadIndex(); + +signals: + void indexReady(); + +public slots: + +private: + + bool updateSchema( int currentver ); + QSqlDatabase db; + TomahawkSqlQuery query; + + QString m_lastart, m_lastalb, m_lasttrk; + int m_lastartid, m_lastalbid, m_lasttrkid; + + QString m_dbid; + + QThread m_indexThread; + FuzzyIndex m_fuzzyIndex; +}; + +#endif // DATABASEIMPL_H diff --git a/src/database/databaseresolver.cpp b/src/database/databaseresolver.cpp new file mode 100644 index 000000000..3dad08e7b --- /dev/null +++ b/src/database/databaseresolver.cpp @@ -0,0 +1,49 @@ +#include "databaseresolver.h" + +#include "tomahawk/tomahawkapp.h" +#include "database.h" +#include "database/databasecommand_resolve.h" + +DatabaseResolver::DatabaseResolver( bool searchlocal, int weight ) + : Resolver() + , m_searchlocal( searchlocal ) + , m_weight( weight ) +{ +} + + +void +DatabaseResolver::resolve( QVariant v ) +{ + //qDebug() << Q_FUNC_INFO << v; + + if( !m_searchlocal ) + { + if( APP->servent().numConnectedPeers() == 0 ) + return; + } + + DatabaseCommand_Resolve* cmd = new DatabaseCommand_Resolve( v, m_searchlocal ); + + connect( cmd, SIGNAL( results( Tomahawk::QID, QList< Tomahawk::result_ptr> ) ), + SLOT( gotResults( Tomahawk::QID, QList< Tomahawk::result_ptr> ) ), Qt::QueuedConnection ); + + APP->database()->enqueue( QSharedPointer( cmd ) ); + +} + + +void +DatabaseResolver::gotResults( const Tomahawk::QID qid, QList< Tomahawk::result_ptr> results ) +{ + //qDebug() << Q_FUNC_INFO << qid << results.length(); + + APP->pipeline()->reportResults( qid, results ); +} + + +QString +DatabaseResolver::name() const +{ + return QString( "Database (%1)" ).arg( m_searchlocal ? "local" : "remote" ); +} diff --git a/src/database/databaseresolver.h b/src/database/databaseresolver.h new file mode 100644 index 000000000..0ffe09ff9 --- /dev/null +++ b/src/database/databaseresolver.h @@ -0,0 +1,29 @@ +#ifndef DATABASERESOLVER_H +#define DATABASERESOLVER_H + +#include "tomahawk/resolver.h" +#include "tomahawk/result.h" + +class DatabaseResolver : public Tomahawk::Resolver +{ +Q_OBJECT + +public: + explicit DatabaseResolver( bool searchlocal, int weight ); + + virtual QString name() const; + virtual unsigned int weight() const { return m_weight; } + virtual unsigned int preference() const { return 100; } + virtual unsigned int timeout() const { return 1000; } + + virtual void resolve( QVariant v ); + +private slots: + void gotResults( const Tomahawk::QID qid, QList< Tomahawk::result_ptr> results ); + +private: + bool m_searchlocal; + int m_weight; +}; + +#endif // DATABASERESOLVER_H diff --git a/src/database/databaseworker.cpp b/src/database/databaseworker.cpp new file mode 100644 index 000000000..d07296ab6 --- /dev/null +++ b/src/database/databaseworker.cpp @@ -0,0 +1,194 @@ +#include "databaseworker.h" + +#include +#include +#include + +#include "tomahawk/tomahawkapp.h" +#include "database.h" +#include "database/databasecommandloggable.h" + + +DatabaseWorker::DatabaseWorker( DatabaseImpl* lib, Database* db, bool mutates ) + : QThread() + , m_dbimpl( lib ) + , m_abort( false ) + , m_outstanding( 0 ) +{ + moveToThread( this ); + if( mutates ) + { + connect( db, SIGNAL( newJobRW(QSharedPointer) ), + SLOT( doWork(QSharedPointer) ), + Qt::QueuedConnection ); + } + else + { + connect( db, SIGNAL( newJobRO(QSharedPointer) ), + SLOT( doWork(QSharedPointer) ), + Qt::QueuedConnection ); + } + + qDebug() << "CTOR DatabaseWorker" << this->thread(); +} + + +DatabaseWorker::~DatabaseWorker() +{ + qDebug() << Q_FUNC_INFO; + + quit(); + wait( 5000 ); +} + + +void +DatabaseWorker::run() +{ + exec(); + qDebug() << Q_FUNC_INFO << "DatabaseWorker finishing..."; +} + + +void +DatabaseWorker::doWork( QSharedPointer cmd ) +{ + /* + Run the dbcmd. Only inside a transaction if the cmd does mutates. + + If the cmd is modifying local content (ie source->isLocal()) then + log to the database oplog for replication to peers. + + */ + QTime timer; + timer.start(); + if( cmd->doesMutates() ) + { + bool transok = m_dbimpl->database().transaction(); + Q_ASSERT( transok ); + } + try + { + cmd->_exec( m_dbimpl ); // runs actual SQL stuff + + if( cmd->loggable() ) + { + // We only save our own ops to the oplog, since incoming ops from peers + // are applied immediately. + // + // Crazy idea: if peers had keypairs and could sign ops/msgs, in theory it + // would be safe to sync ops for friend A from friend B's cache, if he saved them, + // which would mean you could get updates even if a peer was offline. + if( cmd->source()->isLocal() ) + { + // save to op-log + DatabaseCommandLoggable* command = (DatabaseCommandLoggable*)cmd.data(); + logOp( command ); + } + else + { + // Make a note of the last guid we applied for this source + // so we can always request just the newer ops in future. + // + qDebug() << "Setting lastop for source" << cmd->source()->id() << "to" << cmd->guid(); + TomahawkSqlQuery query = m_dbimpl->newquery(); + query.prepare( "UPDATE source SET lastop = ? WHERE id = ?" ); + query.addBindValue( cmd->guid() ); + query.addBindValue( cmd->source()->id() ); + if( !query.exec() ) + { + qDebug() << "Failed to set lastop"; + throw "Failed to set lastop"; + } + } + } + + if( cmd->doesMutates() ) + { + qDebug() << "Comitting" << cmd->commandname();; + if( !m_dbimpl->database().commit() ) + { + + qDebug() << "*FAILED TO COMMIT TRANSACTION*"; + throw "commit failed"; + } + else + { + qDebug() << "Committed" << cmd->commandname(); + } + } + + //uint duration = timer.elapsed(); + //qDebug() << "DBCmd Duration:" << duration << "ms, now running postcommit for" << cmd->commandname(); + cmd->postCommit(); + //qDebug() << "Post commit finished for"<< cmd->commandname(); + } + catch( const char * msg ) + { + qDebug() << endl + << "*ERROR* processing databasecommand:" + << cmd->commandname() + << msg + << m_dbimpl->database().lastError().databaseText() + << m_dbimpl->database().lastError().driverText() + << endl; + + if( cmd->doesMutates() ) + m_dbimpl->database().rollback(); + + Q_ASSERT( false ); + } + catch(...) + { + qDebug() << "Uncaught exception processing dbcmd"; + if( cmd->doesMutates() ) + m_dbimpl->database().rollback(); + + Q_ASSERT( false ); + throw; + } + cmd->emitFinished(); +} + + +// this should take a const command, need to check/make json stuff mutable for some objs tho maybe. +void +DatabaseWorker::logOp( DatabaseCommandLoggable* command ) +{ + TomahawkSqlQuery oplogquery = m_dbimpl->newquery(); + oplogquery.prepare( "INSERT INTO oplog(source, guid, command, compressed, json) " + "VALUES(?, ?, ?, ?, ?) "); + + QVariantMap variant = QJson::QObjectHelper::qobject2qvariant( command ); + QByteArray ba = m_serializer.serialize( variant ); + + //qDebug() << "OP JSON:" << ba; // debug + + bool compressed = false; + if( ba.length() >= 512 ) + { + // We need to compress this in this thread, since inserting into the log + // has to happen as part of the same transaction as the dbcmd. + // (we are in a worker thread for RW dbcmds anyway, so it's ok) + //qDebug() << "Compressing DB OP JSON, uncompressed size:" << ba.length(); + ba = qCompress( ba, 9 ); + compressed = true; + //qDebug() << "Compressed DB OP JSON size:" << ba.length(); + } + + qDebug() << "Saving to oplog:" << command->commandname() + << "bytes:" << ba.length() + << "guid:" << command->guid(); + + oplogquery.bindValue( 0, command->source()->isLocal() ? + QVariant(QVariant::Int) : command->source()->id() ); + oplogquery.bindValue( 1, command->guid() ); + oplogquery.bindValue( 2, command->commandname() ); + oplogquery.bindValue( 3, compressed ); + oplogquery.bindValue( 4, ba ); + if( !oplogquery.exec() ) + { + qDebug() << "Error saving to oplog"; + throw "Failed to save to oplog"; + } +} diff --git a/src/database/databaseworker.h b/src/database/databaseworker.h new file mode 100644 index 000000000..74c0eb22a --- /dev/null +++ b/src/database/databaseworker.h @@ -0,0 +1,46 @@ +#ifndef DATABASEWORKER_H +#define DATABASEWORKER_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "databasecommand.h" +#include "databaseimpl.h" + +class Database; +class DatabaseCommandLoggable; + +class DatabaseWorker : public QThread +{ +Q_OBJECT + +public: + DatabaseWorker( DatabaseImpl*, Database*, bool mutates ); + ~DatabaseWorker(); + //void enqueue( QSharedPointer ); + +protected: + void run(); + +public slots: + void doWork( QSharedPointer ); + +private: + void logOp( DatabaseCommandLoggable* command ); + + QMutex m_mut; + DatabaseImpl* m_dbimpl; + QList< QSharedPointer > m_commands; + bool m_abort; + int m_outstanding; + QJson::Serializer m_serializer; +}; + +#endif // DATABASEWORKER_H diff --git a/src/database/fuzzyindex.cpp b/src/database/fuzzyindex.cpp new file mode 100644 index 000000000..c57b258d3 --- /dev/null +++ b/src/database/fuzzyindex.cpp @@ -0,0 +1,124 @@ +#include "fuzzyindex.h" +#include "databaseimpl.h" +#include + +FuzzyIndex::FuzzyIndex(DatabaseImpl &db) : + QObject(), m_db( db ), m_loaded( false ) +{ +} + +void +FuzzyIndex::loadNgramIndex() +{ + // this updates the index in the DB, if needed: + qDebug() << "Checking catalogue is fully indexed.."; + m_db.updateSearchIndex("artist",0); + m_db.updateSearchIndex("album",0); + m_db.updateSearchIndex("track",0); + + // loads index from DB into memory: + qDebug() << "Loading search index for catalogue metadata..." << thread(); + loadNgramIndex_helper( m_artist_ngrams, "artist" ); + loadNgramIndex_helper( m_album_ngrams, "album" ); + loadNgramIndex_helper( m_track_ngrams, "track" ); + m_loaded = true; + emit indexReady(); +} + + +void +FuzzyIndex::loadNgramIndex_helper( QHash< QString, QMap >& idx, const QString& table, unsigned int fromkey ) +{ + QTime t; + t.start(); + TomahawkSqlQuery query = m_db.newquery(); + query.exec( QString( "SELECT ngram, id, num " + "FROM %1_search_index " + "WHERE id >= %2 " + "ORDER BY ngram" ).arg( table ).arg( fromkey ) ); + + QMap ngram_idx; + QString lastngram; + while( query.next() ) + { + if( lastngram.isEmpty() ) + lastngram = query.value(0).toString(); + + if( query.value( 0 ).toString() != lastngram ) + { + idx.insert( lastngram, ngram_idx ); + lastngram = query.value( 0 ).toString(); + ngram_idx.clear(); + } + + ngram_idx.insert( query.value( 1 ).toUInt(), + query.value( 2 ).toUInt() ); + } + + idx.insert( lastngram, ngram_idx ); + qDebug() << "Loaded" << idx.size() + << "ngram entries for" << table + << "in" << t.elapsed(); +} + +void FuzzyIndex::mergeIndex(const QString& table, QHash< QString, QMap > tomerge) +{ + qDebug() << Q_FUNC_INFO << table << tomerge.keys().size(); + + QHash< QString, QMap >* idx; + if ( table == "artist" ) idx = &m_artist_ngrams; + else if( table == "album" ) idx = &m_album_ngrams; + else if( table == "track" ) idx = &m_track_ngrams; + else Q_ASSERT(false); + + if( tomerge.size() == 0 ) return; + + if( idx->size() == 0 ) + { + *idx = tomerge; + } + else + { + foreach( const QString& ngram, tomerge.keys() ) + { + + if( idx->contains( ngram ) ) + { + foreach( quint32 id, tomerge[ngram].keys() ) + { + (*idx)[ ngram ][ id ] += tomerge[ngram][id]; + } + } + else + { + idx->insert( ngram, tomerge[ngram] ); + } + } + } + qDebug() << Q_FUNC_INFO << table << "merge complete, num items:" << tomerge.size(); +} + +QMap< int, float > FuzzyIndex::search( const QString& table, const QString& name ) +{ + QMap< int, float > resultsmap; + + QHash< QString, QMap >* idx; + if( table == "artist" ) idx = &m_artist_ngrams; + else if( table == "album" ) idx = &m_album_ngrams; + else if( table == "track" ) idx = &m_track_ngrams; + + QMap ngramsmap = DatabaseImpl::ngrams( name ); + foreach( const QString& ngram, ngramsmap.keys() ) + { + if( !idx->contains( ngram ) ) + continue; + //qDebug() << name_orig << "NGRAM:" << ngram << "candidates:" << (*idx)[ngram].size(); + QMapIterator iter( (*idx)[ngram] ); + while( iter.hasNext() ) + { + iter.next(); + resultsmap[ (int) iter.key() ] += (float) iter.value(); + } + } + return resultsmap; +} diff --git a/src/database/fuzzyindex.h b/src/database/fuzzyindex.h new file mode 100644 index 000000000..5734327cf --- /dev/null +++ b/src/database/fuzzyindex.h @@ -0,0 +1,37 @@ +#ifndef FUZZYINDEX_H +#define FUZZYINDEX_H + +#include +#include +#include +#include + +class DatabaseImpl; + + +class FuzzyIndex : public QObject +{ +Q_OBJECT +public: + explicit FuzzyIndex( DatabaseImpl &db ); + +signals: + void indexReady(); + +public slots: + void loadNgramIndex(); + QMap< int, float > search( const QString& table, const QString& name ); + void mergeIndex(const QString& table, QHash< QString, QMap > tomerge); + +private: + void loadNgramIndex_helper( QHash< QString, QMap >& idx, const QString& table, unsigned int fromkey = 0); + + // maps an ngram to {track id, num occurences} + QHash< QString, QMap > m_artist_ngrams, m_album_ngrams, m_track_ngrams; + + DatabaseImpl & m_db; + + bool m_loaded; +}; + +#endif // FUZZYINDEX_H diff --git a/src/database/gen_schema.h.sh b/src/database/gen_schema.h.sh new file mode 100755 index 000000000..b8a6a6b79 --- /dev/null +++ b/src/database/gen_schema.h.sh @@ -0,0 +1,28 @@ +#!/bin/bash +schema=$1 +name=$2 + +if [ -e "$schema" -a -n "$name" ] +then +cat < " + exit 1 +fi diff --git a/src/database/op.h b/src/database/op.h new file mode 100644 index 000000000..a25f6f921 --- /dev/null +++ b/src/database/op.h @@ -0,0 +1,17 @@ +#ifndef OP_H +#define OP_H +#include +#include +#include + +struct DBOp +{ + QString guid; + QString command; + QByteArray payload; + bool compressed; +}; + +typedef QSharedPointer dbop_ptr; + +#endif // OP_H diff --git a/src/database/schema.sql b/src/database/schema.sql new file mode 100644 index 000000000..efaaf45bc --- /dev/null +++ b/src/database/schema.sql @@ -0,0 +1,218 @@ +-- Mutates to the database are entered into the transaction log +-- so they can be sent to peers to replay against a cache of your DB. +-- This allows peers to get diffs/sync your collection easily. + +CREATE TABLE IF NOT EXISTS oplog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE, -- DEFERRABLE INITIALLY DEFERRED, + guid TEXT NOT NULL, + command TEXT NOT NULL, + compressed BOOLEAN NOT NULL, + json TEXT NOT NULL +); +CREATE UNIQUE INDEX oplog_guid ON oplog(guid); +CREATE INDEX oplog_source ON oplog(source); + +-- the basic 3 catalogue tables: + +CREATE TABLE IF NOT EXISTS artist ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + sortname TEXT NOT NULL +); +CREATE UNIQUE INDEX artist_sortname ON artist(sortname); + +CREATE TABLE IF NOT EXISTS track ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + name TEXT NOT NULL, + sortname TEXT NOT NULL +); +CREATE UNIQUE INDEX track_artist_sortname ON track(artist,sortname); + +CREATE TABLE IF NOT EXISTS album ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + name TEXT NOT NULL, + sortname TEXT NOT NULL +); +CREATE UNIQUE INDEX album_artist_sortname ON album(artist,sortname); + +-- Source, typically a remote peer. + +CREATE TABLE IF NOT EXISTS source ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + friendlyname TEXT, + lastop TEXT NOT NULL DEFAULT "", -- guid of last op we've successfully applied + isonline BOOLEAN NOT NULL DEFAULT false +); +CREATE UNIQUE INDEX source_name ON source(name); + + +-- playlists + +CREATE TABLE IF NOT EXISTS playlist ( + guid TEXT PRIMARY KEY, + source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, -- owner + shared BOOLEAN DEFAULT false, + title TEXT, + info TEXT, + creator TEXT, + lastmodified INTEGER NOT NULL DEFAULT 0, + currentrevision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED +); + +--INSERT INTO playlist(guid, title, info, currentrevision) +-- VALUES('playlistguid-1','Test Playlist','this playlist automatically created and used for testing','revisionguid-1'); + +CREATE TABLE IF NOT EXISTS playlist_item ( + guid TEXT PRIMARY KEY, + playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + trackname TEXT NOT NULL, + artistname TEXT NOT NULL, + albumname TEXT, + annotation TEXT, + duration INTEGER, -- in seconds, even tho xspf uses milliseconds + addedon INTEGER NOT NULL DEFAULT 0, -- date added to playlist + addedby INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, -- who added this to the playlist + result_hint TEXT -- hint as to a result, to avoid using the resolver +); + +CREATE INDEX playlist_item_playlist ON playlist_item(playlist); + +--INSERT INTO playlist_item(guid, playlist, trackname, artistname) +-- VALUES('itemguid-1','playlistguid-1','track name 01','artist name 01'); +--INSERT INTO playlist_item(guid, playlist, trackname, artistname) +-- VALUES('itemguid-2','playlistguid-1','track name 02','artist name 02'); +--INSERT INTO playlist_item(guid, playlist, trackname, artistname) +-- VALUES('itemguid-3','playlistguid-1','track name 03','artist name 03'); +-- +CREATE TABLE IF NOT EXISTS playlist_revision ( + guid TEXT PRIMARY KEY, + playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + entries TEXT, -- qlist( guid, guid... ) + author INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + timestamp INTEGER NOT NULL DEFAULT 0, + previous_revision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED +); + +--INSERT INTO playlist_revision(guid, playlist, entries) +-- VALUES('revisionguid-1', 'playlistguid-1', '["itemguid-2","itemguid-1","itemguid-3"]'); + +-- the trigram search indexes + +CREATE TABLE IF NOT EXISTS artist_search_index ( + ngram TEXT NOT NULL, + id INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + num INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY(ngram, id) +); +-- CREATE INDEX artist_search_index_ngram ON artist_search_index(ngram); + +CREATE TABLE IF NOT EXISTS album_search_index ( + ngram TEXT NOT NULL, + id INTEGER NOT NULL REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + num INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY(ngram, id) +); +-- CREATE INDEX album_search_index_ngram ON album_search_index(ngram); + +CREATE TABLE IF NOT EXISTS track_search_index ( + ngram TEXT NOT NULL, + id INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + num INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY(ngram, id) +); +-- CREATE INDEX track_search_index_ngram ON track_search_index(ngram); + +-- files on disk and joinage with catalogue. physical properties of files only: + +-- if source=null, file is local to this machine +CREATE TABLE IF NOT EXISTS file ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + url TEXT NOT NULL, -- file:///music/foo/bar.mp3, + size INTEGER NOT NULL, -- in bytes + mtime INTEGER NOT NULL, -- file mtime, so we know to rescan + md5 TEXT, -- useful when comparing stuff p2p + mimetype TEXT, -- "audio/mpeg" + duration INTEGER NOT NULL DEFAULT 0, -- seconds + bitrate INTEGER NOT NULL DEFAULT 0 -- kbps (or equiv) +); +CREATE UNIQUE INDEX file_url_src_uniq ON file(source, url); +CREATE INDEX file_source ON file(source); + +-- mtime of dir when last scanned. +-- load into memory when rescanning, skip stuff that's unchanged +CREATE TABLE IF NOT EXISTS dirs_scanned ( + name TEXT PRIMARY KEY, + mtime INTEGER NOT NULL +); + + +CREATE TABLE IF NOT EXISTS file_join ( + file INTEGER PRIMARY KEY REFERENCES file(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + track INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + album INTEGER REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + albumpos INTEGER +); +CREATE INDEX file_join_track ON file_join(track); +CREATE INDEX file_join_artist ON file_join(artist); +CREATE INDEX file_join_album ON file_join(album); + + + +-- tags, weighted and by source (rock, jazz etc) +-- weight is always 1.0 if tag provided by our user. +-- may be less from aggregate sources like lastfm global tags + +CREATE TABLE IF NOT EXISTS track_tags ( + id INTEGER PRIMARY KEY, -- track id + source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + tag TEXT NOT NULL, -- always store as lowercase + ns TEXT, -- ie 'last.fm', 'echonest' + weight float DEFAULT 1.0 -- range 0-1 +); +CREATE INDEX track_tags_tag ON track_tags(tag); + +CREATE TABLE IF NOT EXISTS album_tags ( + id INTEGER PRIMARY KEY, -- album id + source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + tag TEXT NOT NULL, -- always store as lowercase + ns TEXT, -- ie 'last.fm', 'echonest' + weight float DEFAULT 1.0 -- range 0-1 +); +CREATE INDEX album_tags_tag ON album_tags(tag); + +CREATE TABLE IF NOT EXISTS artist_tags ( + id INTEGER PRIMARY KEY, -- artist id + source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, + tag TEXT NOT NULL, -- always store as lowercase + ns TEXT, -- ie 'last.fm', 'echonest' + weight float DEFAULT 1.0 -- range 0-1 +); +CREATE INDEX artist_tags_tag ON artist_tags(tag); + +-- all other attributes. +-- like tags that have a value, eg: +-- BPM=120, releaseyear=1980, key=Dminor, composer=Someone +-- NB: since all values are text, numeric values should be zero-padded to a set amount +-- so that we can always do range queries. + +CREATE TABLE IF NOT EXISTS track_attributes ( + id INTEGER NOT NULL, -- track id + k TEXT NOT NULL, + v TEXT NOT NULL +); +CREATE INDEX track_attrib_id ON track_attributes(id); +CREATE INDEX track_attrib_k ON track_attributes(k); + +-- Schema version, and misc tomahawk settings relating to the collection db + +CREATE TABLE IF NOT EXISTS settings ( + k TEXT NOT NULL PRIMARY KEY, + v TEXT NOT NULL DEFAULT '' +); +INSERT INTO settings(k,v) VALUES('schema_version', '14'); diff --git a/src/database/schema.sql.h b/src/database/schema.sql.h new file mode 100644 index 000000000..fd3127304 --- /dev/null +++ b/src/database/schema.sql.h @@ -0,0 +1,163 @@ +/* + This file was automatically generated from schema.sql on Tue Jul 13 12:23:44 CEST 2010. +*/ + +static const char * tomahawk_schema_sql = +"CREATE TABLE IF NOT EXISTS oplog (" +" id INTEGER PRIMARY KEY AUTOINCREMENT," +" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE, " +" guid TEXT NOT NULL," +" command TEXT NOT NULL," +" compressed BOOLEAN NOT NULL," +" json TEXT NOT NULL" +");" +"CREATE UNIQUE INDEX oplog_guid ON oplog(guid);" +"CREATE INDEX oplog_source ON oplog(source);" +"CREATE TABLE IF NOT EXISTS artist (" +" id INTEGER PRIMARY KEY AUTOINCREMENT," +" name TEXT NOT NULL," +" sortname TEXT NOT NULL" +");" +"CREATE UNIQUE INDEX artist_sortname ON artist(sortname);" +"CREATE TABLE IF NOT EXISTS track (" +" id INTEGER PRIMARY KEY AUTOINCREMENT," +" artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" name TEXT NOT NULL," +" sortname TEXT NOT NULL" +");" +"CREATE UNIQUE INDEX track_artist_sortname ON track(artist,sortname);" +"CREATE TABLE IF NOT EXISTS album (" +" id INTEGER PRIMARY KEY AUTOINCREMENT," +" artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" name TEXT NOT NULL," +" sortname TEXT NOT NULL" +");" +"CREATE UNIQUE INDEX album_artist_sortname ON album(artist,sortname);" +"CREATE TABLE IF NOT EXISTS source (" +" id INTEGER PRIMARY KEY AUTOINCREMENT," +" name TEXT NOT NULL," +" friendlyname TEXT," +" lastop TEXT NOT NULL DEFAULT \"\", " +" isonline BOOLEAN NOT NULL DEFAULT false" +");" +"CREATE UNIQUE INDEX source_name ON source(name);" +"CREATE TABLE IF NOT EXISTS playlist (" +" guid TEXT PRIMARY KEY," +" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, " +" shared BOOLEAN DEFAULT false," +" title TEXT," +" info TEXT," +" creator TEXT," +" lastmodified INTEGER NOT NULL DEFAULT 0," +" currentrevision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED" +");" +"CREATE TABLE IF NOT EXISTS playlist_item (" +" guid TEXT PRIMARY KEY," +" playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" trackname TEXT NOT NULL," +" artistname TEXT NOT NULL," +" albumname TEXT," +" annotation TEXT," +" duration INTEGER, " +" addedon INTEGER NOT NULL DEFAULT 0, " +" addedby INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, " +" result_hint TEXT " +");" +"CREATE INDEX playlist_item_playlist ON playlist_item(playlist);" +"CREATE TABLE IF NOT EXISTS playlist_revision (" +" guid TEXT PRIMARY KEY," +" playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" entries TEXT, " +" author INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" timestamp INTEGER NOT NULL DEFAULT 0," +" previous_revision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED" +");" +"CREATE TABLE IF NOT EXISTS artist_search_index (" +" ngram TEXT NOT NULL," +" id INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" num INTEGER NOT NULL DEFAULT 1," +" PRIMARY KEY(ngram, id)" +");" +"CREATE TABLE IF NOT EXISTS album_search_index (" +" ngram TEXT NOT NULL," +" id INTEGER NOT NULL REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" num INTEGER NOT NULL DEFAULT 1," +" PRIMARY KEY(ngram, id)" +");" +"CREATE TABLE IF NOT EXISTS track_search_index (" +" ngram TEXT NOT NULL," +" id INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" num INTEGER NOT NULL DEFAULT 1," +" PRIMARY KEY(ngram, id)" +");" +"CREATE TABLE IF NOT EXISTS file (" +" id INTEGER PRIMARY KEY AUTOINCREMENT," +" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" url TEXT NOT NULL, " +" size INTEGER NOT NULL, " +" mtime INTEGER NOT NULL, " +" md5 TEXT, " +" mimetype TEXT, " +" duration INTEGER NOT NULL DEFAULT 0, " +" bitrate INTEGER NOT NULL DEFAULT 0 " +");" +"CREATE UNIQUE INDEX file_url_src_uniq ON file(source, url);" +"CREATE INDEX file_source ON file(source);" +"CREATE TABLE IF NOT EXISTS dirs_scanned (" +" name TEXT PRIMARY KEY," +" mtime INTEGER NOT NULL" +");" +"CREATE TABLE IF NOT EXISTS file_join (" +" file INTEGER PRIMARY KEY REFERENCES file(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" track INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" album INTEGER REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" albumpos INTEGER" +");" +"CREATE INDEX file_join_track ON file_join(track);" +"CREATE INDEX file_join_artist ON file_join(artist);" +"CREATE INDEX file_join_album ON file_join(album);" +"CREATE TABLE IF NOT EXISTS track_tags (" +" id INTEGER PRIMARY KEY, " +" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" tag TEXT NOT NULL, " +" ns TEXT, " +" weight float DEFAULT 1.0 " +");" +"CREATE INDEX track_tags_tag ON track_tags(tag);" +"CREATE TABLE IF NOT EXISTS album_tags (" +" id INTEGER PRIMARY KEY, " +" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" tag TEXT NOT NULL, " +" ns TEXT, " +" weight float DEFAULT 1.0 " +");" +"CREATE INDEX album_tags_tag ON album_tags(tag);" +"CREATE TABLE IF NOT EXISTS artist_tags (" +" id INTEGER PRIMARY KEY, " +" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED," +" tag TEXT NOT NULL, " +" ns TEXT, " +" weight float DEFAULT 1.0 " +");" +"CREATE INDEX artist_tags_tag ON artist_tags(tag);" +"CREATE TABLE IF NOT EXISTS track_attributes (" +" id INTEGER NOT NULL, " +" k TEXT NOT NULL," +" v TEXT NOT NULL" +");" +"CREATE INDEX track_attrib_id ON track_attributes(id);" +"CREATE INDEX track_attrib_k ON track_attributes(k);" +"CREATE TABLE IF NOT EXISTS settings (" +" k TEXT NOT NULL PRIMARY KEY," +" v TEXT NOT NULL DEFAULT ''" +");" +"INSERT INTO settings(k,v) VALUES('schema_version', '14');" + ; + +const char * get_tomahawk_sql() +{ + return tomahawk_schema_sql; +} + + diff --git a/src/database/tomahawksqlquery.h b/src/database/tomahawksqlquery.h new file mode 100644 index 000000000..543072786 --- /dev/null +++ b/src/database/tomahawksqlquery.h @@ -0,0 +1,59 @@ +#ifndef TOMAHAWKSQLQUERY_H +#define TOMAHAWKSQLQUERY_H +// subclass QSqlQuery so that it prints the error msg if a query fails + +#include +#include + +#define TOMAHAWK_QUERY_THRESHOLD 20 + +class TomahawkSqlQuery : public QSqlQuery +{ + +public: + + TomahawkSqlQuery() + : QSqlQuery() + {} + + TomahawkSqlQuery( QSqlDatabase db ) + : QSqlQuery( db ) + {} + + bool exec( const QString& query ) + { + prepare( query ); + + return exec(); + } + + bool exec() + { + QTime t; + t.start(); + + bool ret = QSqlQuery::exec(); + if( !ret ) + showError(); + + int e = t.elapsed(); + if ( e >= TOMAHAWK_QUERY_THRESHOLD ) + qDebug() << "TomahawkSqlQuery (" << lastQuery() << ") finished in" << t.elapsed() << "ms"; + + return ret; + } + +private: + void showError() + { + qDebug() + << endl << "*** DATABASE ERROR ***" << endl + << this->lastQuery() << endl + << "boundValues:" << this->boundValues() << endl + << this->lastError().text() << endl + ; + Q_ASSERT( false ); + } +}; + +#endif // TOMAHAWKSQLQUERY_H diff --git a/src/dbsyncconnection.cpp b/src/dbsyncconnection.cpp new file mode 100644 index 000000000..df7c142c2 --- /dev/null +++ b/src/dbsyncconnection.cpp @@ -0,0 +1,301 @@ +/* + Database syncing using the oplog table. + ======================================= + Load the last GUID we applied for the peer, tell them it. + In return, they send us all new ops since that guid. + + We then apply those new ops to our cache of their data + + Synced. + +*/ + +#include "dbsyncconnection.h" + +#include + +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/source.h" + +#include "database.h" +#include "databasecommand.h" +#include "databasecommand_collectionstats.h" +#include "databasecommand_loadops.h" +#include "remotecollection.h" + +// close the dbsync connection after this much inactivity. +// it's automatically reestablished as needed. +#define IDLE_TIMEOUT 60000 + +using namespace Tomahawk; + + +DBSyncConnection::DBSyncConnection( Servent* s, source_ptr src ) + : Connection( s ) + , m_source( src ) + , m_state( UNKNOWN ) +{ + qDebug() << Q_FUNC_INFO << thread(); + connect( this, SIGNAL( stateChanged( DBSyncConnection::State, DBSyncConnection::State, QString ) ), + m_source.data(), SIGNAL( loadingStateChanged(DBSyncConnection::State, DBSyncConnection::State, QString ) ) + ); + + m_timer.setInterval( IDLE_TIMEOUT ); + connect( &m_timer, SIGNAL( timeout() ), SLOT( idleTimeout() ) ); + + this->setMsgProcessorModeIn( MsgProcessor::PARSE_JSON | MsgProcessor::UNCOMPRESS_ALL ); + + // msgs are stored compressed in the db, so not typically needed here, but doesnt hurt: + this->setMsgProcessorModeOut( MsgProcessor::COMPRESS_IF_LARGE ); +} + + +DBSyncConnection::~DBSyncConnection() +{ + qDebug() << "DTOR" << Q_FUNC_INFO; +} + + +void +DBSyncConnection::idleTimeout() +{ + qDebug() << Q_FUNC_INFO << "*************"; + shutdown(true); +} + + +void +DBSyncConnection::changeState( State newstate ) +{ + State s = m_state; + m_state = newstate; + qDebug() << "DBSYNC State changed from" << s << "to" << newstate; + emit stateChanged( newstate, s, "" ); + + if( newstate == SYNCED ) + { + qDebug() << "Synced :)"; + } +} + + +void +DBSyncConnection::setup() +{ + qDebug() << Q_FUNC_INFO; + setId( QString("DBSyncConnection/%1").arg(socket()->peerAddress().toString()) ); + check(); +} + + +void +DBSyncConnection::trigger() +{ + qDebug() << Q_FUNC_INFO; + + // if we're still setting up the connection, do nothing - we sync on first connect anyway: + if( !this->isRunning() ) return; + + QMetaObject::invokeMethod( this, "sendMsg", Qt::QueuedConnection, + Q_ARG( msg_ptr, + Msg::factory( "{\"method\":\"trigger\"}", Msg::JSON ) ) + ); +} + + +void +DBSyncConnection::check() +{ + qDebug() << Q_FUNC_INFO; + if( m_state != UNKNOWN && m_state != SYNCED ) + { + qDebug() << "Syncing in progress already."; + return; + } + m_uscache.clear(); + m_themcache.clear(); + m_us.clear(); + + changeState(CHECKING); + + // load last-modified etc data for our collection and theirs from our DB: + DatabaseCommand_CollectionStats * cmd_us = + new DatabaseCommand_CollectionStats( APP->sourcelist().getLocal() ); + + DatabaseCommand_CollectionStats * cmd_them = + new DatabaseCommand_CollectionStats( m_source ); + + connect( cmd_us, SIGNAL( done(const QVariantMap&) ), + this, SLOT( gotUs(const QVariantMap&) ) ); + + connect( cmd_them, SIGNAL( done(const QVariantMap&) ), + this, SLOT( gotThemCache(const QVariantMap&) ) ); + + + APP->database()->enqueue( QSharedPointer(cmd_us) ); + + APP->database()->enqueue( QSharedPointer(cmd_them) ); + + // restarts idle countdown + m_timer.start(); +} + + +/// Called once we've loaded our mtimes etc from the DB for our local +/// collection - send them to the remote peer to compare. +void +DBSyncConnection::gotUs( const QVariantMap& m ) +{ + m_us = m; + if( !m_uscache.empty() ) sendOps(); +} + + +/// Called once we've loaded our cached data about their collection +void +DBSyncConnection::gotThemCache( const QVariantMap& m ) +{ + m_themcache = m; + qDebug() << "Sending a FETCHOPS cmd since:" << m.value("lastop").toString(); + changeState(FETCHING); + QVariantMap msg; + msg.insert( "method", "fetchops" ); + msg.insert( "lastop", m_themcache.value("lastop").toString() ); + sendMsg( msg ); +} + + +void +DBSyncConnection::handleMsg( msg_ptr msg ) +{ + Q_ASSERT( !msg->is( Msg::COMPRESSED ) ); + + if( m_state == FETCHING ) changeState(PARSING); + + // "everything is synced" indicated by non-json msg containing "ok": + if( !msg->is( Msg::JSON ) && + msg->is( Msg::DBOP ) && + msg->payload() == "ok" ) + { + qDebug() << "No ops to apply, we are synced."; + changeState(SYNCED); + // calc the collection stats, to updates the "X tracks" in the sidebar etc + // this is done automatically if you run a dbcmd to add files. + DatabaseCommand_CollectionStats * cmd = new DatabaseCommand_CollectionStats( m_source ); + connect( cmd, SIGNAL( done( const QVariantMap & ) ), + m_source.data(), SLOT( setStats( const QVariantMap& ) ), Qt::QueuedConnection ); + APP->database()->enqueue( QSharedPointer(cmd) ); + return; + } + + Q_ASSERT( msg->is( Msg::JSON ) ); + + QVariantMap m = msg->json().toMap(); + + //qDebug() << ">>>>" << m; + + if( m.empty() ) + { + qDebug() << "Failed to parse msg in dbsync"; + Q_ASSERT( false ); + return; + } + + // a db sync op msg + if( msg->is( Msg::DBOP ) ) + { + DatabaseCommand * cmd = DatabaseCommand::factory( m, m_source ); + Q_ASSERT( cmd ); + + qDebug() << "APPLYING CMD" << cmd->commandname() << cmd->guid(); + + if( !msg->is( Msg::FRAGMENT ) ) // last msg in this batch + { + changeState( SAVING ); // just DB work left to complete + connect( cmd, SIGNAL( finished() ), this, SLOT( lastOpApplied() ) ); + } + APP->database()->enqueue( QSharedPointer( cmd ) ); + return; + } + + if( m.value( "method" ).toString() == "fetchops" ) + { + m_uscache = m; + if( !m_us.empty() ) sendOps(); + return; + } + + if( m.value( "method" ).toString() == "trigger" ) + { + qDebug() << "Got trigger msg on dbsyncconnection, checking for new stuff."; + check(); + return; + } + + qDebug() << Q_FUNC_INFO << "Unhandled msg: " << msg->payload(); + Q_ASSERT( false ); +} + + +void +DBSyncConnection::lastOpApplied() +{ + qDebug() << Q_FUNC_INFO; + changeState(SYNCED); + // check again, until peer reponds we have no new ops to process + check(); +} + + +/// request new copies of anything we've cached that is stale +void +DBSyncConnection::sendOps() +{ + qDebug() << Q_FUNC_INFO; + + const QString sinceguid = m_uscache.value( "lastop" ).toString(); + + qDebug() << "Will send peer all ops since" << sinceguid; + + source_ptr src = APP->sourcelist().getLocal(); + + DatabaseCommand_loadOps * cmd = new DatabaseCommand_loadOps( src, sinceguid ); + connect( cmd, SIGNAL( done( QString, QList< dbop_ptr > ) ), + this, SLOT( sendOpsData( QString, QList< dbop_ptr > ) ) ); + + APP->database()->enqueue( QSharedPointer( cmd ) ); +} + + +void +DBSyncConnection::sendOpsData( QString sinceguid, QList< dbop_ptr > ops ) +{ + qDebug() << Q_FUNC_INFO << sinceguid << "Num ops to send: " << ops.length(); + + if( ops.length() == 0 ) + { + sendMsg( Msg::factory( "ok", Msg::DBOP ) ); + return; + } + + int i; + for( i = 0; i < ops.length(); ++i ) + { + quint8 flags = Msg::JSON | Msg::DBOP; + + if( ops.at(i)->compressed ) + flags |= Msg::COMPRESSED; + if( i != ops.length()-1 ) + flags |= Msg::FRAGMENT; + + sendMsg( Msg::factory( ops.at(i)->payload, flags ) ); + } +} + + +Connection* +DBSyncConnection::clone() +{ + Q_ASSERT( false ); + return 0; +} diff --git a/src/dbsyncconnection.h b/src/dbsyncconnection.h new file mode 100644 index 000000000..762a01111 --- /dev/null +++ b/src/dbsyncconnection.h @@ -0,0 +1,67 @@ +#ifndef DBSYNCCONNECTION_H +#define DBSYNCCONNECTION_H + +#include +#include +#include +#include + +#include "connection.h" +#include "database/op.h" +#include "tomahawk/typedefs.h" + +class DBSyncConnection : public Connection +{ +Q_OBJECT + +public: + enum State + { + UNKNOWN, + CHECKING, + FETCHING, + PARSING, + SAVING, + SYNCED, + SCANNING + }; + + explicit DBSyncConnection( Servent* s, Tomahawk::source_ptr src ); + virtual ~DBSyncConnection(); + + void setup(); + Connection* clone(); + +signals: + void stateChanged( DBSyncConnection::State newstate, DBSyncConnection::State oldstate, const QString& info ); + +protected slots: + virtual void handleMsg( msg_ptr msg ); + +public slots: + void sendOps(); + /// trigger a re-sync to pick up any new ops + void trigger(); + +private slots: + void gotUs( const QVariantMap& m ); + void gotThemCache( const QVariantMap& m ); + void lastOpApplied(); + void sendOpsData( QString sinceguid, QList< dbop_ptr > ops ); + void check(); + void idleTimeout(); + +private: + void compareAndRequest(); + void synced(); + void changeState( State newstate ); + + Tomahawk::source_ptr m_source; + QVariantMap m_us, m_uscache, m_themcache; + State m_state; + + QTimer m_timer; + +}; + +#endif // DBSYNCCONNECTION_H diff --git a/src/filetransferconnection.cpp b/src/filetransferconnection.cpp new file mode 100644 index 000000000..517e5be31 --- /dev/null +++ b/src/filetransferconnection.cpp @@ -0,0 +1,187 @@ +#include "filetransferconnection.h" + +#include + +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/result.h" + +#include "bufferiodevice.h" +#include "controlconnection.h" +#include "databasecommand_loadfile.h" +#include "database.h" + +// Msgs are framed, this is the size each msg we send containing audio data: +#define BLOCKSIZE 4096 + +using namespace Tomahawk; + + +FileTransferConnection::FileTransferConnection( Servent* s, ControlConnection* cc, + QString fid, unsigned int size ) + : Connection( s ) + , m_cc( cc ) + , m_fid( fid ) + , m_type( RECEIVING ) + , m_badded( 0 ) + , m_bsent( 0 ) + , m_allok( false ) +{ + qDebug() << Q_FUNC_INFO; + BufferIODevice * bio = new BufferIODevice(size); + m_iodev = QSharedPointer( bio ); // device audio data gets written to + m_iodev->open( QIODevice::ReadWrite ); + + APP->servent().registerFileTransferConnection( this ); + + // if the audioengine closes the iodev (skip/stop/etc) then kill the connection + // immediately to avoid unnecessary network transfer + connect( m_iodev.data(), SIGNAL( aboutToClose() ), this, SLOT( shutdown() ), Qt::QueuedConnection ); + + // auto delete when connection closes: + connect( this, SIGNAL( finished() ), SLOT( deleteLater() ), Qt::QueuedConnection ); + + // don't fuck with our messages at all. No compression, no parsing, nothing: + this->setMsgProcessorModeIn( MsgProcessor::NOTHING ); + this->setMsgProcessorModeOut( MsgProcessor::NOTHING ); +} + + +FileTransferConnection::FileTransferConnection( Servent* s, QString fid ) + : Connection(s), m_fid(fid), m_type(SENDING), m_badded(0), m_bsent(0), m_allok( false ) +{ + APP->servent().registerFileTransferConnection( this ); + // auto delete when connection closes: + connect( this, SIGNAL( finished() ), SLOT( deleteLater() ), Qt::QueuedConnection ); +} + + +FileTransferConnection::~FileTransferConnection() +{ + qDebug() << Q_FUNC_INFO << "TX/RX:" << bytesSent() << bytesReceived(); + if( m_type == RECEIVING && !m_allok ) + { + qDebug() << "FTConnection closing before last data msg received, shame."; + //TODO log the fact that our peer was bad-mannered enough to not finish the upload + + // protected, we could expose it: + //m_iodev->setErrorString("FTConnection providing data went away mid-transfer"); + } + APP->servent().fileTransferFinished( this ); +} + + +QString +FileTransferConnection::id() const +{ + return QString("FTC[%1 %2]") + .arg( m_type == SENDING ? "TX" : "RX" ) + .arg(m_fid); +} + + +void +FileTransferConnection::showStats( qint64 tx, qint64 rx ) +{ + if( tx > 0 || rx > 0 ) + { + qDebug() << id() + << QString("Down: %L1 bytes/sec, ").arg(rx) + << QString("Up: %L1 bytes/sec").arg(tx); + } +} + + +void +FileTransferConnection::setup() +{ + connect( this, SIGNAL( statsTick( qint64, qint64 ) ), SLOT( showStats( qint64, qint64 ) ) ); + if(m_type == RECEIVING) + { + qDebug() << "in RX mode"; + return; + } + + qDebug() << "in TX mode, fid:" << m_fid; + + DatabaseCommand_LoadFile * cmd = new DatabaseCommand_LoadFile(m_fid); + connect(cmd, SIGNAL(result(QVariantMap)), this, SLOT(startSending(QVariantMap))); + TomahawkApp::instance()->database()->enqueue(QSharedPointer(cmd)); +} + + +void +FileTransferConnection::startSending( QVariantMap f ) +{ + Tomahawk::result_ptr result(new Tomahawk::Result(f, collection_ptr())); + qDebug() << "Starting to transmit" << result->url(); + QSharedPointer io = TomahawkApp::instance()->getIODeviceForUrl(result); + if(!io) + { + qDebug() << "Couldn't read from source:" << result->url(); + shutdown(); + return; + } + m_readdev = QSharedPointer(io); + sendSome(); +} + + +void +FileTransferConnection::handleMsg( msg_ptr msg ) +{ + Q_ASSERT(m_type == FileTransferConnection::RECEIVING); + Q_ASSERT( msg->is( Msg::RAW ) ); + + m_badded += msg->payload().length(); + ((BufferIODevice*)m_iodev.data())->addData( msg->payload() ); + + //qDebug() << Q_FUNC_INFO << "flags" << (int) msg->flags() + // << "payload len" << msg->payload().length() + // << "written to device so far: " << m_badded; + + + if( !msg->is( Msg::FRAGMENT ) ) + { + qDebug() << endl + << "*** Got last msg in filetransfer. added" << m_badded + << "io size" << m_iodev->size() + << endl; + m_allok = true; + // tell our iodev there is no more data to read, no args meaning a success: + ((BufferIODevice*)m_iodev.data())->inputComplete(); + shutdown(); + } +} + + +Connection* FileTransferConnection::clone() +{ + Q_ASSERT(false); return 0; +} + + +void FileTransferConnection::sendSome() +{ + Q_ASSERT(m_type == FileTransferConnection::SENDING); + + QByteArray ba = m_readdev->read(BLOCKSIZE); + m_bsent += ba.length(); + //qDebug() << "Sending " << ba.length() << " bytes of audiofile"; + + if( m_readdev->atEnd() ) + { + sendMsg( Msg::factory( ba, Msg::RAW ) ); + qDebug() << "Sent all. DONE. " << m_bsent; + shutdown(true); + return; + } + else + { + // more to come -> FRAGMENT + sendMsg( Msg::factory( ba, Msg::RAW | Msg::FRAGMENT ) ); + } + + // HINT: change the 0 to 50 to transmit at 640Kbps, for example + // (this is where upload throttling could be implemented) + QTimer::singleShot( 0, this, SLOT( sendSome() ) ); +} diff --git a/src/filetransferconnection.h b/src/filetransferconnection.h new file mode 100644 index 000000000..7814df758 --- /dev/null +++ b/src/filetransferconnection.h @@ -0,0 +1,62 @@ +#ifndef FILETRANSFERCONNECTION_H +#define FILETRANSFERCONNECTION_H + +#include +#include +#include + +#include "connection.h" + +class ControlConnection; +class BufferIODevice; + +class FileTransferConnection : public Connection +{ +Q_OBJECT + +public: + enum Type + { + SENDING = 0, + RECEIVING = 1 + }; + + // RX: + explicit FileTransferConnection( Servent* s, ControlConnection* parent, QString fid, unsigned int size ); + // TX: + explicit FileTransferConnection( Servent* s, QString fid ); + + virtual ~FileTransferConnection(); + + QString id() const; + void setup(); + Connection* clone(); + + const QSharedPointer& iodevice() + { + return m_iodev; + } + + Type type() const { return m_type; } + QString fid() const { return m_fid; } + +protected slots: + virtual void handleMsg( msg_ptr msg ); + +private slots: + void startSending( QVariantMap ); + void sendSome(); + void showStats(qint64 tx, qint64 rx); + +private: + QSharedPointer m_iodev; + ControlConnection* m_cc; + QString m_fid; + Type m_type; + QSharedPointer m_readdev; + + int m_badded, m_bsent; + bool m_allok; // got last msg ok, transfer complete? +}; + +#endif // FILETRANSFERCONNECTION_H diff --git a/src/headlesscheck.h b/src/headlesscheck.h new file mode 100644 index 000000000..9a6e37e4d --- /dev/null +++ b/src/headlesscheck.h @@ -0,0 +1,18 @@ +#ifndef HEADLESSCHECK +#define HEADLESSCHECK + +#ifdef ENABLE_HEADLESS + +#define TOMAHAWK_APPLICATION QCoreApplication +#define TOMAHAWK_HEADLESS +#include + +#else + +#define TOMAHAWK_APPLICATION QApplication +#include +#include "tomahawkwindow.h" + +#endif + +#endif diff --git a/src/jabber/jabber.h b/src/jabber/jabber.h new file mode 100644 index 000000000..76b3a6913 --- /dev/null +++ b/src/jabber/jabber.h @@ -0,0 +1,100 @@ +#ifndef JABBER_H +#define JABBER_H +/* + Pimpl of jabber_p, which inherits from a gazillion gloox classes + and it littered with public methods. + */ +#include "jabber_p.h" + +class Jabber : public QObject +{ + Q_OBJECT +public: + + Jabber(const QString &jid, + const QString password, + const QString server = "", + const int port=-1) + : p( jid, password, server, port ) + { + } + + ~Jabber() + { + // p.disconnect(); + } + +public slots: + + void start() + { + //connect( &p, SIGNAL(finished()), + // this, SIGNAL(finished()) ); + + connect( &p, SIGNAL(msgReceived(QString,QString)), + this, SIGNAL(msgReceived(QString,QString)) ); + + connect( &p, SIGNAL(peerOnline(QString)), + this, SIGNAL(peerOnline(QString)) ); + + connect( &p, SIGNAL(peerOffline(QString)), + this, SIGNAL(peerOffline(QString)) ); + + connect( &p, SIGNAL(connected()), + this, SIGNAL(connected()) ); + + connect( &p, SIGNAL(disconnected()), + this, SIGNAL(disconnected()) ); + + connect( &p, SIGNAL(jidChanged(QString)), + this, SIGNAL(jidChanged(QString)) ); + + connect( &p, SIGNAL(authError(int,const QString&)), + this, SIGNAL(authError(int,const QString&)) ); + + p.go(); + } + + void disconnect() + { + QMetaObject::invokeMethod( &p, + "disconnect", + Qt::QueuedConnection + ); + } + + void sendMsg(const QString& to, const QString& msg) + { + QMetaObject::invokeMethod( &p, + "sendMsg", + Qt::QueuedConnection, + Q_ARG(const QString, to), + Q_ARG(const QString, msg) + ); + } + + void broadcastMsg(const QString &msg) + { + QMetaObject::invokeMethod( &p, + "broadcastMsg", + Qt::QueuedConnection, + Q_ARG(const QString, msg) + ); + } + +signals: + //void finished(); + + void msgReceived(const QString&, const QString&); //from, msg + void peerOnline(const QString&); + void peerOffline(const QString&); + void connected(); + void disconnected(); + void jidChanged(const QString&); + void authError(int, const QString&); + +private: + Jabber_p p; +}; + +#endif diff --git a/src/jabber/jabber_p.cpp b/src/jabber/jabber_p.cpp new file mode 100644 index 000000000..0890dac2c --- /dev/null +++ b/src/jabber/jabber_p.cpp @@ -0,0 +1,532 @@ +#include "jabber_p.h" + +#include +#include + +using namespace gloox; +using namespace std; + + +Jabber_p::Jabber_p( const QString& jid, const QString& password, const QString& server, const int port ) + : QObject() +{ + qDebug() << Q_FUNC_INFO; + qsrand( QTime( 0, 0, 0 ).secsTo( QTime::currentTime() ) ); + + m_presences[Presence::Available] = "available"; + m_presences[Presence::Chat] = "chat"; + m_presences[Presence::Away] = "away"; + m_presences[Presence::DND] = "dnd"; + m_presences[Presence::XA] = "xa"; + m_presences[Presence::Unavailable] = "unavailable"; + m_presences[Presence::Probe] = "probe"; + m_presences[Presence::Error] = "error"; + m_presences[Presence::Invalid] = "invalid"; + + m_jid = JID( jid.toStdString() ); + + if( m_jid.resource().find( "tomahawk" ) == std::string::npos ) + { + qDebug() << "!!! Setting your resource to 'tomahawk' prior to logging in to jabber"; + m_jid.setResource( QString( "tomahawk%1" ).arg( qrand() ).toStdString() ); + } + + qDebug() << "Our JID set to:" << m_jid.full().c_str(); + + // the google hack, because they filter disco features they don't know. + if( m_jid.server().find( "googlemail." ) != string::npos + || m_jid.server().find( "gmail." ) != string::npos + || m_jid.server().find( "gtalk." ) != string::npos ) + { + if( m_jid.resource().find( "tomahawk" ) == string::npos ) + { + qDebug() << "Forcing your /resource to contain 'tomahawk' (the google workaround)"; + m_jid.setResource( "tomahawk-tomahawk" ); + } + } + + m_client = QSharedPointer( new gloox::Client( m_jid, password.toStdString(), port) ); + if( !server.isEmpty() ) + m_client->setServer( server.toStdString() ); +} + + +Jabber_p::~Jabber_p() +{ + // qDebug() << Q_FUNC_INFO; + if( m_client ) + { + // m_client->disco()->removeDiscoHandler( this ); + m_client->rosterManager()->removeRosterListener(); + m_client->removeConnectionListener( this ); + } +} + +void +Jabber_p::go() +{ + m_client->registerConnectionListener( this ); + m_client->rosterManager()->registerRosterListener( this ); + m_client->logInstance().registerLogHandler( LogLevelWarning, LogAreaAll, this ); + m_client->registerMessageHandler( this ); + + /* + m_client->disco()->registerDiscoHandler( this ); + m_client->disco()->setVersion( "gloox_tomahawk", GLOOX_VERSION, "xplatform" ); + m_client->disco()->setIdentity( "client", "bot" ); + m_client->disco()->addFeature( "tomahawk:player" ); + */ + + m_client->setPresence( Presence::Available, 1, "Tomahawk available" ); + + // m_client->connect(); + // return; + + if( m_client->connect( false ) ) + { + emit connected(); + m_timer.singleShot(0, this, SLOT(doJabberRecv())); + } +} + + +void +Jabber_p::doJabberRecv() +{ + ConnectionError ce = m_client->recv(100); + if( ce != ConnNoError ) + { + qDebug() << "Jabber_p::Recv failed, disconnected"; + } + else + { + m_timer.singleShot(100, this, SLOT(doJabberRecv())); + } +} + + +void +Jabber_p::disconnect() +{ + if(m_client) + { + m_client->disconnect(); + } +} + + +void +Jabber_p::sendMsg( const QString& to, const QString& msg ) +{ + if( QThread::currentThread() != thread() ) + { + qDebug() << Q_FUNC_INFO << "invoking in correct thread, not" + << QThread::currentThread(); + + QMetaObject::invokeMethod( this, "sendMsg", + Qt::QueuedConnection, + Q_ARG( const QString, to ), + Q_ARG( const QString, msg ) + ); + return; + } + + qDebug() << Q_FUNC_INFO << to << msg; + Message m( Message::Chat, JID(to.toStdString()), msg.toStdString(), "" ); + m_client->send( m ); // assuming this is threadsafe +} + + +void +Jabber_p::broadcastMsg( const QString &msg ) +{ + if( QThread::currentThread() != thread() ) + { + QMetaObject::invokeMethod( this, "broadcastMsg", + Qt::QueuedConnection, + Q_ARG(const QString, msg) + ); + return; + } + std::string msg_s = msg.toStdString(); + foreach( const QString& jidstr, m_peers.keys() ) + { + Message m(Message::Chat, JID(jidstr.toStdString()), msg_s, ""); + m_client->send( m ); + } +} + + +/// GLOOX IMPL STUFF FOLLOWS + +void +Jabber_p::onConnect() +{ + // update jid resource, servers like gtalk use resource binding and may + // have changed our requested /resource + if( m_client->resource() != m_jid.resource() ) + { + m_jid.setResource( m_client->resource() ); + QString jidstr( m_jid.full().c_str() ); + emit jidChanged( jidstr ); + } + + qDebug() << "Connected as:" << m_jid.full().c_str(); +} + + +void +Jabber_p::onDisconnect( ConnectionError e ) +{ + qDebug() << "Jabber Disconnected"; + QString error; + switch(e) + { + case AuthErrorUndefined: + error = " No error occurred, or error condition is unknown"; + break; + case SaslAborted: + error = "The receiving entity acknowledges an <abort/> element sent " + "by the initiating entity; sent in reply to the <abort/> element."; + break; + case SaslIncorrectEncoding: + error = "The data provided by the initiating entity could not be processed " + "because the [BASE64] encoding is incorrect (e.g., because the encoding " + "does not adhere to the definition in Section 3 of [BASE64]); sent in " + "reply to a <response/> element or an <auth/> element with " + "initial response data."; + break; + case SaslInvalidAuthzid: + error = "The authzid provided by the initiating entity is invalid, either " + "because it is incorrectly formatted or because the initiating entity " + "does not have permissions to authorize that ID; sent in reply to a " + "<response/> element or an <auth/> element with initial " + "response data."; + break; + case SaslInvalidMechanism: + error = "The initiating entity did not provide a mechanism or requested a " + "mechanism that is not supported by the receiving entity; sent in reply " + "to an <auth/> element."; + break; + case SaslMalformedRequest: + error = "The request is malformed (e.g., the <auth/> element includes " + "an initial response but the mechanism does not allow that); sent in " + "reply to an <abort/>, <auth/>, <challenge/>, or " + "<response/> element."; + break; + case SaslMechanismTooWeak: + error = "The mechanism requested by the initiating entity is weaker than " + "server policy permits for that initiating entity; sent in reply to a " + "<response/> element or an <auth/> element with initial " + "response data."; + break; + case SaslNotAuthorized: + error = "The authentication failed because the initiating entity did not " + "provide valid credentials (this includes but is not limited to the " + "case of an unknown username); sent in reply to a <response/> " + "element or an <auth/> element with initial response data. "; + break; + case SaslTemporaryAuthFailure: + error = "The authentication failed because of a temporary error condition " + "within the receiving entity; sent in reply to an <auth/> element " + "or <response/> element."; + break; + case NonSaslConflict: + error = "XEP-0078: Resource Conflict"; + break; + case NonSaslNotAcceptable: + error = "XEP-0078: Required Information Not Provided"; + break; + case NonSaslNotAuthorized: + error = "XEP-0078: Incorrect Credentials"; + break; + case ConnAuthenticationFailed: + error = "Authentication failed"; + break; + case ConnNoSupportedAuth: + error = "No supported auth mechanism"; + break; + default : + error ="UNKNOWN ERROR"; + } + qDebug() << "Connection error msg:" << error; + emit authError(e, error); + + emit disconnected(); + + Q_ASSERT(0); //this->exit(1); +} + + +bool +Jabber_p::onTLSConnect( const CertInfo& info ) +{ + qDebug() << Q_FUNC_INFO + << "Status" << info.status + << "issuer" << info.issuer.c_str() + << "peer" << info.server.c_str() + << "proto" << info.protocol.c_str() + << "mac" << info.mac.c_str() + << "cipher" << info.cipher.c_str() + << "compression" << info.compression.c_str() + << "from" << ctime( (const time_t*)&info.date_from ) + << "to" << ctime( (const time_t*)&info.date_to ) + ; + + //onConnect(); + return true; +} + + +void +Jabber_p::handleMessage( const Message& m, MessageSession * /*session*/ ) +{ + QString from = QString::fromStdString( m.from().full() ); + QString msg = QString::fromStdString( m.body() ); + + if( msg.length() == 0 ) return; + + qDebug() << "Jabber_p::handleMessage" << from << msg; + + //printf( "from: %s, type: %d, subject: %s, message: %s, thread id: %s\n", + // msg.from().full().c_str(), msg.subtype(), + // msg.subject().c_str(), msg.body().c_str(), msg.thread().c_str() ); + + //sendMsg( from, QString("You said %1").arg(msg) ); + + emit msgReceived( from, msg ); +} + + +void +Jabber_p::handleLog( LogLevel level, LogArea area, const std::string& message ) +{ + qDebug() << Q_FUNC_INFO + << "level:" << level + << "area:" << area + << "msg:" << message.c_str(); +} + + +/// ROSTER STUFF +// {{{ +void +Jabber_p::onResourceBindError( ResourceBindError error ) +{ + qDebug() << Q_FUNC_INFO; +} + + +void +Jabber_p::onSessionCreateError( SessionCreateError error ) +{ + qDebug() << Q_FUNC_INFO; +} + + +void +Jabber_p::handleItemSubscribed( const JID& jid ) +{ + qDebug() << Q_FUNC_INFO << jid.full().c_str(); +} + + +void +Jabber_p::handleItemAdded( const JID& jid ) +{ + qDebug() << Q_FUNC_INFO << jid.full().c_str(); +} + + +void +Jabber_p::handleItemUnsubscribed( const JID& jid ) +{ + qDebug() << Q_FUNC_INFO << jid.full().c_str(); +} + + +void +Jabber_p::handleItemRemoved( const JID& jid ) +{ + qDebug() << Q_FUNC_INFO << jid.full().c_str(); +} + + +void +Jabber_p::handleItemUpdated( const JID& jid ) +{ + qDebug() << Q_FUNC_INFO << jid.full().c_str(); +} +// }}} + + +void +Jabber_p::handleRoster( const Roster& roster ) +{ +// qDebug() << Q_FUNC_INFO; + + Roster::const_iterator it = roster.begin(); + for ( ; it != roster.end(); ++it ) + { + if ( (*it).second->subscription() != S10nBoth ) continue; + qDebug() << (*it).second->jid().c_str() << (*it).second->name().c_str(); + //printf("JID: %s\n", (*it).second->jid().c_str()); + } + + // mark ourselves as "extended away" lowest priority: + // there is no "invisible" in the spec. XA is the lowest? + //m_client->setPresence( Presence::Available, 1, "Tomahawk App, not human" ); +} + + +void +Jabber_p::handleRosterError( const IQ& /*iq*/ ) +{ + qDebug() << Q_FUNC_INFO; +} + + +void +Jabber_p::handleRosterPresence( const RosterItem& item, const std::string& resource, + Presence::PresenceType presence, const std::string& /*msg*/ ) +{ + JID jid( item.jid() ); + jid.setResource( resource ); + QString fulljid( jid.full().c_str() ); + + qDebug() << "* handleRosterPresence" << fulljid << presence; + + if( jid == m_jid ) + return; + + // ignore anyone not running tomahawk: + if( jid.full().find( "/tomahawk" ) == string::npos ) + { + // Disco them to check if they are tomahawk-capable + //qDebug() << "No tomahawk resource, DISCOing... " << jid.full().c_str(); + //m_client->disco()->getDiscoInfo( jid, "", this, 0 ); + return; + } + + //qDebug() << Q_FUNC_INFO << "jid: " << QString::fromStdString(item.jid()) + // << " resource: " << QString::fromStdString(resource) + // << " presencetype " << presence; + + // "going offline" event + if ( !presenceMeansOnline( presence ) && + ( !m_peers.contains( fulljid ) || + presenceMeansOnline( m_peers.value(fulljid) ) + ) + ) + { + m_peers[ fulljid ] = presence; + qDebug() << "* Peer goes offline:" << fulljid; + emit peerOffline( fulljid ); + return; + } + + // "coming online " event + if( presenceMeansOnline( presence ) && + ( !m_peers.contains( fulljid ) || + !presenceMeansOnline( m_peers.value( fulljid ) ) + ) + ) + { + m_peers[ fulljid ] = presence; + qDebug() << "* Peer goes online:" << fulljid; + emit peerOnline( fulljid ); + return; + } + + //qDebug() << "Updating presence data for" << fulljid; + m_peers[ fulljid ] = presence; +} + + +void +Jabber_p::handleSelfPresence( const RosterItem& item, const std::string& resource, + Presence::PresenceType presence, const std::string& msg ) +{ +// qDebug() << Q_FUNC_INFO; + handleRosterPresence( item, resource, presence, msg ); +} + + +bool +Jabber_p::handleSubscriptionRequest( const JID& jid, const std::string& /*msg*/ ) +{ + qDebug() << Q_FUNC_INFO << jid.bare().c_str(); + StringList groups; + m_client->rosterManager()->subscribe( jid, "", groups, "" ); + return true; +} + + +bool +Jabber_p::handleUnsubscriptionRequest( const JID& jid, const std::string& /*msg*/ ) +{ + qDebug() << Q_FUNC_INFO << jid.bare().c_str(); + return true; +} + + +void +Jabber_p::handleNonrosterPresence( const Presence& presence ) +{ + qDebug() << Q_FUNC_INFO << presence.from().full().c_str(); +} +/// END ROSTER STUFF + + +/// DISCO STUFF +void +Jabber_p::handleDiscoInfo( const JID& from, const Disco::Info& info, int context) +{ + QString jidstr( from.full().c_str() ); + //qDebug() << "DISCOinfo" << jidstr; + if ( info.hasFeature("tomahawk:player") ) + { + qDebug() << "Peer online and DISCOed ok:" << jidstr; + m_peers.insert( jidstr, Presence::XA ); + emit peerOnline( jidstr ); + } + else + { + //qDebug() << "Peer DISCO has no tomahawk:" << jidstr; + } +} + + +void +Jabber_p::handleDiscoItems( const JID& /*iq*/, const Disco::Items&, int /*context*/ ) +{ + qDebug() << Q_FUNC_INFO; +} + + +void +Jabber_p::handleDiscoError( const JID& j, const Error* e, int /*context*/ ) +{ + qDebug() << Q_FUNC_INFO << j.full().c_str() << e->text().c_str() << e->type(); +} +/// END DISCO STUFF + + +bool Jabber_p::presenceMeansOnline( Presence::PresenceType p ) +{ + switch(p) + { + case Presence::Invalid: + case Presence::Unavailable: + case Presence::Error: + return false; + break; + default: + return true; + } +} + + + + + diff --git a/src/jabber/jabber_p.h b/src/jabber/jabber_p.h new file mode 100644 index 000000000..ddf39d29a --- /dev/null +++ b/src/jabber/jabber_p.h @@ -0,0 +1,131 @@ +/* + This is the Jabber client that the rest of the app sees + Gloox stuff should NOT leak outside this class. + We may replace gloox later, this interface should remain the same. +*/ +#ifndef JABBER_P_H +#define JABBER_P_H + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined( WIN32 ) || defined( _WIN32 ) +# include +#endif + +class Jabber_p : + public QObject, + public gloox::ConnectionListener, + public gloox::RosterListener, + public gloox::MessageHandler, + gloox::LogHandler + //public gloox::DiscoHandler, +{ +Q_OBJECT + +public: + explicit Jabber_p( const QString& jid, const QString& password, const QString& server = "", const int port = -1 ); + virtual ~Jabber_p(); + + void disconnect(); + + /// GLOOX IMPLEMENTATION STUFF FOLLOWS + virtual void onConnect(); + virtual void onDisconnect( gloox::ConnectionError e ); + virtual bool onTLSConnect( const gloox::CertInfo& info ); + + virtual void handleMessage( const gloox::Message& msg, gloox::MessageSession * /*session*/ ); + virtual void handleLog( gloox::LogLevel level, gloox::LogArea area, const std::string& message ); + + /// ROSTER STUFF + virtual void onResourceBindError( gloox::ResourceBindError error ); + virtual void onSessionCreateError( gloox::SessionCreateError error ); + + virtual void handleItemSubscribed( const gloox::JID& jid ); + virtual void handleItemAdded( const gloox::JID& jid ); + virtual void handleItemUnsubscribed( const gloox::JID& jid ); + virtual void handleItemRemoved( const gloox::JID& jid ); + virtual void handleItemUpdated( const gloox::JID& jid ); + + virtual void handleRoster( const gloox::Roster& roster ); + virtual void handleRosterError( const gloox::IQ& /*iq*/ ); + virtual void handleRosterPresence( const gloox::RosterItem& item, const std::string& resource, + gloox::Presence::PresenceType presence, const std::string& /*msg*/ ); + virtual void handleSelfPresence( const gloox::RosterItem& item, const std::string& resource, + gloox::Presence::PresenceType presence, const std::string& msg ); + virtual bool handleSubscriptionRequest( const gloox::JID& jid, const std::string& /*msg*/ ); + virtual bool handleUnsubscriptionRequest( const gloox::JID& jid, const std::string& /*msg*/ ); + virtual void handleNonrosterPresence( const gloox::Presence& presence ); + /// END ROSTER STUFF + + /// DISCO STUFF + virtual void handleDiscoInfo( const gloox::JID& from, const gloox::Disco::Info& info, int context); + virtual void handleDiscoItems( const gloox::JID& /*iq*/, const gloox::Disco::Items&, int /*context*/ ); + virtual void handleDiscoError( const gloox::JID& /*iq*/, const gloox::Error*, int /*context*/ ); + /// END DISCO STUFF + +protected: + /////virtual void run(); + +signals: + void msgReceived( const QString&, const QString& ); //from, msg + void peerOnline( const QString& ); + void peerOffline( const QString& ); + void connected(); + void disconnected(); + void jidChanged( const QString& ); + void authError( int, const QString& ); + +public slots: + void go(); + void sendMsg( const QString& to, const QString& msg ); + void broadcastMsg( const QString &msg ); + +private slots: + + void doJabberRecv(); + +private: + bool presenceMeansOnline( gloox::Presence::PresenceType p ); + + QSharedPointer m_client; + gloox::JID m_jid; + QMap m_presences; + QMap m_peers; + QTimer m_timer; // for recv() +}; + +#endif // JABBER_H diff --git a/src/junk/remoteioconnection.cpp b/src/junk/remoteioconnection.cpp new file mode 100644 index 000000000..9bc7cc655 --- /dev/null +++ b/src/junk/remoteioconnection.cpp @@ -0,0 +1,86 @@ +#include "remoteioconnection.h" +#include + +RemoteIOConnection::RemoteIOConnection(Servent * s, FileTransferSession * fts) + : Connection(s), m_fts(fts) +{ + qDebug() << "CTOR " << id() ; +} + +RemoteIOConnection::~RemoteIOConnection() +{ + qDebug() << "DTOR " << id() ; +} + +QString RemoteIOConnection::id() const +{ + return QString("RemoteIOConnection[%1]").arg(m_fts->fid()); +} + +void RemoteIOConnection::shutdown(bool wait) +{ + Connection::shutdown(wait); + /*if(!wait) + { + Connection::shutdown(wait); + return; + } + qDebug() << id() << " shutdown requested - waiting until we've received all data TODO"; + */ +} + +void RemoteIOConnection::setup() +{ + if(m_fts->type() == FileTransferSession::RECEIVING) + { + qDebug() << "RemoteIOConnection in RX mode"; + return; + } + + qDebug() << "RemoteIOConnection in TX mode, fid:" << m_fts->fid(); + + QString url("/tmp/test.mp3"); + qDebug() << "TODO map fid to file://, hardcoded for now"; + + qDebug() << "Opening for transmission:" << url; + m_readdev = QSharedPointer(new QFile(url)); + m_readdev->open(QIODevice::ReadOnly); + if(!m_readdev->isOpen()) + { + qDebug() << "WARNING file is not readable: " << url; + shutdown(); + } + // send chunks within our event loop, since we're not in our own thread + sendSome(); +} + +void RemoteIOConnection::handleMsg(QByteArray msg) +{ + Q_ASSERT(m_fts->type() == FileTransferSession::RECEIVING); + m_fts->iodevice()->addData(msg); + if(msg.length()==0) qDebug() << "Got 0len msg. end?"; +} + + +Connection * RemoteIOConnection::clone() +{ + Q_ASSERT(false); return 0; +}; + + +void RemoteIOConnection::sendSome() +{ + Q_ASSERT(m_fts->type() == FileTransferSession::SENDING); + if(m_readdev->atEnd()) + { + qDebug() << "Sent all. DONE"; + shutdown(true); + return; + } + QByteArray ba = m_readdev->read(4096); + //qDebug() << "Sending " << ba.length() << " bytes of audiofile"; + sendMsg(ba); + QTimer::singleShot(0, this, SLOT(sendSome())); +} + + diff --git a/src/junk/remoteioconnection.h b/src/junk/remoteioconnection.h new file mode 100644 index 000000000..5874f8a4f --- /dev/null +++ b/src/junk/remoteioconnection.h @@ -0,0 +1,38 @@ +#ifndef REMOTEIOCONNECTION_H +#define REMOTEIOCONNECTION_H + +#include +#include +#include +#include +#include + +#include "controlconnection.h" +#include "filetransfersession.h" + +class RemoteIOConnection : public Connection +{ + Q_OBJECT +public: + RemoteIOConnection(Servent * s, FileTransferSession * fts); + ~RemoteIOConnection(); + QString id() const; + + + void shutdown(bool wait = false); + void setup(); + void handleMsg(QByteArray msg); + Connection * clone(); + +signals: + +private slots: + void sendSome(); + +private: + + FileTransferSession * m_fts; + QSharedPointer m_readdev; +}; + +#endif // REMOTEIOCONNECTION_H diff --git a/src/junk/remoteiodevice.cpp b/src/junk/remoteiodevice.cpp new file mode 100644 index 000000000..92cefbfe9 --- /dev/null +++ b/src/junk/remoteiodevice.cpp @@ -0,0 +1,101 @@ +#include "remoteiodevice.h" + +RemoteIODevice::RemoteIODevice(RemoteIOConnection * c) + : m_eof(false), m_totalAdded(0), m_rioconn(c) +{ + qDebug() << "CTOR RemoteIODevice"; +} + +RemoteIODevice::~RemoteIODevice() +{ + qDebug() << "DTOR RemoteIODevice"; + m_rioconn->shutdown(); +} + +void RemoteIODevice::close() +{ + qDebug() << "RemoteIODevice::close"; + QIODevice::close(); + deleteLater(); +} + +bool RemoteIODevice::open ( OpenMode mode ) +{ + return QIODevice::open(mode & QIODevice::ReadOnly); +} + +qint64 RemoteIODevice::bytesAvailable () const +{ + return m_buffer.length(); +} + +bool RemoteIODevice::isSequential () const +{ + return true; +}; + +bool RemoteIODevice::atEnd() const +{ + return m_eof && m_buffer.length() == 0; +}; + +void RemoteIODevice::addData(QByteArray msg) +{ + m_mut_recv.lock(); + if(msg.length()==0) + { + m_eof=true; + //qDebug() << "addData finished, entire file received. EOF."; + m_mut_recv.unlock(); + m_wait.wakeAll(); + return; + } + else + { + m_buffer.append(msg); + m_totalAdded += msg.length(); + //qDebug() << "RemoteIODevice has seen in total: " << m_totalAdded ; + m_mut_recv.unlock(); + m_wait.wakeAll(); + emit readyRead(); + return; + } +} + +qint64 RemoteIODevice::writeData ( const char * data, qint64 maxSize ) +{ + Q_ASSERT(false); + return 0; +} + +qint64 RemoteIODevice::readData ( char * data, qint64 maxSize ) +{ + //qDebug() << "RemIO::readData, bytes in buffer: " << m_buffer.length(); + m_mut_recv.lock(); + if(m_eof && m_buffer.length() == 0) + { + // eof + qDebug() << "readData called when EOF"; + m_mut_recv.unlock(); + return 0; + } + if(!m_buffer.length())// return 0; + { + //qDebug() << "WARNING readData when buffer is empty"; + m_mut_recv.unlock(); + return 0; + } + int len; + if(maxSize>=m_buffer.length()) // whole buffer + { + len = m_buffer.length(); + memcpy(data, m_buffer.constData(), len); + m_buffer.clear(); + } else { // partial + len = maxSize; + memcpy(data, m_buffer.constData(), len); + m_buffer.remove(0,len); + } + m_mut_recv.unlock(); + return len; +} diff --git a/src/junk/remoteiodevice.h b/src/junk/remoteiodevice.h new file mode 100644 index 000000000..e8ee34fd4 --- /dev/null +++ b/src/junk/remoteiodevice.h @@ -0,0 +1,43 @@ +#ifndef REMOTEIODEVICE_H +#define REMOTEIODEVICE_H +#include +#include +#include +#include +#include +#include "remoteioconnection.h" + +class RemoteIOConnection; + +class RemoteIODevice : public QIODevice +{ + Q_OBJECT +public: + + RemoteIODevice(RemoteIOConnection * c); + ~RemoteIODevice(); + virtual void close(); + virtual bool open ( OpenMode mode ); + qint64 bytesAvailable () const; + virtual bool isSequential () const; + virtual bool atEnd() const; + +public slots: + + void addData(QByteArray msg); + +protected: + + virtual qint64 writeData ( const char * data, qint64 maxSize ); + virtual qint64 readData ( char * data, qint64 maxSize ); + +private: + QByteArray m_buffer; + QMutex m_mut_wait, m_mut_recv; + QWaitCondition m_wait; + bool m_eof; + int m_totalAdded; + + RemoteIOConnection * m_rioconn; +}; +#endif // REMOTEIODEVICE_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 000000000..12d2361b5 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,7 @@ +#include "tomahawk/tomahawkapp.h" + +int main( int argc, char *argv[] ) +{ + TomahawkApp a( argc, argv ); + return a.exec(); +} diff --git a/src/msg.h b/src/msg.h new file mode 100644 index 000000000..f0fd5fa33 --- /dev/null +++ b/src/msg.h @@ -0,0 +1,141 @@ +/* + Msg is a wire msg used by p2p connections. + Msgs have a 5-byte header: + - 4 bytes length, big endian + - 1 byte flags + + Flags indicate if the payload is compressed/json/etc. + + Use static factory method to create, pass around shared pointers: msp_ptr +*/ +#ifndef MSG_H +#define MSG_H +#include +#include +#include +#include +#include + +#include +#include +#include + +class Msg; +typedef QSharedPointer msg_ptr; + +class Msg +{ + friend class MsgProcessor; + +public: + enum Flag + { + RAW = 1, + JSON = 2, + FRAGMENT = 4, + COMPRESSED = 8, + DBOP = 16, + UNUSED_FLAG_6 = 32, + UNUSED_FLAG_7 = 64, + SETUP = 128 // used to handshake/auth the connection prior to handing over to Connection subclass + }; + + virtual ~Msg() + { + //qDebug() << Q_FUNC_INFO; + } + + /// constructs new msg you wish to send + static msg_ptr factory( const QByteArray& ba, char f ) + { + return msg_ptr( new Msg( ba, f ) ); + } + + /// constructs an incomplete new msg that is missing the payload data + static msg_ptr begin( char* headerToParse ) + { + quint32 lenBE = *( (quint32*) headerToParse ); + quint8 flags = *( (quint8*) (headerToParse+4) ); + return msg_ptr( new Msg( qFromBigEndian(lenBE), flags ) ); + } + + /// completes msg construction by providing payload data + void fill( const QByteArray& ba ) + { + Q_ASSERT( m_incomplete ); + Q_ASSERT( ba.length() == (qint32)m_length ); + m_payload = ba; + m_incomplete = false; + } + + /// frames the msg and writes to the wire: + bool write( QIODevice * device ) + { + quint32 size = qToBigEndian( m_length ); + quint8 flags = m_flags; + if( device->write( (const char*) &size, sizeof(quint32) ) != sizeof(quint32) ) return false; + if( device->write( (const char*) &flags, sizeof(quint8) ) != sizeof(quint8) ) return false; + if( device->write( (const char*) m_payload.data(), m_length ) != m_length ) return false; + return true; + } + + // len(4) + flags(1) + static quint8 headerSize() { return sizeof(quint32) + sizeof(quint8); } + + quint32 length() const { return m_length; } + + bool is( Flag flag ) { return m_flags & flag; } + + const QByteArray& payload() const + { + Q_ASSERT( m_incomplete == false ); + return m_payload; + } + + QVariant& json() + { + Q_ASSERT( is(JSON) ); + Q_ASSERT( !is(COMPRESSED) ); + + if( !m_json_parsed ) + { + QJson::Parser p; + bool ok; + m_json = p.parse( m_payload, &ok ); + m_json_parsed = true; + } + return m_json; + } + + char flags() const { return m_flags; } + +private: + /// used when constructing Msg you wish to send + Msg( const QByteArray& ba, char f ) + : m_payload( ba ), + m_length( ba.length() ), + m_flags( f ), + m_incomplete( false ), + m_json_parsed( false) + { + } + + /// used when constructung Msg off the wire: + Msg( quint32 len, quint8 flags ) + : m_length( len ), + m_flags( flags ), + m_incomplete( true ), + m_json_parsed( false) + { + } + + QByteArray m_payload; + quint32 m_length; + char m_flags; + bool m_incomplete; + QVariant m_json; + bool m_json_parsed; +}; + + +#endif // MSG_H diff --git a/src/msgprocessor.cpp b/src/msgprocessor.cpp new file mode 100644 index 000000000..f910b62a6 --- /dev/null +++ b/src/msgprocessor.cpp @@ -0,0 +1,113 @@ +#include "msgprocessor.h" +#include "tomahawk/tomahawkapp.h" + +MsgProcessor::MsgProcessor( quint32 mode, quint32 t ) : + QObject(), m_mode( mode ), m_threshold( t ), m_totmsgsize( 0 ) +{ + moveToThread( APP->servent().thread() ); +} + + +void MsgProcessor::append( msg_ptr msg ) +{ + if( QThread::currentThread() != thread() ) + { + qDebug() << "reinvoking msgprocessor::append in correct thread, ie not" << QThread::currentThread(); + QMetaObject::invokeMethod( this, "append", Qt::QueuedConnection, Q_ARG(msg_ptr, msg) ); + return; + } + + m_msgs.append( msg ); + m_msg_ready.insert( msg.data(), false ); + + m_totmsgsize += msg->payload().length(); + + if( m_mode & NOTHING ) + { + //qDebug() << "MsgProcessor::NOTHING"; + handleProcessedMsg( msg ); + return; + } + + QFuture fut = QtConcurrent::run(&MsgProcessor::process, msg, m_mode, m_threshold); + QFutureWatcher * watcher = new QFutureWatcher; + connect( watcher, SIGNAL( finished() ), + this, SLOT( processed() ), + Qt::QueuedConnection ); + + watcher->setFuture( fut ); +} + + +void MsgProcessor::processed() +{ + QFutureWatcher * watcher = (QFutureWatcher *) sender(); + msg_ptr msg = watcher->result(); + watcher->deleteLater(); + handleProcessedMsg( msg ); +} + +void MsgProcessor::handleProcessedMsg( msg_ptr msg ) +{ + Q_ASSERT( QThread::currentThread() == thread() ); + + m_msg_ready.insert( msg.data(), true ); + + while( !m_msgs.isEmpty() ) + { + if( m_msg_ready.value( m_msgs.first().data() ) ) + { + msg_ptr m = m_msgs.takeFirst(); + m_msg_ready.remove( m.data() ); + //qDebug() << Q_FUNC_INFO << "totmsgsize:" << m_totmsgsize; + emit ready( m ); + } + else + { + return; + } + } + + //qDebug() << Q_FUNC_INFO << "EMPTY, no msgs left."; + emit empty(); +} + + +/// This method is run by QtConcurrent: +msg_ptr MsgProcessor::process( msg_ptr msg, quint32 mode, quint32 threshold ) +{ + // uncompress if needed + if( (mode & UNCOMPRESS_ALL) && msg->is( Msg::COMPRESSED ) ) + { + qDebug() << "MsgProcessor::UNCOMPRESSING"; + msg->m_payload = qUncompress( msg->payload() ); + msg->m_length = msg->m_payload.length(); + msg->m_flags ^= Msg::COMPRESSED; + } + + // parse json payload into qvariant if needed + if( (mode & PARSE_JSON) && + msg->is( Msg::JSON ) && + msg->m_json_parsed == false ) + { + qDebug() << "MsgProcessor::PARSING JSON"; + bool ok; + QJson::Parser parser; + msg->m_json = parser.parse( msg->payload(), &ok ); + msg->m_json_parsed = true; + } + + // compress if needed + if( (mode & COMPRESS_IF_LARGE) && + !msg->is( Msg::COMPRESSED ) + && msg->length() > threshold ) + { + qDebug() << "MsgProcessor::COMPRESSING"; + msg->m_payload = qCompress( msg->payload(), 9 ); + msg->m_length = msg->m_payload.length(); + msg->m_flags |= Msg::COMPRESSED; + } + return msg; +} + + diff --git a/src/msgprocessor.h b/src/msgprocessor.h new file mode 100644 index 000000000..9ecf1e58d --- /dev/null +++ b/src/msgprocessor.h @@ -0,0 +1,63 @@ +/* + MsgProcessor is a FIFO queue of msg_ptr, you .add() a msg_ptr, and + it emits done(msg_ptr) for each msg, preserving the order. + + It can be configured to auto-compress, or de-compress msgs for sending + or receiving. + + It uses QtConcurrent, but preserves msg order. + + NOT threadsafe. +*/ +#ifndef MSGPROCESSOR_H +#define MSGPROCESSOR_H + +#include +#include "msg.h" +#include +#include +#include + +#include +#include +#include + +class MsgProcessor : public QObject +{ +Q_OBJECT +public: + enum Mode + { + NOTHING = 0, + COMPRESS_IF_LARGE = 1, + UNCOMPRESS_ALL = 2, + PARSE_JSON = 4 + }; + + explicit MsgProcessor( quint32 mode = NOTHING, quint32 t = 512 ); + + void setMode( quint32 m ) { m_mode = m ; } + + static msg_ptr process( msg_ptr msg, quint32 mode, quint32 threshold ); + + int length() const { return m_msgs.length(); } + +signals: + void ready( msg_ptr ); + void empty(); + +public slots: + void append( msg_ptr msg ); + void processed(); + +private: + void handleProcessedMsg( msg_ptr msg ); + + quint32 m_mode; + quint32 m_threshold; + QList m_msgs; + QMap< Msg*, bool> m_msg_ready; + unsigned int m_totmsgsize; +}; + +#endif // MSGPROCESSOR_H diff --git a/src/musicscanner.cpp b/src/musicscanner.cpp new file mode 100644 index 000000000..2769968a0 --- /dev/null +++ b/src/musicscanner.cpp @@ -0,0 +1,210 @@ +#include "musicscanner.h" + +#include "tomahawk/tomahawkapp.h" +#include "database.h" +#include "databasecommand_dirmtimes.h" +#include "databasecommand_addfiles.h" + +using namespace Tomahawk; + + +MusicScanner::MusicScanner( const QString& dir, quint32 bs ) + : QThread() + , m_dir( dir ) + , m_batchsize( bs ) +{ + moveToThread( this ); + + m_ext2mime.insert( "mp3", "audio/mpeg" ); + + // m_ext2mime.insert( "aac", "audio/mp4" ); + // m_ext2mime.insert( "m4a", "audio/mp4" ); + // m_ext2mime.insert( "mp4", "audio/mp4" ); + // m_ext2mime.insert( "flac", "audio/flac" ); + +#ifndef NO_OGG + // not compiled on windows yet + m_ext2mime.insert( "ogg", "application/ogg" ); +#endif +} + + +void +MusicScanner::run() +{ + QTimer::singleShot( 0, this, SLOT( startScan() ) ); + exec(); +} + + +void +MusicScanner::startScan() +{ + qDebug() << "Loading mtimes..."; + m_scanned = m_skipped = 0; + m_skippedFiles.clear(); + + // trigger the scan once we've loaded old mtimes for dirs below our path + DatabaseCommand_DirMtimes* cmd = new DatabaseCommand_DirMtimes( m_dir ); + connect( cmd, SIGNAL( done( const QMap& ) ), + SLOT( setMtimes( const QMap& ) ), Qt::DirectConnection ); + connect( cmd, SIGNAL( done( const QMap& ) ), + SLOT( scan() ), Qt::DirectConnection ); + + TomahawkApp::instance()->database()->enqueue( QSharedPointer(cmd) ); +} + + +void +MusicScanner::setMtimes( const QMap& m ) +{ + m_dirmtimes = m; +} + + +void +MusicScanner::scan() +{ + TomahawkApp::instance()->sourcelist().getLocal()->scanningProgress( 0 ); + qDebug() << "Scanning, num saved mtimes from last scan:" << m_dirmtimes.size(); + + connect( this, SIGNAL( batchReady( QVariantList ) ), + SLOT( commitBatch( QVariantList ) ), Qt::DirectConnection ); + + DirLister* lister = new DirLister( QDir( m_dir, 0 ), m_dirmtimes ); + + connect( lister, SIGNAL( fileToScan( QFileInfo ) ), + SLOT( scanFile( QFileInfo ) ), Qt::QueuedConnection ); + + // queued, so will only fire after all dirs have been scanned: + connect( lister, SIGNAL( finished( const QMap& ) ), + SLOT( listerFinished( const QMap& ) ), Qt::QueuedConnection ); + + connect( lister, SIGNAL( finished() ), lister, SLOT( deleteLater() ) ); + + lister->start(); +} + + +void +MusicScanner::listerFinished( const QMap& newmtimes ) +{ + qDebug() << Q_FUNC_INFO; + // any remaining stuff that wasnt emitted as a batch: + if( m_scannedfiles.length() ) + { + TomahawkApp::instance()->sourcelist().getLocal()->scanningProgress( m_scanned ); + commitBatch( m_scannedfiles ); + } + + // save mtimes, then quit thread + DatabaseCommand_DirMtimes* cmd = new DatabaseCommand_DirMtimes( newmtimes ); + connect( cmd, SIGNAL( finished() ), SLOT( quit() ) ); + TomahawkApp::instance()->database()->enqueue( QSharedPointer(cmd) ); + + qDebug() << "Scanning complete, saving to database. " + "(scanned" << m_scanned << "skipped" << m_skipped << ")"; + + qDebug() << "Skipped the following files (no tags / no valid audio):"; + foreach( const QString& s, m_skippedFiles ) + qDebug() << s; +} + + +void +MusicScanner::commitBatch( const QVariantList& tracks ) +{ + if ( tracks.length() ) + { + qDebug() << Q_FUNC_INFO << tracks.length(); + source_ptr localsrc = TomahawkApp::instance()->sourcelist().getLocal(); + TomahawkApp::instance()->database()->enqueue( + QSharedPointer( new DatabaseCommand_AddFiles( tracks, localsrc ) ) + ); + } +} + + +void +MusicScanner::scanFile( const QFileInfo& fi ) +{ + QVariant m = readFile( fi ); + if( m.toMap().isEmpty() ) + return; + + m_scannedfiles << m; + if( m_batchsize != 0 && + (quint32)m_scannedfiles.length() >= m_batchsize ) + { + qDebug() << "batchReady, size:" << m_scannedfiles.length(); + emit batchReady( m_scannedfiles ); + m_scannedfiles.clear(); + } +} + + +QVariant +MusicScanner::readFile( const QFileInfo& fi ) +{ + if ( ! m_ext2mime.contains( fi.suffix().toLower() ) ) + { + m_skipped++; + return QVariantMap(); // invalid extension + } + + if( m_scanned % 3 == 0 ) + TomahawkApp::instance()->sourcelist().getLocal()->scanningProgress( m_scanned ); + if( m_scanned % 100 == 0 ) + qDebug() << "SCAN" << m_scanned << fi.absoluteFilePath(); + + TagLib::FileRef f( fi.absoluteFilePath().toUtf8().constData() ); + if ( f.isNull() || !f.tag() ) + { + // qDebug() << "Doesn't seem to be a valid audiofile:" << fi.absoluteFilePath(); + m_skippedFiles << fi.absoluteFilePath(); + m_skipped++; + return QVariantMap(); + } + + int bitrate = 0; + int duration = 0; + TagLib::Tag *tag = f.tag(); + + if ( f.audioProperties() ) + { + TagLib::AudioProperties *properties = f.audioProperties(); + duration = properties->length(); + bitrate = properties->bitrate(); + } + QString artist = TStringToQString( tag->artist() ).trimmed(); + QString album = TStringToQString( tag->album() ).trimmed(); + QString track = TStringToQString( tag->title() ).trimmed(); + + if ( artist.isEmpty() || track.isEmpty() ) + { + // FIXME: do some clever filename guessing + // qDebug() << "No tags found, skipping" << fi.absoluteFilePath(); + m_skippedFiles << fi.absoluteFilePath(); + m_skipped++; + return QVariantMap(); + } + + QString mimetype = m_ext2mime.value( fi.suffix().toLower() ); + QString url( "file://%1" ); + + QVariantMap m; + m["url"] = url.arg( fi.absoluteFilePath() ); + m["lastmodified"] = fi.lastModified().toUTC().toTime_t(); + m["size"] = (unsigned int)fi.size(); + m["hash"] = ""; // TODO + m["mimetype"] = mimetype; + m["duration"] = duration; + m["bitrate"] = bitrate; + m["artist"] = artist; + m["album"] = album; + m["track"] = track; + m["albumpos"] = tag->track(); + + m_scanned++; + return m; +} diff --git a/src/musicscanner.h b/src/musicscanner.h new file mode 100644 index 000000000..366c1fb5f --- /dev/null +++ b/src/musicscanner.h @@ -0,0 +1,133 @@ +#ifndef MUSICSCANNER_H +#define MUSICSCANNER_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +class MusicScanner : public QThread +{ +Q_OBJECT + +public: + MusicScanner( const QString& dir, quint32 bs = 0 ); + +protected: + void run(); + +signals: + //void fileScanned( QVariantMap ); + void finished( int, int ); + void batchReady( const QVariantList& ); + +private: + QVariant readFile( const QFileInfo& fi ); + +private slots: + void listerFinished( const QMap& newmtimes ); + void scanFile( const QFileInfo& fi ); + void startScan(); + void scan(); + void setMtimes( const QMap& m ); + void commitBatch( const QVariantList& ); + +private: + QString m_dir; + QMap m_ext2mime; // eg: mp3 -> audio/mpeg + unsigned int m_scanned; + unsigned int m_skipped; + + QList m_skippedFiles; + + QMap m_dirmtimes; + QMap m_newdirmtimes; + + QList m_scannedfiles; + quint32 m_batchsize; +}; + + +#include + +// descend dir tree comparing dir mtimes to last known mtime +// emit signal for any dir with new content, so we can scan it. +// finally, emit the list of new mtimes we observed. +class DirLister : public QThread +{ + Q_OBJECT +public: + DirLister( QDir d, QMap& mtimes ) + : QThread(), m_dir( d ), m_dirmtimes( mtimes ) + { + qDebug() << Q_FUNC_INFO; + moveToThread(this); + } + + ~DirLister() + { + qDebug() << Q_FUNC_INFO; + } + +protected: + void run() + { + QTimer::singleShot(0,this,SLOT(go())); + exec(); + } + +signals: + void fileToScan( QFileInfo ); + void finished( const QMap& ); + +private slots: + void go() + { + scanDir( m_dir, 0 ); + emit finished( m_newdirmtimes ); + } + + void scanDir( QDir dir, int depth ) + { + QFileInfoList dirs; + const uint mtime = QFileInfo( dir.absolutePath() ).lastModified().toUTC().toTime_t(); + m_newdirmtimes.insert( dir.absolutePath(), mtime ); + + if ( m_dirmtimes.contains( dir.absolutePath() ) && + mtime == m_dirmtimes.value( dir.absolutePath() ) + ) + { + // dont scan this dir, unchanged since last time. + } + else + { + dir.setFilter( QDir::Files | QDir::Readable | QDir::NoDotAndDotDot ); + dir.setSorting( QDir::Name ); + dirs = dir.entryInfoList(); + foreach( QFileInfo di, dirs ) + { + emit fileToScan( di ); + } + } + dir.setFilter( QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot ); + dirs = dir.entryInfoList(); + + foreach( QFileInfo di, dirs ) + { + scanDir( di.absoluteFilePath(), depth + 1 ); + } + } + +private: + QDir m_dir; + QMap m_dirmtimes; + QMap m_newdirmtimes; +}; + +#endif diff --git a/src/pipeline.cpp b/src/pipeline.cpp new file mode 100644 index 000000000..c78a2d6db --- /dev/null +++ b/src/pipeline.cpp @@ -0,0 +1,198 @@ +#include "tomahawk/pipeline.h" + +#include +#include + +#include "tomahawk/functimeout.h" +#include "tomahawk/tomahawkapp.h" +#include "database/database.h" + +using namespace Tomahawk; + + +Pipeline::Pipeline( QObject* parent ) + : QObject( parent ) + , m_index_ready( false ) +{ + +} + +void +Pipeline::databaseReady() +{ + connect( APP->database(), SIGNAL(indexReady()), this, SLOT(indexReady()), Qt::QueuedConnection ); + APP->database()->loadIndex(); +} + +void Pipeline::indexReady() +{ + qDebug() << Q_FUNC_INFO << "shuting this many pending queries:" << m_queries_pending.size(); + m_index_ready = true; + foreach( const query_ptr& q, m_queries_pending ) + { + q->setLastPipelineWeight( 101 ); + shunt( q ); + } + m_queries_pending.clear(); +} + +void +Pipeline::removeResolver( Resolver* r ) +{ + m_resolvers.removeAll( r ); +} + +void +Pipeline::addResolver( Resolver* r, bool sort ) +{ + m_resolvers.append( r ); + if( sort ) + { + qSort( m_resolvers.begin(), + m_resolvers.end(), + Pipeline::resolverSorter ); + } + qDebug() << "Adding resolver" << r->name(); + +/* qDebug() << "Current pipeline:"; + foreach( Resolver * r, m_resolvers ) + { + qDebug() << "* score:" << r->weight() + << "pref:" << r->preference() + << "name:" << r->name(); + }*/ +} + + +void +Pipeline::add( const QList& qlist ) +{ + { + QMutexLocker lock( &m_mut ); + foreach( const query_ptr& q, qlist ) + { + qDebug() << Q_FUNC_INFO << (qlonglong)q.data() << q->toString(); + if( !m_qids.contains( q->id() ) ) + { + m_qids.insert( q->id(), q ); + } + } + } + + /* + Since resolvers are async, we now dispatch to the highest weighted ones + and after timeout, dispatch to next highest etc, aborting when solved + + If index not yet loaded, leave in the pending list instead. + (they are shunted when index is ready) + */ + if( m_index_ready ) + { + foreach( const query_ptr& q, qlist ) + { + q->setLastPipelineWeight( 101 ); + shunt( q ); // bump into next stage of pipeline (highest weights are 100) + } + } + else + { + qDebug() << "Placing query in pending queue - index not ready yet"; + m_queries_pending.append( qlist ); + } +} + + +void +Pipeline::add( const query_ptr& q ) +{ + //qDebug() << Q_FUNC_INFO << (qlonglong)q.data() << q->toString(); + QList< query_ptr > qlist; + qlist << q; + add( qlist ); +} + + +void +Pipeline::reportResults( QID qid, const QList< result_ptr >& results ) +{ + QMutexLocker lock( &m_mut ); + + if( !m_qids.contains( qid ) ) + { + qDebug() << "reportResults called for unknown QID"; + return; + } + + const query_ptr& q = m_qids.value( qid ); + //qDebug() << Q_FUNC_INFO << qid; + //qDebug() << "solved query:" << (qlonglong)q.data() << q->toString(); + q->addResults( results ); + + //qDebug() << "Results for " << q->toString() << ", just added" << results.length(); + foreach( const result_ptr& r, q->results() ) + { + m_rids.insert( r->id(), r ); + //qDebug() << "* " << (results.contains(r) ? "NEW" : "") << r->toString(); + } + +} + + +void +Pipeline::shunt( const query_ptr& q ) +{ + if( q->solved() ) + { + qDebug() << "Query solved, pipeline aborted:" << q->toString() + << "numresults:" << q->results().length(); + return; + } + unsigned int lastweight = 0; + unsigned int lasttimeout = 0; + foreach( Resolver* r, m_resolvers ) + { + if ( r->weight() >= q->lastPipelineWeight() ) + continue; + + if ( lastweight == 0 ) + { + lastweight = r->weight(); + lasttimeout = r->timeout(); + //qDebug() << "Shunting into weight" << lastweight << "q:" << q->toString(); + } + if ( lastweight == r->weight() ) + { + // snag the lowest timeout at this weight + if ( r->timeout() < lasttimeout ) + lasttimeout = r->timeout(); + + // resolvers aren't allowed to block in this call: + //qDebug() << "Dispaching to resolver" << r->name(); + r->resolve( q->toVariant() ); + } + else + break; + } + + if ( lastweight > 0 ) + { + q->setLastPipelineWeight( lastweight ); + //qDebug() << "Shunting in" << lasttimeout << "ms, q:" << q->toString(); + new FuncTimeout( lasttimeout, boost::bind( &Pipeline::shunt, this, q ) ); + } + else + { + //qDebug() << "Reached end of pipeline for:" << q->toString(); + // reached end of pipeline + } +} + + +bool +Pipeline::resolverSorter( const Resolver* left, const Resolver* right ) +{ + if( left->weight() == right->weight() ) + return left->preference() > right->preference(); + else + return left->weight() > right->weight(); +} diff --git a/src/playlist.cpp b/src/playlist.cpp new file mode 100644 index 000000000..57c7c8eb2 --- /dev/null +++ b/src/playlist.cpp @@ -0,0 +1,354 @@ +#include "tomahawk/playlist.h" + +#include +#include + +#include "tomahawk/tomahawkapp.h" +#include "database.h" +#include "databasecommand_loadplaylistentries.h" +#include "databasecommand_setplaylistrevision.h" +#include "databasecommand_createplaylist.h" +#include "databasecommand_deleteplaylist.h" + +using namespace Tomahawk; + + +void +PlaylistEntry::setQueryvariant( const QVariant& v ) +{ + m_query = query_ptr( new Query( v ) ); +} + + +QVariant +PlaylistEntry::queryvariant() const +{ + return m_query->toVariant(); +} + + +// used when loading from DB: +Playlist::Playlist( const source_ptr& src, + const QString& currentrevision, + const QString& title, + const QString& info, + const QString& creator, + bool shared, + int lastmod, + const QString& guid ) + : QObject() + , m_source( src ) + , m_currentrevision( currentrevision ) + , m_guid( guid == "" ? uuid() : guid ) + , m_title( title ) + , m_info( info ) + , m_creator( creator ) + , m_lastmodified( lastmod ) + , m_shared( shared ) +{ + qDebug() << Q_FUNC_INFO << "1"; +} + + +Playlist::Playlist( const source_ptr& author, + const QString& guid, + const QString& title, + const QString& info, + const QString& creator, + bool shared ) + : QObject() + , m_source( author ) + , m_guid( guid ) + , m_title( title ) + , m_info ( info ) + , m_creator( creator ) + , m_lastmodified( 0 ) + , m_shared( shared ) +{ + qDebug() << Q_FUNC_INFO << "2"; +} + + +playlist_ptr +Playlist::create( const source_ptr& author, + const QString& guid, + const QString& title, + const QString& info, + const QString& creator, + bool shared ) +{ + playlist_ptr playlist( new Playlist( author, guid, title, info, creator, shared ) ); + // save to DB in the background + // Hope this doesn't cause any problems.. + // Watch for the created() signal if you need to be sure it's written. + // + // When a playlist is created it will reportCreated(), adding it to the + // collection it belongs to and emitting the appropriate signal. + // When we create a new playlist for the local source here we call reportCreated() + // immediately, so the GUI can reflect the new playlist without waiting for the DB sync + // + // When createplaylist DBOPs come from peers, the postCommitHook will call + // reportCreated for us automatically, which should cause new playlists to be added to the GUI. + + DatabaseCommand_CreatePlaylist* cmd = new DatabaseCommand_CreatePlaylist( author, playlist ); + connect( cmd, SIGNAL(finished()), playlist.data(), SIGNAL(created()) ); + APP->database()->enqueue( QSharedPointer(cmd) ); + playlist->reportCreated( playlist ); + return playlist; +} + + +bool +Playlist::remove( const playlist_ptr& playlist ) +{ + DatabaseCommand_DeletePlaylist* cmd = new DatabaseCommand_DeletePlaylist( playlist->author(), playlist->guid() ); + APP->database()->enqueue( QSharedPointer(cmd) ); + + return true; // FIXME +} + + +void +Playlist::reportCreated( const playlist_ptr& self ) +{ + qDebug() << Q_FUNC_INFO; + Q_ASSERT( self.data() == this ); + // will emit Collection::playlistCreated(...) + m_source->collection()->addPlaylist( self ); +} + + +void +Playlist::reportDeleted( const Tomahawk::playlist_ptr& self ) +{ + qDebug() << Q_FUNC_INFO; + Q_ASSERT( self.data() == this ); + m_source->collection()->deletePlaylist( self ); +} + + +void +Playlist::loadRevision( const QString& rev ) +{ + qDebug() << Q_FUNC_INFO; + + DatabaseCommand_LoadPlaylistEntries* cmd = + new DatabaseCommand_LoadPlaylistEntries( rev.isEmpty() ? currentrevision() : rev ); + + connect( cmd, SIGNAL( done( const QString&, + const QList&, + const QList&, + bool, + const QMap< QString, Tomahawk::plentry_ptr >&, + bool ) ), + SLOT( setRevision( const QString&, + const QList&, + const QList&, + bool, + const QMap< QString, Tomahawk::plentry_ptr >&, + bool ) ) ); + + APP->database()->enqueue( QSharedPointer( cmd ) ); +} + + +//public, model can call this if user changes a playlist: +void +Playlist::createNewRevision( const QString& newrev, const QString& oldrev, const QList< plentry_ptr >& entries ) +{ + // qDebug() << "m_entries guids:"; + // foreach( plentry_ptr pp, m_entries ) qDebug() << pp->guid(); + + QSet currentguids; + foreach( plentry_ptr p, m_entries ) + currentguids.insert( p->guid() ); // could be cached as member? + + // calc list of newly added entries: + QList added; + QStringList orderedguids; + foreach( plentry_ptr p, entries ) + { + orderedguids << p->guid(); + if( !currentguids.contains(p->guid()) ) + added << p; + } + + // source making the change (localy user in this case) + source_ptr author = APP->sourcelist().getLocal(); + // command writes new rev to DB and calls setRevision, which emits our signal + DatabaseCommand_SetPlaylistRevision* cmd = + new DatabaseCommand_SetPlaylistRevision( author, + guid(), + newrev, + oldrev, + orderedguids, + added ); + APP->database()->enqueue( QSharedPointer( cmd ) ); +} + + +// private, called when we loadRevision, or by our friend class DatabaseCommand_SetPlaylistRevision +// used to save new revision data (either originating locally, or via network msg for syncing) +void +Playlist::setRevision( const QString& rev, + const QList& neworderedguids, + const QList& oldorderedguids, + bool is_newest_rev, + const QMap< QString, Tomahawk::plentry_ptr >& addedmap, + bool applied ) +{ + if( QThread::currentThread() != thread() ) + { + //qDebug() << "Calling setRevision in correct thread, instead of" + // << QThread::currentThread(); + + QMetaObject::invokeMethod( this, + "setRevision", + Qt::BlockingQueuedConnection, + Q_ARG( QString, rev ), + Q_ARG( QList, neworderedguids ), + Q_ARG( QList, oldorderedguids ), + Q_ARG( bool, is_newest_rev ), + QGenericArgument( "QMap< QString,Tomahawk::plentry_ptr >" , (const void*)&addedmap ), + Q_ARG( bool, applied ) + ); + return; + } + //qDebug() << Q_FUNC_INFO << (qlonglong)this + // << rev << neworderedguids << oldorderedguids + // << "isnewest:" << is_newest_rev << addedmap << applied << m_entries + // ; + + // build up correctly ordered new list of plentry_ptrs from + // existing ones, and the ones that have been added + QMap entriesmap; + foreach( const plentry_ptr& p, m_entries ) + entriesmap.insert( p->guid(), p ); + + //qDebug() << "Entries map:" << entriesmap; + + QList entries; + //qDebug() << "m_entries:" << m_entries.count() << m_entries; + + //qDebug() << "counters:" << neworderedguids.count() << entriesmap.count() << addedmap.count(); + foreach( const QString& id, neworderedguids ) + { + //qDebug() << "id:" << id; + //qDebug() << "newordered:" << neworderedguids.count() << neworderedguids; + //qDebug() << "entriesmap:" << entriesmap.count() << entriesmap; + //qDebug() << "addedmap:" << addedmap.count() << addedmap; + //qDebug() << "m_entries" << m_entries; + + if( entriesmap.contains( id ) ) + { + entries.append( entriesmap.value( id ) ); + } + else if( addedmap.contains( id ) ) + { + entries.append( addedmap.value( id ) ); + if( is_newest_rev ) m_entries.append( addedmap.value( id ) ); + } + else + { + Q_ASSERT( false ); // XXX + } + } + + //qDebug() << Q_FUNC_INFO << rev << entries.length() << applied; + + PlaylistRevision pr; + pr.oldrevisionguid = m_currentrevision; + pr.revisionguid = rev; + + // entries that have been removed: + QSet removedguids = oldorderedguids.toSet().subtract( neworderedguids.toSet() ); + //qDebug() << "Removedguids:" << removedguids << "oldorederedguids" << oldorderedguids << "newog" << neworderedguids; + foreach( QString remid, removedguids ) + { + // NB: entriesmap will contain old/removed entries only if the removal was done + // in the same session - after a restart, history is not in memory. + if( entriesmap.contains( remid ) ) + { + pr.removed << entriesmap.value( remid ); + if( is_newest_rev ) + { + //qDebug() << "Removing from m_entries" << remid; + for( int k = 0 ; kguid() == remid ) + { + //qDebug() << "removed at " << k; + m_entries.removeAt(k); + break; + } + } + } + } + } + + pr.added = addedmap.values(); + + //qDebug() << "Revision set:" << rev + // << "added" << pr.added.size() + // << "removed" << pr.removed.size() + // << "total entries" << m_entries.size(); + + pr.newlist = entries; + + if( applied ) + m_currentrevision = rev; + pr.applied = applied; + + + + emit revisionLoaded( pr ); +} + + +void Playlist::resolve() +{ + QList< query_ptr > qlist; + foreach( const plentry_ptr& p, m_entries ) + { + qlist << p->query(); + } + APP->pipeline()->add( qlist ); +} + + +void +Playlist::addEntry( const query_ptr& query, const QString& oldrev ) +{ + QList queries; + queries << query; + + addEntries( queries, oldrev ); +} + + +void +Playlist::addEntries( const QList& queries, const QString& oldrev ) +{ + //qDebug() << Q_FUNC_INFO; + + QList el = entries(); + foreach( const query_ptr& query, queries ) + { + plentry_ptr e( new PlaylistEntry() ); + e->setGuid( uuid() ); + + if ( query->results().count() ) + e->setDuration( query->results().at( 0 )->duration() ); + else + e->setDuration( 0 ); + + e->setLastmodified( 0 ); + e->setAnnotation( "" ); // FIXME + e->setQuery( query ); + + el << e; + } + + QString newrev = uuid(); + createNewRevision( newrev, oldrev, el ); +} diff --git a/src/playlist/playlistitem.cpp b/src/playlist/playlistitem.cpp new file mode 100644 index 000000000..157da8d99 --- /dev/null +++ b/src/playlist/playlistitem.cpp @@ -0,0 +1,118 @@ +#include "playlistitem.h" + +#include "utils/tomahawkutils.h" + +#include + +using namespace Tomahawk; + + +PlaylistItem::PlaylistItem( const Tomahawk::query_ptr& query, QObject* parent ) + : QObject( parent ) +{ + setupItem( query ); +} + + +PlaylistItem::PlaylistItem( const Tomahawk::plentry_ptr& entry, QObject* parent ) + : QObject( parent ) + , m_entry( entry ) +{ + setupItem( entry->query() ); +} + + +void +PlaylistItem::setupItem( const Tomahawk::query_ptr& query ) +{ + m_beingRemoved = false; + m_query = query; + + QVariantMap map = query->toVariant().toMap(); + qlonglong ptr = qlonglong( this ); + + QStandardItem* item = new QStandardItem( map.value( "artist" ).toString() ); + item->setData( ptr, Qt::UserRole ); + item->setSizeHint( QSize( 0, 18 ) ); + m_columns << item; + + item = new QStandardItem( map.value( "track" ).toString() ); + m_columns << item; + + item = new QStandardItem( map.value( "album" ).toString() ); + m_columns << item; + + item = new QStandardItem( TomahawkUtils::timeToString( map.value( "duration" ).toInt() ) ); + m_columns << item; + + item = new QStandardItem( map.value( "bitrate" ).toString() ); + m_columns << item; + + item = new QStandardItem( "" ); // sources + m_columns << item; + + foreach( QStandardItem* item, m_columns ) + item->setEditable( false ); + + if ( query->numResults() ) + onResultsAdded( query->results() ); + + connect( query.data(), SIGNAL( resultsAdded( const QList& ) ), + SLOT( onResultsAdded( const QList& ) ), Qt::DirectConnection ); +} + + +void +PlaylistItem::onResultsAdded( const QList& results ) +{ + //qDebug() << "Found results for playlist item:" << this; + const Tomahawk::result_ptr& result = m_query->results().at( 0 ); + + // Since we have a result now, we can enable the PlaylistItem and update the actual metadata + m_columns.at( 0 )->setText( result->artist() ); + m_columns.at( 1 )->setText( result->track() ); + m_columns.at( 2 )->setText( result->album() ); + m_columns.at( 3 )->setText( TomahawkUtils::timeToString( result->duration() ) ); + m_columns.at( 4 )->setText( QString::number( result->bitrate() ) ); + + if ( m_query->results().count() > 1 ) + { + // count unique sources + QList uniqsrcs; + unsigned int c = m_query->results().count(); + for ( unsigned int i = 0; i < c; i++ ) + { + if ( !uniqsrcs.contains( m_query->results().at( i )->collection()->source()->id() ) ) + uniqsrcs.append( m_query->results().at( i )->collection()->source()->id() ); + } + + m_columns.at( 5 )->setText( QString( "%1%2" ) + .arg( result->collection()->source()->friendlyName() ) + .arg( uniqsrcs.count() > 1 ? QString( " (%1)" ).arg( uniqsrcs.count() ) : "" ) ); + } + else + m_columns.at( 5 )->setText( result->collection()->source()->friendlyName() ); + + foreach( QStandardItem* item, m_columns ) + { + item->setEnabled( false ); // FIXME: not exactly elegant + item->setEnabled( true ); + } +} + + +QModelIndex +PlaylistItem::index() const +{ + if ( m_columns.length() ) + return m_columns.at( 0 )->index(); + + return QModelIndex(); +} + + +void +PlaylistItem::setBeingRemoved( bool state ) +{ + m_beingRemoved = state; +} diff --git a/src/playlist/playlistitem.h b/src/playlist/playlistitem.h new file mode 100644 index 000000000..9533472c5 --- /dev/null +++ b/src/playlist/playlistitem.h @@ -0,0 +1,40 @@ +#ifndef PLAYLISTITEM_H +#define PLAYLISTITEM_H + +#include + +#include "tomahawk/query.h" +#include "tomahawk/result.h" + +class PlaylistItem : public QObject +{ +Q_OBJECT + +public: +// explicit PlaylistItem() {} +// explicit PlaylistItem( const PlaylistItem& item ) { m_query = item.query(); m_columns << item.columns(); } + explicit PlaylistItem( const Tomahawk::query_ptr& query, QObject* parent = 0 ); + explicit PlaylistItem( const Tomahawk::plentry_ptr& entry, QObject* parent = 0 ); + + const Tomahawk::plentry_ptr& entry() const { return m_entry; }; + const Tomahawk::query_ptr& query() const { return m_query; }; + QList columns() const { return m_columns; }; + bool beingRemoved() { return m_beingRemoved; } + QModelIndex index() const; + +public slots: + void setBeingRemoved( bool state ); + +private slots: + void onResultsAdded( const QList& result ); + +private: + void setupItem( const Tomahawk::query_ptr& query ); + + Tomahawk::plentry_ptr m_entry; + Tomahawk::query_ptr m_query; + QList m_columns; + bool m_beingRemoved; +}; + +#endif // PLAYLISTITEM_H diff --git a/src/playlist/playlistitemdelegate.cpp b/src/playlist/playlistitemdelegate.cpp new file mode 100644 index 000000000..8609620d0 --- /dev/null +++ b/src/playlist/playlistitemdelegate.cpp @@ -0,0 +1,68 @@ +#include "playlistitemdelegate.h" + +#include +#include + +#include "tomahawk/query.h" +#include "tomahawk/result.h" +#include "playlistview.h" +#include "playlistitem.h" +#include "playlistmodel.h" + + +PlaylistItemDelegate::PlaylistItemDelegate( QObject* parent ) + : QStyledItemDelegate( parent ) +{ +} + + +void +PlaylistItemDelegate::updateRowSize( const QModelIndex& index ) +{ + emit sizeHintChanged( index ); +} + + +QSize +PlaylistItemDelegate::sizeHint( const QStyleOptionViewItem& option, const QModelIndex& index ) const +{ + QSize size = QStyledItemDelegate::sizeHint( option, index ); + return size; // + QSize( 0, 3 ); // FIXME: a hack for now (due to UniformRowHeights) + + PlaylistItem* item = PlaylistModel::indexToPlaylistItem( index ); + if ( !item ) + return size; + + if ( item->beingRemoved() && m_removalProgress > 0.0 && m_removalProgress < 50.0 ) + { + int h = (((qreal)m_removalProgress * 2.0 ) / (qreal)100.0 ) * (qreal)size.height(); + if ( h < 2 ) + h = 0; + return QSize( size.width(), h ); + } + else + return size; + + return size; +} + + +void +PlaylistItemDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const +{ + PlaylistItem* item = PlaylistModel::indexToPlaylistItem( index ); + if ( item ) + { + if ( item->beingRemoved() ) + painter->setOpacity( (qreal)m_removalProgress / (qreal)100.0 ); + else + { + if ( item->query()->results().count() ) + painter->setOpacity( item->query()->results().at( 0 )->score() ); + else + painter->setOpacity( 0.3 ); + } + } + + QStyledItemDelegate::paint( painter, option, index ); +} diff --git a/src/playlist/playlistitemdelegate.h b/src/playlist/playlistitemdelegate.h new file mode 100644 index 000000000..543c9666b --- /dev/null +++ b/src/playlist/playlistitemdelegate.h @@ -0,0 +1,26 @@ +#ifndef PLAYLISTITEMDELEGATE_H +#define PLAYLISTITEMDELEGATE_H + +#include + +class PlaylistItemDelegate : public QStyledItemDelegate +{ +Q_OBJECT + +public: + PlaylistItemDelegate( QObject* parent = 0 ); + + void updateRowSize( const QModelIndex& index ); + +public slots: + void setRemovalProgress( unsigned int progress ) { m_removalProgress = progress; } + +protected: + void paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const; + QSize sizeHint( const QStyleOptionViewItem& option, const QModelIndex& index ) const; + +private: + unsigned int m_removalProgress; +}; + +#endif // PLAYLISTITEMDELEGATE_H diff --git a/src/playlist/playlistmodel.cpp b/src/playlist/playlistmodel.cpp new file mode 100644 index 000000000..2a8d3cef5 --- /dev/null +++ b/src/playlist/playlistmodel.cpp @@ -0,0 +1,737 @@ +#include "playlistmodel.h" + +#include +#include +#include + +#include "playlistitem.h" +#include "playlistview.h" +#include "playlistmodelworker.h" +#include "playlistproxymodel.h" +#include "animatedrowremover.h" +#include "databasecollection.h" +#include "tomahawk/collection.h" +#include "tomahawk/tomahawkapp.h" + +using namespace Tomahawk; + +PlaylistModel::PlaylistModel( QObject* parent ) + : QStandardItemModel( parent ) + , m_collectionCount( 0 ) + , m_sourceCount( 0 ) + , m_remoteUpdate( false ) + , m_readOnly( false ) + , m_nowPlayingIcon( QIcon( RESPATH "images/now-playing-speaker.png" ) ) + , m_sources( 0 ) + , m_busy( false ) +{ + qDebug() << Q_FUNC_INFO; + + setupHeaders(); + setColumnCount( 6 ); + setReadOnly( true ); + + connect( this, SIGNAL( rowsAboutToBeRemoved( QModelIndex, int, int ) ), SLOT( onRowsAboutToBeRemoved( QModelIndex, int, int ) ) ); + connect( this, SIGNAL( rowsInserted( QModelIndex, int, int ) ), SLOT( onRowsInserted( QModelIndex, int, int ) ) ); + connect( this, SIGNAL( rowsRemoved( QModelIndex, int, int ) ), SLOT( onRowsRemoved( QModelIndex, int, int ) ) ); +} + + +void +PlaylistModel::setupHeaders() +{ + QStringList headers; + headers << tr( "Artist" ) << tr( "Track" ) << tr( "Album" ) << tr( "Duration" ) << tr( "Bitrate" ) << tr( "Origin" ); + + setHorizontalHeaderLabels( headers ); +} + + +void +PlaylistModel::updateStats() +{ + emit numTracksChanged( rowCount() ); + emit numArtistsChanged( m_artists.size() ); + emit numSourcesChanged( m_sources ); + emit collectionCountChanged( m_collectionCount ); +} + + +bool +PlaylistModel::appendItem( Tomahawk::query_ptr query, bool emitsig ) +{ + PlaylistItem* item = new PlaylistItem( query ); + invisibleRootItem()->appendRow( item->columns() ); + + if( emitsig ) + emit numTracksChanged( rowCount() ); + + return true; +} + + +bool +PlaylistModel::appendItems( const QList& items, bool emitsig ) +{ +// qDebug() << Q_FUNC_INFO << items.length(); +// ProgressTreeView* tree = ((ProgressTreeView*)( parent()->parent() )); +// tree->setProgressStarted( items.length() ); + +// PlaylistProxyModel* p = ((PlaylistProxyModel*)( parent() )); + blockSignals( true ); + + foreach( PlaylistItem* i, items ) + { + invisibleRootItem()->appendRow( i->columns() ); + } + + if ( emitsig ) + emit numTracksChanged( rowCount() ); + + blockSignals( false ); + +// tree->setProgressEnded(); + + return true; +} + + +bool +PlaylistModel::insertItems( int pos, const QList& items, bool emitsig ) +{ + qDebug() << Q_FUNC_INFO << items.length(); +// PlaylistProxyModel* p = ((PlaylistProxyModel*)( parent() )); + + blockSignals( true ); + + foreach( PlaylistItem* i, items ) + { + invisibleRootItem()->insertRow( pos++, i->columns() ); + } + + if ( emitsig ) + emit numTracksChanged( rowCount() ); + + blockSignals( false ); + + return true; +} + + +void +PlaylistModel::removeItems( const QList& items, bool emitsig, bool animated ) +{ + if ( emitsig ) + { + emit numArtistsChanged( m_artists.size() ); + emit numTracksChanged( rowCount() - items.count() ); + } + + // Now trigger the row removal + AnimatedRowRemover* arr = new AnimatedRowRemover( ((PlaylistView*)parent()->parent()), + this, + items ); // will auto delete + + connect( arr, SIGNAL( finished() ), SLOT( onRowRemoverFinished() ) ); + arr->start( animated ); +} + + +void +PlaylistModel::loadPlaylist( const playlist_ptr& playlist ) +{ + m_playlist = playlist; + if ( m_playlist->author()->isLocal() ) + setReadOnly( false ); + else + setReadOnly( true ); + + connect( m_playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), SLOT( onRevisionLoaded( Tomahawk::PlaylistRevision ) ) ); + appendPlaylistEntries( playlist->entries(), collection_ptr( 0 ) ); // FIXME: collection_ptr 0? really? +} + + +void +PlaylistModel::addCollection( const collection_ptr& collection ) +{ + qDebug() << Q_FUNC_INFO << collection->name() + << collection->source()->id() + << collection->source()->userName(); + + m_busy = true; + + collection->loadTracks( this, "onTracksAdded" ); + emit collectionCountChanged( m_collectionCount++ ); //FIXME pre-increment for clarity? + + connect( collection.data(), SIGNAL( tracksAdded( const QList&, Tomahawk::collection_ptr ) ), + SLOT( onTracksAdded( const QList&, Tomahawk::collection_ptr ) ) ); + + connect( collection.data(), SIGNAL( tracksRemoved( const QList&, Tomahawk::collection_ptr ) ), + SLOT( onTracksRemoved( const QList&, Tomahawk::collection_ptr ) ) ); +} + + +void +PlaylistModel::removeCollection( const collection_ptr& collection ) +{ + emit collectionCountChanged( m_collectionCount-- ); + m_busy = true; + + QList list; + int rows = rowCount(); + for ( int i = rows - 1; i >= 0; i-- ) + { + PlaylistItem* item = indexToPlaylistItem( index( i, 0 ) ); + if ( item && item->query()->numResults() ) + { + foreach( const result_ptr& result, item->query()->results() ) + { + if ( result->collection() == collection ) + { + // must be the last collection that was just + // deactivated, so remove the item from the model + if ( item->query()->numResults() == 1 ) + { + if ( m_currentItem == QPersistentModelIndex( item->index() ) ) + setCurrentItem( QModelIndex() ); + + item->setBeingRemoved( true ); + list << item; + unsigned int g = m_artists.value( result->artist(), 0 ); + if ( g == 1 ) + m_artists.remove( result->artist() ); + else if ( g > 0 ) + m_artists[result->artist()]--; + } + } + } + } + } + + removeItems( list, true, true ); + + disconnect( collection.data(), SIGNAL( tracksAdded( const QList&, Tomahawk::collection_ptr ) ), + this, SLOT( onTracksAdded( const QList&, Tomahawk::collection_ptr ) ) ); + + disconnect( collection.data(), SIGNAL( tracksRemoved( const QList&, Tomahawk::collection_ptr ) ), + this, SLOT( onTracksRemoved( const QList&, Tomahawk::collection_ptr ) ) ); +} + + +void +PlaylistModel::addSource( const source_ptr& source ) +{ + m_sources++; + emit numSourcesChanged( m_sources ); + addCollection( source->collection() ); +} + + +void +PlaylistModel::removeSource( const source_ptr& source ) +{ + m_sources--; + emit numSourcesChanged( m_sources ); + removeCollection( source->collection() ); +} + + +void +PlaylistModel::onTracksAdded( const QList& tracks, collection_ptr collection ) +{ + qDebug() << Q_FUNC_INFO; + + QList queries; + foreach( const QVariant& v, tracks ) + { + Tomahawk::query_ptr query = query_ptr( new Query( v ) ); + queries << query; + } + + appendTracks( queries, collection ); +} + + +void +PlaylistModel::appendTracks( const QList& queries, collection_ptr collection ) +{ + qDebug() << "Adding queries:" << queries.length(); + m_busy = true; + + PlaylistModelWorker* worker = new PlaylistModelWorker( queries, this, collection ); + connect( worker, SIGNAL( appendBatch( const QList& , bool ) ), + SLOT( appendItems( const QList& , bool ) ), Qt::QueuedConnection ); + connect( worker, SIGNAL( finished() ), SLOT( workerFinished() ) ); + + emit numTracksChanged( rowCount() + worker->queries().length() ); + + qDebug() << Q_FUNC_INFO << "Starting worker. our thread" << thread(); + m_remoteUpdate = true; + worker->start(); +} + + +void +PlaylistModel::appendPlaylistEntries( const QList& entries, Tomahawk::collection_ptr collection ) +{ + qDebug() << "Adding entries:" << entries.length(); + m_busy = true; + + PlaylistModelWorker* worker = new PlaylistModelWorker( entries, this, collection ); + connect( worker, SIGNAL( appendBatch( const QList& , bool ) ), + SLOT( appendItems( const QList& , bool ) ), Qt::QueuedConnection ); + connect( worker, SIGNAL( finished() ), SLOT( workerFinished() ), Qt::QueuedConnection ); + + emit numTracksChanged( rowCount() + worker->entries().length() ); + + qDebug() << Q_FUNC_INFO << "Starting worker. our thread" << thread(); + m_remoteUpdate = true; + worker->start(); +} + + +void PlaylistModel::workerFinished() +{ + qDebug() << "Worker finished"; + + updateStats(); + m_remoteUpdate = false; + m_busy = false; + + emit layoutChanged(); +} + + +void +PlaylistModel::onTracksRemoved( const QList& tracks, collection_ptr collection ) +{ + // FIXME +} + + +QList const +PlaylistModel::playlistEntries() +{ + QList list; + + for ( int i = 0; i < rowCount(); i++ ) + { + QModelIndex idx = index( i, 0 ); + if ( !idx.isValid() ) + continue; + + PlaylistItem* item = indexToPlaylistItem( idx ); + if ( item ) + list << item->entry(); + } + + return list; +} + + +void +PlaylistModel::setCurrentItem( const QModelIndex& index ) +{ + PlaylistItem* item = indexToPlaylistItem( m_currentItem ); + if ( item && item->columns().count() ) + { + item->columns().at( 0 )->setIcon( QIcon( "" ) ); + } + + m_currentItem = index; + + item = indexToPlaylistItem( m_currentItem ); + if ( item && item->columns().count() ) + { + item->columns().at( 0 )->setIcon( m_nowPlayingIcon ); + } +} + + +PlaylistItem* +PlaylistModel::previousItem() +{ + return siblingItem( -1 ); +} + + +PlaylistItem* +PlaylistModel::nextItem() +{ + return siblingItem( 1 ); +} + + +PlaylistItem* +PlaylistModel::siblingItem( int itemsAway ) +{ + QModelIndex idx; + + if ( m_currentItem.isValid() ) + { + idx = m_currentItem; + idx = index( idx.row() + itemsAway, 0, idx.parent() ); + } + else + idx = index( 0, 0, idx ); + + // Try to find the next available PlaylistItem (with results) + do + { + if ( !idx.isValid() ) + break; + + + PlaylistItem* item = indexToPlaylistItem( idx ); + if ( item && item->query()->numResults() ) + { + qDebug() << "Next PlaylistItem found: " << item->query()->toString() << item->query()->results().at( 0 )->url(); + setCurrentItem( item->index() ); + return item; + } + + idx = index( idx.row() + ( itemsAway > 0 ? 1 : -1 ), 0, idx.parent() ); + } + while ( idx.isValid() ); + + setCurrentItem( QModelIndex() ); + return 0; +} + + +unsigned int +PlaylistModel::trackCount() +{ + return rowCount(); +} + + +Qt::DropActions +PlaylistModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + + +Qt::ItemFlags +PlaylistModel::flags( const QModelIndex& index ) const +{ + Qt::ItemFlags defaultFlags = QStandardItemModel::flags( index ); + + if ( index.isValid() ) + return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + + +QStringList +PlaylistModel::mimeTypes() const +{ + QStringList types; + types << "application/tomahawk.query.list"; + types << "application/tomahawk.plentry.list"; + return types; +} + + +QMimeData* +PlaylistModel::mimeData( const QModelIndexList &indexes ) const +{ + qDebug() << Q_FUNC_INFO; + + m_currentPlEntry = plentry_ptr(); + QByteArray queryData; + QByteArray itemData; + QDataStream queryStream( &queryData, QIODevice::WriteOnly ); + QDataStream itemStream( &itemData, QIODevice::WriteOnly ); + + foreach ( const QModelIndex& i, indexes ) + { + if ( i.column() > 0 ) + continue; + + QModelIndex idx = index( i.row(), 0, i.parent() ); + PlaylistItem* item = indexToPlaylistItem( idx ); + if ( item ) + { + if ( isPlaylistBacked() ) + { + const plentry_ptr& entry = item->entry(); + itemStream << qlonglong( &entry ); + + if ( m_currentItem == idx ) + m_currentPlEntry = entry; + } + + const query_ptr& query = item->query(); + queryStream << qlonglong( &query ); + } + } + + QMimeData* mimeData = new QMimeData(); + mimeData->setData( "application/tomahawk.query.list", queryData ); + if ( isPlaylistBacked() ) + mimeData->setData( "application/tomahawk.plentry.list", itemData ); + + return mimeData; +} + + +bool +PlaylistModel::dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent ) +{ + if ( action == Qt::IgnoreAction || m_readOnly ) + return true; + + if ( !data->hasFormat( "application/tomahawk.query.list" ) && !data->hasFormat( "application/tomahawk.plentry.list" ) ) + return false; + + int beginRow; + if ( row != -1 ) + beginRow = row; + else if ( parent.isValid() ) + beginRow = parent.row(); + else + beginRow = rowCount(); + + m_remoteUpdate = true; + if ( data->hasFormat( "application/tomahawk.plentry.list" ) ) + { + QByteArray itemData = data->data( "application/tomahawk.plentry.list" ); + QDataStream stream( &itemData, QIODevice::ReadOnly ); + QList entries; + + while ( !stream.atEnd() ) + { + qlonglong qptr; + stream >> qptr; + + Tomahawk::plentry_ptr* entry = reinterpret_cast(qptr); + if ( entry && !entry->isNull() ) + { + qDebug() << "Dropped playlist item:" << entry->data()->query()->artist() << "-" << entry->data()->query()->track(); + entries << *entry; + } + } + + foreach( const Tomahawk::plentry_ptr& entry, entries ) + { + PlaylistItem* item = new PlaylistItem( entry ); + invisibleRootItem()->insertRow( beginRow++, item->columns() ); + + if ( !m_currentPlEntry.isNull() && m_currentPlEntry->guid() == entry->guid() ) + { + setCurrentItem( item->index() ); + m_currentPlEntry = plentry_ptr(); + } + } + } + else + { + QByteArray itemData = data->data( "application/tomahawk.query.list" ); + QDataStream stream( &itemData, QIODevice::ReadOnly ); + QList queries; + + while ( !stream.atEnd() ) + { + qlonglong qptr; + stream >> qptr; + + Tomahawk::query_ptr* query = reinterpret_cast(qptr); + if ( query && !query->isNull() ) + { + qDebug() << "Dropped query item:" << query->data()->artist() << "-" << query->data()->track(); + queries << *query; + } + } + + foreach( const Tomahawk::query_ptr& query, queries ) + { + PlaylistItem* item = new PlaylistItem( query ); + invisibleRootItem()->insertRow( beginRow++, item->columns() ); + } + } + m_remoteUpdate = false; + + return true; +} + + +void +PlaylistModel::updateInternalPlaylist() +{ + if ( !isPlaylistBacked() ) + return; + + QList e = playlistEntries(); + foreach( const plentry_ptr& ple, e ) + { + qDebug() << "updateinternal:" << ple->query()->toString(); + } + + m_remoteUpdate = true; + setReadOnly( true ); + QString newrev = uuid(); + m_playlist->createNewRevision( newrev, m_playlist->currentrevision(), e ); +} + + +void +PlaylistModel::onRevisionLoaded( Tomahawk::PlaylistRevision revision ) +{ + if ( !m_remoteUpdate ) + { + m_remoteUpdate = true; + + foreach( const plentry_ptr& p, revision.newlist ) + { + qDebug() << "newlist:" << p->query()->toString(); + } + + foreach( const plentry_ptr& p, revision.added ) + { + qDebug() << "added:" << p->query()->toString(); + } + + foreach( const plentry_ptr& p, revision.removed ) + { + qDebug() << "removed:" << p->query()->toString(); + } + + qDebug() << rowCount() << revision.newlist.count(); + for ( int i = 0; i < revision.newlist.count(); i++ ) + { + if ( ( rowCount() - 1 ) < i ) + { + qDebug() << "item has been added"; + + QList items; + items << new PlaylistItem( revision.newlist.at( i ) ); + APP->pipeline()->add( items.at( 0 )->query() ); + + appendItems( items, collection_ptr( 0 ) ); + } + else + { + PlaylistItem* item = indexToPlaylistItem( index( i, 0 ) ); + qDebug() << "items:" << i << item->query()->toString() << revision.newlist.at( i )->query()->toString(); + + if ( item->entry()->guid() != revision.newlist.at( i )->guid() ) + { + if ( revision.added.contains( revision.newlist.at( i ) ) ) + { + qDebug() << "item has been added"; + + QList items; + items << new PlaylistItem( revision.newlist.at( i ) ); + + insertItems( i, items, collection_ptr( 0 ) ); + } + // FIXME: add additional check for removed items, iterating over + // FIXME: revision.removed only, instead of the entire list as we do below + else + { + qDebug() << "item seems to have moved in the list"; + QString guid = item->entry()->guid(); + QList li = takeRow( i ); + + int x = 0; + foreach( const plentry_ptr& nlp, revision.newlist ) + { + if ( guid == nlp->guid() ) + break; + x++; + } + + if ( x < revision.newlist.count() ) + invisibleRootItem()->insertRow( x, li ); + else + delete item; + + i--; + } + } + } + } + } + m_remoteUpdate = false; + emit layoutChanged(); + + if ( isPlaylistBacked() && m_playlist->author()->isLocal() ) + setReadOnly( false ); +} + + +void +PlaylistModel::onRowsAboutToBeRemoved( const QModelIndex& parent, int start, int end ) +{ +// qDebug() << Q_FUNC_INFO << start << end; + + if ( m_remoteUpdate ) + return; + + for ( int i = start; i <= end; i++ ) + { + PlaylistItem* item = indexToPlaylistItem( index( i, 0, parent ) ); + if ( item ) + delete item; + } +} + + +void +PlaylistModel::onRowsInserted( const QModelIndex& parent, int start, int end ) +{ +// qDebug() << Q_FUNC_INFO << start << end; + + if ( m_remoteUpdate ) + return; + + if ( isPlaylistBacked() ) + updateInternalPlaylist(); +} + + +void +PlaylistModel::onRowsRemoved( const QModelIndex& parent, int start, int end ) +{ +// qDebug() << Q_FUNC_INFO << start << end; + + if ( m_remoteUpdate ) + return; + + if ( isPlaylistBacked() ) + updateInternalPlaylist(); +} + + +int +PlaylistModel::indexType( const QModelIndex& index ) +{ + if ( !index.isValid() ) + return -1; + + QModelIndex idx = index.model()->index( index.row(), 0, index.parent() ); + return idx.data( Qt::UserRole + 1 ).toInt(); +} + + +PlaylistItem* +PlaylistModel::indexToPlaylistItem( const QModelIndex& index ) +{ + if ( !index.isValid() ) + return 0; + + QModelIndex idx = index.model()->index( index.row(), 0, index.parent() ); + int type = idx.data( Qt::UserRole + 1 ).toInt(); + if ( type == 0 ) + { + qlonglong pptr = idx.data( Qt::UserRole ).toLongLong(); + PlaylistItem* item = reinterpret_cast(pptr); + if ( item ) + return item; + } + + return 0; +} diff --git a/src/playlist/playlistmodel.h b/src/playlist/playlistmodel.h new file mode 100644 index 000000000..ff9bf4095 --- /dev/null +++ b/src/playlist/playlistmodel.h @@ -0,0 +1,121 @@ +#ifndef PLAYLISTMODEL_H +#define PLAYLISTMODEL_H + +#include +#include +#include +#include + +#include "playlistitem.h" +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/collection.h" +#include "tomahawk/query.h" +#include "tomahawk/typedefs.h" +#include "tomahawk/playlist.h" +#include "tomahawk/playlistmodelinterface.h" + +class QMetaData; + +class PlaylistModel : public QStandardItemModel, public PlaylistModelInterface +{ +Q_OBJECT +friend class PlaylistModelWorker; + +public: + explicit PlaylistModel( QObject* parent = 0 ); + + virtual QMimeData* mimeData( const QModelIndexList& indexes ) const; + virtual QStringList mimeTypes() const; + virtual Qt::DropActions supportedDropActions() const; + virtual Qt::ItemFlags flags( const QModelIndex& index ) const; + virtual bool dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent ); + + bool isReadOnly() const { return m_readOnly; } + bool isBusy() const { return m_busy; } + + static int indexType( const QModelIndex& index ); + static PlaylistItem* indexToPlaylistItem( const QModelIndex& index ); + + bool appendItem( Tomahawk::query_ptr query, bool emitsig = true ); + bool insertItems( int pos, const QList& items, bool emitsig = true ); + void removeItems( const QList& items, bool emitsig = true, bool animated = false ); + + virtual void loadPlaylist( const Tomahawk::playlist_ptr& playlist ); + bool isPlaylistBacked() const { return !m_playlist.isNull(); } + const Tomahawk::playlist_ptr& playlist() const { return m_playlist; } + QList const playlistEntries(); + + void addCollection( const Tomahawk::collection_ptr& collection ); + void removeCollection( const Tomahawk::collection_ptr& collection ); + + virtual void addSource( const Tomahawk::source_ptr& source ); + virtual void removeSource( const Tomahawk::source_ptr& source ); + + virtual void setCurrentItem( const QModelIndex& index ); + virtual PlaylistItem* previousItem(); + virtual PlaylistItem* nextItem(); + virtual PlaylistItem* siblingItem( int itemsAway ); + + virtual unsigned int sourceCount() { return m_sourceCount; } + virtual unsigned int collectionCount() { return m_collectionCount; } + virtual unsigned int trackCount(); + virtual unsigned int artistCount() { return m_artists.size(); } + void updateStats(); + + QPersistentModelIndex currentItem() const { return m_currentItem; } + +public slots: + void setReadOnly( bool readOnly ) { m_readOnly = readOnly; } + virtual void setRepeatMode( RepeatMode mode ) { qWarning() << Q_FUNC_INFO << "This should never get called directly!"; } + virtual void setShuffled( bool enabled ) { qWarning() << Q_FUNC_INFO << "This should never get called directly!"; } + + bool appendItems( const QList& items, bool emitsig = true ); + void onTracksAdded( const QList& tracks, Tomahawk::collection_ptr collection ); + void onTracksRemoved( const QList& tracks, Tomahawk::collection_ptr collection ); + +signals: + //void sourceCountChanged( unsigned int counter ); + void collectionCountChanged( unsigned int counter ); + //void trackCountChanged( unsigned int counter ); + + void numSourcesChanged( unsigned int i ); + void numTracksChanged( unsigned int i ); + void numArtistsChanged( unsigned int i ); + void numShownChanged( unsigned int i ); + + void repeatModeChanged( PlaylistModelInterface::RepeatMode mode ); + void shuffleModeChanged( bool enabled ); + +private slots: + void workerFinished(); + void updateInternalPlaylist(); + void onRevisionLoaded( Tomahawk::PlaylistRevision revision ); + + void onRowsAboutToBeRemoved( const QModelIndex& parent, int start, int end ); + void onRowsInserted( const QModelIndex& parent, int start, int end ); + void onRowsRemoved( const QModelIndex& parent, int start, int end ); + + void onRowRemoverFinished() { m_busy = false; } + +private: + void setupHeaders(); + void appendTracks( const QList& queries, Tomahawk::collection_ptr collection ); + void appendPlaylistEntries( const QList& entries, Tomahawk::collection_ptr collection ); + + Tomahawk::playlist_ptr m_playlist; + + unsigned int m_collectionCount; + unsigned int m_sourceCount; + + bool m_remoteUpdate; + bool m_readOnly; + QIcon m_nowPlayingIcon; + + mutable Tomahawk::plentry_ptr m_currentPlEntry; + QPersistentModelIndex m_currentItem; + QMap m_artists; + unsigned int m_sources; + bool m_busy; +}; + +#endif // PLAYLISTMODEL_H diff --git a/src/playlist/playlistmodelworker.cpp b/src/playlist/playlistmodelworker.cpp new file mode 100644 index 000000000..c887dd213 --- /dev/null +++ b/src/playlist/playlistmodelworker.cpp @@ -0,0 +1,90 @@ +#include "playlistmodelworker.h" + +#include "playlistmodel.h" +#include "playlistitem.h" +#include "databasecollection.h" +#include "tomahawk/collection.h" + +using namespace Tomahawk; + + +void +PlaylistModelWorker::run() +{ + qDebug() << Q_FUNC_INFO << thread(); + QTimer::singleShot( 0, this, SLOT( go() ) ); + exec(); +} + + +void +PlaylistModelWorker::go() +{ + qDebug() << Q_FUNC_INFO << thread(); + + using namespace Tomahawk; + QList items; + foreach( const query_ptr& query, m_queries ) + { + if ( !m_collection.isNull() ) + { + // FIXME: needs merging + // Manually add a result, since it's coming from the local collection + QVariantMap t = query->toVariant().toMap(); + t["score"] = 1.0; + QList results; + result_ptr result = result_ptr( new Result( t, m_collection ) ); + results << result; + query->addResults( results ); + } + + items << new PlaylistItem( query ); + + if ( m_model->m_artists.contains( query->artist() ) ) + m_model->m_artists[query->artist()]++; + else + m_model->m_artists.insert( query->artist(), 1 ); + + if ( items.length() % 100 == 0 ) + { + emit appendBatch( items, false ); + items.clear(); + } + } + + foreach( const plentry_ptr& entry, m_entries ) + { + if ( !m_collection.isNull() ) + { + // FIXME: needs merging + // Manually add a result, since it's coming from the local collection + QVariantMap t = entry->query()->toVariant().toMap(); + t["score"] = 1.0; + QList results; + result_ptr result = result_ptr( new Result( t, m_collection ) ); + results << result; + entry->query()->addResults( results ); + } + + items << new PlaylistItem( entry ); + + if ( m_model->m_artists.contains( entry->query()->artist() ) ) + m_model->m_artists[entry->query()->artist()]++; + else + m_model->m_artists.insert( entry->query()->artist(), 1 ); + + if ( items.length() % 400 == 0 ) + { + emit appendBatch( items, false ); + items.clear(); + } + } + + if ( items.length() > 0 ) + { + emit appendBatch( items, false ); + items.clear(); + } + + exit( 0 ); +} diff --git a/src/playlist/playlistmodelworker.h b/src/playlist/playlistmodelworker.h new file mode 100644 index 000000000..e9a9ead13 --- /dev/null +++ b/src/playlist/playlistmodelworker.h @@ -0,0 +1,49 @@ +#ifndef PLAYLISTMODELWORKER_H +#define PLAYLISTMODELWORKER_H + +#include + +#include "tomahawk/collection.h" +#include "tomahawk/typedefs.h" + +class PlaylistItem; +class PlaylistModel; + +class PlaylistModelWorker : public QThread +{ +Q_OBJECT + +public: + explicit PlaylistModelWorker( const QList& q, PlaylistModel* m, Tomahawk::collection_ptr c ) + : QThread(), m_queries( q ), m_model( m ), m_collection( c ) + { + moveToThread( this ); + } + + explicit PlaylistModelWorker( const QList& e, PlaylistModel* m, Tomahawk::collection_ptr c ) + : QThread(), m_entries( e ), m_model( m ), m_collection( c ) + { + moveToThread( this ); + } + + virtual void run(); + +public slots: + void go(); + + const QList& queries() { return m_queries; } + const QList& entries() { return m_entries; } + PlaylistModel* model() {return m_model; } + Tomahawk::collection_ptr collection() { return m_collection; } + +signals: + void appendBatch( const QList& items, bool emitsig ); + +private: + const QList m_queries; + const QList m_entries; + PlaylistModel* m_model; + Tomahawk::collection_ptr m_collection; +}; + +#endif // PLAYLISTMODELWORKER_H diff --git a/src/playlist/playlistproxymodel.cpp b/src/playlist/playlistproxymodel.cpp new file mode 100644 index 000000000..4613a4f8d --- /dev/null +++ b/src/playlist/playlistproxymodel.cpp @@ -0,0 +1,294 @@ +#include "playlistproxymodel.h" + +#include +#include + +#include "tomahawk/query.h" + + +PlaylistProxyModel::PlaylistProxyModel( QObject* parent ) + : QSortFilterProxyModel( parent ) + , m_model( 0 ) + , m_repeatMode( PlaylistModelInterface::NoRepeat ) + , m_shuffled( false ) +{ + qsrand( QTime( 0, 0, 0 ).secsTo( QTime::currentTime() ) ); + + setFilterCaseSensitivity( Qt::CaseInsensitive ); + setSortCaseSensitivity( Qt::CaseInsensitive ); + setDynamicSortFilter( true ); + + setSourceModel( 0 ); +} + + +void +PlaylistProxyModel::setSourceModel( PlaylistModel* sourceModel ) +{ + if ( m_model ) + { + disconnect( m_model, SIGNAL( numSourcesChanged( unsigned int ) ), + this, SIGNAL( numSourcesChanged( unsigned int ) ) ); + + disconnect( m_model, SIGNAL( numTracksChanged( unsigned int ) ), + this, SIGNAL( numTracksChanged( unsigned int ) ) ); + + disconnect( m_model, SIGNAL( numArtistsChanged( unsigned int ) ), + this, SIGNAL( numArtistsChanged( unsigned int ) ) ); + + disconnect( m_model, SIGNAL( numShownChanged( unsigned int ) ), + this, SIGNAL( numShownChanged( unsigned int ) ) ); + } + + ((QTreeView*)parent())->setSortingEnabled( false ); + ((QTreeView*)parent())->sortByColumn( -1 ); + + m_model = sourceModel; + QSortFilterProxyModel::setSourceModel( sourceModel ); + + if ( m_model && !m_model->isPlaylistBacked() ) + { + ((QTreeView*)parent())->setSortingEnabled( true ); + ((QTreeView*)parent())->sortByColumn( 0, Qt::AscendingOrder ); + } + + if ( m_model ) + { + connect( m_model, SIGNAL( numSourcesChanged( unsigned int ) ), + SIGNAL( numSourcesChanged( unsigned int ) ) ); + + connect( m_model, SIGNAL( numTracksChanged( unsigned int ) ), + SIGNAL( numTracksChanged( unsigned int ) ) ); + + connect( m_model, SIGNAL( numArtistsChanged( unsigned int ) ), + SIGNAL( numArtistsChanged( unsigned int ) ) ); + + connect( m_model, SIGNAL( numShownChanged( unsigned int ) ), + SIGNAL( numShownChanged( unsigned int ) ) ); + + m_model->updateStats(); + } +} + + +void +PlaylistProxyModel::setFilterRegExp( const QString& pattern ) +{ + qDebug() << Q_FUNC_INFO; + QSortFilterProxyModel::setFilterRegExp( pattern ); + emit numShownChanged( rowCount() ); +} + + +void +PlaylistProxyModel::setCurrentItem( const QModelIndex& index ) +{ + qDebug() << Q_FUNC_INFO; + return m_model->setCurrentItem( index ); +} + + +PlaylistItem* +PlaylistProxyModel::previousItem() +{ + return siblingItem( -1 ); +} + + +PlaylistItem* +PlaylistProxyModel::nextItem() +{ + return siblingItem( 1 ); +} + + +PlaylistItem* +PlaylistProxyModel::siblingItem( int itemsAway ) +{ + qDebug() << Q_FUNC_INFO; + + QModelIndex idx = index( 0, 0 ); + + if( rowCount() ) + { + if ( m_shuffled ) + { + // random mode is enabled + // TODO come up with a clever random logic, that keeps track of previously played items + idx = index( qrand() % rowCount(), 0 ); + } + else if ( currentItem().isValid() ) + { + idx = currentItem(); + + // random mode is disabled + if ( m_repeatMode == PlaylistModelInterface::RepeatOne ) + { + // repeat one track + idx = index( idx.row(), 0 ); + } + else + { + // keep progressing through the playlist normally + idx = index( idx.row() + itemsAway, 0 ); + } + } + } + + if ( !idx.isValid() && m_repeatMode == PlaylistModelInterface::RepeatAll ) + { + // repeat all tracks + if ( itemsAway > 0 ) + { + // reset to first item + idx = index( 0, 0 ); + } + else + { + // reset to last item + idx = index( rowCount() - 1, 0 ); + } + } + + // Try to find the next available PlaylistItem (with results) + if ( idx.isValid() ) do + { + PlaylistItem* item = PlaylistModel::indexToPlaylistItem( idx ); + if ( item && item->query()->numResults() ) + { + qDebug() << "Next PlaylistItem found: " << item->query()->toString() << item->query()->results().at( 0 )->url(); + setCurrentItem( item->index() ); + return item; + } + + idx = index( idx.row() + ( itemsAway > 0 ? 1 : -1 ), 0 ); + } + while ( idx.isValid() ); + + setCurrentItem( QModelIndex() ); + return 0; +} + + +bool +PlaylistProxyModel::filterAcceptsRow( int sourceRow, const QModelIndex& sourceParent ) const +{ + if ( filterRegExp().isEmpty() ) + return true; + + PlaylistItem* pi = PlaylistModel::indexToPlaylistItem( sourceModel()->index( sourceRow, 0, sourceParent ) ); + if ( !pi ) + return false; + + const Tomahawk::query_ptr& q = pi->query(); + Tomahawk::result_ptr r; + if ( q->numResults() ) + r = q->results().at( 0 ); + + QStringList sl = filterRegExp().pattern().split( " ", QString::SkipEmptyParts ); + bool found = true; + + foreach( const QString& s, sl ) + { + if ( !r.isNull() ) + { + if ( !r->artist().contains( s, Qt::CaseInsensitive ) && + !r->album() .contains( s, Qt::CaseInsensitive ) && + !r->track() .contains( s, Qt::CaseInsensitive ) ) + { + found = false; + } + } + else + { + if ( !q->artist().contains( s, Qt::CaseInsensitive ) && + !q->album() .contains( s, Qt::CaseInsensitive ) && + !q->track() .contains( s, Qt::CaseInsensitive ) ) + { + found = false; + } + } + } + + return found; +} + + +bool +PlaylistProxyModel::lessThan( const QModelIndex& left, const QModelIndex& right ) const +{ + PlaylistItem* p1 = PlaylistModel::indexToPlaylistItem( left ); + PlaylistItem* p2 = PlaylistModel::indexToPlaylistItem( right ); + + if ( !p1 ) + return true; + if ( !p2 ) + return false; + + const Tomahawk::query_ptr& q1 = p1->query(); + const Tomahawk::query_ptr& q2 = p2->query(); + + QString artist1 = q1->artist(); + QString artist2 = q2->artist(); + QString album1 = q1->album(); + QString album2 = q2->album(); + QString track1 = q1->track(); + QString track2 = q2->track(); + unsigned int albumpos1 = 0, albumpos2 = 0; + unsigned int bitrate1 = 0, bitrate2 = 0; + + if ( q1->numResults() ) + { + const Tomahawk::result_ptr& r = q1->results().at( 0 ); + artist1 = r->artist(); + album1 = r->album(); + track1 = r->track(); + albumpos1 = r->albumpos(); + bitrate1 = r->bitrate(); + } + if ( q2->numResults() ) + { + const Tomahawk::result_ptr& r = q2->results().at( 0 ); + artist2 = r->artist(); + album2 = r->album(); + track2 = r->track(); + albumpos2 = r->albumpos(); + bitrate2 = r->bitrate(); + } + + if ( left.column() == 0 ) // sort by artist + { + if ( artist1 == artist2 ) + { + if ( album1 == album2 ) + { + if ( albumpos1 == albumpos2 ) + { + return QString::localeAwareCompare( track1, track2 ) < 0; + } + + return albumpos1 < albumpos2; + } + + return QString::localeAwareCompare( album1, album2 ) < 0; + } + + return QString::localeAwareCompare( artist1, artist2 ) < 0; + } + else if ( left.column() == 2 ) // sort by album + { + if ( album1 == album2 ) + { + return albumpos1 < albumpos2; + } + + return QString::localeAwareCompare( album1, album2 ) < 0; + } + else if ( left.column() == 4 ) // sort by bitrate + { + return bitrate1 < bitrate2; + } + + return QString::localeAwareCompare( sourceModel()->data( left ).toString(), + sourceModel()->data( right ).toString() ) < 0; +} diff --git a/src/playlist/playlistproxymodel.h b/src/playlist/playlistproxymodel.h new file mode 100644 index 000000000..44bd8b85f --- /dev/null +++ b/src/playlist/playlistproxymodel.h @@ -0,0 +1,58 @@ +#ifndef PLAYLISTPROXYMODEL_H +#define PLAYLISTPROXYMODEL_H + +#include + +#include "tomahawk/playlistmodelinterface.h" +#include "playlistmodel.h" + +class PlaylistProxyModel : public QSortFilterProxyModel, public PlaylistModelInterface +{ +Q_OBJECT + +public: + explicit PlaylistProxyModel( QObject* parent = 0 ); + + virtual void setSourceModel( PlaylistModel* sourceModel ); + + virtual void addSource( const Tomahawk::source_ptr& source ) { m_model->addSource( source ); } + virtual void removeSource( const Tomahawk::source_ptr& source ) { m_model->removeSource( source ); } + + virtual void setCurrentItem( const QModelIndex& index ); + virtual PlaylistItem* previousItem(); + virtual PlaylistItem* nextItem(); + virtual PlaylistItem* siblingItem( int itemsAway ); + + virtual unsigned int sourceCount() { return m_model->sourceCount(); } + virtual unsigned int collectionCount() { return m_model->collectionCount(); } + virtual unsigned int trackCount() { return rowCount(); } + virtual unsigned int artistCount() { return 0; } // FIXME + + void setFilterRegExp( const QString& pattern ); + + QPersistentModelIndex currentItem() const { return mapFromSource( m_model->currentItem() ); } + +signals: + void repeatModeChanged( PlaylistModelInterface::RepeatMode mode ); + void shuffleModeChanged( bool enabled ); + + void numSourcesChanged( unsigned int i ); + void numTracksChanged( unsigned int i ); + void numArtistsChanged( unsigned int i ); + void numShownChanged( unsigned int i ); + +public slots: + virtual void setRepeatMode( RepeatMode mode ) { m_repeatMode = mode; emit repeatModeChanged( mode ); } + virtual void setShuffled( bool enabled ) { m_shuffled = enabled; emit shuffleModeChanged( enabled ); } + +protected: + bool filterAcceptsRow( int sourceRow, const QModelIndex& sourceParent ) const; + bool lessThan( const QModelIndex& left, const QModelIndex& right ) const; + +private: + PlaylistModel* m_model; + RepeatMode m_repeatMode; + bool m_shuffled; +}; + +#endif // PLAYLISTPROXYMODEL_H diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp new file mode 100644 index 000000000..642320f17 --- /dev/null +++ b/src/playlist/playlistview.cpp @@ -0,0 +1,379 @@ +#include "playlistview.h" + +#include +#include +#include +#include +#include + +#include "tomahawk/tomahawkapp.h" +#include "audioengine.h" +#include "playlistproxymodel.h" + +#define PLAYLISTVIEW_FILTER_TIMEOUT 250 +#include + +using namespace Tomahawk; + + +PlaylistView::PlaylistView( QWidget* parent ) + : ProgressTreeView( parent ) + , m_proxyModel( new PlaylistProxyModel( this ) ) + , m_model( 0 ) + , m_delegate( new PlaylistItemDelegate( this ) ) + , m_resizing( false ) +{ + setRootIsDecorated( false ); + setSortingEnabled( false ); + setAlternatingRowColors( true ); + setUniformRowHeights( true ); + setDragEnabled( true ); + setAcceptDrops( true ); + setMouseTracking( true ); + setSelectionMode( QAbstractItemView::ExtendedSelection ); + setSelectionBehavior( QAbstractItemView::SelectRows ); + setDropIndicatorShown( false ); + setDragDropMode( QAbstractItemView::InternalMove ); + setDragDropOverwriteMode ( false ); + setAllColumnsShowFocus( true ); + + setItemDelegate( m_delegate ); + + header()->setMinimumSectionSize( 60 ); + restoreColumnsState(); + + connect( this, SIGNAL( activated( QModelIndex ) ), SLOT( onItemActivated( QModelIndex ) ) ); + + connect( header(), SIGNAL( sectionResized( int, int, int ) ), SLOT( onSectionResized( int, int, int ) ) ); + + connect( APP->audioEngine(), SIGNAL( stopped() ), SLOT( onAudioStopped() ), Qt::QueuedConnection ); + + connect( this, SIGNAL( itemStarted( PlaylistModelInterface*, PlaylistItem* ) ), + APP->audioEngine(), SLOT( playItem( PlaylistModelInterface*, PlaylistItem* ) ), Qt::QueuedConnection ); + + connect( this, SIGNAL( playlistModelChanged( PlaylistModelInterface* ) ), + APP->audioEngine(), SLOT( onPlaylistActivated( PlaylistModelInterface* ) ), Qt::QueuedConnection ); + + connect( m_proxyModel, SIGNAL( numSourcesChanged( unsigned int ) ), + SIGNAL( numSourcesChanged( unsigned int ) ) ); + + connect( m_proxyModel, SIGNAL( numTracksChanged( unsigned int ) ), + SIGNAL( numTracksChanged( unsigned int ) ) ); + + connect( m_proxyModel, SIGNAL( numArtistsChanged( unsigned int ) ), + SIGNAL( numArtistsChanged( unsigned int ) ) ); + + connect( m_proxyModel, SIGNAL( numShownChanged( unsigned int ) ), + SIGNAL( numShownChanged( unsigned int ) ) ); + + connect( &m_filterTimer, SIGNAL( timeout() ), SLOT( applyFilter() ) ); + + QTreeView::setModel( m_proxyModel ); +} + + +PlaylistView::~PlaylistView() +{ + qDebug() << Q_FUNC_INFO; + + saveColumnsState(); +} + + +void +PlaylistView::setModel( PlaylistModel* model ) +{ + m_model = model; + m_model->setParent( m_proxyModel ); + m_proxyModel->setSourceModel( m_model ); + + emit playlistModelChanged( m_proxyModel ); +} + + +void +PlaylistView::restoreColumnsState() +{ + TomahawkSettings* s = APP->settings(); + QList list = s->playlistColumnSizes(); + + if ( list.count() != 6 ) // FIXME: const + { + m_columnWeights << 0.22 << 0.29 << 0.19 << 0.08 << 0.08 << 0.14; + } + else + { + foreach( const QVariant& v, list ) + m_columnWeights << v.toDouble(); + } + + for ( int i = 0; i < m_columnWeights.count(); i++ ) + m_columnWidths << 0; +} + + +void +PlaylistView::saveColumnsState() +{ + TomahawkSettings *s = APP->settings(); + QList wlist; +// int i = 0; + + foreach( double w, m_columnWeights ) + { + wlist << QVariant( w ); +// qDebug() << "Storing weight for column" << i++ << w; + } + + s->setPlaylistColumnSizes( wlist ); +} + + +void +PlaylistView::onSectionResized( int logicalIndex, int oldSize, int newSize ) +{ + return; +} + + +void +PlaylistView::setFilter( const QString& filter ) +{ + m_filter = filter; + + m_filterTimer.stop(); + m_filterTimer.setInterval( PLAYLISTVIEW_FILTER_TIMEOUT ); + m_filterTimer.setSingleShot( true ); + m_filterTimer.start(); +} + + +void +PlaylistView::applyFilter() +{ + qDebug() << Q_FUNC_INFO; + m_proxyModel->setFilterRegExp( m_filter ); + + if ( selectedIndexes().count() ) + scrollTo( selectedIndexes().at( 0 ) ); +} + + +void +PlaylistView::onItemActivated( const QModelIndex& index ) +{ + PlaylistItem* item = PlaylistModel::indexToPlaylistItem( index ); + if ( item && item->query()->numResults() ) + { + qDebug() << "PlaylistItem activated:" << item->query()->toString() << item->query()->results().at( 0 )->url(); + m_proxyModel->setCurrentItem( item->index() ); + emit itemStarted( m_proxyModel, item ); + } +} + + +void +PlaylistView::onAudioStopped() +{ + m_proxyModel->setCurrentItem( QModelIndex() ); +} + + +void +PlaylistView::addSource( const source_ptr& source ) +{ + qDebug() << "Adding source to PlaylistView:" << source->userName(); + + m_proxyModel->addSource( source ); + emit playlistModelChanged( m_proxyModel ); + //emit sourceAdded( source ); +} + + +void +PlaylistView::removeSource( const source_ptr& source ) +{ + qDebug() << "Removing source from PlaylistView:" << source->userName(); + + m_proxyModel->removeSource( source ); + emit playlistModelChanged( m_proxyModel ); + //emit sourceRemoved( source ); +} + + +void +PlaylistView::resizeEvent( QResizeEvent* event ) +{ +// qDebug() << Q_FUNC_INFO; + resizeColumns(); +} + + +void +PlaylistView::resizeColumns() +{ + double cw = contentsRect().width(); + int i = 0; + int total = 0; + + if ( verticalScrollBar() && verticalScrollBar()->isVisible() ) + { + cw -= verticalScrollBar()->width() + 1; + } + + m_resizing = true; + foreach( double w, m_columnWeights ) + { + int fw = (int)( cw * w ); + if ( fw < header()->minimumSectionSize() ) + fw = header()->minimumSectionSize(); + + if ( i + 1 == header()->count() ) + fw = cw - total; + + total += fw; +// qDebug() << "Resizing column:" << i << fw; + + m_columnWidths[ i ] = fw; + header()->resizeSection( i++, fw ); + } + m_resizing = false; +} + + +void +PlaylistView::keyPressEvent( QKeyEvent* event ) +{ +// qDebug() << Q_FUNC_INFO; + QTreeView::keyPressEvent( event ); + + if ( !m_model ) + return; + + if ( event->key() == Qt::Key_Delete ) + { + if ( m_model->isPlaylistBacked() && selectedIndexes().count() ) + { + qDebug() << "Removing selected items"; + QList items; + + QModelIndexList sidxs = selectedIndexes(); + foreach( const QModelIndex& idx, sidxs ) + { + if ( idx.column() > 0 ) + continue; + + PlaylistItem* item = PlaylistModel::indexToPlaylistItem( idx ); + if ( item ) + items << item; + } + + m_model->removeItems( items ); + } + } +} + + +void +PlaylistView::dragEnterEvent( QDragEnterEvent* event ) +{ + qDebug() << Q_FUNC_INFO; + QTreeView::dragEnterEvent( event ); + + if ( event->mimeData()->hasFormat( "application/tomahawk.query.list" ) || event->mimeData()->hasFormat( "application/tomahawk.plentry.list" ) ) + { + m_dragging = true; + m_dropRect = QRect(); + + qDebug() << "Accepting Drag Event"; + event->acceptProposedAction(); + } +} + + +void +PlaylistView::dragMoveEvent( QDragMoveEvent* event ) +{ + QTreeView::dragMoveEvent( event ); + + if ( m_model->isReadOnly() ) + { + event->ignore(); + return; + } + + if ( event->mimeData()->hasFormat( "application/tomahawk.query.list" ) || event->mimeData()->hasFormat( "application/tomahawk.plentry.list" ) ) + { + setDirtyRegion( m_dropRect ); + const QPoint pos = event->pos(); + const QModelIndex index = indexAt( pos ); + + if ( index.isValid() ) + { + const QRect rect = visualRect( index ); + m_dropRect = rect; + + // indicate that the item will be inserted above the current place + const int gap = 5; // FIXME constant + m_dropRect = QRect( rect.left(), rect.top() - gap / 2, rect.width(), gap ); + + event->acceptProposedAction(); + } + + setDirtyRegion( m_dropRect ); + } +} + + +void +PlaylistView::dropEvent( QDropEvent* event ) +{ +/* const QPoint pos = event->pos(); + const QModelIndex index = indexAt( pos ); + + if ( event->mimeData()->hasFormat( "application/tomahawk.query.list" ) ) + { + const QPoint pos = event->pos(); + const QModelIndex index = indexAt( pos ); + + if ( index.isValid() ) + { + event->acceptProposedAction(); + } + }*/ + + QTreeView::dropEvent( event ); + m_dragging = false; +} + + +void +PlaylistView::paintEvent( QPaintEvent* event ) +{ + QTreeView::paintEvent( event ); + + if ( m_dragging ) + { + // draw drop indicator + QPainter painter( viewport() ); + { + // draw indicator for inserting items + QBrush blendedBrush = viewOptions().palette.brush( QPalette::Normal, QPalette::Highlight ); + QColor color = blendedBrush.color(); + + const int y = ( m_dropRect.top() + m_dropRect.bottom() ) / 2; + const int thickness = m_dropRect.height() / 2; + + int alpha = 255; + const int alphaDec = alpha / ( thickness + 1 ); + for ( int i = 0; i < thickness; i++ ) + { + color.setAlpha( alpha ); + alpha -= alphaDec; + painter.setPen( color ); + painter.drawLine( 0, y - i, width(), y - i ); + painter.drawLine( 0, y + i, width(), y + i ); + } + } + } +} diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h new file mode 100644 index 000000000..e6a81d98e --- /dev/null +++ b/src/playlist/playlistview.h @@ -0,0 +1,84 @@ +#ifndef PLAYLISTVIEW_H +#define PLAYLISTVIEW_H + +#include +#include +#include + +#include "tomahawk/source.h" +#include "tomahawk/playlistmodelinterface.h" +#include "progresstreeview.h" +#include "playlistitem.h" +#include "playlistitemdelegate.h" + +class PlaylistModel; +class PlaylistProxyModel; + +class PlaylistView : public ProgressTreeView +{ +Q_OBJECT + +public: + explicit PlaylistView( QWidget* parent = 0 ); + ~PlaylistView(); + + PlaylistProxyModel* model() { return m_proxyModel; } + PlaylistModel* playlistModel() { return m_model; } + PlaylistItemDelegate* delegate() { return m_delegate; } + + void setModel( PlaylistModel* model ); + +signals: + void itemStarted( PlaylistModelInterface* model, PlaylistItem* item ); + void playlistModelChanged( PlaylistModelInterface* model ); + + void numSourcesChanged( unsigned int i ); + void numTracksChanged( unsigned int i ); + void numArtistsChanged( unsigned int i ); + void numShownChanged( unsigned int i ); + +public slots: + void addSource( const Tomahawk::source_ptr& source ); + void removeSource( const Tomahawk::source_ptr& source ); + + void setFilter( const QString& filter ); + +protected: + virtual void resizeEvent( QResizeEvent* event ); + virtual void keyPressEvent( QKeyEvent* event ); + + virtual void dragEnterEvent( QDragEnterEvent* event ); + virtual void dragLeaveEvent( QDragLeaveEvent* event ) { m_dragging = false; setDirtyRegion( m_dropRect ); } + virtual void dragMoveEvent( QDragMoveEvent* event ); + virtual void dropEvent( QDropEvent* event ); + + void paintEvent( QPaintEvent* event ); + +private slots: + void onItemActivated( const QModelIndex& index ); + void onAudioStopped(); + void applyFilter(); + + void resizeColumns(); + void onSectionResized( int logicalIndex, int oldSize, int newSize ); + +private: + void restoreColumnsState(); + void saveColumnsState(); + + PlaylistProxyModel* m_proxyModel; + PlaylistModel* m_model; + PlaylistItemDelegate* m_delegate; + + QTimer m_filterTimer; + QString m_filter; + + QList m_columnWeights; + QList m_columnWidths; + + bool m_resizing; + bool m_dragging; + QRect m_dropRect; +}; + +#endif // PLAYLISTVIEW_H diff --git a/src/pluginapi.cpp b/src/pluginapi.cpp new file mode 100644 index 000000000..0649c6fb4 --- /dev/null +++ b/src/pluginapi.cpp @@ -0,0 +1,43 @@ +#include "tomahawk/pluginapi.h" +#include "tomahawk/tomahawkapp.h" + +using namespace Tomahawk; + +PluginAPI::PluginAPI( Pipeline* p ) + : m_pipeline( p ) +{ +} + + +/*void +PluginAPI::reportResults( const QString& qid, const QList& vresults ) +{ + QList< result_ptr > results; + foreach( QVariantMap m, vresults ) + { + result_ptr rp( new Result( m ) ); + results.append( rp ); + } + m_pipeline->reportResults( QID( qid ), results ); +}*/ + + +void +PluginAPI::addSource( source_ptr s ) +{ + TomahawkApp::instance()->sourcelist().add( s ); +} + + +void +PluginAPI::removeSource( source_ptr s ) +{ + TomahawkApp::instance()->sourcelist().remove( s ); +} + + +void +PluginAPI::addResolver( Resolver* r ) +{ + TomahawkApp::instance()->pipeline()->addResolver( r ); +} diff --git a/src/plugins/fake/CMakeLists.txt b/src/plugins/fake/CMakeLists.txt new file mode 100644 index 000000000..8fe1f24bf --- /dev/null +++ b/src/plugins/fake/CMakeLists.txt @@ -0,0 +1,45 @@ +project( tomahawk ) +cmake_minimum_required(VERSION 2.6) +find_package( Qt4 REQUIRED ) + +include( ${QT_USE_FILE} ) + + +SET(TOMAHAWK_INC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../include/") + +SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${TOMAHAWK_INC_DIR}/..") + +set(cpps + fakeplugin.cpp + fakecollection.cpp +) + +set(hs +fakeplugin.h +fakecollection.h +) + +include_directories( + . + .. + ${TOMAHAWK_INC_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${QT_INCLUDE_DIR} +) + +qt4_wrap_cpp( mocs ${hs} ) + +ADD_DEFINITIONS(${QT_DEFINITIONS}) +ADD_DEFINITIONS(-DQT_PLUGIN) +#ADD_DEFINITIONS(-DQT_NO_DEBUG) +ADD_DEFINITIONS(-DQT_SHARED) + +add_library(fake SHARED + ${cpps} + ${mocs} +) + +target_link_libraries(fake + ${QT_LIBRARIES} + ${QT_QTSQL_LIBRARIES} +) diff --git a/src/plugins/fake/fakecollection.cpp b/src/plugins/fake/fakecollection.cpp new file mode 100644 index 000000000..fc01399f8 --- /dev/null +++ b/src/plugins/fake/fakecollection.cpp @@ -0,0 +1,27 @@ +#include "fakecollection.h" +#include "tomahawk/functimeout.h" + +FakeCollection::FakeCollection(QObject *parent) : + Collection("FakeCollection", parent) +{ +} + +void FakeCollection::load() +{ + QList tracks; + QVariantMap t1, t2, t3; + t1["artist"] = "0AAAAAArtist 1"; + t1["track"] = "0TTTTTTrack 1"; + t1["album"] = "0AAAAAAlbum 1"; + t1["url"] = "fake://1"; + t1["filesize"] = 5000000; + t1["duration"] = 300; + t1["bitrate"] = 192; + tracks << t1; + + new Tomahawk::FuncTimeout(5000, boost::bind(&FakeCollection::removeTracks, + this, tracks)); + + addTracks(tracks); + reportFinishedLoading(); + } diff --git a/src/plugins/fake/fakecollection.h b/src/plugins/fake/fakecollection.h new file mode 100644 index 000000000..72cb9bc70 --- /dev/null +++ b/src/plugins/fake/fakecollection.h @@ -0,0 +1,23 @@ +#ifndef FAKECOLLECTION_H +#define FAKECOLLECTION_H +#include "tomahawk/collection.h" + +class FakeCollection : public Collection +{ +Q_OBJECT +public: + explicit FakeCollection(QObject *parent = 0); + ~FakeCollection() + { + qDebug() << Q_FUNC_INFO; + } + + virtual void load(); + +signals: + +public slots: + +}; + +#endif // FAKECOLLECTION_H diff --git a/src/plugins/fake/fakeplugin.cpp b/src/plugins/fake/fakeplugin.cpp new file mode 100644 index 000000000..90f50c74e --- /dev/null +++ b/src/plugins/fake/fakeplugin.cpp @@ -0,0 +1,26 @@ +#include "fakeplugin.h" +#include "fakecollection.h" + +Q_EXPORT_PLUGIN2(fake, FakePlugin) + +FakePlugin::FakePlugin(Tomahawk::PluginAPI* api) + : TomahawkPlugin(api), m_api(api) +{ + init(); +} + +TomahawkPlugin * +FakePlugin::factory(Tomahawk::PluginAPI* api) +{ + return new FakePlugin(api); +} + +void FakePlugin::init() +{ + source_ptr src(new Source("Mr. Fake")); + collection_ptr coll(new FakeCollection); + src->addCollection(coll); + m_api->addSource(src); + coll->load(); +}; + diff --git a/src/plugins/fake/fakeplugin.h b/src/plugins/fake/fakeplugin.h new file mode 100644 index 000000000..4244aa64c --- /dev/null +++ b/src/plugins/fake/fakeplugin.h @@ -0,0 +1,31 @@ +#ifndef TOMAHAWK_LIB_PLUGIN_H +#define TOMAHAWK_LIB_PLUGIN_H +#include + +#include "tomahawk/plugin_includes.h" + +class FakePlugin : public QObject, public TomahawkPlugin +{ + Q_OBJECT + Q_INTERFACES(TomahawkPlugin) + +public: + + FakePlugin(){}; + + FakePlugin(Tomahawk::PluginAPI* api); + TomahawkPlugin * factory(Tomahawk::PluginAPI* api); + QString name() const { return "FakePlugin"; }; + QString description() const { return "Fake stuff, hardcoded"; }; + +private: + + void init(); + + Tomahawk::PluginAPI* m_api; +}; + + + +#endif + diff --git a/src/proxystyle.cpp b/src/proxystyle.cpp new file mode 100644 index 000000000..edac0a743 --- /dev/null +++ b/src/proxystyle.cpp @@ -0,0 +1,13 @@ +#include "proxystyle.h" + +#include +#include +#include + + +void +ProxyStyle::drawPrimitive( PrimitiveElement pe, const QStyleOption* opt, QPainter* p, const QWidget* w ) const +{ + if ( pe != PE_FrameStatusBar ) + QProxyStyle::drawPrimitive( pe, opt, p, w ); +} diff --git a/src/proxystyle.h b/src/proxystyle.h new file mode 100644 index 000000000..356175f8b --- /dev/null +++ b/src/proxystyle.h @@ -0,0 +1,15 @@ +#ifndef PROXYSTYLE_H +#define PROXYSTYLE_H + +#include +#include + +class ProxyStyle : public QProxyStyle +{ +public: + ProxyStyle() {} + + virtual void drawPrimitive( PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = 0 ) const; +}; + +#endif // PROXYSTYLE_H diff --git a/src/query.cpp b/src/query.cpp new file mode 100644 index 000000000..11d68a5c8 --- /dev/null +++ b/src/query.cpp @@ -0,0 +1,97 @@ +#include "tomahawk/query.h" + +#include + +using namespace Tomahawk; + +void +Query::addResults( const QList< Tomahawk::result_ptr >& newresults ) +{ + bool becameSolved = false; + { + QMutexLocker lock( &m_mut ); + m_results.append( newresults ); + qStableSort( m_results.begin(), m_results.end(), Query::resultSorter ); + + // hook up signals, and check solved status + foreach( const result_ptr& rp, newresults ) + { + connect( rp.data(), SIGNAL( becomingUnavailable() ), SLOT( resultUnavailable() ) ); + if( !m_solved && rp->score() > 0.99 ) + { + m_solved = true; + becameSolved = true; + } + } + } + emit resultsAdded( newresults ); + if( becameSolved ) emit solvedStateChanged( true ); +} + + +void +Query::resultUnavailable() +{ + Result * result = (Result*) sender(); + Q_ASSERT( result ); + + for(int i = 0; i < m_results.length(); ++i ) + { + if( m_results.value( i ).data() == result ) + { + m_results.removeAt( i ); + break; + } + } +} + + +void +Query::removeResult( Tomahawk::result_ptr result ) +{ + bool becameUnsolved = false; + { + QMutexLocker lock( &m_mut ); + m_results.removeAll( result ); + + if ( m_results.isEmpty() ) // FIXME proper score checking + becameUnsolved = true; + + } + emit resultsRemoved( result ); + if( becameUnsolved ) emit solvedStateChanged( false ); +} + + +QList< result_ptr > +Query::results() const +{ + QMutexLocker lock( &m_mut ); + return m_results; +} + + +unsigned int +Query::numResults() const +{ + QMutexLocker lock( &m_mut ); + return m_results.length(); +} + + +QID Query::id() const +{ + if ( m_qid.isEmpty() ) + { + m_qid = m_v.toMap().value( "qid" ).toString(); + Q_ASSERT( !m_qid.isEmpty() ); + } + return m_qid; +} + + +bool Query::resultSorter( const result_ptr& left, const result_ptr& right ) +{ + return left->score() > right->score(); +} + diff --git a/src/remotecollection.cpp b/src/remotecollection.cpp new file mode 100644 index 000000000..c4d52538a --- /dev/null +++ b/src/remotecollection.cpp @@ -0,0 +1,25 @@ +#include "remotecollection.h" + +using namespace Tomahawk; + +RemoteCollection::RemoteCollection( source_ptr source, QObject* parent ) + : DatabaseCollection( source, parent ) +{ + qDebug() << Q_FUNC_INFO; +} + + +// adding/removing is done by dbsyncconnection, and the dbcmd objects that modify +// the database will make us emit the appropriate signals (tracksAdded etc.) +void RemoteCollection::addTracks( const QList &newitems ) +{ + qDebug() << Q_FUNC_INFO; + Q_ASSERT( false ); +} + + +void RemoteCollection::removeTracks( const QList &olditems ) +{ + qDebug() << Q_FUNC_INFO; + Q_ASSERT( false ); +} diff --git a/src/remotecollection.h b/src/remotecollection.h new file mode 100644 index 000000000..bde90af93 --- /dev/null +++ b/src/remotecollection.h @@ -0,0 +1,27 @@ +#ifndef REMOTECOLLECTION_H +#define REMOTECOLLECTION_H + +#include "tomahawk/typedefs.h" + +#include "controlconnection.h" +#include "databasecollection.h" + +class RemoteCollection : public DatabaseCollection +{ +Q_OBJECT + +friend class ControlConnection; // for receiveTracks() + +public: + explicit RemoteCollection( Tomahawk::source_ptr source, QObject* parent = 0 ); + ~RemoteCollection() + { + qDebug() << Q_FUNC_INFO; + } + +public slots: + virtual void addTracks( const QList &newitems ); + virtual void removeTracks( const QList &olditems ); +}; + +#endif // REMOTECOLLECTION_H diff --git a/src/result.cpp b/src/result.cpp new file mode 100644 index 000000000..27adab0a7 --- /dev/null +++ b/src/result.cpp @@ -0,0 +1,30 @@ +#include "tomahawk/result.h" + +using namespace Tomahawk; + + +Result::Result( QVariant v, collection_ptr collection ) + : m_v( v ) + , m_collection( collection ) +{ + if ( !m_collection.isNull() ) + connect( m_collection->source().data(), SIGNAL( offline() ), SIGNAL( becomingUnavailable() ), Qt::QueuedConnection ); +} + + +float +Result::score() const +{ + return m_v.toMap().value( "score", 0.0 ).toFloat(); +} + + +RID +Result::id() const +{ + if ( m_rid.isEmpty() ) + { + m_rid = m_v.toMap().value( "sid" ).toString(); + } + return m_rid; +}; diff --git a/src/scriptresolver.cpp b/src/scriptresolver.cpp new file mode 100644 index 000000000..b0b3c5813 --- /dev/null +++ b/src/scriptresolver.cpp @@ -0,0 +1,140 @@ +#include +#include "tomahawk/tomahawkapp.h" +#include "scriptresolver.h" + +ScriptResolver::ScriptResolver(const QString& exe) : + Tomahawk::Resolver() + , m_cmd( exe ) + , m_num_restarts( 0 ) + , m_msgsize( 0 ) + , m_ready( false ) +{ + qDebug() << Q_FUNC_INFO << exe; + connect( &m_proc, SIGNAL(readyReadStandardError()), SLOT(readStderr()) ); + connect( &m_proc, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()) ); + connect( &m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(cmdExited(int,QProcess::ExitStatus)) ); + + m_proc.start( m_cmd ); +} + +void ScriptResolver::readStderr() +{ + qDebug() << "SCRIPT_STDERR" << m_cmd << m_proc.readAllStandardError(); +} + +void ScriptResolver::readStdout() +{ + qDebug() << Q_FUNC_INFO << m_proc.bytesAvailable(); + if( m_msgsize == 0 ) + { + if( m_proc.bytesAvailable() < 4 ) return; + quint32 len_nbo; + m_proc.read( (char*) &len_nbo, 4 ); + m_msgsize = qFromBigEndian( len_nbo ); + qDebug() << Q_FUNC_INFO << "msgsize" << m_msgsize; + } + + if( m_msgsize > 0 ) + { + m_msg.append( m_proc.read( m_msgsize - m_msg.length() ) ); + } + + if( m_msgsize == (quint32) m_msg.length() ) + { + handleMsg( m_msg ); + m_msgsize = 0; + m_msg.clear(); + if( m_proc.bytesAvailable() ) QTimer::singleShot( 0, this, SLOT(readStdout()) ); + } +} + +void ScriptResolver::sendMsg( const QByteArray& msg ) +{ + qDebug() << Q_FUNC_INFO << m_ready << msg; + + if( !m_ready ) return; + + quint32 len; + qToBigEndian( msg.length(), (uchar*) &len ); + m_proc.write( (const char*) &len, 4 ); + m_proc.write( msg ); +} + +void ScriptResolver::handleMsg( const QByteArray& msg ) +{ + qDebug() << Q_FUNC_INFO << msg.size() << QString::fromAscii(msg); + bool ok; + QVariant v = m_parser.parse( msg, &ok ); + if( !ok || v.type() != QVariant::Map ) + { + Q_ASSERT(false); + return; + } + QVariantMap m = v.toMap(); + const QString& msgtype = m.value( "_msgtype" ).toString(); + + if( msgtype == "settings" ) + { + doSetup( m ); + return; + } + + if( msgtype == "results" ) + { + QList< Tomahawk::result_ptr > results; + const QString& qid = m.value( "qid" ).toString(); + const QVariantList& reslist = m.value( "results" ).toList(); + Tomahawk::collection_ptr coll = APP->sourcelist().getLocal()->collection(); + foreach( const QVariant& rv, reslist ) + { + qDebug() << "RES" << rv; + Tomahawk::result_ptr rp( new Tomahawk::Result( rv, coll ) ); + results << rp; + } + APP->pipeline()->reportResults( qid, results ); + } +} + + +void ScriptResolver::cmdExited(int code, QProcess::ExitStatus status) +{ + m_ready = false; + qDebug() << Q_FUNC_INFO << "SCRIPT EXITED, code" << code << "status" << status << m_cmd; + APP->pipeline()->removeResolver( this ); + + if( m_num_restarts < 10 ) + { + m_num_restarts++; + qDebug() << "*** Restart num" << m_num_restarts; + m_proc.start( m_cmd ); + } + else + { + qDebug() << "*** Reached max restarts, not restarting."; + } +} + +void ScriptResolver::resolve( QVariant v ) +{ + QVariantMap m = v.toMap(); + m.insert( "_msgtype", "rq" ); + const QByteArray msg = m_serializer.serialize( m ); + sendMsg( msg ); +} + +void ScriptResolver::doSetup( const QVariantMap& m ) +{ + qDebug() << Q_FUNC_INFO << m; + m_name = m.value( "name" ).toString(); + m_weight = m.value( "weight", 0 ).toUInt(); + m_timeout = m.value( "timeout", 5000 ).toUInt(); + m_preference = m.value( "preference", 0 ).toUInt(); + qDebug() << "SCRIPT" << m_cmd << "READY, " << endl + << " name" << m_name << endl + << " weight" << m_weight << endl + << " timeout" << m_timeout << endl + << " preference" << m_preference + ; + m_ready = true; + APP->pipeline()->addResolver( this ); +} diff --git a/src/scriptresolver.h b/src/scriptresolver.h new file mode 100644 index 000000000..5082d4f73 --- /dev/null +++ b/src/scriptresolver.h @@ -0,0 +1,54 @@ +#ifndef SCRIPTRESOLVER_H +#define SCRIPTRESOLVER_H +#include + +#include +#include +#include + +#include "tomahawk/resolver.h" +#include "tomahawk/result.h" + +class ScriptResolver : public Tomahawk::Resolver +{ +Q_OBJECT +public: + explicit ScriptResolver(const QString& exe); + + virtual QString name() const { return m_name; } + virtual unsigned int weight() const { return m_weight; } + virtual unsigned int preference() const { return m_preference; } + virtual unsigned int timeout() const { return m_timeout; } + + virtual void resolve( QVariant v ); + +signals: + +public slots: + +private slots: + void readStderr(); + void readStdout(); + void cmdExited(int code, QProcess::ExitStatus status); + +private: + void handleMsg( const QByteArray& msg ); + void sendMsg( const QByteArray& msg ); + void doSetup( const QVariantMap& m ); + + + QProcess m_proc; + QString m_name, m_cmd; + unsigned int m_weight, m_preference, m_timeout, m_num_restarts; + + quint32 m_msgsize; + QByteArray m_msg; + + bool m_ready; + + QJson::Parser m_parser; + QJson::Serializer m_serializer; + +}; + +#endif // SCRIPTRESOLVER_H diff --git a/src/scrobbler.cpp b/src/scrobbler.cpp new file mode 100644 index 000000000..5c412c802 --- /dev/null +++ b/src/scrobbler.cpp @@ -0,0 +1,228 @@ +#include "scrobbler.h" + +#include +#include +#include + +#include +#include "tomahawk/typedefs.h" +#include "audio/audioengine.h" +#include "tomahawksettings.h" + +#include +#include + + +static QString +md5( const QByteArray& src ) +{ + QByteArray const digest = QCryptographicHash::hash( src, QCryptographicHash::Md5 ); + return QString::fromLatin1( digest.toHex() ).rightJustified( 32, '0' ); +} + + +Scrobbler::Scrobbler( QObject* parent ) + : QObject( parent ) + , m_scrobbler( 0 ) + , m_reachedScrobblePoint( false ) + , m_authJob( 0 ) +{ + lastfm::ws::ApiKey = "2aa1089093868876bba20b0482b9cef9"; + lastfm::ws::SharedSecret = "a7085ef81d7b46fe6ffe11c15b85902f"; + lastfm::ws::Username = TomahawkApp::instance()->settings()->lastFmUsername(); + + m_pw = TomahawkApp::instance()->settings()->lastFmPassword(); + + if( TomahawkApp::instance()->settings()->scrobblingEnabled() && !lastfm::ws::Username.isEmpty() ) + { + createScrobbler(); + } + + //HACK work around a bug in liblastfm---it doesn't create its config dir, so when it + // tries to write the track cache, it fails silently. until we have a fixed version, do this + // code taken from Amarok (src/services/lastfm/ScrobblerAdapter.cpp) + QString lpath = QDir::home().filePath( ".local/share/Last.fm" ); + QDir ldir = QDir( lpath ); + if( !ldir.exists() ) + { + ldir.mkpath( lpath ); + } + + connect( TomahawkApp::instance(), SIGNAL( settingsChanged() ), + SLOT( settingsChanged() ), Qt::QueuedConnection ); + + connect( TomahawkApp::instance()->audioEngine(), SIGNAL( timerSeconds( unsigned int ) ), + SLOT( engineTick( unsigned int ) ), Qt::QueuedConnection ); +} + + +Scrobbler::~Scrobbler() +{ + delete m_scrobbler; +} + + +void +Scrobbler::trackStarted( const Tomahawk::result_ptr& track ) +{ + Q_ASSERT( QThread::currentThread() == thread() ); +// qDebug() << Q_FUNC_INFO; + + if( !m_scrobbler ) + return; + + if( m_reachedScrobblePoint ) + { + m_reachedScrobblePoint = false; + scrobble(); + } + + m_track = lastfm::MutableTrack(); + m_track.stamp(); + + m_track.setTitle( track->track() ); + m_track.setArtist( track->artist() ); + m_track.setAlbum( track->album() ); + m_track.setDuration( track->duration() ); + m_track.setSource( lastfm::Track::Player ); + + m_scrobbler->nowPlaying( m_track ); + m_scrobblePoint = ScrobblePoint( m_track.duration() / 2 ); +} + + +void +Scrobbler::trackPaused() +{ + Q_ASSERT( QThread::currentThread() == thread() ); +} + + +void +Scrobbler::trackResumed() +{ + Q_ASSERT( QThread::currentThread() == thread() ); +} + + +void +Scrobbler::trackStopped() +{ + Q_ASSERT( QThread::currentThread() == thread() ); + + if( m_reachedScrobblePoint ) + { + m_reachedScrobblePoint = false; + scrobble(); + } +} + + +void +Scrobbler::engineTick( unsigned int secondsElapsed ) +{ + if ( secondsElapsed > m_scrobblePoint ) + m_reachedScrobblePoint = true; +} + + +void +Scrobbler::scrobble() +{ + Q_ASSERT( QThread::currentThread() == thread() ); + + qDebug() << Q_FUNC_INFO << m_track.toString(); + m_scrobbler->cache( m_track ); + m_scrobbler->submit(); +} + + +void +Scrobbler::settingsChanged() +{ + if( !m_scrobbler && TomahawkApp::instance()->settings()->scrobblingEnabled() ) + { // can simply create the scrobbler + lastfm::ws::Username = TomahawkApp::instance()->settings()->lastFmUsername(); + m_pw = TomahawkApp::instance()->settings()->lastFmPassword(); + + createScrobbler(); + } + else if( m_scrobbler && !TomahawkApp::instance()->settings()->scrobblingEnabled() ) + { + delete m_scrobbler; + m_scrobbler = 0; + } + else if( TomahawkApp::instance()->settings()->lastFmUsername() != lastfm::ws::Username || + TomahawkApp::instance()->settings()->lastFmPassword() != m_pw ) + { + lastfm::ws::Username = TomahawkApp::instance()->settings()->lastFmUsername(); + // credentials have changed, have to re-create scrobbler for them to take effect + if( m_scrobbler ) + delete m_scrobbler; + + createScrobbler(); + } +} + + +void +Scrobbler::onAuthenticated() +{ + if( !m_authJob ) + { + qDebug() << Q_FUNC_INFO << "Help! No longer got a last.fm auth job!"; + return; + } + + if( m_authJob->error() == QNetworkReply::NoError ) + { + lastfm::XmlQuery lfm = lastfm::XmlQuery( m_authJob->readAll() ); + + if( lfm.children( "error" ).size() > 0 ) + { + qDebug() << "Error from authenticating with Last.fm service:" << lfm.text(); + TomahawkApp::instance()->settings()->setLastFmSessionKey( QByteArray() ); + + } + else + { + lastfm::ws::SessionKey = lfm[ "session" ][ "key" ].text(); + + TomahawkApp::instance()->settings()->setLastFmSessionKey( lastfm::ws::SessionKey.toLatin1() ); + + if( TomahawkApp::instance()->settings()->scrobblingEnabled() ) + m_scrobbler = new lastfm::Audioscrobbler( "tst" ); + } + } + else + { + qDebug() << "Got error in Last.fm authentication job:" << m_authJob->errorString(); + } + + m_authJob->deleteLater(); +} + + +void +Scrobbler::createScrobbler() +{ + if( TomahawkApp::instance()->settings()->lastFmSessionKey().isEmpty() ) // no session key, so get one + { + QString authToken = md5( ( lastfm::ws::Username.toLower() + md5( m_pw.toUtf8() ) ).toUtf8() ); + + QMap query; + query[ "method" ] = "auth.getMobileSession"; + query[ "username" ] = lastfm::ws::Username; + query[ "authToken" ] = authToken; + m_authJob = lastfm::ws::post( query ); + + connect( m_authJob, SIGNAL( finished() ), SLOT( onAuthenticated() ) ); + } + else + { + lastfm::ws::SessionKey = TomahawkApp::instance()->settings()->lastFmSessionKey(); + + m_scrobbler = new lastfm::Audioscrobbler( "tst" ); + m_scrobbler->moveToThread( thread() ); + } +} diff --git a/src/scrobbler.h b/src/scrobbler.h new file mode 100644 index 000000000..72c5a3a55 --- /dev/null +++ b/src/scrobbler.h @@ -0,0 +1,49 @@ + +#ifndef TOMAHAWK_SCROBBLER_H +#define TOMAHAWK_SCROBBLER_H + +#include "tomahawk/result.h" + +#include +#include +#include + +#include + +class QNetworkReply; +/** + * Simple class that listens to signals from AudioEngine and scrobbles + * what it is playing. + */ +class Scrobbler : public QObject +{ + Q_OBJECT +public: + Scrobbler( QObject* parent = 0 ); + virtual ~Scrobbler(); + +public slots: + void trackStarted( const Tomahawk::result_ptr& ); + void trackPaused(); + void trackResumed(); + void trackStopped(); + void engineTick( unsigned int secondsElapsed ); + + void settingsChanged(); + void onAuthenticated(); + +private: + void scrobble( ); + void createScrobbler(); + + lastfm::MutableTrack m_track; + lastfm::Audioscrobbler* m_scrobbler; + QString m_pw; + bool m_reachedScrobblePoint; + ScrobblePoint m_scrobblePoint; + + QNetworkReply* m_authJob; +}; + + +#endif diff --git a/src/servent.cpp b/src/servent.cpp new file mode 100644 index 000000000..9806850c5 --- /dev/null +++ b/src/servent.cpp @@ -0,0 +1,656 @@ +#include "servent.h" + +#include +#include +#include +#include + +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/result.h" +#include "tomahawk/source.h" +#include "bufferiodevice.h" +#include "connection.h" +#include "controlconnection.h" +#include "filetransferconnection.h" + +using namespace Tomahawk; + + +Servent::Servent( QObject* parent ) + : QTcpServer( parent ) + , m_port( 0 ) + , m_externalPort( 0 ) +{ + qsrand( QTime( 0, 0, 0 ).secsTo( QTime::currentTime() ) ); +} + + +Servent::~Servent() +{ + if( m_externalPort ) + { + qDebug() << "Unregistering port fwd"; + pf.remove( m_externalPort ); + } +} + + +bool +Servent::startListening( QHostAddress ha, bool upnp, int port ) +{ + m_port = port; + // try listening on one port higher as well, to aid debugging + // and let you run 2 instances easily + if( !listen( ha, m_port ) && !listen( ha, ++m_port ) ) + { + qDebug() << "Failed to listen on port" << m_port; + return false; + } + else + { + qDebug() << "Servent listening on port" << m_port << " servent thread:" << thread(); + } + + // TODO check if we have a public/internet IP on this machine directly + // FIXME the portfwd stuff is blocking, so we hang here for 2 secs atm + if( upnp ) + { + // try and pick an available port: + if( pf.init( 2000 ) ) + { + int tryport = m_port; + + // last.fm office firewall policy hack + // (corp. firewall allows outgoing connections to this port, + // so listen on this if you want lastfmers to connect to you) + if( APP->arguments().contains("--porthack") ) + { + tryport = 3389; + pf.remove( tryport ); + } + + for( int r=0; r<5; ++r ) + { + qDebug() << "Trying to setup portfwd on" << tryport; + if( pf.add( tryport, m_port ) ) + { + QString pubip = QString( pf.external_ip().c_str() ); + m_externalAddress = QHostAddress( pubip ); + m_externalPort = tryport; + qDebug() << "External servent address detected as" << pubip << ":" << m_externalPort; + qDebug() << "Max upstream " << pf.max_upstream_bps() << "bps"; + qDebug() << "Max downstream" << pf.max_downstream_bps() << "bps"; + break; + } + tryport = 10000 + 50000 * (float)qrand()/RAND_MAX; + } + if( !m_externalPort ) + { + qDebug() << "Could not setup fwd for port:" << m_port; + } + } + else qDebug() << "No UPNP Gateway device found?"; + } + + if( m_externalPort == 0 ) + { + qDebug() << "No external access, LAN and outbound connections only!"; + } + + // --lanhack means to advertise your LAN IP over jabber as if it were externallyVisible + if( TomahawkApp::instance()->arguments().contains( "--lanhack" ) ) + { + QList ifs = QNetworkInterface::allAddresses(); + foreach( QHostAddress ha, ifs ) + { + if( ha.toString() == "127.0.0.1" ) continue; + if( ha.toString().contains( ":" ) ) continue; //ipv6 + + m_externalAddress = ha; + m_externalPort = m_port; + qDebug() << "LANHACK: set external address to lan address" << ha.toString(); + break; + } + } + + return true; +} + + +QString +Servent::createConnectionKey( const QString& name ) +{ + Q_ASSERT( this->thread() == QThread::currentThread() ); + QString key = uuid(); + ControlConnection * cc = new ControlConnection( this ); + cc->setName( name=="" ? QString( "KEY(%1)" ).arg(key) : name ); + registerOffer( key, cc ); + return key; +} + + +void +Servent::setExternalAddress( QHostAddress ha, int port ) +{ + m_externalAddress = ha; + m_externalPort = port; +} + + +void +Servent::registerOffer( const QString& key, Connection* conn ) +{ + m_offers[key] = QPointer(conn); +} + + +void +Servent::registerControlConnection( ControlConnection* conn ) +{ + qDebug() << Q_FUNC_INFO << conn->id(); + m_controlconnections.append( conn ); +} + + +void +Servent::unregisterControlConnection( ControlConnection* conn ) +{ + QList n; + foreach( ControlConnection* c, m_controlconnections ) + if( c!=conn ) + n.append( c ); + + m_controlconnections = n; +} + + +ControlConnection* +Servent::lookupControlConnection( const QString& name ) +{ + foreach( ControlConnection* c, m_controlconnections ) + if( c->name() == name ) + return c; + + return NULL; +} + + +void +Servent::incomingConnection( int sd ) +{ + Q_ASSERT( this->thread() == QThread::currentThread() ); + QTcpSocketExtra* sock = new QTcpSocketExtra; + qDebug() << Q_FUNC_INFO << "Accepting connection, sock" << sock; + sock->moveToThread( thread() ); + sock->_disowned = false; + sock->_outbound = false; + if( !sock->setSocketDescriptor( sd ) ) + { + qDebug() << "Out of system resources for new ports?"; + Q_ASSERT( false ); + return; + } + + connect( sock, SIGNAL( readyRead() ), SLOT( readyRead() ), Qt::QueuedConnection ); + connect( sock, SIGNAL( disconnected() ), sock, SLOT( deleteLater() ), Qt::QueuedConnection ); + qDebug() << "connection accepted."; +} + + +void +Servent::readyRead() +{ + Q_ASSERT( this->thread() == QThread::currentThread() ); + QTcpSocketExtra* sock = (QTcpSocketExtra*)sender(); + //qDebug() << Q_FUNC_INFO << thread() << sock; + + if( sock->_disowned ) + { + //qDebug() << "Socket already disowned"; + return ; + } + + if( sock->_msg.isNull() ) + { + char msgheader[ Msg::headerSize() ]; + if( sock->bytesAvailable() < Msg::headerSize() ) + return; + + sock->read( (char*) &msgheader, Msg::headerSize() ); + sock->_msg = Msg::begin( (char*) &msgheader ); + } + + if( sock->bytesAvailable() < sock->_msg->length() ) + return; + + QByteArray ba = sock->read( sock->_msg->length() ); + sock->_msg->fill( ba ); + Q_ASSERT( sock->_msg->is( Msg::JSON ) ); + + bool ok; + int pport = 0; + QString key, conntype, nodeid; + QVariantMap m = parser.parse( sock->_msg->payload(), &ok ).toMap(); + if( !ok ) + { + qDebug() << "Invalid JSON on new conection, aborting"; + goto closeconnection; + } + conntype = m.value( "conntype" ).toString(); + key = m.value( "key" ).toString(); + pport = m.value( "port" ).toInt(); + nodeid = m.value( "nodeid" ).toString(); + + qDebug() << m; + + if( !nodeid.isEmpty() ) // only control connections send nodeid + foreach( ControlConnection* cc, m_controlconnections ) + { + if( cc->id() == nodeid ) + { + qDebug() << "Duplicate control connection detected, dropping:" << nodeid; + goto closeconnection; + } + } + + // they connected to us and want something we are offering + if( conntype == "accept-offer" || "push-offer" ) + { + sock->_msg.clear(); + Connection * conn = claimOffer( key, sock->peerAddress() ); + if( !conn ) + { + qDebug() << "claimOffer FAILED, key:" << key; + goto closeconnection; + } + qDebug() << "claimOffer OK:" << key; + + if( !nodeid.isEmpty() ) conn->setId( nodeid ); + + handoverSocket( conn, sock ); + + return; + } + else + { + qDebug() << "Invalid or unhandled conntype"; + } + + // fallthru to cleanup: +closeconnection: + qDebug() << "Closing incoming connection, something was wrong."; + sock->_msg.clear(); + sock->disconnectFromHost(); +} + + +// creates a new tcp connection to peer from conn, handled by given connector +// new_conn is responsible for sending the first msg, if needed +void +Servent::createParallelConnection( Connection* orig_conn, Connection* new_conn, const QString& key ) +{ + qDebug() << "Servent::createParallelConnection, key:" << key << thread(); + // if we can connect to them directly: + if( orig_conn->outbound() ) + { + qDebug() << "Connecting directly"; + connectToPeer( orig_conn->socket()->peerAddress().toString(), + orig_conn->peerPort(), + key, + new_conn ); + } + else // ask them to connect to us: + { + QString tmpkey = uuid(); + qDebug() << "Asking them to connect to us using" << tmpkey ; + registerOffer( tmpkey, new_conn ); + + QVariantMap m; + m.insert( "conntype", "request-offer" ); + m.insert( "key", tmpkey ); + m.insert( "offer", key ); + m.insert( "port", externalPort() ); + + QJson::Serializer ser; + orig_conn->sendMsg( Msg::factory( ser.serialize(m), Msg::JSON ) ); + } +} + + +/// for outbound connections. DRY out the socket handover code from readyread too? +void +Servent::socketConnected() +{ + QTcpSocketExtra* sock = (QTcpSocketExtra*)sender(); + + qDebug() << "Servent::SocketConnected" << thread() << "socket:" << sock; + + Connection* conn = sock->_conn; + + handoverSocket( conn, sock ); +} + + +// transfers ownership of socket to the connection and inits the connection +void Servent::handoverSocket( Connection* conn, QTcpSocketExtra* sock ) +{ + Q_ASSERT( conn ); + Q_ASSERT( sock ); + Q_ASSERT( conn->socket().isNull() ); + Q_ASSERT( sock->isValid() ); + + disconnect( sock, SIGNAL( readyRead() ), this, SLOT( readyRead() ) ); + disconnect( sock, SIGNAL( disconnected() ), sock, SLOT( deleteLater() ) ); + disconnect( sock, SIGNAL( error( QAbstractSocket::SocketError ) ), + this, SLOT( socketError( QAbstractSocket::SocketError ) ) ); + + + sock->_disowned = true; + conn->setOutbound( sock->_outbound ); + conn->setPeerPort( sock->peerPort() ); + + conn->start( sock ); +} + + +void +Servent::socketError( QAbstractSocket::SocketError e ) +{ + qDebug() << Q_FUNC_INFO; + QTcpSocketExtra* sock = (QTcpSocketExtra*)sender(); + if( !sock ) + { + qDebug() << "SocketError, sock is null"; + return; + } + + Connection* conn = sock->_conn; + qDebug() << "Servent::SocketError:" << e << conn->id() << conn->name(); + if(!sock->_disowned) + { + // connection will delete if we already transferred ownership, otherwise: + sock->deleteLater(); + } + conn->markAsFailed(); // will emit failed, then finished +} + + +void +Servent::connectToPeer( const QString& ha, int port, const QString &key, const QString& name, const QString& id ) +{ + qDebug() << Q_FUNC_INFO << ha << port << key << name << id; + Q_ASSERT( this->thread() == QThread::currentThread() ); + + ControlConnection* conn = new ControlConnection( this ); + QVariantMap m; + m["conntype"] = "accept-offer"; + m["key"] = key; + m["port"] = externalPort(); + m["nodeid"] = APP->nodeID(); + + conn->setFirstMessage( m ); + if( name.length() ) + conn->setName( name ); + if( id.length() ) + conn->setId( id ); + + connectToPeer( ha, port, key, conn ); +} + + +void +Servent::connectToPeer( const QString& ha, int port, const QString &key, Connection* conn ) +{ + Q_ASSERT( port > 0 ); + Q_ASSERT( conn ); +// Q_ASSERT( this->thread() == QThread::currentThread() ); + + qDebug() << "Servent::connectToPeer:" << ha << ":" << port + << thread() << QThread::currentThread(); + + if( key.length() && conn->firstMessage().isNull() ) + { + QVariantMap m; + m["conntype"] = "accept-offer"; + m["key"] = key; + m["port"] = externalPort(); + conn->setFirstMessage( m ); + } + + QTcpSocketExtra* sock = new QTcpSocketExtra(); + sock->_disowned = false; + sock->_conn = conn; + sock->_outbound = true; + //qDebug() << "connectToPeer, sock:" << sock->thread(); + + connect( sock, SIGNAL( connected() ), SLOT( socketConnected() ), Qt::QueuedConnection ); + connect( sock, SIGNAL( error( QAbstractSocket::SocketError ) ), + SLOT( socketError( QAbstractSocket::SocketError ) ), Qt::QueuedConnection ); + + //qDebug() << "About to connectToHost..."; + sock->connectToHost( ha, port, QTcpSocket::ReadWrite ); + sock->moveToThread( thread() ); + //qDebug() << "tried to connectToHost (waiting on a connected signal)"; +} + + +void +Servent::reverseOfferRequest( Connection* orig_conn, const QString& key, const QString& theirkey ) +{ + Q_ASSERT( this->thread() == QThread::currentThread() ); + + qDebug() << "Servent::reverseOfferRequest received for" << key; + Connection * new_conn = claimOffer( key ); + if( !new_conn ) + { + qDebug() << "claimOffer failed, killing requesting connection out of spite"; + orig_conn->shutdown(); + return; + } + + QVariantMap m; + m["conntype"] = "push-offer"; + m["key"] = theirkey; + m["port"] = externalPort(); + new_conn->setFirstMessage( m ); + createParallelConnection( orig_conn, new_conn, QString() ); +} + + +// return the appropriate connection for a given offer key, or NULL if invalid +Connection* +Servent::claimOffer( const QString &key, const QHostAddress peer ) +{ + bool noauth = qApp->arguments().contains( "--noauth" ); + + // magic key for file transfers: + if( key.startsWith("FILE_REQUEST_KEY:") ) + { + // check if the source IP matches an existing, authenticated connection + if ( !noauth && peer != QHostAddress::Any && !isIPWhitelisted( peer ) ) + { + bool authed = false; + foreach( ControlConnection* cc, m_controlconnections ) + { + if( cc->socket()->peerAddress() == peer ) + { + authed = true; + break; + } + } + if( !authed ) + { + qDebug() << "File transfer request rejected, invalid source IP"; + return NULL; + } + } + + QString fid = key.right( key.length() - 17 ); + FileTransferConnection* ftc = new FileTransferConnection( this, fid ); + return ftc; + } + + if( key == "whitelist" ) // LAN IP address, check source IP + { + if( isIPWhitelisted(peer) ) + { + qDebug() << "Connection is from whitelisted IP range (LAN)"; + Connection* conn = new ControlConnection( this ); + conn->setName( peer.toString() ); + return conn; + } + else + { + qDebug() << "Connection claimed to be whitelisted, but wasn't."; + return NULL; + } + } + + if( m_offers.contains( key ) ) + { + QPointer conn = m_offers.value( key ); + if( conn.isNull() ) + { + // This can happen if it's a filetransferconnection, but the audioengine has + // already closed the iodevice, causing the connection to be deleted before + // the peer connects and provides the first byte + qDebug() << Q_FUNC_INFO << "invalid/expired offer:" << key; + return NULL; + } + + if( conn->onceOnly() ) + { + m_offers.remove( key ); + return conn.data(); + } + else + { + return conn->clone(); + } + } + else if ( noauth ) + { + Connection* conn; + conn = new ControlConnection( this ); + conn->setName( key ); + return conn; + } + else + { + qDebug() << "Invalid offer key:" << key; + return NULL; + } +} + + +QSharedPointer +Servent::remoteIODeviceFactory( const result_ptr& result ) +{ + qDebug() << Q_FUNC_INFO << thread() ; + QSharedPointer sp; + + QStringList parts = result->url().mid( QString( "servent://" ).length()).split( "\t" ); + const QString& sourceName = parts.at( 0 ); + const QString& fileId = parts.at( 1 ); + const source_ptr& s = TomahawkApp::instance()->sourcelist().lookup( sourceName ); + if ( s.isNull() ) + return sp; + + ControlConnection* cc = s->controlConnection(); + FileTransferConnection* ftc = new FileTransferConnection( this, cc, fileId, result->size() ); + createParallelConnection( cc, ftc, QString( "FILE_REQUEST_KEY:%1" ).arg( fileId ) ); + return ftc->iodevice(); +} + + +void +Servent::registerFileTransferConnection( FileTransferConnection* ftc ) +{ + Q_ASSERT( !m_ftsessions.contains( ftc ) ); + QMutexLocker lock( &m_ftsession_mut ); + qDebug() << "Registering FileTransfer" << m_ftsessions.length() + 1; + m_ftsessions.append( ftc ); + printCurrentTransfers(); +} + + +void +Servent::fileTransferFinished( FileTransferConnection* ftc ) +{ + Q_ASSERT( ftc ); + + qDebug() << "FileTransfer Finished, unregistering" << ftc->id(); + QMutexLocker lock( &m_ftsession_mut ); + int rem = m_ftsessions.removeAll( ftc ); + Q_ASSERT( rem == 1 ); + printCurrentTransfers(); +} + + +// used for debug output: +void +Servent::printCurrentTransfers() +{ + int k = 1; + qDebug() << "~~~ Active file transfer connections:" << m_ftsessions.length(); + foreach( FileTransferConnection* i, m_ftsessions ) + { + qDebug() << k << ") " << i->id(); + } + qDebug() << endl; +} + + +bool +Servent::isIPWhitelisted( QHostAddress ip ) +{ + typedef QPair range; + static QList whitelist; + if( whitelist.isEmpty() ) + { + whitelist << range( QHostAddress("10.0.0.0"), 8 ) + << range( QHostAddress("172.16.0.0"), 12 ) + << range( QHostAddress("192.168.0.0"), 16 ) + << range( QHostAddress("169.254.0.0"), 16 ) + << range( QHostAddress("127.0.0.0"), 24 ); + +// qDebug() << "Loaded whitelist IP range:" << whitelist; + } + + foreach( const range& r, whitelist ) + if( ip.isInSubnet( r ) ) + return true; + + return false; +} + + +bool +Servent::connectedToSession( const QString& session ) +{ + foreach( ControlConnection* cc, m_controlconnections ) + { + if( cc->id() == session ) + return true; + } + + return false; +} + + +void +Servent::triggerDBSync() +{ + qDebug() << Q_FUNC_INFO; + + // tell peers we have new stuff they should sync + QList sources = APP->sourcelist().sources(); + foreach( const source_ptr& src, sources ) + { + // local src doesnt have a control connection, skip it: + if( src.isNull() || src->isLocal() ) + continue; + + src->controlConnection()->dbSyncConnection()->trigger(); + } +} diff --git a/src/servent.h b/src/servent.h new file mode 100644 index 000000000..64d830cd4 --- /dev/null +++ b/src/servent.h @@ -0,0 +1,135 @@ +#ifndef SERVENT_H +#define SERVENT_H + +// port for servent to listen on +#define DEFAULT_LISTEN_PORT 50210 +// time before new connection terminates if no auth received +#define AUTH_TIMEOUT 15000 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "portfwd/portfwd.h" +#include "tomahawk/typedefs.h" +#include "msg.h" + +class Connection; +class Connector; +class ControlConnection; +class FileTransferConnection; +class ProxyConnection; +class RemoteCollectionConnection; + +// this is used to hold a bit of state, so when a connected signal is emitted +// from a socket, we can associate it with a Connection object etc. +class QTcpSocketExtra : public QTcpSocket +{ +Q_OBJECT + +public: + QTcpSocketExtra() : QTcpSocket() + { + QTimer::singleShot( AUTH_TIMEOUT, this, SLOT( authTimeout() ) ) ; + } + + Connection* _conn; + bool _outbound; + bool _disowned; + msg_ptr _msg; + +private slots: + void authTimeout() + { + if( _disowned ) + return; + + qDebug() << "Connection timed out before providing a valid offer-key"; + this->disconnectFromHost(); + } +}; + +class Servent : public QTcpServer +{ +Q_OBJECT + +public: + explicit Servent( QObject* parent = 0 ); + virtual ~Servent(); + + bool startListening( QHostAddress ha, bool upnp = false, int port = DEFAULT_LISTEN_PORT ); + + int port() const { return m_port; } + + // creates new token that allows a controlconnection to be set up + QString createConnectionKey( const QString& name = "" ); + + void registerOffer( const QString& key, Connection* conn ); + + void registerControlConnection( ControlConnection* conn ); + void unregisterControlConnection( ControlConnection* conn ); + ControlConnection* lookupControlConnection( const QString& name ); + + void connectToPeer( const QString& ha, int port, const QString &key, const QString& name = "", const QString& id = "" ); + void connectToPeer( const QString& ha, int port, const QString &key, Connection* conn ); + void reverseOfferRequest( Connection* orig_conn, const QString& key, const QString& theirkey ); + + void setExternalAddress( QHostAddress ha, int port ); + bool visibleExternally() const { return m_externalPort > 0; } + QHostAddress externalAddress() const { return m_externalAddress; } + int externalPort() const { return m_externalPort; } + + QSharedPointer remoteIODeviceFactory( const Tomahawk::result_ptr& ); + static bool isIPWhitelisted( QHostAddress ip ); + + bool connectedToSession( const QString& session ); + + unsigned int numConnectedPeers() const { return m_controlconnections.length(); } + +protected: + void incomingConnection( int sd ); + +public slots: + void socketError( QAbstractSocket::SocketError ); + void createParallelConnection( Connection* orig_conn, Connection* new_conn, const QString& key ); + + void registerFileTransferConnection( FileTransferConnection* ); + void fileTransferFinished( FileTransferConnection* ftc ); + + void socketConnected(); + void triggerDBSync(); + +private slots: + void readyRead(); + + Connection* claimOffer( const QString &key, const QHostAddress peer = QHostAddress::Any ); + +private: + void handoverSocket( Connection* conn, QTcpSocketExtra* sock ); + + void printCurrentTransfers(); + + QJson::Parser parser; + QList< ControlConnection* > m_controlconnections; // canonical list of authed peers + QMap< QString, QPointer > m_offers; + int m_port, m_externalPort; + QHostAddress m_externalAddress; + + // currently active file transfers: + QList< FileTransferConnection* > m_ftsessions; + QMutex m_ftsession_mut; + + Portfwd pf; +}; + +#endif // SERVENT_H diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp new file mode 100644 index 000000000..e8cfde839 --- /dev/null +++ b/src/settingsdialog.cpp @@ -0,0 +1,235 @@ +#include "settingsdialog.h" +#include "ui_settingsdialog.h" + +#include +#include +#include +#include + +#ifndef NO_LIBLASTFM +#include +#include +#endif + +#include "tomahawk/tomahawkapp.h" +#include "musicscanner.h" +#include "tomahawksettings.h" +#include + + +static QString md5( const QByteArray& src ) +{ + QByteArray const digest = QCryptographicHash::hash( src, QCryptographicHash::Md5 ); + return QString::fromLatin1( digest.toHex() ).rightJustified( 32, '0' ); +} + + +SettingsDialog::SettingsDialog( QWidget *parent ) + : QDialog( parent ) + , ui( new Ui::SettingsDialog ) + , m_rejected( false ) + , m_testLastFmQuery( 0 ) +{ + ui->setupUi( this ); + TomahawkSettings* s = TomahawkApp::instance()->settings(); + + ui->checkBoxHttp->setChecked( s->httpEnabled() ); + ui->checkBoxUpnp->setChecked( s->upnpEnabled() ); + + // JABBER + ui->checkBoxJabberAutoConnect->setChecked( s->jabberAutoConnect() ); + ui->jabberUsername->setText( s->jabberUsername() ); + ui->jabberPassword->setText(s->jabberPassword() ); + ui->jabberServer->setText( s->jabberServer() ); + ui->jabberPort->setValue( s->jabberPort() ); + + if ( ui->jabberPort->text().toInt() != 5222 || !ui->jabberServer->text().isEmpty() ) + { + ui->checkBoxJabberAdvanced->setChecked( true ); + } + else + { + // hide advanved settings + ui->checkBoxJabberAdvanced->setChecked( false ); + ui->jabberServer->setVisible( false ); + ui->jabberPort->setVisible( false ); + ui->labelJabberServer->setVisible( false ); + ui->labelJabberPort->setVisible( false ); + } + + // MUSIC SCANNER + ui->lineEditMusicPath->setText( s->scannerPath() ); + + // LAST FM + ui->checkBoxEnableLastfm->setChecked( s->scrobblingEnabled() ); + ui->lineEditLastfmUsername->setText( s->lastFmUsername() ); + ui->lineEditLastfmPassword->setText(s->lastFmPassword() ); + connect( ui->pushButtonTestLastfmLogin, SIGNAL( clicked( bool) ), this, SLOT( testLastFmLogin() ) ); + + connect( ui->buttonBrowse, SIGNAL( clicked() ), SLOT( showPathSelector() ) ); + connect( this, SIGNAL( rejected() ), SLOT( onRejected() ) ); +} + + +SettingsDialog::~SettingsDialog() +{ + qDebug() << Q_FUNC_INFO; + + if ( !m_rejected ) + { + TomahawkSettings* s = TomahawkApp::instance()->settings(); + + // if jabber or scan dir changed, reconnect/rescan + bool rescan = ui->lineEditMusicPath->text() != s->scannerPath(); + bool rejabber = false; + if ( ui->jabberUsername->text() != s->jabberUsername() || + ui->jabberPassword->text() != s->jabberPassword() || + ui->jabberServer->text() != s->jabberServer() || + ui->jabberPort->value() != s->jabberPort() + ) + { + rejabber = true; + } + + s->setHttpEnabled( ui->checkBoxHttp->checkState() == Qt::Checked ); + s->setUPnPEnabled( ui->checkBoxUpnp->checkState() == Qt::Checked ); + + s->setJabberAutoConnect( ui->checkBoxJabberAutoConnect->checkState() == Qt::Checked ); + s->setJabberUsername( ui->jabberUsername->text() ); + s->setJabberPassword( ui->jabberPassword->text() ); + s->setJabberServer( ui->jabberServer->text() ); + s->setJabberPort( ui->jabberPort->value() ); + + s->setScannerPath( ui->lineEditMusicPath->text() ); + + s->setScrobblingEnabled( ui->checkBoxEnableLastfm->isChecked() ); + s->setLastFmUsername( ui->lineEditLastfmUsername->text() ); + s->setLastFmPassword( ui->lineEditLastfmPassword->text() ); + + if( rescan ) + { + MusicScanner* scanner = new MusicScanner(s->scannerPath() ); + connect( scanner, SIGNAL( finished() ), scanner, SLOT( deleteLater() ) ); + scanner->start(); + } + + if( rejabber ) + { + APP->reconnectJabber(); + } + + } + else + qDebug() << "Settings dialog cancelled, NOT saving prefs."; + + delete ui; +} + + +void SettingsDialog::showPathSelector() +{ + QString path = QFileDialog::getExistingDirectory( + this, + tr( "Select Music Folder" ), + QDesktopServices::storageLocation( QDesktopServices::MusicLocation ) + ); + + if ( path.isEmpty() ) + return; + + ui->lineEditMusicPath->setText( path ); +} + + +void SettingsDialog::doScan() +{ + // TODO this doesnt really belong here.. + QString path = ui->lineEditMusicPath->text(); + MusicScanner* scanner = new MusicScanner( path ); + connect( scanner, SIGNAL( finished() ), scanner, SLOT( deleteLater() ) ); + scanner->start(); + + QMessageBox::information( this, tr( "Scanning Started" ), + tr( "Scanning now, check console output. TODO." ), + QMessageBox::Ok ); +} + + +void SettingsDialog::onRejected() +{ + m_rejected = true; +} + + +void SettingsDialog::changeEvent( QEvent *e ) +{ + QDialog::changeEvent( e ); + switch ( e->type() ) + { + case QEvent::LanguageChange: + ui->retranslateUi( this ); + break; + + default: + break; + } +} + + +void SettingsDialog::testLastFmLogin() +{ +#ifndef NO_LIBLASTFM + ui->pushButtonTestLastfmLogin->setEnabled( false ); + ui->pushButtonTestLastfmLogin->setText( "Testing..." ); + + QString authToken = md5( ( ui->lineEditLastfmUsername->text() + md5( ui->lineEditLastfmPassword->text().toUtf8() ) ).toUtf8() ); + + // now authenticate w/ last.fm and get our session key + QMap query; + query[ "method" ] = "auth.getMobileSession"; + query[ "username" ] = ui->lineEditLastfmUsername->text(); + query[ "authToken" ] = authToken; + m_testLastFmQuery = lastfm::ws::post( query ); + + connect( m_testLastFmQuery, SIGNAL( finished() ), SLOT( onLastFmFinished() ) ); +#endif +} + + +void SettingsDialog::onLastFmFinished() +{ +#ifndef NO_LIBLASTFM + lastfm::XmlQuery lfm = lastfm::XmlQuery( m_testLastFmQuery->readAll() ); + + switch( m_testLastFmQuery->error() ) + { + case QNetworkReply::NoError: + qDebug() << "NoError in getting lastfm auth check result"; + if( lfm.children( "error" ).size() > 0 ) + { + qDebug() << "ERROR from last.fm:" << lfm.text(); + ui->pushButtonTestLastfmLogin->setText( tr( "Failed" ) ); + ui->pushButtonTestLastfmLogin->setEnabled( true ); + + } else + { + ui->pushButtonTestLastfmLogin->setText( tr( "Success" ) ); + ui->pushButtonTestLastfmLogin->setEnabled( false ); + } + break; + + case QNetworkReply::ContentOperationNotPermittedError: + case QNetworkReply::AuthenticationRequiredError: + ui->pushButtonTestLastfmLogin->setText( tr( "Failed" ) ); + ui->pushButtonTestLastfmLogin->setEnabled( true ); + break; + + default: + qDebug() << "Couldn't get last.fm auth result"; + ui->pushButtonTestLastfmLogin->setText( tr( "Could not contact server" ) ); + ui->pushButtonTestLastfmLogin->setEnabled( true ); + return; + } +#endif +} + diff --git a/src/settingsdialog.h b/src/settingsdialog.h new file mode 100644 index 000000000..57a031110 --- /dev/null +++ b/src/settingsdialog.h @@ -0,0 +1,44 @@ +#ifndef SETTINGSDIALOG_H +#define SETTINGSDIALOG_H + +#include + +class QNetworkReply; + +namespace Ui +{ + class SettingsDialog; +} + +class SettingsDialog : public QDialog +{ +Q_OBJECT + +public: + static const unsigned int VERSION = 1; // config version + + explicit SettingsDialog( QWidget *parent = 0 ); + ~SettingsDialog(); + +Q_SIGNALS: + void settingsChanged(); + +protected: + void changeEvent( QEvent *e ); + +private slots: + void onRejected(); + void showPathSelector(); + void doScan(); + + void testLastFmLogin(); + void onLastFmFinished(); + +private: + Ui::SettingsDialog *ui; + + bool m_rejected; + QNetworkReply* m_testLastFmQuery; +}; + +#endif // SETTINGSDIALOG_H diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui new file mode 100644 index 000000000..9baa910c9 --- /dev/null +++ b/src/settingsdialog.ui @@ -0,0 +1,535 @@ + + + SettingsDialog + + + + 0 + 0 + 621 + 353 + + + + + 0 + 353 + + + + Music Player Settings + + + + + + 0 + + + + Jabber + + + + 16 + + + + + 16 + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Jabber ID: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + jabberUsername + + + + + + + + 0 + 0 + + + + + + + + Password: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + jabberPassword + + + + + + + + 0 + 0 + + + + + + + QLineEdit::Password + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + Show Advanced Settings + + + + + + + + + 16 + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Jabber Server: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + jabberServer + + + + + + + + 0 + 0 + + + + + + + + Port: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 8 + + + + + + + + + 0 + 0 + + + + + 90 + 0 + + + + + 90 + 16777215 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + 65535 + + + 5222 + + + + + + + + + + + Qt::Vertical + + + + 20 + 146 + + + + + + + + Qt::RightToLeft + + + Tomahawk HTTP API - NO AUTH + + + false + + + + + + + Qt::RightToLeft + + + Use UPnP to establish port forward + + + true + + + + + + + Qt::RightToLeft + + + Connect automatically when Tomahawk starts + + + true + + + + + + + + Music + + + + 16 + + + + + + + Path to scan for music files: + + + + + + + + + + + + ... + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Last.fm + + + + 16 + + + + + Scrobble tracks to Last.fm + + + + + + + Last.fm Login + + + + + + + + Username: + + + + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + + + Test Login + + + + + + + Qt::Vertical + + + + 20 + 221 + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SettingsDialog + accept() + + + 398 + 374 + + + 157 + 274 + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 466 + 380 + + + 286 + 274 + + + + + checkBoxJabberAdvanced + toggled(bool) + jabberServer + setVisible(bool) + + + 480 + 124 + + + 496 + 154 + + + + + checkBoxJabberAdvanced + toggled(bool) + labelJabberServer + setVisible(bool) + + + 483 + 124 + + + 114 + 154 + + + + + checkBoxJabberAdvanced + toggled(bool) + labelJabberPort + setVisible(bool) + + + 461 + 124 + + + 114 + 182 + + + + + checkBoxJabberAdvanced + toggled(bool) + jabberPort + setVisible(bool) + + + 441 + 112 + + + 439 + 163 + + + + + diff --git a/src/source.cpp b/src/source.cpp new file mode 100644 index 000000000..e11367637 --- /dev/null +++ b/src/source.cpp @@ -0,0 +1,144 @@ +#include "tomahawk/source.h" + +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/collection.h" + +#include "controlconnection.h" +#include "databasecommand_addsource.h" +#include "databasecommand_sourceoffline.h" +#include "database.h" + +using namespace Tomahawk; + + +Source::Source( const QString &username, ControlConnection* cc ) + : QObject() + , m_isLocal( false ) + , m_online( false ) + , m_username( username ) + , m_id( 0 ) + , m_cc( cc ) +{ + // source for local machine doesn't have a controlconnection. this is normal. + if ( cc ) + connect( cc, SIGNAL( finished() ), SLOT( remove() ), Qt::QueuedConnection ); +} + + +Source::Source( const QString &username ) + : QObject() + , m_isLocal( true ) + , m_online( false ) + , m_username( username ) + , m_id( 0 ) + , m_cc( 0 ) +{ +} + + +Source::~Source() +{ + qDebug() << Q_FUNC_INFO; + // TODO mark source as offline in database + DatabaseCommand_SourceOffline * cmd = new DatabaseCommand_SourceOffline( id() ); + APP->database()->enqueue( QSharedPointer(cmd) ); +} + +collection_ptr Source::collection() const +{ + if( m_collections.length() ) return m_collections.first(); + collection_ptr tmp; + return tmp; +} + + +void +Source::doDBSync() +{ + // ensure username is in the database + DatabaseCommand_addSource * cmd = new DatabaseCommand_addSource( m_username, m_friendlyname ); + connect( cmd, SIGNAL( done( unsigned int, QString ) ), + SLOT( dbLoaded( unsigned int, const QString& ) ) ); + APP->database()->enqueue( QSharedPointer(cmd) ); +} + + +void +Source::setStats( const QVariantMap& m ) +{ + m_stats = m; + emit stats( m_stats ); +} + + +void +Source::remove() +{ + qDebug() << Q_FUNC_INFO; + m_cc = 0; + APP->sourcelist().remove( this ); + m_collections.clear(); +} + + +QString +Source::friendlyName() const +{ + if ( m_friendlyname.isEmpty() ) + return m_username; + + if ( m_friendlyname.contains( "/tomahawk" ) ) + return m_friendlyname.left( m_friendlyname.indexOf( "/tomahawk" ) ); + + return m_friendlyname; +} + + +void +Source::addCollection( collection_ptr c ) +{ + Q_ASSERT( m_collections.length() == 0 ); // only 1 source supported atm + m_collections.append( c ); + emit collectionAdded( c ); +} + + +void +Source::removeCollection( collection_ptr c ) +{ + Q_ASSERT( m_collections.length() == 1 && m_collections.first() == c ); // only 1 source supported atm + m_collections.removeAll( c ); + emit collectionRemoved( c ); +} + + +void +Source::setOffline() +{ + if ( !m_online ) + return; + + m_online = false; + emit offline(); +} + + +void +Source::setOnline() +{ + if ( m_online ) + return; + + m_online = true; + emit online(); +} + + +void +Source::dbLoaded( unsigned int id, const QString& fname ) +{ + qDebug() << Q_FUNC_INFO << id << fname; + m_id = id; + m_friendlyname = fname; + emit syncedWithDatabase(); +} diff --git a/src/sourcelist.cpp b/src/sourcelist.cpp new file mode 100644 index 000000000..2b8789085 --- /dev/null +++ b/src/sourcelist.cpp @@ -0,0 +1,100 @@ +#include "tomahawk/sourcelist.h" + +#include + +using namespace Tomahawk; + +SourceList::SourceList( QObject* parent ) + : QObject( parent ) +{ +} + + +const source_ptr& +SourceList::getLocal() +{ + return m_local; +} + + +void +SourceList::add( const Tomahawk::source_ptr& s ) +{ + { + QMutexLocker lock( &m_mut ); + if ( m_sources.contains( s->userName() ) ) + return; + + m_sources.insert( s->userName(), s ); + if( !s->isLocal() ) + { + Q_ASSERT( s->id() ); + m_sources_id2name.insert( s->id(), s->userName() ); + } + qDebug() << "SourceList::add(" << s->userName() << "), total sources now:" << m_sources.size(); + if( s->isLocal() ) + { + Q_ASSERT( m_local.isNull() ); + m_local = s; + } + } + emit sourceAdded( s ); + s->collection()->loadPlaylists(); +} + + +void +SourceList::remove( const Tomahawk::source_ptr& s ) +{ + remove( s.data() ); +} + + +void +SourceList::remove( Tomahawk::Source* s ) +{ + qDebug() << Q_FUNC_INFO; + source_ptr src; + { + QMutexLocker lock( &m_mut ); + if ( !m_sources.contains( s->userName() ) ) + return; + + src = m_sources.value( s->userName() ); + m_sources_id2name.remove( src->id() ); + m_sources.remove( s->userName() ); + qDebug() << "SourceList::remove(" << s->userName() << "), total sources now:" << m_sources.size(); + } + emit sourceRemoved( src ); +} + + +QList +SourceList::sources() const +{ + QMutexLocker lock( &m_mut ); + return m_sources.values(); +} + + +source_ptr +SourceList::lookup( unsigned int id ) const +{ + QMutexLocker lock( &m_mut ); + return m_sources.value( m_sources_id2name.value( id ) ); +} + +source_ptr +SourceList::lookup( const QString& username ) const +{ + QMutexLocker lock( &m_mut ); + return m_sources.value( username ); +} + + +unsigned int +SourceList::length() const +{ + QMutexLocker lock( &m_mut ); + return m_sources.size(); +} diff --git a/src/sourcetree/sourcesmodel.cpp b/src/sourcetree/sourcesmodel.cpp new file mode 100644 index 000000000..f800c0824 --- /dev/null +++ b/src/sourcetree/sourcesmodel.cpp @@ -0,0 +1,201 @@ +#include "sourcesmodel.h" + +#include +#include +#include +#include + +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/query.h" +#include "tomahawk/sourcelist.h" +#include "sourcetreeitem.h" +#include "imagebutton.h" +#include "playlistview.h" + +using namespace Tomahawk; + + +SourcesModel::SourcesModel( QObject* parent ) + : QStandardItemModel( parent ) +{ + setColumnCount( 1 ); + + connect( &APP->sourcelist(), SIGNAL( sourceAdded( Tomahawk::source_ptr ) ), SLOT( onSourceAdded( Tomahawk::source_ptr ) ) ); + connect( &APP->sourcelist(), SIGNAL( sourceRemoved( Tomahawk::source_ptr ) ), SLOT( onSourceRemoved( Tomahawk::source_ptr ) ) ); + + connect( parent, SIGNAL( onOnline( QModelIndex ) ), SLOT( onItemOnline( QModelIndex ) ) ); + connect( parent, SIGNAL( onOffline( QModelIndex ) ), SLOT( onItemOffline( QModelIndex ) ) ); + + // load sources after the view initialised completely +// QTimer::singleShot( 0, this, SLOT( loadSources() ) ); +} + + +QStringList +SourcesModel::mimeTypes() const +{ + QStringList types; + types << "application/tomahawk.query.list"; + return types; +} + + +Qt::DropActions +SourcesModel::supportedDropActions() const +{ + return Qt::CopyAction; +} + + +Qt::ItemFlags +SourcesModel::flags( const QModelIndex& index ) const +{ + Qt::ItemFlags defaultFlags = QStandardItemModel::flags( index ); + + if ( index.isValid() ) + return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return defaultFlags; +} + + +void +SourcesModel::loadSources() +{ + QList sources = APP->sourcelist().sources(); + + foreach( const source_ptr& source, sources ) + appendItem( source ); +} + + +void +SourcesModel::onSourceAdded( const source_ptr& source ) +{ + appendItem( source ); +} + + +void +SourcesModel::onSourceRemoved( const source_ptr& source ) +{ + removeItem( source ); +} + + +bool +SourcesModel::appendItem( const source_ptr& source ) +{ + SourceTreeItem* item = new SourceTreeItem( source, this ); + connect( item, SIGNAL( clicked( QModelIndex ) ), this, SIGNAL( clicked( QModelIndex ) ) ); + +// qDebug() << "Appending source item:" << item->source()->username(); + invisibleRootItem()->appendRow( item->columns() ); + + ((QTreeView*)parent())->setIndexWidget( index( rowCount() - 1, 0 ), item->widget() ); + return true; // FIXME +} + + +bool +SourcesModel::removeItem( const source_ptr& source ) +{ +// qDebug() << "Removing source item from SourceTree:" << source->username(); + + QModelIndex idx; + int rows = rowCount(); + for ( int row = 0; row < rows; row++ ) + { + QModelIndex idx = index( row, 0 ); + SourceTreeItem* item = indexToTreeItem( idx ); + if ( item ) + { + if ( item->source() == source ) + { + qDebug() << "Found removed source item:" << item->source()->userName(); + invisibleRootItem()->removeRow( row ); + + onItemOffline( idx ); + + delete item; + return true; + } + } + } + + return false; +} + + +void +SourcesModel::onItemOnline( const QModelIndex& idx ) +{ + qDebug() << Q_FUNC_INFO; + + SourceTreeItem* item = indexToTreeItem( idx ); + if ( item ) + item->onOnline(); +} + + +void +SourcesModel::onItemOffline( const QModelIndex& idx ) +{ + qDebug() << Q_FUNC_INFO; + + SourceTreeItem* item = indexToTreeItem( idx ); + if ( item ) + item->onOffline(); +} + + +int +SourcesModel::indexType( const QModelIndex& index ) +{ + if ( !index.isValid() ) + return -1; + + QModelIndex idx = index.model()->index( index.row(), 0, index.parent() ); + return idx.data( Qt::UserRole + 1 ).toInt(); +} + + +playlist_ptr +SourcesModel::indexToPlaylist( const QModelIndex& index ) +{ + playlist_ptr res; + if ( !index.isValid() ) + return res; + + QModelIndex idx = index.model()->index( index.row(), 0, index.parent() ); + int type = idx.data( Qt::UserRole + 1 ).toInt(); + if ( type == 1 ) + { + qlonglong pptr = idx.data( Qt::UserRole + 3 ).toLongLong(); + playlist_ptr* playlist = reinterpret_cast(pptr); + if ( playlist ) + return *playlist; + } + + return res; +} + + +SourceTreeItem* +SourcesModel::indexToTreeItem( const QModelIndex& index ) +{ + if ( !index.isValid() ) + return 0; + + QModelIndex idx = index.model()->index( index.row(), 0, index.parent() ); + int type = idx.data( Qt::UserRole + 1 ).toInt(); + if ( type == 0 || type == 1 ) + { + qlonglong pptr = idx.data( Qt::UserRole + 2 ).toLongLong(); + SourceTreeItem* item = reinterpret_cast(pptr); + if ( item ) + return item; + } + + return 0; +} diff --git a/src/sourcetree/sourcesmodel.h b/src/sourcetree/sourcesmodel.h new file mode 100644 index 000000000..c53f40b85 --- /dev/null +++ b/src/sourcetree/sourcesmodel.h @@ -0,0 +1,43 @@ +#ifndef SOURCESMODEL_H +#define SOURCESMODEL_H + +#include + +#include "tomahawk/source.h" +#include "tomahawk/typedefs.h" + +class SourceTreeItem; + +class SourcesModel : public QStandardItemModel +{ +Q_OBJECT + +public: + explicit SourcesModel( QObject* parent = 0 ); + + virtual QStringList mimeTypes() const; + virtual Qt::DropActions supportedDropActions() const; + virtual Qt::ItemFlags flags( const QModelIndex& index ) const; + + bool appendItem( const Tomahawk::source_ptr& source ); + bool removeItem( const Tomahawk::source_ptr& source ); + + static int indexType( const QModelIndex& index ); + static Tomahawk::playlist_ptr indexToPlaylist( const QModelIndex& index ); + static SourceTreeItem* indexToTreeItem( const QModelIndex& index ); + +signals: + void clicked( const QModelIndex& ); + +private slots: + void onSourceAdded( const Tomahawk::source_ptr& source ); + void onSourceRemoved( const Tomahawk::source_ptr& source ); + + void onItemOnline( const QModelIndex& idx ); + void onItemOffline( const QModelIndex& idx ); + +public slots: + void loadSources(); +}; + +#endif // SOURCESMODEL_H diff --git a/src/sourcetree/sourcetreeitem.cpp b/src/sourcetree/sourcetreeitem.cpp new file mode 100644 index 000000000..4d61fb07b --- /dev/null +++ b/src/sourcetree/sourcetreeitem.cpp @@ -0,0 +1,137 @@ +#include "sourcetreeitem.h" + +#include +#include + +#include "tomahawk/collection.h" +#include "tomahawk/playlist.h" +#include "tomahawk/tomahawkapp.h" + +using namespace Tomahawk; + + +SourceTreeItem::SourceTreeItem( const source_ptr& source, QObject* parent ) + : QObject( parent ) + , m_source( source ) +{ + QStandardItem* item = new QStandardItem( "" ); + item->setEditable( false ); + item->setData( (qlonglong)this, Qt::UserRole + 2 ); + item->setData( 0, Qt::UserRole + 1 ); + m_columns << item; + + connect( source.data()->collection().data(), SIGNAL( playlistsAdded( QList ) ), + SLOT( onPlaylistsAdded( QList ) ) ); + + connect( source.data()->collection().data(), SIGNAL( playlistsDeleted( QList ) ), + SLOT( onPlaylistsDeleted( QList ) ) ); + + m_widget = new SourceTreeItemWidget( source, (QWidget*)parent->parent() ); + connect( m_widget, SIGNAL( clicked() ), SLOT( onClicked() ) ); +} + + +void +SourceTreeItem::onClicked() +{ + emit clicked( m_columns.at( 0 )->index() ); +} + + +void +SourceTreeItem::onOnline() +{ + m_widget->onOnline(); +} + + +void +SourceTreeItem::onOffline() +{ + m_widget->onOffline(); +} + + +void +SourceTreeItem::onPlaylistsAdded( const QList& playlists ) +{ +// qDebug() << playlists; + + // const-ness is important for getting the right pointer! + foreach( const playlist_ptr& p, playlists ) + { + m_playlists.append( p ); + qlonglong ptr = qlonglong( &m_playlists.last() ); + qDebug() << "Playlist added:" << p->title() << p->creator() << p->info() << ptr; + + connect( p.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), + SLOT( onPlaylistLoaded( Tomahawk::PlaylistRevision ) ), + Qt::QueuedConnection); + + QStandardItem* subitem = new QStandardItem( p->title() ); + subitem->setIcon( QIcon( RESPATH "images/playlist-icon.png" ) ); + subitem->setEditable( false ); + subitem->setEnabled( false ); + subitem->setData( ptr, Qt::UserRole + 3 ); + subitem->setData( 1, Qt::UserRole + 1 ); + subitem->setData( (qlonglong)this, Qt::UserRole + 2 ); + + m_columns.at( 0 )->appendRow( subitem ); + ((QTreeView*)parent()->parent())->expandAll(); + + p->loadRevision(); + } +} + + +void +SourceTreeItem::onPlaylistsDeleted( const QList& playlists ) +{ + // const-ness is important for getting the right pointer! + foreach( const playlist_ptr& p, playlists ) + { + qlonglong ptr = qlonglong( p.data() ); + qDebug() << "Playlist removed:" << p->title() << p->creator() << p->info() << ptr; + + QStandardItem* item = m_columns.at( 0 ); + int rows = item->rowCount(); + for ( int i = rows - 1; i >= 0; i-- ) + { + QStandardItem* pi = item->child( i ); + qlonglong piptr = pi->data( Qt::UserRole + 3 ).toLongLong(); + playlist_ptr* pl = reinterpret_cast(piptr); + int type = pi->data( Qt::UserRole + 1 ).toInt(); + + if ( type == 1 && ptr == qlonglong( pl->data() ) ) + { + m_playlists.removeAll( p ); + item->removeRow( i ); + } + } + } +} + + +void +SourceTreeItem::onPlaylistLoaded( Tomahawk::PlaylistRevision revision ) +{ + qlonglong ptr = qlonglong( sender() ); + //qDebug() << "sender ptr:" << ptr; + + QStandardItem* item = m_columns.at( 0 ); + int rows = item->rowCount(); + for ( int i = 0; i < rows; i++ ) + { + QStandardItem* pi = item->child( i ); + qlonglong piptr = pi->data( Qt::UserRole + 3 ).toLongLong(); + playlist_ptr* pl = reinterpret_cast(piptr); + int type = pi->data( Qt::UserRole + 1 ).toInt(); + + if ( type == 1 && ptr == qlonglong( pl->data() ) ) + { + //qDebug() << "Found playlist!"; + pi->setEnabled( true ); + m_current_revisions.insert( pl->data()->guid(), revision.revisionguid ); + } + } +} diff --git a/src/sourcetree/sourcetreeitem.h b/src/sourcetree/sourcetreeitem.h new file mode 100644 index 000000000..3020835a4 --- /dev/null +++ b/src/sourcetree/sourcetreeitem.h @@ -0,0 +1,56 @@ +#ifndef SOURCETREEITEM_H +#define SOURCETREEITEM_H + +#include +#include + +#include "tomahawk/typedefs.h" +#include "sourcetreeitemwidget.h" + +class SourceTreeItem : public QObject +{ +Q_OBJECT + +public: + explicit SourceTreeItem( const Tomahawk::source_ptr& source, QObject* parent ); + virtual ~SourceTreeItem() + { + qDebug() << Q_FUNC_INFO; + } + + const Tomahawk::source_ptr& source() const { return m_source; }; + QList columns() const { return m_columns; }; + + QWidget* widget() const { return m_widget; }; + + // returns revision ID we are curently displaying for given playlist ID + QString currentlyLoadedPlaylistRevision( const QString& plguid ) const + { + return m_current_revisions.value( plguid ); + } + +signals: + void clicked( const QModelIndex& index ); + +public slots: + void onOnline(); + void onOffline(); + +private slots: + void onClicked(); + + void onPlaylistsAdded( const QList& playlists ); + void onPlaylistsDeleted( const QList& playlists ); + void onPlaylistLoaded( Tomahawk::PlaylistRevision revision ); + +private: + QList m_columns; + Tomahawk::source_ptr m_source; + SourceTreeItemWidget* m_widget; + QList m_playlists; + + // playist->guid() -> currently loaded revision + QMap m_current_revisions; +}; + +#endif // SOURCETREEITEM_H diff --git a/src/sourcetree/sourcetreeitemwidget.cpp b/src/sourcetree/sourcetreeitemwidget.cpp new file mode 100644 index 000000000..7e0ca3697 --- /dev/null +++ b/src/sourcetree/sourcetreeitemwidget.cpp @@ -0,0 +1,117 @@ +#include "sourcetreeitemwidget.h" + +#include "ui_sourcetreeitemwidget.h" + +#include "tomahawk/tomahawkapp.h" +#include "database.h" +#include "databasecommand_collectionstats.h" +#include "dbsyncconnection.h" + +using namespace Tomahawk; + + +SourceTreeItemWidget::SourceTreeItemWidget( const source_ptr& source, QWidget* parent ) : + QWidget( parent ), + m_source( source ), + ui( new Ui::SourceTreeItemWidget ) +{ +// qDebug() << Q_FUNC_INFO; + ui->setupUi( this ); + ui->verticalLayout->setSpacing( 3 ); + + connect( source.data(), SIGNAL( loadingStateChanged( DBSyncConnection::State, DBSyncConnection::State, QString ) ), + SLOT( onLoadingStateChanged( DBSyncConnection::State, DBSyncConnection::State, QString ) ) ); + + connect( source.data(), SIGNAL( stats( const QVariantMap& ) ), SLOT( gotStats( const QVariantMap& ) ) ); + + ui->avatarImage->setPixmap( QPixmap( RESPATH "images/user-avatar.png" ) ); + + QString displayname = source->friendlyName(); + if( displayname.isEmpty() ) + displayname = source->userName(); + + ui->nameLabel->setText( displayname ); + ui->infoLabel->setForegroundRole( QPalette::Dark ); + ui->infoLabel->setText( "???" ); + + connect( ui->onOffButton, SIGNAL( clicked() ), SIGNAL( clicked() ) ); + + onOffline(); +} + + +SourceTreeItemWidget::~SourceTreeItemWidget() +{ + qDebug() << Q_FUNC_INFO; + delete ui; +} + + +void +SourceTreeItemWidget::changeEvent( QEvent* e ) +{ + QWidget::changeEvent( e ); + switch ( e->type() ) + { + case QEvent::LanguageChange: +// ui->retranslateUi( this ); + break; + + default: + break; + } +} + + +void +SourceTreeItemWidget::gotStats( const QVariantMap& stats ) +{ + ui->infoLabel->setText( tr( "%L1 tracks" ).arg( stats.value( "numfiles" ).toInt() ) ); +} + + +void +SourceTreeItemWidget::onLoadingStateChanged( DBSyncConnection::State newstate, DBSyncConnection::State, const QString& info ) +{ + QString msg; + switch( newstate ) + { + case DBSyncConnection::CHECKING: + msg = tr( "Checking" ); + break; + case DBSyncConnection::FETCHING: + msg = tr( "Fetching" ); + break; + case DBSyncConnection::PARSING: + msg = tr( "Parsing" ); + break; + case DBSyncConnection::SAVING: + msg = tr( "Saving" ); + break; + case DBSyncConnection::SYNCED: + msg = tr( "Synced" ); + break; + case DBSyncConnection::SCANNING: + msg = tr( "Scanning (%L1 tracks)" ).arg( info ); + break; + + default: + msg = "???"; + } + + ui->infoLabel->setText( msg ); +} + + +void +SourceTreeItemWidget::onOnline() +{ + ui->onOffButton->setPixmap( RESPATH "images/source-on-rest.png" ); +} + + +void +SourceTreeItemWidget::onOffline() +{ + ui->onOffButton->setPixmap( RESPATH "images/source-off-rest.png" ); +} diff --git a/src/sourcetree/sourcetreeitemwidget.h b/src/sourcetree/sourcetreeitemwidget.h new file mode 100644 index 000000000..015412b3a --- /dev/null +++ b/src/sourcetree/sourcetreeitemwidget.h @@ -0,0 +1,41 @@ +#ifndef SOURCETREEITEMWIDGET_H +#define SOURCETREEITEMWIDGET_H + +#include + +#include "tomahawk/source.h" + +namespace Ui +{ + class SourceTreeItemWidget; +} + +class SourceTreeItemWidget : public QWidget +{ +Q_OBJECT + +public: + SourceTreeItemWidget( const Tomahawk::source_ptr& source, QWidget* parent = 0 ); + ~SourceTreeItemWidget(); + +signals: + void clicked(); + +public slots: + void onOnline(); + void onOffline(); + +protected: + void changeEvent( QEvent* e ); + +private slots: + void gotStats( const QVariantMap& stats ); + void onLoadingStateChanged( DBSyncConnection::State newstate, DBSyncConnection::State oldstate, const QString& info ); + +private: + Tomahawk::source_ptr m_source; + + Ui::SourceTreeItemWidget* ui; +}; + +#endif // SOURCETREEITEMWIDGET_H diff --git a/src/sourcetree/sourcetreeitemwidget.ui b/src/sourcetree/sourcetreeitemwidget.ui new file mode 100644 index 000000000..8910bcde2 --- /dev/null +++ b/src/sourcetree/sourcetreeitemwidget.ui @@ -0,0 +1,170 @@ + + + SourceTreeItemWidget + + + + 0 + 0 + 400 + 44 + + + + + 0 + 0 + + + + + 0 + 44 + + + + + 16777215 + 44 + + + + Form + + + + 1 + + + 0 + + + 1 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 2 + 20 + + + + + + + + + 0 + 0 + + + + + 29 + 29 + + + + + 29 + 29 + + + + TextLabel + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 4 + 20 + + + + + + + + 5 + + + 0 + + + 4 + + + + + + 75 + true + + + + TextLabel + + + + + + + TextLabel + + + + + + + + + + 0 + 0 + + + + + 56 + 0 + + + + + 56 + 16777215 + + + + Off + + + + + + + + ImageButton + QPushButton +
imagebutton.h
+
+
+ + +
diff --git a/src/sourcetree/sourcetreeview.cpp b/src/sourcetree/sourcetreeview.cpp new file mode 100644 index 000000000..0225405fe --- /dev/null +++ b/src/sourcetree/sourcetreeview.cpp @@ -0,0 +1,338 @@ +#include "sourcetreeview.h" + +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/playlist.h" +#include "sourcetreeitem.h" +#include "sourcesmodel.h" +#include "playlistview.h" +#include "playlistmodel.h" + +#include +#include +#include +#include +#include + +using namespace Tomahawk; + + +SourceTreeView::SourceTreeView( QWidget* parent ) + : QTreeView( parent ) + , m_collectionModel( new PlaylistModel() ) + , m_dragging( false ) +{ + setHeaderHidden( true ); + setRootIsDecorated( false ); + setExpandsOnDoubleClick( false ); + + setSelectionBehavior( QAbstractItemView::SelectRows ); + setDragDropMode( QAbstractItemView::DropOnly ); + setAcceptDrops( true ); + setDropIndicatorShown( false ); + setMouseTracking( true ); + setAllColumnsShowFocus( false ); + + setupMenus(); + + m_model = new SourcesModel( this ); + setModel( m_model ); + + header()->setStretchLastSection( false ); + header()->setResizeMode( 0, QHeaderView::Stretch ); + + connect( m_model, SIGNAL( clicked( QModelIndex ) ), SIGNAL( clicked( QModelIndex ) ) ); + connect( this, SIGNAL( clicked( QModelIndex ) ), SLOT( onItemActivated( QModelIndex ) ) ); + + connect( selectionModel(), SIGNAL( selectionChanged( const QItemSelection&, const QItemSelection& ) ), SLOT( onSelectionChanged() ) ); + connect( &APP->sourcelist(), SIGNAL( sourceRemoved( Tomahawk::source_ptr ) ), SLOT( onSourceOffline( Tomahawk::source_ptr ) ) ); +} + + +void +SourceTreeView::setupMenus() +{ + m_loadPlaylistAction = m_playlistMenu.addAction( tr( "&Load Playlist" ) ); + m_playlistMenu.addSeparator(); + m_deletePlaylistAction = m_playlistMenu.addAction( tr( "&Delete Playlist" ) ); + + connect( m_loadPlaylistAction, SIGNAL( triggered() ), SLOT( loadPlaylist() ) ); + connect( m_deletePlaylistAction, SIGNAL( triggered() ), SLOT( deletePlaylist() ) ); + + setContextMenuPolicy( Qt::CustomContextMenu ); + connect( this, SIGNAL( customContextMenuRequested( const QPoint& ) ), SLOT( onCustomContextMenu( const QPoint& ) ) ); +} + + +void +SourceTreeView::onSourceOffline( Tomahawk::source_ptr src ) +{ + qDebug() << Q_FUNC_INFO; + if ( m_sources.contains( src ) ) + { + qDebug() << "Removing source from active view"; + m_sources.removeAll( src ); + m_collectionModel->removeSource( src ); + // emit onOffline( index ); // FIXME still need to emit this here i think + } + else + { + qDebug() << "not removing source from active view (not active)"; + } +} + +void +SourceTreeView::onItemActivated( const QModelIndex& index ) +{ + if ( !index.isValid() ) + return; + + int type = SourcesModel::indexType( index ); + if ( type == 0 ) + { + SourceTreeItem* item = SourcesModel::indexToTreeItem( index ); + if ( item ) + { + if ( APP->playlistView()->playlistModel() == 0 || APP->playlistView()->playlistModel() == m_collectionModel ) + { + if ( m_collectionModel->isBusy() ) + return; + + qDebug() << "SourceTreeItem toggled:" << item->source()->userName(); + + if ( !APP->playlistView()->playlistModel() ) + APP->playlistView()->setModel( m_collectionModel ); + + // Merge / unmerge the Source's collection(s) in the PlaylistView + if ( m_sources.contains( item->source() ) ) + { + m_sources.removeAll( item->source() ); + m_collectionModel->removeSource( item->source() ); + emit onOffline( index ); + } + else + { + m_sources.append( item->source() ); + m_collectionModel->addSource( item->source() ); + emit onOnline( index ); + } + } + else + APP->playlistView()->setModel( m_collectionModel ); + } + } + else if ( type == 1 ) + { + playlist_ptr playlist = SourcesModel::indexToPlaylist( index ); + if ( !playlist.isNull() ) + { + qDebug() << "Playlist activated:" << playlist->title(); + + PlaylistModel* pm = new PlaylistModel(); + pm->loadPlaylist( playlist ); + + // load playlist before setting it on the model + // so the view knows about sorting settings + APP->playlistView()->setModel( pm ); + playlist->resolve(); + } + } +} + + +void +SourceTreeView::onSelectionChanged() +{ + QModelIndexList si = selectedIndexes(); + + foreach( const QModelIndex& idx, si ) + { + int type = SourcesModel::indexType( idx ); + if ( type == 0 ) + selectionModel()->select( idx, QItemSelectionModel::Deselect ); + } +} + + +void +SourceTreeView::loadPlaylist() +{ + onItemActivated( m_contextMenuIndex ); +} + + +void +SourceTreeView::deletePlaylist() +{ + qDebug() << Q_FUNC_INFO; + + QModelIndex idx = m_contextMenuIndex; + if ( !idx.isValid() ) + return; + + int type = SourcesModel::indexType( idx ); + if ( type == 1 ) + { + playlist_ptr playlist = SourcesModel::indexToPlaylist( idx ); + if ( !playlist.isNull() ) + { + qDebug() << "Playlist about to be deleted:" << playlist->title(); + Playlist::remove( playlist ); + } + } +} + + +void +SourceTreeView::onCustomContextMenu( const QPoint& pos ) +{ + qDebug() << Q_FUNC_INFO; + + QModelIndex idx = m_contextMenuIndex = indexAt( pos ); + if ( !idx.isValid() ) + return; + + if ( SourcesModel::indexType( idx ) ) + { + m_playlistMenu.exec( mapToGlobal( pos ) ); + } +} + + +void +SourceTreeView::dragEnterEvent( QDragEnterEvent* event ) +{ + qDebug() << Q_FUNC_INFO; + QTreeView::dragEnterEvent( event ); + + if ( event->mimeData()->hasFormat( "application/tomahawk.query.list" ) ) + { + m_dragging = true; + m_dropRect = QRect(); + + qDebug() << "Accepting Drag Event"; + event->setDropAction( Qt::CopyAction ); + event->accept(); + } +} + + +void +SourceTreeView::dragMoveEvent( QDragMoveEvent* event ) +{ + bool accept = false; + QTreeView::dragMoveEvent( event ); + + if ( event->mimeData()->hasFormat( "application/tomahawk.query.list" ) ) + { + setDirtyRegion( m_dropRect ); + const QPoint pos = event->pos(); + const QModelIndex index = indexAt( pos ); + + if ( index.isValid() ) + { + const QRect rect = visualRect( index ); + m_dropRect = rect; + + if ( SourcesModel::indexType( index ) == 1 ) + { + playlist_ptr playlist = SourcesModel::indexToPlaylist( index ); + if ( !playlist.isNull() && playlist->author()->isLocal() ) + accept = true; + } + } + + if ( accept ) + { + event->setDropAction( Qt::CopyAction ); + event->accept(); + } + else + event->ignore(); + + setDirtyRegion( m_dropRect ); + } +} + + +void +SourceTreeView::dropEvent( QDropEvent* event ) +{ + bool accept = false; + const QPoint pos = event->pos(); + const QModelIndex index = indexAt( pos ); + + if ( event->mimeData()->hasFormat( "application/tomahawk.query.list" ) ) + { + const QPoint pos = event->pos(); + const QModelIndex index = indexAt( pos ); + + if ( index.isValid() ) + { + if ( SourcesModel::indexType( index ) == 1 ) + { + playlist_ptr playlist = SourcesModel::indexToPlaylist( index ); + if ( !playlist.isNull() && playlist->author()->isLocal() ) + { + accept = true; + QByteArray itemData = event->mimeData()->data( "application/tomahawk.query.list" ); + QDataStream stream( &itemData, QIODevice::ReadOnly ); + QList queries; + + while ( !stream.atEnd() ) + { + qlonglong qptr; + stream >> qptr; + + Tomahawk::query_ptr* query = reinterpret_cast(qptr); + if ( query && !query->isNull() ) + { + qDebug() << "Dropped query item:" << query->data()->artist() << "-" << query->data()->track(); + queries << *query; + } + } + + qDebug() << "on playlist:" << playlist->title() << playlist->guid(); + + SourceTreeItem* treeItem = SourcesModel::indexToTreeItem( index ); + if ( treeItem ) + { + QString rev = treeItem->currentlyLoadedPlaylistRevision( playlist->guid() ); + playlist->addEntries( queries, rev ); + } + } + } + } + + if ( accept ) + { + event->setDropAction( Qt::CopyAction ); + event->accept(); + } + else + event->ignore(); + } + + QTreeView::dropEvent( event ); + m_dragging = false; +} + + +void +SourceTreeView::paintEvent( QPaintEvent* event ) +{ + QTreeView::paintEvent( event ); + + if ( m_dragging && !m_dropRect.isEmpty() ) + { + QPainter painter( viewport() ); + const QModelIndex index = indexAt( m_dropRect.topLeft() ); + const QRect itemRect = visualRect( index ); + + QStyleOptionViewItemV4 opt; + opt.initFrom( this ); + opt.rect = itemRect; + opt.state = QStyle::State_Enabled | QStyle::State_MouseOver; + + style()->drawPrimitive( QStyle::PE_PanelItemViewItem, &opt, &painter, this ); + } +} diff --git a/src/sourcetree/sourcetreeview.h b/src/sourcetree/sourcetreeview.h new file mode 100644 index 000000000..3c9cd4918 --- /dev/null +++ b/src/sourcetree/sourcetreeview.h @@ -0,0 +1,60 @@ +#ifndef SOURCETREEVIEW_H +#define SOURCETREEVIEW_H + +#include +#include + +#include "tomahawk/source.h" + +class PlaylistModel; +class SourcesModel; + +class SourceTreeView : public QTreeView +{ +Q_OBJECT + +public: + explicit SourceTreeView( QWidget* parent = 0 ); + +signals: + void onOnline( const QModelIndex& index ); + void onOffline( const QModelIndex& index ); + +private slots: + void onItemActivated( const QModelIndex& index ); + void onSelectionChanged(); + + void loadPlaylist(); + void deletePlaylist(); + + void onCustomContextMenu( const QPoint& pos ); + void onSourceOffline( Tomahawk::source_ptr ); + +protected: + void drawBranches( QPainter* painter, const QRect& rect, const QModelIndex& index ) const {} + void drawTree( QPainter* painter, const QRegion& region ) const {} + + virtual void paintEvent( QPaintEvent* event ); + + virtual void dragEnterEvent( QDragEnterEvent* event ); + virtual void dragLeaveEvent( QDragLeaveEvent* event ) { m_dragging = false; setDirtyRegion( m_dropRect ); } + virtual void dragMoveEvent( QDragMoveEvent* event ); + virtual void dropEvent( QDropEvent* event ); + +private: + void setupMenus(); + + PlaylistModel* m_collectionModel; + SourcesModel* m_model; + QModelIndex m_contextMenuIndex; + + QMenu m_playlistMenu; + QAction* m_loadPlaylistAction; + QAction* m_deletePlaylistAction; + + bool m_dragging; + QRect m_dropRect; + QList m_sources; +}; + +#endif // SOURCETREEVIEW_H diff --git a/src/tomahawkapp.cpp b/src/tomahawkapp.cpp new file mode 100644 index 000000000..b5e876884 --- /dev/null +++ b/src/tomahawkapp.cpp @@ -0,0 +1,671 @@ +#include "tomahawk/tomahawkapp.h" + +#include +#include +#include +#include +#include +#include + +#include "tomahawk/collection.h" +#include "database/database.h" +#include "database/databasecollection.h" +#include "database/databasecommand_collectionstats.h" +#include "database/databaseresolver.h" +#include "jabber/jabber.h" +#include "utils/tomahawkutils.h" +#include "web/api_v1.h" +#include "scriptresolver.h" + +#include "controlconnection.h" +#include "tomahawkzeroconf.h" +#include "settingsdialog.h" + +#ifndef TOMAHAWK_HEADLESS + #include "audioengine.h" + #include "tomahawkwindow.h" + #include "playlistview.h" +#endif + +#include +#include + +#define LOGFILE TomahawkUtils::appDataDir().filePath( "tomahawk.log" ).toLocal8Bit() +#define LOGFILE_SIZE 1024 * 512 +#include "tomahawksettings.h" + +using namespace std; +ofstream logfile; + +void TomahawkLogHandler( QtMsgType type, const char *msg ) +{ + switch( type ) + { + case QtDebugMsg: + logfile << QTime::currentTime().toString().toAscii().data() << " Debug: " << msg << "\n"; + break; + + case QtCriticalMsg: + logfile << QTime::currentTime().toString().toAscii().data() << " Critical: " << msg << "\n"; + break; + + case QtWarningMsg: + logfile << QTime::currentTime().toString().toAscii().data() << " Warning: " << msg << "\n"; + break; + + case QtFatalMsg: + logfile << QTime::currentTime().toString().toAscii().data() << " Fatal: " << msg << "\n"; + logfile.flush(); + + cout << msg << "\n"; + cout.flush(); + abort(); + break; + } + + cout << msg << "\n"; + cout.flush(); + logfile.flush(); +} + +void setupLogfile() +{ + if ( QFileInfo( LOGFILE ).size() > LOGFILE_SIZE ) + { + QByteArray lc; + { + QFile f( LOGFILE ); + f.open( QIODevice::ReadOnly | QIODevice::Text ); + lc = f.readAll(); + f.close(); + } + + QFile::remove( LOGFILE ); + + { + QFile f( LOGFILE ); + f.open( QIODevice::WriteOnly | QIODevice::Text ); + f.write( lc.right( LOGFILE_SIZE - (LOGFILE_SIZE / 4) ) ); + f.close(); + } + } + + logfile.open( LOGFILE, ios::app ); + qInstallMsgHandler( TomahawkLogHandler ); +} + + +using namespace Tomahawk; + +TomahawkApp::TomahawkApp( int& argc, char *argv[] ) + : TOMAHAWK_APPLICATION( argc, argv ) + , m_zeroconf( 0 ) + , m_settings( 0 ) + , m_nam( 0 ) +{ + qsrand( QTime( 0, 0, 0 ).secsTo( QTime::currentTime() ) ); + +#ifdef TOMAHAWK_HEADLESS + m_headless = true; +#else + m_audioEngine = 0; + m_mainwindow = 0; + m_headless = arguments().contains( "--headless" ); + setWindowIcon( QIcon( RESPATH "icons/tomahawk-icon-128.png" ) ); +#ifndef NO_LIBLASTFM + m_scrobbler = 0; +#endif +#endif + + qDebug() << "TomahawkApp thread:" << this->thread(); + setOrganizationName( "Tomahawk" ); + setOrganizationDomain( "tomahawk.org" ); + setApplicationName( "Player" ); + setApplicationVersion( "1.0" ); // FIXME: last.fm "tst" auth requires 1.0 version according to docs, will change when we get our own identifier + registerMetaTypes(); + setupLogfile(); + + m_settings = new TomahawkSettings( this ); + setupDatabase(); + +#ifndef TOMAHAWK_HEADLESS + if ( !m_headless ) + { + m_audioEngine = new AudioEngine; + +#ifndef NO_LIBLASTFM + m_scrobbler = new Scrobbler( this ); + m_nam = new lastfm::NetworkAccessManager( this ); + + connect( m_audioEngine, SIGNAL( started( const Tomahawk::result_ptr& ) ), + m_scrobbler, SLOT( trackStarted( const Tomahawk::result_ptr& ) ), Qt::QueuedConnection ); + + connect( m_audioEngine, SIGNAL( paused() ), + m_scrobbler, SLOT( trackPaused() ), Qt::QueuedConnection ); + + connect( m_audioEngine, SIGNAL( resumed() ), + m_scrobbler, SLOT( trackResumed() ), Qt::QueuedConnection ); + + connect( m_audioEngine, SIGNAL( stopped() ), + m_scrobbler, SLOT( trackStopped() ), Qt::QueuedConnection ); +#else + m_nam = new QNetworkAccessManager; +#endif + + m_mainwindow = new TomahawkWindow(); + m_mainwindow->show(); + connect( m_mainwindow, SIGNAL( settingsChanged() ), SIGNAL( settingsChanged() ) ); + } +#endif + + boost::function(result_ptr)> fac = + boost::bind( &TomahawkApp::httpIODeviceFactory, this, _1 ); + this->registerIODeviceFactory( "http", fac ); + + setupPipeline(); + initLocalCollection(); + startServent(); + //loadPlugins(); + + if( arguments().contains( "--http" ) || settings()->value( "network/http", true ).toBool() ) + startHTTP(); + + if( !arguments().contains("--nojabber") ) setupJabber(); + + if ( !arguments().contains( "--nozeroconf" ) ) + { + // advertise our servent on the LAN + m_zeroconf = new TomahawkZeroconf( m_servent.port(), this ); + connect( m_zeroconf, SIGNAL( tomahawkHostFound( const QString&, int, const QString&, const QString& ) ), + SLOT( lanHostFound( const QString&, int, const QString&, const QString& ) ) ); + m_zeroconf->advertise(); + } + + if ( !m_settings->hasScannerPath() ) + { + m_mainwindow->showSettingsDialog(); + } +} + + +TomahawkApp::~TomahawkApp() +{ + qDebug() << Q_FUNC_INFO; + + if ( !m_jabber.isNull() ) + { + m_jabber.clear(); + } + +#ifndef TOMAHAWK_HEADLESS + delete m_mainwindow; + delete m_audioEngine; +#endif + + delete m_zeroconf; + delete m_db; + + // always last thing, incase other objects save state on exit: + delete m_settings; +} + + +TomahawkApp* +TomahawkApp::instance() +{ + return (TomahawkApp*)TOMAHAWK_APPLICATION::instance(); +} + + +#ifndef TOMAHAWK_HEADLESS +AudioControls* +TomahawkApp::audioControls() +{ + return m_mainwindow->audioControls(); +} + + +PlaylistView* +TomahawkApp::playlistView() +{ + return m_mainwindow->playlistView(); +} +#endif + + +void +TomahawkApp::registerMetaTypes() +{ + qRegisterMetaType< QSharedPointer >("QSharedPointer"); + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< DBSyncConnection::State >("DBSyncConnection::State"); + qRegisterMetaType< msg_ptr >("msg_ptr"); + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< Connection* >("Connection*"); + qRegisterMetaType< QAbstractSocket::SocketError >("QAbstractSocket::SocketError"); + qRegisterMetaType< QSharedPointer >("QSharedPointer"); + qRegisterMetaType< QFileInfo >("QFileInfo"); + qRegisterMetaType< QMap >("QMap"); + qRegisterMetaType< QMap< QString, plentry_ptr > >("QMap< QString, plentry_ptr >"); + qRegisterMetaType< QHash< QString, QMap > >("QHash< QString, QMap >"); + + // Extra definition for namespaced-versions of signals/slots required + qRegisterMetaType< Tomahawk::collection_ptr >("Tomahawk::collection_ptr"); + qRegisterMetaType< Tomahawk::result_ptr >("Tomahawk::result_ptr"); + qRegisterMetaType< Tomahawk::source_ptr >("Tomahawk::source_ptr"); + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< QMap< QString, Tomahawk::plentry_ptr > >("QMap< QString, Tomahawk::plentry_ptr >"); + qRegisterMetaType< Tomahawk::PlaylistRevision >("Tomahawk::PlaylistRevision"); + qRegisterMetaType< Tomahawk::QID >("Tomahawk::QID"); + qRegisterMetaType< QTcpSocket* >("QTcpSocket*"); + + #ifndef TOMAHAWK_HEADLESS + qRegisterMetaType< QList >("QList"); + qRegisterMetaType< AudioErrorCode >("AudioErrorCode"); + #endif +} + + +void +TomahawkApp::setupDatabase() +{ + QString dbpath; + if( arguments().contains( "--testdb" ) ) + { + dbpath = QDir::currentPath() + "/test.db"; + } + else + { + dbpath = TomahawkUtils::appDataDir().absoluteFilePath( "tomahawk.db" ); + } + + qDebug() << "Using database:" << dbpath; + m_db = new Database( dbpath, this ); + m_pipeline.databaseReady(); +} + + +void +TomahawkApp::lanHostFound( const QString& host, int port, const QString& name, const QString& nodeid ) +{ + qDebug() << "Found LAN host:" << host << port << nodeid; + + if ( !m_servent.connectedToSession( nodeid ) ) + m_servent.connectToPeer( host, port, "whitelist", name, nodeid ); +} + + +void +TomahawkApp::startHTTP() +{ + m_session.setPort( 60210 ); //TODO config + m_session.setListenInterface( QHostAddress::LocalHost ); + m_session.setConnector( &m_connector ); + + Api_v1* api = new Api_v1( &m_session ); + m_session.setStaticContentService( api ); + + qDebug() << "Starting HTTPd on" << m_session.listenInterface().toString() << m_session.port(); + m_session.start(); + +} + + +void +TomahawkApp::setupPipeline() +{ + // setup resolvers for local content, and (cached) remote collection content + m_pipeline.addResolver( new DatabaseResolver( true, 100 ) ); + m_pipeline.addResolver( new DatabaseResolver( false, 90 ) ); + +// new ScriptResolver("/home/rj/src/tomahawk-core/contrib/magnatune/magnatune-resolver.php"); +} + + +void +TomahawkApp::initLocalCollection() +{ + source_ptr src( new Source( "My Collection" ) ); + collection_ptr coll( new DatabaseCollection( src ) ); + + src->addCollection( coll ); + this->sourcelist().add( src ); + + boost::function(result_ptr)> fac = + boost::bind( &TomahawkApp::localFileIODeviceFactory, this, _1 ); + this->registerIODeviceFactory( "file", fac ); + + // to make the stats signal be emitted by our local source + // this will update the sidebar, etc. + DatabaseCommand_CollectionStats* cmd = new DatabaseCommand_CollectionStats( src ); + connect( cmd, SIGNAL( done( const QVariantMap& ) ), + src.data(), SLOT( setStats( const QVariantMap& ) ), Qt::QueuedConnection ); + database()->enqueue( QSharedPointer( cmd ) ); +} + + +void +TomahawkApp::startServent() +{ + bool upnp = arguments().contains( "--upnp" ) || settings()->value( "network/upnp", true ).toBool(); + if ( !m_servent.startListening( QHostAddress( QHostAddress::Any ), upnp ) ) + { + qDebug() << "Failed to start listening with servent"; + exit( 1 ); + } + + //QString key = m_servent.createConnectionKey(); + //qDebug() << "Generated an offer key: " << key; + + boost::function(result_ptr)> fac = + boost::bind( &Servent::remoteIODeviceFactory, &m_servent, _1 ); + + this->registerIODeviceFactory( "servent", fac ); +} + + +void +TomahawkApp::loadPlugins() +{ + // look in same dir as executable for plugins + QDir dir( TomahawkApp::instance()->applicationDirPath() ); + QStringList filters; + filters << "*.so" << "*.dll" << "*.dylib"; + + QStringList files = dir.entryList( filters ); + foreach( const QString& filename, files ) + { + qDebug() << "Attempting to load" << QString( "%1/%2" ).arg( dir.absolutePath() ).arg( filename ); + + QPluginLoader loader( dir.absoluteFilePath( filename ) ); + if ( QObject* inst = loader.instance() ) + { + TomahawkPlugin* pluginst = qobject_cast(inst); + if ( !pluginst ) + continue; + + PluginAPI* api = new PluginAPI( this->pipeline() ); + TomahawkPlugin* plugin = pluginst->factory( api ); + qDebug() << "Loaded Plugin:" << plugin->name(); + qDebug() << plugin->description(); + m_plugins.append( plugin ); + + // plugins responsibility to register itself as a resolver/collection + // all we need to do is create an instance of it. + } + else + { + qDebug() << "PluginLoader failed to create instance:" << filename << " Err:" << loader.errorString(); + } + } +} + + +void +TomahawkApp::setupJabber() //const QString& jid, const QString& pass, const QString server +{ + qDebug() << Q_FUNC_INFO; + if( !m_jabber.isNull() ) + return; + + if ( !m_settings->value( "jabber/autoconnect", true ).toBool() ) + return; + + QString jid = m_settings->value( "jabber/username" ).toString(); + QString server = m_settings->value( "jabber/server" ).toString(); + QString password = m_settings->value( "jabber/password" ).toString(); + unsigned int port = m_settings->value( "jabber/port", 5222 ).toUInt(); + + // gtalk check + if( server.isEmpty() && ( + jid.contains("@gmail.com") || + jid.contains("@googlemail.com") ) ) + { + qDebug() << "Setting jabber server to talk.google.com"; + server = "talk.google.com"; + } + + if ( port < 1 || port > 65535 || jid.isEmpty() || password.isEmpty() ) + { + qDebug() << "Jabber credentials look wrong, not connecting"; + return; + } + + m_jabber = QSharedPointer( new Jabber( jid, password, server, port ) ); + + connect( m_jabber.data(), SIGNAL( peerOnline( const QString& ) ), + SLOT( jabberPeerOnline( const QString& ) ) ); + connect( m_jabber.data(), SIGNAL( peerOffline( const QString& ) ), + SLOT( jabberPeerOffline( const QString& ) ) ); + connect( m_jabber.data(), SIGNAL( msgReceived( const QString&, const QString& ) ), + SLOT( jabberMessage( const QString&, const QString& ) ) ); + + connect( m_jabber.data(), SIGNAL( disconnected() ), SLOT( jabberDisconnected() ) ); + //connect( m_jabber.data(), SIGNAL( finished() ), SLOT( jabberDisconnected() ) ); + connect( m_jabber.data(), SIGNAL( connected() ), SLOT( jabberConnected() ) ); + connect( m_jabber.data(), SIGNAL( authError(int, const QString&) ), SLOT( jabberAuthError(int,const QString&) ) ); + + m_jabber->start(); +} + + +void +TomahawkApp::reconnectJabber() +{ + m_jabber.clear(); + setupJabber(); +} + + +void +TomahawkApp::jabberAuthError( int code, const QString& msg ) +{ + qWarning() << "Failed to connect to jabber" << code << msg; + +#ifndef TOMAHAWK_HEADLESS + if( m_mainwindow ) + { + m_mainwindow->setWindowTitle( QString("Tomahawk [jabber: %1, portfwd: %2]") + .arg( "AUTH_ERROR" ) + .arg( (servent().externalPort() > 0) ? QString( "YES:%1" ).arg(servent().externalPort()) :"NO" ) ); + + QMessageBox::warning( m_mainwindow, + "Jabber Auth Error", + QString("Error connecting to Jabber (%1) %2").arg(code).arg(msg), + QMessageBox::Ok ); + } +#endif +} + + +void +TomahawkApp::jabberConnected() +{ + qDebug() << Q_FUNC_INFO; + +#ifndef TOMAHAWK_HEADLESS + if( m_mainwindow ) + { + m_mainwindow->setWindowTitle( QString("Tomahawk [jabber: %1, portfwd: %2]") + .arg( "CONNECTED" ) + .arg( (servent().externalPort() > 0) ? QString( "YES:%1" ).arg(servent().externalPort()):"NO" ) ); + } +#endif +} + + +void +TomahawkApp::jabberDisconnected() +{ + qDebug() << Q_FUNC_INFO; + +#ifndef TOMAHAWK_HEADLESS + if( m_mainwindow ) + { + m_mainwindow->setWindowTitle( QString("Tomahawk [jabber: %1, portfwd: %2]") + .arg( "DISCONNECTED" ) + .arg( (servent().externalPort() > 0) ? QString( "YES:%1" ).arg(servent().externalPort()):"NO" ) ); + } +#endif + m_jabber.clear(); +} + + +void +TomahawkApp::jabberPeerOnline( const QString& jid ) +{ +// qDebug() << Q_FUNC_INFO; +// qDebug() << "Jabber Peer online:" << jid; + + QVariantMap m; + if( m_servent.visibleExternally() ) + { + QString key = uuid(); + ControlConnection* conn = new ControlConnection( &m_servent ); + + const QString& nodeid = APP->nodeID(); + conn->setName( jid.left( jid.indexOf( "/" ) ) ); + conn->setId( nodeid ); + + // FIXME strip /resource, but we should use a UID per database install + //QString uniqname = jid.left( jid.indexOf("/") ); + //conn->setName( uniqname ); //FIXME + + // FIXME: + //QString ouruniqname = m_settings->value( "jabber/username" ).toString() + // .left( m_settings->value( "jabber/username" ).toString().indexOf("/") ); + + m_servent.registerOffer( key, conn ); + m["visible"] = true; + m["ip"] = m_servent.externalAddress().toString(); + m["port"] = m_servent.externalPort(); + m["key"] = key; + m["uniqname"] = nodeid; + + qDebug() << "Asking them to connect to us:" << m; + } + else + { + m["visible"] = false; + qDebug() << "We are not visible externally:" << m; + } + + QJson::Serializer ser; + QByteArray ba = ser.serialize( m ); + m_jabber->sendMsg( jid, QString::fromAscii( ba ) ); +} + + +void +TomahawkApp::jabberPeerOffline( const QString& jid ) +{ +// qDebug() << Q_FUNC_INFO; +// qDebug() << "Jabber Peer offline:" << jid; +} + + +void +TomahawkApp::jabberMessage( const QString& from, const QString& msg ) +{ + qDebug() << Q_FUNC_INFO; + qDebug() << "Jabber Message:" << from << msg; + + QJson::Parser parser; + bool ok; + QVariant v = parser.parse( msg.toAscii(), &ok ); + if ( !ok || v.type() != QVariant::Map ) + { + qDebug() << "Invalid JSON in XMPP msg"; + return; + } + + QVariantMap m = v.toMap(); + /* + If only one party is externally visible, connection is obvious + If both are, peer with lowest IP address initiates the connection. + This avoids dupe connections. + */ + if ( m.value( "visible" ).toBool() ) + { + if( !m_servent.visibleExternally() || + m_servent.externalAddress().toString() <= m.value( "ip" ).toString() ) + { + qDebug() << "Initiate connection to" << from; + m_servent.connectToPeer( m.value( "ip" ).toString(), + m.value( "port" ).toInt(), + m.value( "key" ).toString(), + from, + m.value( "uniqname" ).toString() ); + } + else + { + qDebug() << Q_FUNC_INFO << "THey should be conecting to us..."; + } + } + else + { + qDebug() << Q_FUNC_INFO << "THey are not visible, doing nothing atm"; + if( m_servent.visibleExternally() ) jabberPeerOnline( from ); // HACK FIXME + } +} + + +void +TomahawkApp::registerIODeviceFactory( const QString &proto, boost::function(Tomahawk::result_ptr)> fac ) +{ + m_iofactories.insert( proto, fac ); + qDebug() << "Registered IODevice Factory for" << proto; +} + + +QSharedPointer +TomahawkApp::getIODeviceForUrl( const Tomahawk::result_ptr& result ) +{ + qDebug() << Q_FUNC_INFO << thread(); + QSharedPointer sp; + + QRegExp rx( "^([a-zA-Z0-9]+)://(.+)$" ); + if ( rx.indexIn( result->url() ) == -1 ) + return sp; + + const QString proto = rx.cap( 1 ); + //const QString urlpart = rx.cap( 2 ); + if ( !m_iofactories.contains( proto ) ) + return sp; + + return m_iofactories.value( proto )( result ); +} + + +QSharedPointer +TomahawkApp::localFileIODeviceFactory( const Tomahawk::result_ptr& result ) +{ + // ignore "file://" at front of url + QFile * io = new QFile( result->url().mid( QString( "file://" ).length() ) ); + if ( io ) + io->open( QIODevice::ReadOnly ); + + return QSharedPointer( io ); +} + + +QSharedPointer +TomahawkApp::httpIODeviceFactory( const Tomahawk::result_ptr& result ) +{ + qDebug() << Q_FUNC_INFO << result->url(); + QNetworkRequest req( result->url() ); + QNetworkReply* reply = APP->nam()->get( req ); + return QSharedPointer( reply ); +} + + + +const QString& +TomahawkApp::nodeID() const +{ + return m_db->dbid(); +} diff --git a/src/tomahawksettings.cpp b/src/tomahawksettings.cpp new file mode 100644 index 000000000..5509d06b4 --- /dev/null +++ b/src/tomahawksettings.cpp @@ -0,0 +1,188 @@ +#include "tomahawksettings.h" +#include "settingsdialog.h" + +#include +#include +#include + + +TomahawkSettings::TomahawkSettings( QObject* parent ) + : QSettings( parent ) +{ + if( !contains( "configversion") ) + { + setValue( "configversion", SettingsDialog::VERSION ); + } + else if( value( "configversion" ).toUInt() != SettingsDialog::VERSION ) + { + qDebug() << "Config version outdated, old:" << value( "configversion" ).toUInt() + << "new:" << SettingsDialog::VERSION + << "Doing upgrade, if any..."; + + // insert upgrade code here as required + setValue( "configversion", SettingsDialog::VERSION ); + } +} + +TomahawkSettings::~TomahawkSettings() +{ +} + + +QString TomahawkSettings::scannerPath() const +{ + return value( "scannerpath", QDesktopServices::storageLocation( QDesktopServices::MusicLocation ) ).toString(); +} + +void TomahawkSettings::setScannerPath(const QString& path) +{ + setValue( "scannerpath", path ); +} + +bool TomahawkSettings::hasScannerPath() const +{ + return contains( "scannerpath" ); +} + +bool TomahawkSettings::httpEnabled() const +{ + return value( "network/http", true ).toBool(); +} + +void TomahawkSettings::setHttpEnabled(bool enable) +{ + setValue( "network/http", enable ); +} + +QByteArray TomahawkSettings::mainWindowGeometry() const +{ + return value( "ui/mainwindow/geometry" ).toByteArray(); +} + +void TomahawkSettings::setMainWindowGeometry(const QByteArray& geom) +{ + setValue( "ui/mainwindow/geometry", geom ); +} + +QByteArray TomahawkSettings::mainWindowState() const +{ + return value( "ui/mainwindow/state" ).toByteArray(); +} + +void TomahawkSettings::setMainWindowState(const QByteArray& state) +{ + setValue( "ui/mainwindow/state", state ); +} + +QList TomahawkSettings::playlistColumnSizes() const +{ + return value( "ui/playlist/columnSize" ).toList(); +} + +void TomahawkSettings::setPlaylistColumnSizes(const QList& cols) +{ + setValue( "ui/playlist/geometry", cols ); +} + + +bool TomahawkSettings::jabberAutoConnect() const +{ + return value( "jabber/autoconnect", true ).toBool(); +} + +void TomahawkSettings::setJabberAutoConnect(bool autoconnect) +{ + setValue( "jabber/autoconnect", autoconnect ); +} + +int TomahawkSettings::jabberPort() const +{ + return value( "jabber/port", 5222 ).toInt(); +} + +void TomahawkSettings::setJabberPort(int port) +{ + setValue( "jabber/port", port ); +} + +QString TomahawkSettings::jabberServer() const +{ + return value( "jabber/server" ).toString(); +} + +void TomahawkSettings::setJabberServer(const QString& server) +{ + setValue( "jabber/server", server ); +} + +QString TomahawkSettings::jabberUsername() const +{ + return value( "jabber/username" ).toString(); +} + +void TomahawkSettings::setJabberUsername(const QString& username) +{ + setValue( "jabber/username", username ); +} + +QString TomahawkSettings::jabberPassword() const +{ + return value( "jabber/password" ).toString(); +} + +void TomahawkSettings::setJabberPassword(const QString& pw) +{ + setValue( "jabber/password", pw ); +} + +bool TomahawkSettings::upnpEnabled() const +{ + return value( "network/upnp", true ).toBool(); +} + + +void TomahawkSettings::setUPnPEnabled(bool enable) +{ + setValue( "network/upnp", enable ); +} + +QString TomahawkSettings::lastFmPassword() const +{ + return value( "lastfm/password" ).toString(); +} + +void TomahawkSettings::setLastFmPassword(const QString& password) +{ + setValue( "lastfm/password", password ); +} + +QByteArray TomahawkSettings::lastFmSessionKey() const +{ + return value( "lastfm/sessionkey" ).toByteArray(); +} + +void TomahawkSettings::setLastFmSessionKey(const QByteArray& key) +{ + setValue( "lastfm/sessionkey", key ); +} + +QString TomahawkSettings::lastFmUsername() const +{ + return value( "lastfm/username" ).toString(); +} + +void TomahawkSettings::setLastFmUsername(const QString& username ) +{ + setValue( "lastfm/username", username ); +} + +bool TomahawkSettings::scrobblingEnabled() const +{ + return value( "lastfm/enablescrobbling", false ).toBool(); +} + +void TomahawkSettings::setScrobblingEnabled(bool enable) +{ + setValue( "lastfm/enablescrobbling", enable ); +} + diff --git a/src/tomahawksettings.h b/src/tomahawksettings.h new file mode 100644 index 000000000..af865b864 --- /dev/null +++ b/src/tomahawksettings.h @@ -0,0 +1,68 @@ +#ifndef TOMAHAWK_SETTINGS_H +#define TOMAHAWK_SETTINGS_h + +#include + +/** + * Convenience wrapper around QSettings for tomahawk-specific config + */ +class TomahawkSettings : public QSettings +{ + Q_OBJECT +public: + explicit TomahawkSettings(QObject* parent = 0); + virtual ~TomahawkSettings(); + + /// General settings + QString scannerPath() const; /// QDesktopServices::MusicLocation by default + void setScannerPath( const QString& path ); + bool hasScannerPath() const; + + /// UI settings + QByteArray mainWindowGeometry() const; + void setMainWindowGeometry( const QByteArray& geom ); + + QByteArray mainWindowState() const; + void setMainWindowState( const QByteArray& state ); + + QList playlistColumnSizes() const; + void setPlaylistColumnSizes( const QList& cols ); + + /// Jabber settings + bool jabberAutoConnect() const; /// true by default + void setJabberAutoConnect( bool autoconnect = false ); + + QString jabberUsername() const; + void setJabberUsername( const QString& username ); + + QString jabberPassword() const; + void setJabberPassword( const QString& pw ); + + QString jabberServer() const; + void setJabberServer( const QString& server ); + + int jabberPort() const; // default is 5222 + void setJabberPort( int port ); + + /// Network settings + bool httpEnabled() const; /// true by default + void setHttpEnabled( bool enable ); + + bool upnpEnabled() const; /// true by default + void setUPnPEnabled( bool enable ); + + /// Last.fm settings + bool scrobblingEnabled() const; /// false by default + void setScrobblingEnabled( bool enable ); + + QString lastFmUsername() const; + void setLastFmUsername( const QString& username ); + + QString lastFmPassword() const; + void setLastFmPassword( const QString& password ); + + QByteArray lastFmSessionKey() const; + void setLastFmSessionKey( const QByteArray& key ); +}; + +#endif diff --git a/src/tomahawkwindow.cpp b/src/tomahawkwindow.cpp new file mode 100644 index 000000000..f91b0350f --- /dev/null +++ b/src/tomahawkwindow.cpp @@ -0,0 +1,298 @@ +#include "tomahawkwindow.h" +#include "ui_tomahawkwindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tomahawk/tomahawkapp.h" +#include "tomahawk/functimeout.h" +#include "tomahawk/playlist.h" +#include "tomahawk/query.h" + +#include "database/databasecommand_collectionstats.h" +#include "topbar/topbar.h" + +#include "audiocontrols.h" +#include "audioengine.h" +#include "controlconnection.h" +#include "database.h" +#include "musicscanner.h" +#include "playlistview.h" +#include "playlistproxymodel.h" +#include "settingsdialog.h" +#include "xspfloader.h" +#include "proxystyle.h" +#include "tomahawksettings.h" +#include "widgetdragfilter.h" + +using namespace Tomahawk; + + +TomahawkWindow::TomahawkWindow( QWidget* parent ) + : QMainWindow( parent ) + , ui( new Ui::TomahawkWindow ) + , m_topbar( new TopBar( this ) ) + , m_audioControls( new AudioControls( this ) ) +{ + qApp->setStyle( new ProxyStyle() ); + setWindowIcon( QIcon( RESPATH "icons/tomahawk-icon-128.png" ) ); + +#ifdef Q_WS_MAC + setUnifiedTitleAndToolBarOnMac( true ); +#endif + + ui->setupUi( this ); + ui->playlistView->connectProgressBar( ui->actionProgress ); + ui->playlistView->setFocus(); + +#ifndef Q_WS_MAC + ui->centralWidget->layout()->setContentsMargins( 4, 4, 4, 2 ); +#else + ui->verticalLayout->setSpacing( 0 ); + ui->actionProgress->setAttribute( Qt::WA_MacShowFocusRect, 0 ); + ui->playlistView->setAttribute( Qt::WA_MacShowFocusRect, 0 ); + ui->sourceTreeView->setAttribute( Qt::WA_MacShowFocusRect, 0 ); +#endif + + QToolBar* toolbar = addToolBar( "TomahawkToolbar" ); + toolbar->setObjectName( "TomahawkToolbar" ); + toolbar->addWidget( m_topbar ); + toolbar->setMovable( false ); + toolbar->setFloatable( false ); + toolbar->installEventFilter( new WidgetDragFilter( toolbar ) ); + + m_audioControls->installEventFilter( new WidgetDragFilter( m_audioControls ) ); + + menuBar()->installEventFilter( new WidgetDragFilter( menuBar() ) ); + statusBar()->addPermanentWidget( m_audioControls, 1 ); + + loadSettings(); + setupSignals(); +} + + +TomahawkWindow::~TomahawkWindow() +{ + saveSettings(); + delete ui; +} + + +void +TomahawkWindow::loadSettings() +{ + TomahawkSettings* s = APP->settings(); + + if ( !s->mainWindowGeometry().isEmpty() ) + restoreGeometry( s->mainWindowGeometry() ); + if ( !s->mainWindowState().isEmpty() ) + restoreState( s->mainWindowState() ); +} + + +void +TomahawkWindow::saveSettings() +{ + TomahawkSettings* s = APP->settings(); + s->setMainWindowGeometry( saveGeometry() ); + s->setMainWindowState( saveState() ); +} + + +void +TomahawkWindow::setupSignals() +{ + connect( ui->actionExit, SIGNAL( triggered() ), + qApp, SLOT( closeAllWindows() ) ); + + connect( ui->actionLoadXSPF, SIGNAL( triggered() ), SLOT( loadSpiff() )); + connect( ui->actionCreatePlaylist, SIGNAL( triggered() ), SLOT( createPlaylist() )); + + // + connect( m_topbar, SIGNAL( filterTextChanged( const QString& ) ), + ui->playlistView, SLOT( setFilter( const QString& ) ) ); + + connect( ui->playlistView, SIGNAL( numSourcesChanged( unsigned int ) ), + m_topbar, SLOT( setNumSources( unsigned int ) ) ); + + connect( ui->playlistView, SIGNAL( numTracksChanged( unsigned int ) ), + m_topbar, SLOT( setNumTracks( unsigned int ) ) ); + + connect( ui->playlistView, SIGNAL( numArtistsChanged( unsigned int ) ), + m_topbar, SLOT( setNumArtists( unsigned int ) ) ); + + connect( ui->playlistView, SIGNAL( numShownChanged( unsigned int ) ), + m_topbar, SLOT( setNumShown( unsigned int ) ) ); + + // + connect( ui->playlistView->model(), SIGNAL( repeatModeChanged( PlaylistModelInterface::RepeatMode ) ), + m_audioControls, SLOT( onRepeatModeChanged( PlaylistModelInterface::RepeatMode ) ) ); + + connect( ui->playlistView->model(), SIGNAL( shuffleModeChanged( bool ) ), + m_audioControls, SLOT( onShuffleModeChanged( bool ) ) ); + + // + connect( ui->actionPreferences, SIGNAL( triggered() ), + SLOT( showSettingsDialog() ) ); + + connect( ui->actionAddPeerManually, SIGNAL( triggered() ), + SLOT( addPeerManually() ) ); + + connect( ui->actionRescanCollection, SIGNAL( triggered() ), + SLOT( rescanCollectionManually() ) ); + + //