diff --git a/CMakeLists.txt b/CMakeLists.txt index 3921f5d6d5..94c6e5ac31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,7 +200,7 @@ macro_log_feature(CLucene_FOUND "CLucene" "The open-source, C++ search engine" " macro_optional_find_package(QJSON) macro_log_feature(QJSON_FOUND "QJson" "Qt library that maps JSON data to QVariant objects" "http://qjson.sf.net" TRUE "" "libqjson is used for encoding communication between Tomahawk instances") -macro_optional_find_package(Taglib 1.6.0) +macro_optional_find_package(Taglib 1.8.0) macro_log_feature(TAGLIB_FOUND "TagLib" "Audio Meta-Data Library" "http://developer.kde.org/~wheeler/taglib.html" TRUE "" "taglib is needed for reading meta data from audio files") include( CheckTagLibFileName ) check_taglib_filename( COMPLEX_TAGLIB_FILENAME ) diff --git a/CMakeModules/FindTaglib.cmake b/CMakeModules/FindTaglib.cmake index e0efbef977..17518a350c 100644 --- a/CMakeModules/FindTaglib.cmake +++ b/CMakeModules/FindTaglib.cmake @@ -15,7 +15,7 @@ IF(TAGLIB_FOUND) ELSE() if(NOT TAGLIB_MIN_VERSION) - set(TAGLIB_MIN_VERSION "1.6") + set(TAGLIB_MIN_VERSION "1.8") endif(NOT TAGLIB_MIN_VERSION) if(NOT WIN32) diff --git a/data/images/dropbox-icon.png b/data/images/dropbox-icon.png new file mode 100644 index 0000000000..ebde4d0729 Binary files /dev/null and b/data/images/dropbox-icon.png differ diff --git a/data/images/dropbox-icon.svg b/data/images/dropbox-icon.svg new file mode 100644 index 0000000000..6f63627d18 --- /dev/null +++ b/data/images/dropbox-icon.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/js/tomahawk.js b/data/js/tomahawk.js index 611ea1876b..8b6d1fdf36 100644 --- a/data/js/tomahawk.js +++ b/data/js/tomahawk.js @@ -223,13 +223,23 @@ Tomahawk.valueForSubNode = function(node, tag) }; -Tomahawk.syncRequest = function(url) +Tomahawk.syncRequest = function(url, extraHeaders) { var xmlHttpRequest = new XMLHttpRequest(); xmlHttpRequest.open('GET', url, false); + + if (extraHeaders) { + for(var headerName in extraHeaders) { + xmlHttpRequest.setRequestHeader(headerName, extraHeaders[headerName]); + } + } + xmlHttpRequest.send(null); - if (xmlHttpRequest.status == 200){ + if (xmlHttpRequest.status == 200) { return xmlHttpRequest.responseText; + } else if (xmlHttpRequest.readyState === 4) { + Tomahawk.log("Failed to do Get request: to: " + url); + Tomahawk.log("Status Code was: " + xmlHttpRequest.status); } }; @@ -237,11 +247,13 @@ Tomahawk.asyncRequest = function(url, callback, extraHeaders) { var xmlHttpRequest = new XMLHttpRequest(); xmlHttpRequest.open('GET', url, true); + if (extraHeaders) { for(var headerName in extraHeaders) { xmlHttpRequest.setRequestHeader(headerName, extraHeaders[headerName]); } } + xmlHttpRequest.onreadystatechange = function() { if (xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200) { callback.call(window, xmlHttpRequest); @@ -250,9 +262,37 @@ Tomahawk.asyncRequest = function(url, callback, extraHeaders) Tomahawk.log("Status Code was: " + xmlHttpRequest.status); } } + xmlHttpRequest.send(null); }; +Tomahawk.asyncFormPostRequest = function(url, params, callback, extraHeaders) +{ + var xmlHttpRequest = new XMLHttpRequest(); + xmlHttpRequest.open('POST', url, true); + + xmlHttpRequest.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); + xmlHttpRequest.setRequestHeader('Content-length', params.length); + xmlHttpRequest.setRequestHeader('Connection', 'close'); + + if (extraHeaders) { + for(var headerName in extraHeaders) { + xmlHttpRequest.setRequestHeader(headerName, extraHeaders[headerName]); + } + } + + xmlHttpRequest.onreadystatechange = function() { + if (xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200) { + callback.call(window, xmlHttpRequest); + } else if (xmlHttpRequest.readyState === 4) { + Tomahawk.log("Failed to do POST request: to: " + url); + Tomahawk.log("Status Code was: " + xmlHttpRequest.status); + } + } + + xmlHttpRequest.send(params); +}; + /** * * Secure Hash Algorithm (SHA256) @@ -387,6 +427,34 @@ Tomahawk.sha256=function(s){ }; +/* + * Bind the function to be executed in the scope of the object oThis. + * Any copyright is dedicated to the Public Domain. + * Source : http://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind#Compatibility + */ +if (!Function.prototype.bind) { + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} // some aliases diff --git a/src/AudioControls.cpp b/src/AudioControls.cpp index a08a69f52a..9ee18bd06a 100644 --- a/src/AudioControls.cpp +++ b/src/AudioControls.cpp @@ -234,8 +234,9 @@ AudioControls::onPlaybackStarted( const Tomahawk::result_ptr& result ) qint64 duration = AudioEngine::instance()->currentTrackTotalTime(); - if ( duration == -1 ) - duration = result.data()->track()->duration() * 1000; + //TODO : check if result->track()->duration() is always valid + if ( duration <= 0 ) + duration = result->track()->duration() * 1000; ui->seekSlider->setRange( 0, duration ); ui->seekSlider->setValue( 0 ); diff --git a/src/libtomahawk/CMakeLists.txt b/src/libtomahawk/CMakeLists.txt index 269697d04b..97493e2982 100644 --- a/src/libtomahawk/CMakeLists.txt +++ b/src/libtomahawk/CMakeLists.txt @@ -119,6 +119,7 @@ set( libGuiSources utils/SharedTimeLine.cpp utils/WebResultHintChecker.cpp utils/NetworkReply.cpp + utils/CloudStream.cpp widgets/AnimatedCounterLabel.cpp widgets/BasicHeader.cpp diff --git a/src/libtomahawk/resolvers/QtScriptResolver.cpp b/src/libtomahawk/resolvers/QtScriptResolver.cpp index da51ce5b8a..987bf15937 100644 --- a/src/libtomahawk/resolvers/QtScriptResolver.cpp +++ b/src/libtomahawk/resolvers/QtScriptResolver.cpp @@ -27,6 +27,11 @@ #include "ScriptCollection.h" #include "SourceList.h" +#include "utils/CloudStream.h" + +#include "utils/TomahawkUtils.h" +#include "TomahawkSettings.h" + #include "accounts/AccountConfigWidget.h" #include "network/Servent.h" @@ -45,6 +50,10 @@ #include #include #include +#include +#include +#include +#include #include @@ -321,6 +330,84 @@ QtScriptResolverHelper::base64Decode( const QByteArray& input ) } +void +QtScriptResolverHelper::readCloudFile(const QString& fileName, const QString& fileId, + const QString& sizeS, const QString& mime_type, + const QVariant& requestJS, const QString& javascriptCallbackFunction, + const QString& javascriptRefreshUrlFunction, const bool refreshUrlEachTime) +{ + + QVariantMap request; + QUrl download_url; + QVariantMap headers; + long size = sizeS.toLong(); + QString urlString; + + if ( requestJS.type() == QVariant::Map ) + { + request = requestJS.toMap(); + + urlString = request["url"].toString(); + + headers = request["headers"].toMap(); + } + else + { + urlString = requestJS.toString(); + } + + download_url.setUrl( urlString ); + + tDebug( LOGINFO ) << "#### ReadCloudFile : Loading tags of " << fileName << " from " << download_url.toString() << " which have id " << fileId; + + + CloudStream* stream = new CloudStream( download_url, fileName, fileId, + size, mime_type, headers, m_resolver, + javascriptRefreshUrlFunction, javascriptCallbackFunction, refreshUrlEachTime ); + + connect( stream, SIGNAL( tagsReady(QVariantMap &, const QString& ) ), this, SLOT( onTagReady( QVariantMap&, const QString& ) ) ); + stream->precache(); +} + + +void +QtScriptResolverHelper::onTagReady( QVariantMap &tags, const QString& javascriptCallbackFunction ) +{ + + QJson::Serializer serializer; + + QByteArray json = serializer.serialize( tags ); + + tDebug() << "#### ReadCloudFile : Sending tags to js : " << json; + + QString getUrl = QString( "Tomahawk.resolver.instance.%1( %2 );" ).arg( javascriptCallbackFunction ) + .arg( QString(json) ); + + m_resolver->m_engine->mainFrame()->evaluateJavaScript( getUrl ); +} + + +void +QtScriptResolverHelper::requestWebView(const QString &varName, const QString &url) +{ + QWebView *view = new QWebView(); + view->load(QUrl(url)); + + //TODO: move this to JS. + view->setWindowModality(Qt::ApplicationModal); + + m_resolver->m_engine->mainFrame()->addToJavaScriptWindowObject(varName, view); +} + +void +QtScriptResolverHelper::showWebInspector() +{ + QWebInspector *inspector = new QWebInspector; + inspector->setPage(m_resolver->m_engine); + inspector->show(); +} + + void QtScriptResolverHelper::customIODeviceFactory( const Tomahawk::result_ptr& result, boost::function< void( QSharedPointer< QIODevice >& ) > callback ) @@ -343,28 +430,46 @@ QtScriptResolverHelper::customIODeviceFactory( const Tomahawk::result_ptr& resul QString getUrl = QString( "Tomahawk.resolver.instance.%1( '%2' );" ).arg( m_urlCallback ) .arg( origResultUrl ); - QString urlStr = m_resolver->m_engine->mainFrame()->evaluateJavaScript( getUrl ).toString(); + QString urlStr; + QVariantMap headers; + QVariant jsResult = m_resolver->m_engine->mainFrame()->evaluateJavaScript( getUrl ); + + if ( jsResult.type() == QVariant::String ) + { + urlStr = jsResult.toString(); + } + else if ( jsResult.type() == QVariant::Map ) + { + QVariantMap request = jsResult.toMap(); + + urlStr = request["url"].toString(); + + headers = request["headers"].toMap(); + } - returnStreamUrl( urlStr, callback ); + returnStreamUrl( urlStr, callback, headers ); } } void QtScriptResolverHelper::reportStreamUrl( const QString& qid, - const QString& streamUrl ) + const QString& streamUrl, + const QVariantMap& headers ) { if ( !m_streamCallbacks.contains( qid ) ) return; boost::function< void( QSharedPointer< QIODevice >& ) > callback = m_streamCallbacks.take( qid ); - returnStreamUrl( streamUrl, callback ); + returnStreamUrl( streamUrl, callback, headers ); } void -QtScriptResolverHelper::returnStreamUrl( const QString& streamUrl, boost::function< void( QSharedPointer< QIODevice >& ) > callback ) +QtScriptResolverHelper::returnStreamUrl( const QString& streamUrl, + boost::function< void( QSharedPointer< QIODevice >& ) > callback, + const QVariantMap& headers) { QSharedPointer< QIODevice > sp; if ( streamUrl.isEmpty() ) @@ -375,6 +480,12 @@ QtScriptResolverHelper::returnStreamUrl( const QString& streamUrl, boost::functi QUrl url = QUrl::fromEncoded( streamUrl.toUtf8() ); QNetworkRequest req( url ); + + foreach ( const QString& headerName, headers.keys() ) + { + req.setRawHeader( headerName.toLocal8Bit(), headers[headerName].toString().toLocal8Bit() ); + } + tDebug() << "Creating a QNetowrkReply with url:" << req.url().toString(); QNetworkReply* reply = TomahawkUtils::nam()->get( req ); @@ -400,6 +511,7 @@ QtScriptResolver::QtScriptResolver( const QString& scriptPath, const QStringList , m_stopped( true ) , m_error( Tomahawk::ExternalResolver::NoError ) , m_resolverHelper( new QtScriptResolverHelper( scriptPath, this ) ) + , m_signalMapper( new QSignalMapper(this) ) , m_requiredScriptPaths( additionalScriptPaths ) { tLog() << Q_FUNC_INFO << "Loading JS resolver:" << scriptPath; @@ -410,6 +522,8 @@ QtScriptResolver::QtScriptResolver( const QString& scriptPath, const QStringList // set the icon, if we launch properly we'll get the icon the resolver reports m_icon = TomahawkUtils::defaultPixmap( TomahawkUtils::DefaultResolver, TomahawkUtils::Original, QSize( 128, 128 ) ); + connect( m_signalMapper, SIGNAL( mapped ( const QString & ) ), this, SLOT( executeJavascript(const QString &) ) ); + if ( !QFile::exists( filePath() ) ) { tLog() << Q_FUNC_INFO << "Failed loading JavaScript resolver:" << scriptPath; @@ -923,9 +1037,11 @@ QtScriptResolver::loadDataFromWidgets() QString widgetName = data["widget"].toString(); QWidget* widget= m_configWidget.data()->findChild( widgetName ); - QVariant value = widgetData( widget, data["property"].toString() ); - - saveData[ data["name"].toString() ] = value; + if( data.contains("property") ) + { + QVariant value = widgetData( widget, data["property"].toString() ); + saveData[ data["name"].toString() ] = value; + } } return saveData; @@ -937,7 +1053,9 @@ QtScriptResolver::fillDataInWidgets( const QVariantMap& data ) { foreach(const QVariant& dataWidget, m_dataWidgets) { - QString widgetName = dataWidget.toMap()["widget"].toString(); + QVariantMap mapDataWidget = dataWidget.toMap(); + QString widgetName = mapDataWidget["widget"].toString(); + QWidget* widget= m_configWidget.data()->findChild( widgetName ); if( !widget ) { @@ -945,11 +1063,46 @@ QtScriptResolver::fillDataInWidgets( const QVariantMap& data ) Q_ASSERT(false); return; } + if( mapDataWidget.contains("property") ) + { + QString propertyName = mapDataWidget["property"].toString(); + QString name = mapDataWidget["name"].toString(); + + setWidgetData( data[ name ], widget, propertyName ); + } + if( mapDataWidget.contains("connections") ) + { + connectUISlots( widget, mapDataWidget["connections"].toList() ); + } + } +} + - QString propertyName = dataWidget.toMap()["property"].toString(); - QString name = dataWidget.toMap()["name"].toString(); +void QtScriptResolver::connectUISlots( QWidget* widget, const QVariantList &connectionsList ) +{ + foreach( const QVariant& connection, connectionsList ) + { + QVariantMap params = connection.toMap(); - setWidgetData( data[ name ], widget, propertyName ); + if( params.contains("signal") && params.contains("javascriptCallback") ) + { + int iSignal = widget->metaObject()->indexOfSignal( params["signal"].toString() + .toLocal8Bit() + .data() + ); + + if( iSignal != -1 ){ + + QMetaMethod signal = widget->metaObject()->method( iSignal ); + QMetaMethod slot = m_signalMapper->metaObject()->method( m_signalMapper + ->metaObject()-> + indexOfSlot("map()") ); + + connect( widget , signal , m_signalMapper, slot ); + //TODO : check if mapping were previously done on widget, if you set the same widget twice the first mapping will be replaced. + m_signalMapper->setMapping( widget, params["javascriptCallback"].toString() ); + } + } } } @@ -1091,3 +1244,8 @@ QtScriptResolver::resolverCollections() // + data. } +QVariant QtScriptResolver::executeJavascript(const QString &js) +{ + return m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE + js ); +} + diff --git a/src/libtomahawk/resolvers/QtScriptResolver.h b/src/libtomahawk/resolvers/QtScriptResolver.h index f9dd4b4b0c..fd9e7f5474 100644 --- a/src/libtomahawk/resolvers/QtScriptResolver.h +++ b/src/libtomahawk/resolvers/QtScriptResolver.h @@ -33,6 +33,8 @@ #include #include #include +#include +#include #ifdef QCA2_FOUND #include @@ -55,11 +57,20 @@ Q_OBJECT Q_INVOKABLE QString md5( const QByteArray& input ); Q_INVOKABLE void addCustomUrlHandler( const QString& protocol, const QString& callbackFuncName, const QString& isAsynchronous = "false" ); - Q_INVOKABLE void reportStreamUrl( const QString& qid, const QString& streamUrl ); + Q_INVOKABLE void reportStreamUrl( const QString& qid, const QString& streamUrl, const QVariantMap& headers = QVariantMap() ); Q_INVOKABLE QByteArray base64Encode( const QByteArray& input ); Q_INVOKABLE QByteArray base64Decode( const QByteArray& input ); + // send ID3Tags of the stream as argument of the callback function + Q_INVOKABLE void readCloudFile( const QString& fileName, const QString& fileId, const QString& sizeS, + const QString& mime_type, const QVariant& requestJS, const QString& javascriptCallbackFunction, + const QString& javascriptRefreshUrlFunction = QString(), const bool refreshUrlEachTime = false ); + + Q_INVOKABLE void requestWebView(const QString& varName, const QString& url); + + Q_INVOKABLE void showWebInspector(); + void customIODeviceFactory( const Tomahawk::result_ptr& result, boost::function< void( QSharedPointer< QIODevice >& ) > callback ); // async @@ -82,8 +93,13 @@ public slots: void reportCapabilities( const QVariant& capabilities ); +private slots: + void onTagReady(QVariantMap &tags, const QString&); + private: - void returnStreamUrl( const QString& streamUrl, boost::function< void( QSharedPointer< QIODevice >& ) > callback ); + void returnStreamUrl( const QString& streamUrl, + boost::function< void( QSharedPointer< QIODevice >& ) > callback , + const QVariantMap &headers = QVariantMap() ); QString m_scriptPath, m_urlCallback; QHash< QString, boost::function< void( QSharedPointer< QIODevice >& ) > > m_streamCallbacks; bool m_urlCallbackIsAsync; @@ -110,6 +126,7 @@ Q_OBJECT settings()->setAttribute( QWebSettings::LocalStorageDatabaseEnabled, true ); settings()->setAttribute( QWebSettings::LocalContentCanAccessFileUrls, true ); settings()->setAttribute( QWebSettings::LocalContentCanAccessRemoteUrls, true ); + settings()->setAttribute( QWebSettings::DeveloperExtrasEnabled, true ); // Tomahawk is not a user agent m_header = QWebPage::userAgentForUrl( QUrl() ).replace( QString( "%1/%2" ) @@ -183,6 +200,8 @@ public slots: virtual void albums( const Tomahawk::collection_ptr& collection, const Tomahawk::artist_ptr& artist ); virtual void tracks( const Tomahawk::collection_ptr& collection, const Tomahawk::album_ptr& album ); + QVariant executeJavascript(const QString& ); + signals: void stopped(); @@ -199,6 +218,7 @@ private slots: void fillDataInWidgets( const QVariantMap& data ); void onCapabilitiesChanged( Capabilities capabilities ); void loadCollections(); + void connectUISlots( QWidget*, const QVariantList & ); // encapsulate javascript calls QVariantMap resolverSettings(); @@ -217,6 +237,7 @@ private slots: QPixmap m_icon; unsigned int m_weight, m_timeout; Capabilities m_capabilities; + QSignalMapper* m_signalMapper; bool m_ready, m_stopped; ExternalResolver::ErrorState m_error; diff --git a/src/libtomahawk/utils/CloudStream.cpp b/src/libtomahawk/utils/CloudStream.cpp new file mode 100644 index 0000000000..2134f2ed96 --- /dev/null +++ b/src/libtomahawk/utils/CloudStream.cpp @@ -0,0 +1,485 @@ +/* This file is part of Clementine. + Copyright 2012, David Sansome + + Clementine 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 3 of the License, or + (at your option) any later version. + + Clementine 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 Clementine. If not, see . +*/ + +#include "CloudStream.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef TAGLIB_HAS_OPUS +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "qjson/serializer.h" + +#include "utils/Logger.h" + +#include "resolvers/QtScriptResolver.h" + +namespace +{ + static const int kTaglibPrefixCacheBytes = 64 * 1024; // Should be enough. + static const int kTaglibSuffixCacheBytes = 8 * 1024; +} + +const static int MAX_ALLOW_ERROR_QUERY = 2; + +CloudStream::CloudStream( QUrl& url, + const QString& filename, + const QString& fileId, + const long length, + const QString& mimeType, + QVariantMap& headers, + QtScriptResolver* scriptResolver, + const QString & javascriptRefreshUrlFunction, + const QString& javascriptCallbackFunction, + const bool refreshUrlEachTime ) + : m_url( url ) + , m_filename( filename ) + , m_fileId( fileId ) + , m_encoded_filename( m_filename.toUtf8() ) + , m_length( length ) + , m_headers( headers ) + , m_cursor( 0 ) + , m_cache( length ) + , m_num_requests( 0 ) + , m_num_requests_in_error( 0 ) + , m_scriptResolver( scriptResolver ) + , m_javascriptRefreshUrlFunction( javascriptRefreshUrlFunction ) + , m_javascriptCallbackFunction( javascriptCallbackFunction ) + , m_refreshUrlEachTime( refreshUrlEachTime ) + , m_currentBlocklength( 0 ) + , m_cacheState( CloudStream::BeginningCache ) + , m_tags( QVariantMap() ) +{ + m_network = TomahawkUtils::nam(); + connect( this, SIGNAL( cacheReadFinished() ), this, SLOT( precache() ) ); + m_tags["fileId"] = fileId; + m_tags["mimetype"] = mimeType; +} + +TagLib::FileName +CloudStream::name() const +{ + return m_encoded_filename.data(); +} + +bool +CloudStream::CheckCache( int start, int end ) +{ + for ( int i = start; i <= end; ++i ) { + if ( !m_cache.test( i ) ) { + return false; + } + } + return true; +} + +void +CloudStream::FillCache( int start, TagLib::ByteVector data ) +{ + for ( int i = 0; i < data.size(); ++i ) + { + m_cache.set( start + i, data[i] ); + } +} + +TagLib::ByteVector +CloudStream::GetCached( int start, int end ) +{ + const uint size = end - start + 1; + TagLib::ByteVector ret( size ); + for ( int i = 0; i < size; ++i ) + { + ret[i] = m_cache.get( start + i ); + } + return ret; +} + +void +CloudStream::precache() +{ + // For reading the tags of an MP3, TagLib tends to request: + // 1. The first 1024 bytes + // 2. Somewhere between the first 2KB and first 60KB + // 3. The last KB or two. + // 4. Somewhere in the first 64KB again + // + // OGG Vorbis may read the last 4KB. + // + // So, if we precache the first 64KB and the last 8KB we should be sorted :-) + // Ideally, we would use bytes=0-655364,-8096 but Google Drive does not seem + // to support multipart byte ranges yet so we have to make do with two + // requests. + tDebug( LOGINFO ) << "#### CloudStream : Precaching from :" << m_filename; + + switch( m_cacheState ) + { + case CloudStream::BeginningCache : + { + seek( 0, TagLib::IOStream::Beginning ); + readBlock( kTaglibPrefixCacheBytes ); + m_cacheState = CloudStream::EndCache; + break; + } + + case CloudStream::EndCache : + { + seek( kTaglibSuffixCacheBytes, TagLib::IOStream::End ); + readBlock( kTaglibSuffixCacheBytes ); + m_cacheState = CloudStream::EndCacheDone; + break; + } + + case CloudStream::EndCacheDone : + { + clear(); + // construct the tag map + QString mimeType = m_tags["mimetype"].toString(); + boost::scoped_ptr tag; + + if ( mimeType == "audio/mpeg" ) // && title.endsWith(".mp3")) + { + tag.reset(new TagLib::MPEG::File( + this, // Takes ownership. + TagLib::ID3v2::FrameFactory::instance(), + TagLib::AudioProperties::Accurate)); + } + else if ( mimeType == "audio/mp4" || ( mimeType == "audio/mpeg" ) ) // && title.endsWith(".m4a"))) + { + tag.reset( new TagLib::MP4::File( this, true, TagLib::AudioProperties::Accurate ) ); + } + else if ( mimeType == "application/ogg" || mimeType == "audio/ogg" ) + { + tag.reset( new TagLib::Ogg::Vorbis::File( this, true, TagLib::AudioProperties::Accurate ) ); + } + #ifdef TAGLIB_HAS_OPUS + else if ( mimeType == "application/opus" || mimeType == "audio/opus" ) + { + tag.reset( new TagLib::Ogg::Opus::File( this, true, TagLib::AudioProperties::Accurate ) ); + } + #endif + else if ( mimeType == "application/x-flac" || mimeType == "audio/flac" ) + { + tag.reset( new TagLib::FLAC::File( this, TagLib::ID3v2::FrameFactory::instance(), true, + TagLib::AudioProperties::Accurate ) ); + } + else if ( mimeType == "audio/x-ms-wma" ) + { + tag.reset( new TagLib::ASF::File( this, true, TagLib::AudioProperties::Accurate ) ); + } + else + { + tDebug( LOGINFO ) << "Unknown mime type for tagging:" << mimeType; + } + + if (this->num_requests() > 2) { + // Warn if pre-caching failed. + tDebug( LOGINFO ) << "Total requests for file:" << m_tags["fileId"] + << " : " << this->num_requests() << " with " + << this->cached_bytes() << " bytes cached"; + } + + //construction of the tag's map + if ( tag->tag() && !tag->tag()->isEmpty() ) + { + m_tags["track"] = tag->tag()->title().toCString( true ); + m_tags["artist"] = tag->tag()->artist().toCString( true ); + m_tags["album"] = tag->tag()->album().toCString( true ); + m_tags["size"] = QString::number( m_length ); + + if ( tag->tag()->track() != 0 ) + { + m_tags["albumpos"] = tag->tag()->track(); + } + if ( tag->tag()->year() != 0 ) + { + m_tags["year"] = tag->tag()->year(); + } + + if ( tag->audioProperties() ) + { + m_tags["duration"] = tag->audioProperties()->length(); + m_tags["bitrate"] = tag->audioProperties()->bitrate(); + } + } + emit tagsReady( m_tags, m_javascriptCallbackFunction ); + break; + } + } + +} + +TagLib::ByteVector +CloudStream::readBlock( ulong length ) +{ + const uint start = m_cursor; + const uint end = qMin( m_cursor + length - 1, m_length - 1 ); + + //tDebug( LOGINFO ) << "#### CloudStream : parsing from " << m_url.toString(); + //tDebug( LOGINFO ) << "#### CloudStream : parsing from (encoded) " << m_url.toEncoded().constData(); + if ( end < start ) + { + return TagLib::ByteVector(); + } + + if ( CheckCache( start, end ) ) + { + TagLib::ByteVector cached = GetCached( start, end ); + m_cursor += cached.size(); + return cached; + } + + if ( m_num_requests_in_error > MAX_ALLOW_ERROR_QUERY ) + { + //precache(); + return TagLib::ByteVector(); + } + + if ( m_refreshUrlEachTime ) + { + if( !refreshStreamUrl() ) + { + tDebug( LOGINFO ) << "#### CloudStream : cannot refresh streamUrl for " << m_filename; + } + } + + QNetworkRequest request = QNetworkRequest( m_url ); + + //setings of specials OAuth (1 or 2) headers + foreach ( const QString& headerName, m_headers.keys() ) + { + request.setRawHeader( headerName.toLocal8Bit(), m_headers[headerName].toString().toLocal8Bit() ); + + } + + request.setRawHeader( "Range", QString( "bytes=%1-%2" ).arg( start ).arg( end ).toUtf8() ); + request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork ); + // The Ubuntu One server applies the byte range to the gzipped data, rather + // than the raw data so we must disable compression. + if ( m_url.host() == "files.one.ubuntu.com" ) + { + request.setRawHeader( "Accept-Encoding", "identity" ); + } + + tDebug() << "######## CloudStream : HTTP request : "; + tDebug() << "#### CloudStream : url : " << request.url(); + + m_currentBlocklength = length; + m_currentStart = start; + + m_reply = m_network->get( request ); + + connect( m_reply, SIGNAL( sslErrors( QList ) ), SLOT( SSLErrors( QList ) ) ); + connect( m_reply, SIGNAL( finished() ), this, SLOT( onRequestFinished() ) ); + + ++m_num_requests; + return TagLib::ByteVector(); +} + + +void +CloudStream::onRequestFinished() +{ + m_reply->deleteLater(); + + int code = m_reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + + tDebug() << "######### CloudStream : HTTP reply : #########"; + tDebug( LOGINFO ) << "#### Cloudstream : HttpStatusCode : " << code; + tDebug( LOGINFO ) << "#### Cloudstream : URL : " << m_reply->url(); + + QByteArray data = m_reply->readAll(); + + if ( code != 206 ) + { + m_num_requests_in_error++; + tDebug( LOGINFO ) << "#### Cloudstream : Error " << code << " retrieving url to tag for " << m_filename; + tDebug() << "#### CloudStream : body response : " << data; + + if ( refreshStreamUrl() ) + { + readBlock (m_currentBlocklength); + return; + } + else + { + //return TagLib::ByteVector(); + precache(); + return ; + } + } + + + TagLib::ByteVector bytes( data.data(), data.size() ); + m_cursor += data.size(); + + FillCache( m_currentStart, bytes ); + precache(); +} + + +bool +CloudStream::refreshStreamUrl() +{ + if ( m_javascriptRefreshUrlFunction.isEmpty() ) + { + return false; + } + tDebug( LOGINFO ) << "####### Cloudstream : refreshing streamUrl for " << m_filename; + QString refreshUrl = QString( "resolver.%1( \"%2\" );" ).arg( m_javascriptRefreshUrlFunction ) + .arg( m_fileId ); + QVariant response = m_scriptResolver->executeJavascript( refreshUrl ); + + if ( response.isNull() ) + { + tDebug( LOGINFO ) << "####### Cloudstream : refreshUrl response is empty, returning"; + return false; + } + + QVariantMap request; + QString urlString; + + if ( response.type() == QVariant::Map ) + { + request = response.toMap(); + + urlString = request["url"].toString(); + + m_headers = request["headers"].toMap(); + } + else + { + urlString = response.toString(); + } + + m_url.setUrl( urlString ); + return true; +} + +void +CloudStream::writeBlock( const TagLib::ByteVector& ) +{ + tDebug( LOGINFO ) << "writeBlock not implemented"; +} + +void +CloudStream::insert( const TagLib::ByteVector&, ulong, ulong ) +{ + tDebug( LOGINFO ) << "insert not implemented"; +} + +void +CloudStream::removeBlock( ulong, ulong ) +{ + tDebug( LOGINFO ) << "removeBlock not implemented"; +} + +bool +CloudStream::readOnly() const +{ + tDebug( LOGINFO ) << "readOnly not implemented"; + return true; +} + +bool +CloudStream::isOpen() const +{ + return true; +} + +void +CloudStream::seek( long offset, TagLib::IOStream::Position p ) +{ + switch ( p ) + { + case TagLib::IOStream::Beginning: + m_cursor = offset; + break; + + case TagLib::IOStream::Current: + m_cursor = qMin( ulong( m_cursor + offset ), m_length ); + break; + + case TagLib::IOStream::End: + // This should really not have qAbs(), but OGG reading needs it. + m_cursor = qMax( 0UL, m_length - qAbs( offset ) ); + break; + } +} + +void +CloudStream::clear() +{ + m_cursor = 0; +} + +long +CloudStream::tell() const +{ + return m_cursor; +} + +long +CloudStream::length() +{ + return m_length; +} + +void +CloudStream::truncate( long ) +{ + tDebug( LOGINFO ) << "not implemented"; +} + +void +CloudStream::SSLErrors( const QList& errors ) +{ + foreach ( const QSslError& error, errors ) + { + tDebug( LOGINFO ) << "#### Cloudstream : Error for " << m_filename << " : "; + tDebug( LOGINFO ) << error.error() << error.errorString(); + tDebug( LOGINFO ) << error.certificate(); + } +} diff --git a/src/libtomahawk/utils/CloudStream.h b/src/libtomahawk/utils/CloudStream.h new file mode 100644 index 0000000000..d05565ffa0 --- /dev/null +++ b/src/libtomahawk/utils/CloudStream.h @@ -0,0 +1,130 @@ +/* This file is part of Clementine. + Copyright 2012, David Sansome + + Clementine 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 3 of the License, or + (at your option) any later version. + + Clementine 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 Clementine. If not, see . +*/ + +//#ifndef GOOGLEDRIVESTREAM_H +//#define GOOGLEDRIVESTREAM_H + +#include +#include +#include +#include +#include ; + + +#include +#include + +class QNetworkAccessManager; +class QtScriptResolver; +class QNetworkReply; + +class CloudStream : public QObject, public TagLib::IOStream +{ + Q_OBJECT +public: + CloudStream( QUrl& url, + const QString& filename, + const QString& fileId, + const long length, + const QString& mimeType, + QVariantMap& headers, + QtScriptResolver *scriptResolver, + const QString& javascriptRefreshUrlFunction, + const QString& javascriptCallbackFunction, + const bool refreshUrlEachTime ); + + //Taglib::IOStream; + virtual TagLib::FileName name() const; + virtual TagLib::ByteVector readBlock( ulong length ); + virtual void writeBlock( const TagLib::ByteVector& ); + virtual void insert( const TagLib::ByteVector&, ulong, ulong ); + virtual void removeBlock( ulong, ulong ); + virtual bool readOnly() const; + virtual bool isOpen() const; + virtual void seek( long offset, TagLib::IOStream::Position p ); + virtual void clear(); + virtual long tell() const; + virtual long length(); + virtual void truncate( long ); + + google::sparsetable::size_type cached_bytes() const + { + return m_cache.num_nonempty(); + } + + int num_requests() const + { + return m_num_requests; + } + + int num_requests_in_error() const + { + return m_num_requests_in_error; + } + + bool refreshStreamUrl(); + +public slots: + // Use educated guess to request the bytes that TagLib will probably want. + void precache(); + +signals: + void tagsReady( QVariantMap &, const QString & ); + void cacheReadFinished(); + +private: + bool CheckCache( int start, int end ); + void FillCache( int start, TagLib::ByteVector data ); + TagLib::ByteVector GetCached( int start, int end ); + + enum ReadingCacheState + { + BeginningCache, + EndCache, + EndCacheDone + }; + +private slots: + void SSLErrors( const QList& errors ); + void onRequestFinished(); + +private: + QUrl m_url; + const QString m_filename; + const QString m_fileId; + const QByteArray m_encoded_filename; + const ulong m_length; + QVariantMap m_headers; + const QString m_javascriptRefreshUrlFunction; + const bool m_refreshUrlEachTime; + ulong m_currentBlocklength; + ReadingCacheState m_cacheState; + QVariantMap m_tags; + uint m_currentStart; + const QString m_javascriptCallbackFunction; + + int m_cursor; + QNetworkAccessManager* m_network; + QNetworkReply* m_reply; + QtScriptResolver* m_scriptResolver; + + google::sparsetable m_cache; + int m_num_requests; + int m_num_requests_in_error; +}; + +//#endif // GOOGLEDRIVESTREAM_H