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