mirror of
https://github.com/tomahawk-player/tomahawk.git
synced 2025-08-23 06:02:53 +02:00
Initial Tomahawk import.
This commit is contained in:
10
src/CMakeLists.linux.txt
Normal file
10
src/CMakeLists.linux.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
IF( "${gui}" STREQUAL "no" )
|
||||
ELSE()
|
||||
SET( OS_SPECIFIC_LINK_LIBRARIES
|
||||
${OS_SPECIFIC_LINK_LIBRARIES}
|
||||
alsaplayback
|
||||
gnutls
|
||||
)
|
||||
ENDIF()
|
||||
|
||||
#include( "CPack.txt" )
|
21
src/CMakeLists.osx.txt
Normal file
21
src/CMakeLists.osx.txt
Normal file
@@ -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()
|
298
src/CMakeLists.txt
Normal file
298
src/CMakeLists.txt
Normal file
@@ -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" )
|
28
src/CMakeLists.unix.txt
Normal file
28
src/CMakeLists.unix.txt
Normal file
@@ -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()
|
51
src/CMakeLists.win32.txt
Normal file
51
src/CMakeLists.win32.txt
Normal file
@@ -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()
|
70
src/CPack.txt
Normal file
70
src/CPack.txt
Normal file
@@ -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)
|
387
src/audio/audioengine.cpp
Normal file
387
src/audio/audioengine.cpp
Normal file
@@ -0,0 +1,387 @@
|
||||
#include "audioengine.h"
|
||||
|
||||
#include <QUrl>
|
||||
#include <QMutexLocker>
|
||||
|
||||
#include <tomahawk/tomahawkapp.h>
|
||||
|
||||
#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<QIODevice> 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<TranscodeInterface>(new MADTranscode());
|
||||
}
|
||||
#ifndef NO_OGG
|
||||
else if ( m_currentTrack->mimetype() == "application/ogg" )
|
||||
{
|
||||
m_transcode = QSharedPointer<TranscodeInterface>(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() ) );
|
||||
}
|
94
src/audio/audioengine.h
Normal file
94
src/audio/audioengine.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#ifndef AUDIOENGINE_H
|
||||
#define AUDIOENGINE_H
|
||||
|
||||
#include <QThread>
|
||||
#include <QMutex>
|
||||
#include <QBuffer>
|
||||
|
||||
#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<QIODevice> m_input;
|
||||
QSharedPointer<TranscodeInterface> 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
|
225
src/audio/madtranscode.cpp
Normal file
225
src/audio/madtranscode.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
/***************************************************************************
|
||||
* Copyright (C) 2005 - 2007 by *
|
||||
* Christian Muehlhaeuser, Last.fm Ltd <chris@last.fm> *
|
||||
* Erik Jaelevik, Last.fm Ltd <erik@last.fm> *
|
||||
* *
|
||||
* 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 <QDebug>
|
||||
|
||||
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 );
|
||||
}
|
||||
|
80
src/audio/madtranscode.h
Normal file
80
src/audio/madtranscode.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/***************************************************************************
|
||||
* Copyright (C) 2005 - 2007 by *
|
||||
* Christian Muehlhaeuser, Last.fm Ltd <chris@last.fm> *
|
||||
* *
|
||||
* 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 <QStringList>
|
||||
#include <QByteArray>
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
|
||||
#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
|
262
src/audio/rtaudiooutput.cpp
Normal file
262
src/audio/rtaudiooutput.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
#include <QMutexLocker>
|
||||
#include <QStringList>
|
||||
#include <QMessageBox>
|
||||
#include <QDebug>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
71
src/audio/rtaudiooutput.h
Normal file
71
src/audio/rtaudiooutput.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#ifndef RTAUDIOPLAYBACK_H
|
||||
#define RTAUDIOPLAYBACK_H
|
||||
|
||||
#include "RtAudio.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
|
||||
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
|
32
src/audio/transcodeinterface.h
Normal file
32
src/audio/transcodeinterface.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef TRANSCODEINTERFACE_H
|
||||
#define TRANSCODEINTERFACE_H
|
||||
|
||||
#include <QStringList>
|
||||
#include <QByteArray>
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
|
||||
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
|
140
src/audio/vorbistranscode.cpp
Normal file
140
src/audio/vorbistranscode.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
/***************************************************************************
|
||||
* Copyright (C) 2005 - 2006 by *
|
||||
* Christian Muehlhaeuser, Last.fm Ltd <chris@last.fm> *
|
||||
* *
|
||||
* 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] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
78
src/audio/vorbistranscode.h
Normal file
78
src/audio/vorbistranscode.h
Normal file
@@ -0,0 +1,78 @@
|
||||
/***************************************************************************
|
||||
* Copyright (C) 2005 - 2006 by *
|
||||
* Christian Muehlhaeuser, Last.fm Ltd <chris@last.fm> *
|
||||
* *
|
||||
* 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 <vorbis/codec.h>
|
||||
#include <vorbis/vorbisfile.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QDebug>
|
||||
#include <QStringList>
|
||||
|
||||
// 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
|
405
src/audiocontrols.cpp
Normal file
405
src/audiocontrols.cpp
Normal file
@@ -0,0 +1,405 @@
|
||||
#include "audiocontrols.h"
|
||||
#include "ui_audiocontrols.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
|
||||
#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() ) );
|
||||
|
||||
// <From AudioEngine>
|
||||
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<QNetworkReply*>( 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 );
|
||||
}
|
58
src/audiocontrols.h
Normal file
58
src/audiocontrols.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#ifndef AUDIOCONTROLS_H
|
||||
#define AUDIOCONTROLS_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#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
|
458
src/audiocontrols.ui
Normal file
458
src/audiocontrols.ui
Normal file
@@ -0,0 +1,458 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AudioControls</class>
|
||||
<widget class="QWidget" name="AudioControls">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>706</width>
|
||||
<height>70</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>70</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>70</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="leftMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="buttonArea" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>254</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>254</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="buttonAreaLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ImageButton" name="prevButton">
|
||||
<property name="text">
|
||||
<string>Prev</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ImageButton" name="playPauseButton">
|
||||
<property name="text">
|
||||
<string>Play</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ImageButton" name="pauseButton">
|
||||
<property name="text">
|
||||
<string>Pause</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ImageButton" name="nextButton">
|
||||
<property name="text">
|
||||
<string>Next</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="metadataArea" native="true">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>66</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<property name="leftMargin">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="coverImage">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>49</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>49</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cover</string>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_2" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="trackLabelLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="artistTrackLabel">
|
||||
<property name="text">
|
||||
<string>Artist</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="albumLabel">
|
||||
<property name="text">
|
||||
<string>Album</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="ownerLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>7</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Owner</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="timeLabel">
|
||||
<property name="text">
|
||||
<string>Time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="seekSlider">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="timeLeftLabel">
|
||||
<property name="text">
|
||||
<string>Time Left</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_4" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>66</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_6" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ImageButton" name="shuffleButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Shuffle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ImageButton" name="repeatButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Repeat</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_5" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="ImageButton" name="volumeLowButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Low</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="volumeSlider">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ImageButton" name="volumeHighButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>High</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ImageButton</class>
|
||||
<extends>QPushButton</extends>
|
||||
<header>imagebutton.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
108
src/bufferiodevice.cpp
Normal file
108
src/bufferiodevice.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#include <QDebug>
|
||||
#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();
|
||||
}
|
39
src/bufferiodevice.h
Normal file
39
src/bufferiodevice.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef BUFFERIODEVICE_H
|
||||
#define BUFFERIODEVICE_H
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QMutexLocker>
|
||||
#include <QDebug>
|
||||
|
||||
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
|
118
src/collection.cpp
Normal file
118
src/collection.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "tomahawk/collection.h"
|
||||
|
||||
#include <QMetaObject>
|
||||
#include <QGenericArgument>
|
||||
|
||||
#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<QVariant>& val,
|
||||
collection_ptr collection )
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO << obj << slotname;
|
||||
QMetaObject::invokeMethod( obj, slotname, Qt::QueuedConnection,
|
||||
Q_ARG( QList<QVariant>, 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<playlist_ptr> 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<playlist_ptr> 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<QVariant>&, 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;
|
||||
}
|
||||
}
|
445
src/connection.cpp
Normal file
445
src/connection.cpp
Normal file
@@ -0,0 +1,445 @@
|
||||
#include "connection.h"
|
||||
|
||||
#include <QTime>
|
||||
#include <QThread>
|
||||
|
||||
#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 );
|
||||
}
|
129
src/connection.h
Normal file
129
src/connection.h
Normal file
@@ -0,0 +1,129 @@
|
||||
#ifndef CONNECTION_H
|
||||
#define CONNECTION_H
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <QTcpSocket>
|
||||
#include <QHostAddress>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QString>
|
||||
#include <QDebug>
|
||||
#include <QDataStream>
|
||||
#include <QtEndian>
|
||||
#include <QTimer>
|
||||
#include <QTime>
|
||||
#include <QPointer>
|
||||
|
||||
#include <qjson/parser.h>
|
||||
#include <qjson/serializer.h>
|
||||
#include <qjson/qobjecthelper.h>
|
||||
|
||||
#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<QTcpSocket>& 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<QTcpSocket> 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
|
209
src/controlconnection.cpp
Normal file
209
src/controlconnection.cpp
Normal file
@@ -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());
|
||||
}
|
51
src/controlconnection.h
Normal file
51
src/controlconnection.h
Normal file
@@ -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
|
22
src/database/README.txt
Normal file
22
src/database/README.txt
Normal file
@@ -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.
|
||||
|
50
src/database/database.cpp
Normal file
50
src/database/database.cpp
Normal file
@@ -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<DatabaseCommand> 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();
|
||||
}
|
48
src/database/database.h
Normal file
48
src/database/database.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#ifndef DATABASE_H
|
||||
#define DATABASE_H
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <QVariant>
|
||||
|
||||
#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<DatabaseCommand> );
|
||||
void newJobRW( QSharedPointer<DatabaseCommand> );
|
||||
|
||||
public slots:
|
||||
void enqueue( QSharedPointer<DatabaseCommand> lc );
|
||||
|
||||
private:
|
||||
DatabaseImpl* m_impl;
|
||||
DatabaseWorker *m_workerRO, *m_workerRW;
|
||||
bool m_indexReady;
|
||||
};
|
||||
|
||||
#endif // DATABASE_H
|
75
src/database/databasecollection.cpp
Normal file
75
src/database/databasecollection.cpp
Normal file
@@ -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<Tomahawk::playlist_ptr>& ) ),
|
||||
SLOT( setPlaylists( const QList<Tomahawk::playlist_ptr>& ) ) );
|
||||
|
||||
TomahawkApp::instance()->database()->enqueue(
|
||||
QSharedPointer<DatabaseCommand>( cmd )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
DatabaseCollection::loadAllTracks( boost::function<void( const QList<QVariant>&, 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<QVariant>& ) ),
|
||||
SLOT( callCallback( const QList<QVariant>& ) ) );
|
||||
|
||||
TomahawkApp::instance()->database()->enqueue(
|
||||
QSharedPointer<DatabaseCommand>( cmd )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
DatabaseCollection::addTracks( const QList<QVariant> &newitems )
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO << newitems.length();
|
||||
DatabaseCommand_AddFiles* cmd = new DatabaseCommand_AddFiles( newitems, source() );
|
||||
TomahawkApp::instance()->database()->enqueue(
|
||||
QSharedPointer<DatabaseCommand>( cmd )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
DatabaseCollection::removeTracks( const QList<QVariant> &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<QVariant>& res )
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO << res.length() << this->source()->collection().data();
|
||||
m_callback( res, this->source()->collection() );
|
||||
}
|
31
src/database/databasecollection.h
Normal file
31
src/database/databasecollection.h
Normal file
@@ -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<void( const QList<QVariant>&, Tomahawk::collection_ptr )> callback );
|
||||
virtual void loadPlaylists();
|
||||
|
||||
public slots:
|
||||
virtual void addTracks( const QList<QVariant> &newitems );
|
||||
virtual void removeTracks( const QList<QVariant> &olditems );
|
||||
|
||||
void callCallback( const QList<QVariant>& res );
|
||||
|
||||
private:
|
||||
boost::function<void( const QList<QVariant>&, Tomahawk::collection_ptr )> m_callback;
|
||||
};
|
||||
|
||||
#endif // DATABASECOLLECTION_H
|
83
src/database/databasecommand.cpp
Normal file
83
src/database/databasecommand.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "databasecommand.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#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;
|
||||
}
|
82
src/database/databasecommand.h
Normal file
82
src/database/databasecommand.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#ifndef DATABASECOMMAND_H
|
||||
#define DATABASECOMMAND_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMetaType>
|
||||
#include <QTime>
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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
|
174
src/database/databasecommand_addfiles.cpp
Normal file
174
src/database/databasecommand_addfiles.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include "databasecommand_addfiles.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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<QVariant>&, Tomahawk::collection_ptr ) ),
|
||||
coll, SIGNAL( tracksAdded( const QList<QVariant>&, 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<DatabaseCommand>( 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<QVariant>::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() );
|
||||
}
|
||||
|
43
src/database/databasecommand_addfiles.h
Normal file
43
src/database/databasecommand_addfiles.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef DATABASECOMMAND_ADDFILES_H
|
||||
#define DATABASECOMMAND_ADDFILES_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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<QVariant>& 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<QVariant>&, Tomahawk::collection_ptr );
|
||||
void notify( const QList<QVariant>&, Tomahawk::collection_ptr );
|
||||
|
||||
private:
|
||||
QVariantList m_files;
|
||||
};
|
||||
|
||||
#endif // DATABASECOMMAND_ADDFILES_H
|
45
src/database/databasecommand_addsource.cpp
Normal file
45
src/database/databasecommand_addsource.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include <QSqlQuery>
|
||||
#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 );
|
||||
}
|
25
src/database/databasecommand_addsource.h
Normal file
25
src/database/databasecommand_addsource.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef DATABASECOMMAND_ADDSOURCE_H
|
||||
#define DATABASECOMMAND_ADDSOURCE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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
|
58
src/database/databasecommand_alltracks.cpp
Normal file
58
src/database/databasecommand_alltracks.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "databasecommand_alltracks.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#include "databaseimpl.h"
|
||||
|
||||
|
||||
void
|
||||
DatabaseCommand_AllTracks::exec( DatabaseImpl* dbi )
|
||||
{
|
||||
Q_ASSERT( !m_source.isNull() );
|
||||
|
||||
TomahawkSqlQuery query = dbi->newquery();
|
||||
QList<QVariant> 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 );
|
||||
}
|
31
src/database/databasecommand_alltracks.h
Normal file
31
src/database/databasecommand_alltracks.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifndef DATABASECOMMAND_ALLTRACKS_H
|
||||
#define DATABASECOMMAND_ALLTRACKS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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<QVariant>& );
|
||||
|
||||
private:
|
||||
Tomahawk::source_ptr m_source;
|
||||
};
|
||||
|
||||
#endif // DATABASECOMMAND_ALLTRACKS_H
|
57
src/database/databasecommand_collectionstats.cpp
Normal file
57
src/database/databasecommand_collectionstats.cpp
Normal file
@@ -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 );
|
||||
}
|
24
src/database/databasecommand_collectionstats.h
Normal file
24
src/database/databasecommand_collectionstats.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef DATABASECOMMAND_COLLECTIONSTATS_H
|
||||
#define DATABASECOMMAND_COLLECTIONSTATS_H
|
||||
|
||||
#include <QVariantMap>
|
||||
|
||||
#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
|
71
src/database/databasecommand_createplaylist.cpp
Normal file
71
src/database/databasecommand_createplaylist.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "databasecommand_createplaylist.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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();
|
||||
}
|
44
src/database/databasecommand_createplaylist.h
Normal file
44
src/database/databasecommand_createplaylist.h
Normal file
@@ -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
|
53
src/database/databasecommand_deleteplaylist.cpp
Normal file
53
src/database/databasecommand_deleteplaylist.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "databasecommand_deleteplaylist.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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();
|
||||
}
|
34
src/database/databasecommand_deleteplaylist.h
Normal file
34
src/database/databasecommand_deleteplaylist.h
Normal file
@@ -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
|
57
src/database/databasecommand_dirmtimes.cpp
Normal file
57
src/database/databasecommand_dirmtimes.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "databasecommand_dirmtimes.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#include "databaseimpl.h"
|
||||
|
||||
|
||||
void
|
||||
DatabaseCommand_DirMtimes::exec( DatabaseImpl* dbi )
|
||||
{
|
||||
if( m_update )
|
||||
execUpdate( dbi );
|
||||
else
|
||||
execSelect( dbi );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
DatabaseCommand_DirMtimes::execSelect( DatabaseImpl* dbi )
|
||||
{
|
||||
QMap<QString,unsigned int> 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.";
|
||||
}
|
42
src/database/databasecommand_dirmtimes.h
Normal file
42
src/database/databasecommand_dirmtimes.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#ifndef DATABASECOMMAND_DIRMTIMES_H
|
||||
#define DATABASECOMMAND_DIRMTIMES_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
#include <QMap>
|
||||
|
||||
#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<QString, unsigned int> 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<QString, unsigned int>& );
|
||||
|
||||
public slots:
|
||||
|
||||
private:
|
||||
void execSelect( DatabaseImpl* dbi );
|
||||
void execUpdate( DatabaseImpl* dbi );
|
||||
QString m_prefix;
|
||||
bool m_update;
|
||||
QMap<QString, unsigned int> m_tosave;
|
||||
};
|
||||
|
||||
#endif // DATABASECOMMAND_DIRMTIMES_H
|
40
src/database/databasecommand_importplaylist.cpp
Normal file
40
src/database/databasecommand_importplaylist.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "databasecommand_importplaylist.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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);
|
||||
*/
|
||||
}
|
||||
|
29
src/database/databasecommand_importplaylist.h
Normal file
29
src/database/databasecommand_importplaylist.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifndef DATABASECOMMAND_IMPORTPLAYLIST_H
|
||||
#define DATABASECOMMAND_IMPORTPLAYLIST_H
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
#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
|
38
src/database/databasecommand_loadallplaylists.cpp
Normal file
38
src/database/databasecommand_loadallplaylists.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#include "databasecommand_loadallplaylists.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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<playlist_ptr> 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 );
|
||||
}
|
||||
|
27
src/database/databasecommand_loadallplaylists.h
Normal file
27
src/database/databasecommand_loadallplaylists.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef DATABASECOMMAND_IMPORTALLPLAYLIST_H
|
||||
#define DATABASECOMMAND_IMPORTALLPLAYLIST_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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<Tomahawk::playlist_ptr>& playlists );
|
||||
};
|
||||
|
||||
#endif // DATABASECOMMAND_ADDFILES_H
|
29
src/database/databasecommand_loadfile.cpp
Normal file
29
src/database/databasecommand_loadfile.cpp
Normal file
@@ -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 );
|
||||
}
|
27
src/database/databasecommand_loadfile.h
Normal file
27
src/database/databasecommand_loadfile.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef DATABASECOMMAND_LOADFILE_H
|
||||
#define DATABASECOMMAND_LOADFILE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
#include <QMap>
|
||||
|
||||
#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
|
37
src/database/databasecommand_loadops.cpp
Normal file
37
src/database/databasecommand_loadops.cpp
Normal file
@@ -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 );
|
||||
}
|
28
src/database/databasecommand_loadops.h
Normal file
28
src/database/databasecommand_loadops.h
Normal file
@@ -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
|
106
src/database/databasecommand_loadplaylistentries.cpp
Normal file
106
src/database/databasecommand_loadplaylistentries.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "databasecommand_loadplaylistentries.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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 );
|
||||
}
|
35
src/database/databasecommand_loadplaylistentries.h
Normal file
35
src/database/databasecommand_loadplaylistentries.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef DATABASECOMMAND_LOADPLAYLIST_H
|
||||
#define DATABASECOMMAND_LOADPLAYLIST_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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<QString>& orderedguid,
|
||||
const QList<QString>& oldorderedguid,
|
||||
bool islatest,
|
||||
const QMap< QString, Tomahawk::plentry_ptr >& added,
|
||||
bool applied );
|
||||
|
||||
private:
|
||||
QString m_guid;
|
||||
};
|
||||
|
||||
#endif
|
17
src/database/databasecommand_modifyplaylist.cpp
Normal file
17
src/database/databasecommand_modifyplaylist.cpp
Normal file
@@ -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 )
|
||||
{
|
||||
}
|
39
src/database/databasecommand_modifyplaylist.h
Normal file
39
src/database/databasecommand_modifyplaylist.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef DATABASECOMMAND_MODIFYPLAYLIST_H
|
||||
#define DATABASECOMMAND_MODIFYPLAYLIST_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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
|
202
src/database/databasecommand_resolve.cpp
Normal file
202
src/database/databasecommand_resolve.cpp
Normal file
@@ -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<int,float> 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<Tomahawk::result_ptr> 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;
|
||||
}
|
103
src/database/databasecommand_resolve.h
Normal file
103
src/database/databasecommand_resolve.h
Normal file
@@ -0,0 +1,103 @@
|
||||
#ifndef DATABASECOMMAND_RESOLVE_H
|
||||
#define DATABASECOMMAND_RESOLVE_H
|
||||
#include "databasecommand.h"
|
||||
#include "databaseimpl.h"
|
||||
#include "tomahawk/result.h"
|
||||
#include <QVariant>
|
||||
|
||||
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<Tomahawk::result_ptr> 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<int> > 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<int> 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
|
183
src/database/databasecommand_setplaylistrevision.cpp
Normal file
183
src/database/databasecommand_setplaylistrevision.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "databasecommand_setplaylistrevision.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
|
||||
#include "tomahawksqlquery.h"
|
||||
#include "tomahawk/tomahawkapp.h"
|
||||
|
||||
|
||||
DatabaseCommand_SetPlaylistRevision::DatabaseCommand_SetPlaylistRevision(
|
||||
const source_ptr& s,
|
||||
QString playlistguid,
|
||||
QString newrev,
|
||||
QString oldrev,
|
||||
QStringList orderedguids,
|
||||
QList<plentry_ptr> 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";
|
||||
}
|
||||
|
||||
}
|
82
src/database/databasecommand_setplaylistrevision.h
Normal file
82
src/database/databasecommand_setplaylistrevision.h
Normal file
@@ -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<Tomahawk::plentry_ptr> 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<Tomahawk::plentry_ptr> m_addedentries;
|
||||
bool m_applied;
|
||||
QMap<QString, Tomahawk::plentry_ptr> m_addedmap;
|
||||
};
|
||||
|
||||
#endif // DATABASECOMMAND_SETPLAYLISTREVISION_H
|
16
src/database/databasecommand_sourceoffline.cpp
Normal file
16
src/database/databasecommand_sourceoffline.cpp
Normal file
@@ -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 ) );
|
||||
}
|
20
src/database/databasecommand_sourceoffline.h
Normal file
20
src/database/databasecommand_sourceoffline.h
Normal file
@@ -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
|
114
src/database/databasecommand_updatesearchindex.cpp
Normal file
114
src/database/databasecommand_updatesearchindex.cpp
Normal file
@@ -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<QString, int> ngrammap;
|
||||
|
||||
// this is the new ngram map we build up, to be merged into the
|
||||
// main one in FuzzyIndex:
|
||||
QHash< QString, QMap<quint32, quint16> > 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<QString, int> 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<QString, int> iter( ngrammap );
|
||||
while ( iter.hasNext() )
|
||||
{
|
||||
iter.next();
|
||||
if( idx.contains( iter.key() ) )
|
||||
{
|
||||
idx[ iter.key() ][ id ] += iter.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
QMap<quint32, quint16> 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<quint32, quint16> >", &idx )
|
||||
);
|
||||
|
||||
qDebug() << "Finished indexing" << num_names <<" names," << num_ngrams << "ngrams.";
|
||||
}
|
27
src/database/databasecommand_updatesearchindex.h
Normal file
27
src/database/databasecommand_updatesearchindex.h
Normal file
@@ -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
|
33
src/database/databasecommandloggable.cpp
Normal file
33
src/database/databasecommandloggable.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "databasecommandloggable.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
29
src/database/databasecommandloggable.h
Normal file
29
src/database/databasecommandloggable.h
Normal file
@@ -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
|
455
src/database/databaseimpl.cpp
Normal file
455
src/database/databaseimpl.cpp
Normal file
@@ -0,0 +1,455 @@
|
||||
#include "databaseimpl.h"
|
||||
|
||||
#include <QRegExp>
|
||||
#include <QStringList>
|
||||
#include <QtAlgorithms>
|
||||
#include <QFile>
|
||||
#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<DatabaseCommand>( 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<int,float> > resultslist;
|
||||
foreach( int i, resultsmap.keys() )
|
||||
{
|
||||
resultslist << QPair<int,float>( 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<QString, int> > memo;
|
||||
if( memo.contains( str_orig ) )
|
||||
return memo.value( str_orig );
|
||||
|
||||
int n = 3;
|
||||
QMap<QString, int> 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;
|
||||
}
|
||||
|
79
src/database/databaseimpl.h
Normal file
79
src/database/databaseimpl.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#ifndef DATABASEIMPL_H
|
||||
#define DATABASEIMPL_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlError>
|
||||
#include <QSqlQuery>
|
||||
#include <QDebug>
|
||||
#include <QHash>
|
||||
#include <QThread>
|
||||
|
||||
#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<QString,int> 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<int,float>& left, const QPair<int,float>& 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
|
49
src/database/databaseresolver.cpp
Normal file
49
src/database/databaseresolver.cpp
Normal file
@@ -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<DatabaseCommand>( 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" );
|
||||
}
|
29
src/database/databaseresolver.h
Normal file
29
src/database/databaseresolver.h
Normal file
@@ -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
|
194
src/database/databaseworker.cpp
Normal file
194
src/database/databaseworker.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "databaseworker.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QTime>
|
||||
#include <QSqlQuery>
|
||||
|
||||
#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<DatabaseCommand>) ),
|
||||
SLOT( doWork(QSharedPointer<DatabaseCommand>) ),
|
||||
Qt::QueuedConnection );
|
||||
}
|
||||
else
|
||||
{
|
||||
connect( db, SIGNAL( newJobRO(QSharedPointer<DatabaseCommand>) ),
|
||||
SLOT( doWork(QSharedPointer<DatabaseCommand>) ),
|
||||
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<DatabaseCommand> 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";
|
||||
}
|
||||
}
|
46
src/database/databaseworker.h
Normal file
46
src/database/databaseworker.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef DATABASEWORKER_H
|
||||
#define DATABASEWORKER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QMutex>
|
||||
#include <QList>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include <qjson/parser.h>
|
||||
#include <qjson/serializer.h>
|
||||
#include <qjson/qobjecthelper.h>
|
||||
|
||||
#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<DatabaseCommand> );
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
public slots:
|
||||
void doWork( QSharedPointer<DatabaseCommand> );
|
||||
|
||||
private:
|
||||
void logOp( DatabaseCommandLoggable* command );
|
||||
|
||||
QMutex m_mut;
|
||||
DatabaseImpl* m_dbimpl;
|
||||
QList< QSharedPointer<DatabaseCommand> > m_commands;
|
||||
bool m_abort;
|
||||
int m_outstanding;
|
||||
QJson::Serializer m_serializer;
|
||||
};
|
||||
|
||||
#endif // DATABASEWORKER_H
|
124
src/database/fuzzyindex.cpp
Normal file
124
src/database/fuzzyindex.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
#include "fuzzyindex.h"
|
||||
#include "databaseimpl.h"
|
||||
#include <QTime>
|
||||
|
||||
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<quint32, quint16> >& 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<quint32, quint16> 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<quint32, quint16> > tomerge)
|
||||
{
|
||||
qDebug() << Q_FUNC_INFO << table << tomerge.keys().size();
|
||||
|
||||
QHash< QString, QMap<quint32, quint16> >* 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<quint32, quint16> >* 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<QString,int> 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<quint32, quint16> iter( (*idx)[ngram] );
|
||||
while( iter.hasNext() )
|
||||
{
|
||||
iter.next();
|
||||
resultsmap[ (int) iter.key() ] += (float) iter.value();
|
||||
}
|
||||
}
|
||||
return resultsmap;
|
||||
}
|
37
src/database/fuzzyindex.h
Normal file
37
src/database/fuzzyindex.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef FUZZYINDEX_H
|
||||
#define FUZZYINDEX_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
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<quint32, quint16> > tomerge);
|
||||
|
||||
private:
|
||||
void loadNgramIndex_helper( QHash< QString, QMap<quint32, quint16> >& idx, const QString& table, unsigned int fromkey = 0);
|
||||
|
||||
// maps an ngram to {track id, num occurences}
|
||||
QHash< QString, QMap<quint32, quint16> > m_artist_ngrams, m_album_ngrams, m_track_ngrams;
|
||||
|
||||
DatabaseImpl & m_db;
|
||||
|
||||
bool m_loaded;
|
||||
};
|
||||
|
||||
#endif // FUZZYINDEX_H
|
28
src/database/gen_schema.h.sh
Executable file
28
src/database/gen_schema.h.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
schema=$1
|
||||
name=$2
|
||||
|
||||
if [ -e "$schema" -a -n "$name" ]
|
||||
then
|
||||
cat <<EOF
|
||||
/*
|
||||
This file was automatically generated from $schema on `date`.
|
||||
*/
|
||||
|
||||
static const char * ${name}_schema_sql =
|
||||
EOF
|
||||
awk '!/^-/ && length($0) {gsub(/[ \t]+$/, "", $0); gsub("\"","\\\"",$0); gsub("--.*$","",$0); printf("\"%s\"\n",$0);}' "$schema"
|
||||
cat <<EOF
|
||||
;
|
||||
|
||||
const char * get_${name}_sql()
|
||||
{
|
||||
return ${name}_schema_sql;
|
||||
}
|
||||
|
||||
|
||||
EOF
|
||||
else
|
||||
echo "Usage: $0 <schema.sql> <name>"
|
||||
exit 1
|
||||
fi
|
17
src/database/op.h
Normal file
17
src/database/op.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef OP_H
|
||||
#define OP_H
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <QSharedPointer>
|
||||
|
||||
struct DBOp
|
||||
{
|
||||
QString guid;
|
||||
QString command;
|
||||
QByteArray payload;
|
||||
bool compressed;
|
||||
};
|
||||
|
||||
typedef QSharedPointer<DBOp> dbop_ptr;
|
||||
|
||||
#endif // OP_H
|
218
src/database/schema.sql
Normal file
218
src/database/schema.sql
Normal file
@@ -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, <guid or hash?>
|
||||
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');
|
163
src/database/schema.sql.h
Normal file
163
src/database/schema.sql.h
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
|
59
src/database/tomahawksqlquery.h
Normal file
59
src/database/tomahawksqlquery.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef TOMAHAWKSQLQUERY_H
|
||||
#define TOMAHAWKSQLQUERY_H
|
||||
// subclass QSqlQuery so that it prints the error msg if a query fails
|
||||
|
||||
#include <QSqlQuery>
|
||||
#include <QTime>
|
||||
|
||||
#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
|
301
src/dbsyncconnection.cpp
Normal file
301
src/dbsyncconnection.cpp
Normal file
@@ -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 <QDebug>
|
||||
|
||||
#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<DatabaseCommand>(cmd_us) );
|
||||
|
||||
APP->database()->enqueue( QSharedPointer<DatabaseCommand>(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<DatabaseCommand>(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<DatabaseCommand>( 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<DatabaseCommand>( 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;
|
||||
}
|
67
src/dbsyncconnection.h
Normal file
67
src/dbsyncconnection.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#ifndef DBSYNCCONNECTION_H
|
||||
#define DBSYNCCONNECTION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <QSharedPointer>
|
||||
#include <QIODevice>
|
||||
|
||||
#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
|
187
src/filetransferconnection.cpp
Normal file
187
src/filetransferconnection.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "filetransferconnection.h"
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#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<QIODevice>( 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<DatabaseCommand>(cmd));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
FileTransferConnection::startSending( QVariantMap f )
|
||||
{
|
||||
Tomahawk::result_ptr result(new Tomahawk::Result(f, collection_ptr()));
|
||||
qDebug() << "Starting to transmit" << result->url();
|
||||
QSharedPointer<QIODevice> io = TomahawkApp::instance()->getIODeviceForUrl(result);
|
||||
if(!io)
|
||||
{
|
||||
qDebug() << "Couldn't read from source:" << result->url();
|
||||
shutdown();
|
||||
return;
|
||||
}
|
||||
m_readdev = QSharedPointer<QIODevice>(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() ) );
|
||||
}
|
62
src/filetransferconnection.h
Normal file
62
src/filetransferconnection.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#ifndef FILETRANSFERCONNECTION_H
|
||||
#define FILETRANSFERCONNECTION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSharedPointer>
|
||||
#include <QIODevice>
|
||||
|
||||
#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<QIODevice>& 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<QIODevice> m_iodev;
|
||||
ControlConnection* m_cc;
|
||||
QString m_fid;
|
||||
Type m_type;
|
||||
QSharedPointer<QIODevice> m_readdev;
|
||||
|
||||
int m_badded, m_bsent;
|
||||
bool m_allok; // got last msg ok, transfer complete?
|
||||
};
|
||||
|
||||
#endif // FILETRANSFERCONNECTION_H
|
18
src/headlesscheck.h
Normal file
18
src/headlesscheck.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef HEADLESSCHECK
|
||||
#define HEADLESSCHECK
|
||||
|
||||
#ifdef ENABLE_HEADLESS
|
||||
|
||||
#define TOMAHAWK_APPLICATION QCoreApplication
|
||||
#define TOMAHAWK_HEADLESS
|
||||
#include <QCoreApplication>
|
||||
|
||||
#else
|
||||
|
||||
#define TOMAHAWK_APPLICATION QApplication
|
||||
#include <QApplication>
|
||||
#include "tomahawkwindow.h"
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
100
src/jabber/jabber.h
Normal file
100
src/jabber/jabber.h
Normal file
@@ -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
|
532
src/jabber/jabber_p.cpp
Normal file
532
src/jabber/jabber_p.cpp
Normal file
@@ -0,0 +1,532 @@
|
||||
#include "jabber_p.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QTime>
|
||||
|
||||
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<gloox::Client>( 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
131
src/jabber/jabber_p.h
Normal file
131
src/jabber/jabber_p.h
Normal file
@@ -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 <QObject>
|
||||
#include <QSharedPointer>
|
||||
#include <QMap>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <gloox/client.h>
|
||||
#include <gloox/messagesessionhandler.h>
|
||||
#include <gloox/messagehandler.h>
|
||||
#include <gloox/messageeventhandler.h>
|
||||
#include <gloox/messageeventfilter.h>
|
||||
#include <gloox/chatstatehandler.h>
|
||||
#include <gloox/chatstatefilter.h>
|
||||
#include <gloox/connectionlistener.h>
|
||||
#include <gloox/disco.h>
|
||||
#include <gloox/message.h>
|
||||
#include <gloox/discohandler.h>
|
||||
#include <gloox/stanza.h>
|
||||
#include <gloox/gloox.h>
|
||||
#include <gloox/lastactivity.h>
|
||||
#include <gloox/loghandler.h>
|
||||
#include <gloox/logsink.h>
|
||||
#include <gloox/connectiontcpclient.h>
|
||||
#include <gloox/connectionsocks5proxy.h>
|
||||
#include <gloox/connectionhttpproxy.h>
|
||||
#include <gloox/messagehandler.h>
|
||||
#include <gloox/rostermanager.h>
|
||||
#include <gloox/siprofileft.h>
|
||||
#include <gloox/siprofilefthandler.h>
|
||||
#include <gloox/bytestreamdatahandler.h>
|
||||
#include <gloox/error.h>
|
||||
#include <gloox/presence.h>
|
||||
#include <gloox/rosteritem.h>
|
||||
|
||||
#if defined( WIN32 ) || defined( _WIN32 )
|
||||
# include <windows.h>
|
||||
#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<gloox::Client> m_client;
|
||||
gloox::JID m_jid;
|
||||
QMap<gloox::Presence::PresenceType, QString> m_presences;
|
||||
QMap<QString, gloox::Presence::PresenceType> m_peers;
|
||||
QTimer m_timer; // for recv()
|
||||
};
|
||||
|
||||
#endif // JABBER_H
|
86
src/junk/remoteioconnection.cpp
Normal file
86
src/junk/remoteioconnection.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "remoteioconnection.h"
|
||||
#include <QFile>
|
||||
|
||||
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<QFile>(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()));
|
||||
}
|
||||
|
||||
|
38
src/junk/remoteioconnection.h
Normal file
38
src/junk/remoteioconnection.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef REMOTEIOCONNECTION_H
|
||||
#define REMOTEIOCONNECTION_H
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
#include <QDebug>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#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<QIODevice> m_readdev;
|
||||
};
|
||||
|
||||
#endif // REMOTEIOCONNECTION_H
|
101
src/junk/remoteiodevice.cpp
Normal file
101
src/junk/remoteiodevice.cpp
Normal file
@@ -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;
|
||||
}
|
43
src/junk/remoteiodevice.h
Normal file
43
src/junk/remoteiodevice.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef REMOTEIODEVICE_H
|
||||
#define REMOTEIODEVICE_H
|
||||
#include <QIODevice>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
#include <QDebug>
|
||||
#include <QBuffer>
|
||||
#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
|
7
src/main.cpp
Normal file
7
src/main.cpp
Normal file
@@ -0,0 +1,7 @@
|
||||
#include "tomahawk/tomahawkapp.h"
|
||||
|
||||
int main( int argc, char *argv[] )
|
||||
{
|
||||
TomahawkApp a( argc, argv );
|
||||
return a.exec();
|
||||
}
|
141
src/msg.h
Normal file
141
src/msg.h
Normal file
@@ -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 <QByteArray>
|
||||
#include <QSharedPointer>
|
||||
#include <QtEndian>
|
||||
#include <QIODevice>
|
||||
#include <QDebug>
|
||||
|
||||
#include <qjson/parser.h>
|
||||
#include <qjson/serializer.h>
|
||||
#include <qjson/qobjecthelper.h>
|
||||
|
||||
class Msg;
|
||||
typedef QSharedPointer<Msg> 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
|
113
src/msgprocessor.cpp
Normal file
113
src/msgprocessor.cpp
Normal file
@@ -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<msg_ptr> fut = QtConcurrent::run(&MsgProcessor::process, msg, m_mode, m_threshold);
|
||||
QFutureWatcher<msg_ptr> * watcher = new QFutureWatcher<msg_ptr>;
|
||||
connect( watcher, SIGNAL( finished() ),
|
||||
this, SLOT( processed() ),
|
||||
Qt::QueuedConnection );
|
||||
|
||||
watcher->setFuture( fut );
|
||||
}
|
||||
|
||||
|
||||
void MsgProcessor::processed()
|
||||
{
|
||||
QFutureWatcher<msg_ptr> * watcher = (QFutureWatcher<msg_ptr> *) 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;
|
||||
}
|
||||
|
||||
|
63
src/msgprocessor.h
Normal file
63
src/msgprocessor.h
Normal file
@@ -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 <QObject>
|
||||
#include "msg.h"
|
||||
#include <QtConcurrentRun>
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include <qjson/parser.h>
|
||||
#include <qjson/serializer.h>
|
||||
#include <qjson/qobjecthelper.h>
|
||||
|
||||
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<msg_ptr> m_msgs;
|
||||
QMap< Msg*, bool> m_msg_ready;
|
||||
unsigned int m_totmsgsize;
|
||||
};
|
||||
|
||||
#endif // MSGPROCESSOR_H
|
210
src/musicscanner.cpp
Normal file
210
src/musicscanner.cpp
Normal file
@@ -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<QString, unsigned int>& ) ),
|
||||
SLOT( setMtimes( const QMap<QString, unsigned int>& ) ), Qt::DirectConnection );
|
||||
connect( cmd, SIGNAL( done( const QMap<QString,unsigned int>& ) ),
|
||||
SLOT( scan() ), Qt::DirectConnection );
|
||||
|
||||
TomahawkApp::instance()->database()->enqueue( QSharedPointer<DatabaseCommand>(cmd) );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
MusicScanner::setMtimes( const QMap<QString, unsigned int>& 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<QString, unsigned int>& ) ),
|
||||
SLOT( listerFinished( const QMap<QString, unsigned int>& ) ), Qt::QueuedConnection );
|
||||
|
||||
connect( lister, SIGNAL( finished() ), lister, SLOT( deleteLater() ) );
|
||||
|
||||
lister->start();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
MusicScanner::listerFinished( const QMap<QString, unsigned int>& 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<DatabaseCommand>(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<DatabaseCommand>( 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;
|
||||
}
|
133
src/musicscanner.h
Normal file
133
src/musicscanner.h
Normal file
@@ -0,0 +1,133 @@
|
||||
#ifndef MUSICSCANNER_H
|
||||
#define MUSICSCANNER_H
|
||||
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/tag.h>
|
||||
|
||||
#include <QVariantMap>
|
||||
#include <QThread>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
|
||||
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<QString, unsigned int>& newmtimes );
|
||||
void scanFile( const QFileInfo& fi );
|
||||
void startScan();
|
||||
void scan();
|
||||
void setMtimes( const QMap<QString, unsigned int>& m );
|
||||
void commitBatch( const QVariantList& );
|
||||
|
||||
private:
|
||||
QString m_dir;
|
||||
QMap<QString,QString> m_ext2mime; // eg: mp3 -> audio/mpeg
|
||||
unsigned int m_scanned;
|
||||
unsigned int m_skipped;
|
||||
|
||||
QList<QString> m_skippedFiles;
|
||||
|
||||
QMap<QString, unsigned int> m_dirmtimes;
|
||||
QMap<QString, unsigned int> m_newdirmtimes;
|
||||
|
||||
QList<QVariant> m_scannedfiles;
|
||||
quint32 m_batchsize;
|
||||
};
|
||||
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
// 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<QString, unsigned int>& 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<QString, unsigned int>& );
|
||||
|
||||
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<QString, unsigned int> m_dirmtimes;
|
||||
QMap<QString, unsigned int> m_newdirmtimes;
|
||||
};
|
||||
|
||||
#endif
|
198
src/pipeline.cpp
Normal file
198
src/pipeline.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "tomahawk/pipeline.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QMutexLocker>
|
||||
|
||||
#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<query_ptr>& 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();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user