1
0
mirror of https://github.com/tomahawk-player/tomahawk.git synced 2025-08-08 15:16:34 +02:00

Merge pull request #339 from tomahawk-player/promisify-all-the-things

[WIP] Promisify JSResolver
This commit is contained in:
Dominik Schmidt
2015-12-14 21:51:11 +01:00
33 changed files with 694 additions and 834 deletions

View File

@@ -49,7 +49,7 @@ RSVP.on('error', function (reason) {
resolverName = Tomahawk.resolver.instance.settings.name + " - "; resolverName = Tomahawk.resolver.instance.settings.name + " - ";
} }
if (reason) { if (reason) {
console.error(resolverName + 'Uncaught error:' + JSON.stringify(reason)); console.error(resolverName + 'Uncaught error:', reason);
} else { } else {
console.error(resolverName + 'Uncaught error: error thrown from RSVP but it was empty'); console.error(resolverName + 'Uncaught error: error thrown from RSVP but it was empty');
} }
@@ -173,7 +173,8 @@ var TomahawkUrlType = {
Playlist: 1, Playlist: 1,
Track: 2, Track: 2,
Album: 4, Album: 4,
Artist: 8 Artist: 8,
Xspf: 16
}; };
//Deprecated for 0.9 resolvers. Use Tomahawk.ConfigTestResultType instead. //Deprecated for 0.9 resolvers. Use Tomahawk.ConfigTestResultType instead.
@@ -272,74 +273,19 @@ Tomahawk.Resolver = {
getStreamUrl: function (params) { getStreamUrl: function (params) {
return params; return params;
}, },
_adapter_resolve: function (params) {
_convertUrls: function (results) { return RSVP.Promise.resolve(this.resolve(params)).then(function (results) {
var that = this; return {
return results.map(function (r) { 'tracks': results
if (r && r.url) { };
r.url = that._urlProtocol + '://' + r.url;
}
return r;
}); });
}, },
_adapter_resolve: function (qid, artist, album, title) { _adapter_search: function (params) {
var that = this; return RSVP.Promise.resolve(this.search(params)).then(function (results) {
var collectionPromises = []; return {
Tomahawk.collections.forEach(function (col) { 'tracks': results
if (col.resolve) { };
collectionPromises.push(col.resolve({artist: artist, album: album, track: title}));
}
});
RSVP.Promise.all(collectionPromises).then(function (collectionResults) {
var merged = [];
return merged.concat.apply(merged, collectionResults);
}).then(function (collectionResults) {
RSVP.Promise.resolve(that.resolve({
artist: artist,
album: album,
track: title
})).then(function (results) {
Tomahawk.addTrackResults({
'qid': qid,
'results': that._convertUrls(results.concat(collectionResults))
});
});
});
},
_adapter_init: function () {
this._urlProtocol = this.settings.name.replace(/[^a-zA-Z]/g, '').toLowerCase();
Tomahawk.addCustomUrlHandler(this._urlProtocol, '_adapter_getStreamUrl', true);
Tomahawk.log('Registered custom url handler for protocol "' + this._urlProtocol + '"');
this.init();
},
_adapter_getStreamUrl: function (params) {
params.url = params.url.slice(this._urlProtocol.length + 3);
RSVP.Promise.resolve(this.getStreamUrl(params)).then(function (result) {
Tomahawk.reportStreamUrl(params.qid, result.url, result.headers);
});
},
_adapter_search: function (qid, query) {
var that = this;
var collectionPromises = [];
Tomahawk.collections.forEach(function (col) {
if (col.search) {
collectionPromises.push(col.search({query: query}));
}
});
RSVP.Promise.all(collectionPromises).then(function (collectionResults) {
var merged = [];
return merged.concat.apply(merged, collectionResults);
}).then(function (collectionResults) {
RSVP.Promise.resolve(that.search({query: query})).then(function (results) {
Tomahawk.addTrackResults({
'qid': qid,
'results': that._convertUrls(results.concat(collectionResults))
});
});
}); });
}, },
@@ -366,34 +312,6 @@ Tomahawk.valueForSubNode = function (node, tag) {
return element.textContent; return element.textContent;
}; };
/**
* Do a synchronous HTTP(S) request. For further options see
* Tomahawk.asyncRequest
*/
Tomahawk.syncRequest = function (url, extraHeaders, options) {
// unpack options
var opt = options || {};
var method = opt.method || 'GET';
var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open(method, url, false, opt.username, opt.password);
if (extraHeaders) {
for (var headerName in extraHeaders) {
xmlHttpRequest.setRequestHeader(headerName, extraHeaders[headerName]);
}
}
xmlHttpRequest.send(null);
if (httpSuccessStatuses.indexOf(xmlHttpRequest.status) != -1) {
return xmlHttpRequest.responseText;
} else {
Tomahawk.log("Failed to do GET request: to: " + url);
Tomahawk.log("Status Code was: " + xmlHttpRequest.status);
if (opt.hasOwnProperty('errorHandler')) {
opt.errorHandler.call(window, xmlHttpRequest);
}
}
};
/** /**
* Internal counter used to identify retrievedMetadata call back from native * Internal counter used to identify retrievedMetadata call back from native
* code. * code.
@@ -489,6 +407,23 @@ Tomahawk.nativeAsyncRequestDone = function (reqId, xhr) {
delete Tomahawk.asyncRequestCallbacks[reqId]; delete Tomahawk.asyncRequestCallbacks[reqId];
}; };
/**
* This method is externalized from Tomahawk.asyncRequest, so that other clients
* (like tomahawk-android) can inject their own logic that determines whether or not to do a request
* natively.
*
* @returns boolean indicating whether or not to do a request with the given parameters natively
*/
var shouldDoNativeRequest = function (options) {
var extraHeaders = options.headers;
return (extraHeaders && (extraHeaders.hasOwnProperty("Referer")
|| extraHeaders.hasOwnProperty("referer")
|| extraHeaders.hasOwnProperty("User-Agent")));
};
/** /**
* Possible options: * Possible options:
* - method: The HTTP request method (default: GET) * - method: The HTTP request method (default: GET)
@@ -498,59 +433,45 @@ Tomahawk.nativeAsyncRequestDone = function (reqId, xhr) {
* - data: body data included in POST requests * - data: body data included in POST requests
* - needCookieHeader: boolean indicating whether or not the request needs to be able to get the * - needCookieHeader: boolean indicating whether or not the request needs to be able to get the
* "Set-Cookie" response header * "Set-Cookie" response header
* - headers: headers set on the request
*/ */
Tomahawk.asyncRequest = function (url, callback, extraHeaders, options) { var doRequest = function(options) {
// unpack options if (shouldDoNativeRequest(options)) {
var opt = options || {}; return Tomahawk.NativeScriptJobManager.invoke('httpRequest', options).then(function(xhr) {
var method = opt.method || 'GET'; xhr.responseHeaders = xhr.responseHeaders || {};
xhr.getAllResponseHeaders = function() {
return this.responseHeaders;
};
xhr.getResponseHeader = function (header) {
return this.responseHeaders[header];
};
if (shouldDoNativeRequest(url, callback, extraHeaders, options)) { return xhr;
// Assign a request Id to the callback so we can use it when we are });
// returning from the native call.
var reqId = Tomahawk.asyncRequestIdCounter;
Tomahawk.asyncRequestIdCounter++;
Tomahawk.asyncRequestCallbacks[reqId] = {
callback: callback,
errorHandler: opt.errorHandler
};
Tomahawk.nativeAsyncRequest(reqId, url, extraHeaders, options);
} else { } else {
var xmlHttpRequest = new XMLHttpRequest(); return new RSVP.Promise(function(resolve, reject) {
xmlHttpRequest.open(method, url, true, opt.username, opt.password); var xmlHttpRequest = new XMLHttpRequest();
if (extraHeaders) { xmlHttpRequest.open(options.method, options.url, true, options.username, options.password);
for (var headerName in extraHeaders) { if (options.headers) {
xmlHttpRequest.setRequestHeader(headerName, extraHeaders[headerName]); for (var headerName in options.headers) {
} xmlHttpRequest.setRequestHeader(headerName, options.headers[headerName]);
}
xmlHttpRequest.onreadystatechange = function () {
if (xmlHttpRequest.readyState == 4
&& httpSuccessStatuses.indexOf(xmlHttpRequest.status) != -1) {
callback.call(window, xmlHttpRequest);
} else if (xmlHttpRequest.readyState === 4) {
Tomahawk.log("Failed to do " + method + " request: to: " + url);
Tomahawk.log("Status Code was: " + xmlHttpRequest.status);
if (opt.hasOwnProperty('errorHandler')) {
opt.errorHandler.call(window, xmlHttpRequest);
} }
} }
}; xmlHttpRequest.onreadystatechange = function () {
xmlHttpRequest.send(opt.data || null); if (xmlHttpRequest.readyState == 4
&& httpSuccessStatuses.indexOf(xmlHttpRequest.status) != -1) {
resolve(xmlHttpRequest);
} else if (xmlHttpRequest.readyState === 4) {
Tomahawk.log("Failed to do " + options.method + " request: to: " + options.url);
Tomahawk.log("Status Code was: " + xmlHttpRequest.status);
reject(xmlHttpRequest);
}
};
xmlHttpRequest.send(options.data || null);
});
} }
}; };
/**
* This method is externalized from Tomahawk.asyncRequest, so that other clients
* (like tomahawk-android) can inject their own logic that determines whether or not to do a request
* natively.
*
* @returns boolean indicating whether or not to do a request with the given parameters natively
*/
var shouldDoNativeRequest = function (url, callback, extraHeaders, options) {
return (extraHeaders && (extraHeaders.hasOwnProperty("Referer")
|| extraHeaders.hasOwnProperty("referer")
|| extraHeaders.hasOwnProperty("User-Agent")));
};
Tomahawk.ajax = function (url, settings) { Tomahawk.ajax = function (url, settings) {
if (typeof url === "object") { if (typeof url === "object") {
settings = url; settings = url;
@@ -608,10 +529,7 @@ Tomahawk.ajax = function (url, settings) {
} }
} }
return new RSVP.Promise(function (resolve, reject) { return doRequest(settings).then(function (xhr) {
settings.errorHandler = reject;
Tomahawk.asyncRequest(settings.url, resolve, settings.headers, settings);
}).then(function (xhr) {
if (settings.rawResponse) { if (settings.rawResponse) {
return xhr; return xhr;
} }
@@ -817,7 +735,9 @@ Tomahawk.base64Encode = function (b) {
return window.btoa(b); return window.btoa(b);
}; };
Tomahawk.PluginManager = { Tomahawk.PluginManager = {
wrapperPrefix: '_adapter_',
objects: {}, objects: {},
objectCounter: 0, objectCounter: 0,
identifyObject: function (object) { identifyObject: function (object) {
@@ -829,10 +749,6 @@ Tomahawk.PluginManager = {
}, },
registerPlugin: function (type, object) { registerPlugin: function (type, object) {
this.objects[this.identifyObject(object)] = object; this.objects[this.identifyObject(object)] = object;
if (type === 'collection') {
Tomahawk.collections.push(object);
}
Tomahawk.log("registerPlugin: " + type + " id: " + object.id); Tomahawk.log("registerPlugin: " + type + " id: " + object.id);
Tomahawk.registerScriptPlugin(type, object.id); Tomahawk.registerScriptPlugin(type, object.id);
}, },
@@ -846,14 +762,11 @@ Tomahawk.PluginManager = {
resolve: [], resolve: [],
invokeSync: function (requestId, objectId, methodName, params) { invokeSync: function (requestId, objectId, methodName, params) {
if (!Tomahawk.resolver.instance.apiVersion || Tomahawk.resolver.instance.apiVersion < 0.9) { if (this.objects[objectId][this.wrapperPrefix + methodName]) {
if (methodName === 'artistAlbums') { methodName = this.wrapperPrefix + methodName;
methodName = 'albums';
} else if (methodName === 'albumTracks') {
methodName = 'tracks';
}
} }
var pluginManager = this; var pluginManager = this;
if (!this.objects[objectId]) { if (!this.objects[objectId]) {
Tomahawk.log("Object not found! objectId: " + objectId + " methodName: " + methodName); Tomahawk.log("Object not found! objectId: " + objectId + " methodName: " + methodName);
@@ -863,52 +776,13 @@ Tomahawk.PluginManager = {
} }
} }
if (typeof this.objects[objectId][methodName] === 'function') { if (typeof this.objects[objectId][methodName] !== 'function' && this.objects[objectId][methodName]) {
if (!Tomahawk.resolver.instance.apiVersion return this.objects[objectId][methodName];
|| Tomahawk.resolver.instance.apiVersion < 0.9) { } else if (typeof this.objects[objectId][methodName] !== 'function') {
if (methodName == 'artists') { throw new Error('\'' + methodName + '\' on ScriptObject ' + objectId + ' is not a function', typeof this.objects[objectId][methodName]);
return new RSVP.Promise(function (resolve, reject) {
pluginManager.resolve[requestId] = resolve;
Tomahawk.resolver.instance.artists(requestId);
});
} else if (methodName == 'albums') {
return new RSVP.Promise(function (resolve, reject) {
pluginManager.resolve[requestId] = resolve;
Tomahawk.resolver.instance.albums(requestId, params.artist);
});
} else if (methodName == 'tracks') {
return new RSVP.Promise(function (resolve, reject) {
pluginManager.resolve[requestId] = resolve;
Tomahawk.resolver.instance.tracks(requestId, params.artist, params.album);
});
} else if (methodName == 'lookupUrl') {
return new RSVP.Promise(function (resolve, reject) {
pluginManager.resolve[params.url] = resolve;
Tomahawk.resolver.instance.lookupUrl(params.url);
});
} else if (methodName == 'getStreamUrl') {
return new RSVP.Promise(function (resolve, reject) {
pluginManager.resolve[requestId] = resolve;
Tomahawk.resolver.instance.getStreamUrl(requestId, params.url);
});
} else if (methodName == 'resolve') {
return new RSVP.Promise(function (resolve, reject) {
pluginManager.resolve[requestId] = resolve;
Tomahawk.resolver.instance.resolve(requestId, params.artist,
params.album, params.track);
});
} else if (methodName == 'search') {
return new RSVP.Promise(function (resolve, reject) {
pluginManager.resolve[requestId] = resolve;
Tomahawk.resolver.instance.search(requestId, params.query);
});
}
}
return this.objects[objectId][methodName](params);
} }
return this.objects[objectId][methodName]; return this.objects[objectId][methodName](params);
}, },
invoke: function (requestId, objectId, methodName, params) { invoke: function (requestId, objectId, methodName, params) {
@@ -927,21 +801,38 @@ Tomahawk.PluginManager = {
} }
}; };
var encodeParamsToNativeFunctions = function(param) {
return param;
};
Tomahawk.NativeScriptJobManager = { Tomahawk.NativeScriptJobManager = {
idCounter: 0, idCounter: 0,
deferreds: {}, deferreds: {},
invoke: function (methodName, params) { invoke: function (methodName, params) {
params = params || {};
var requestId = this.idCounter++; var requestId = this.idCounter++;
Tomahawk.invokeNativeScriptJob(requestId, methodName, JSON.stringify(params)); var deferred = RSVP.defer();
this.deferreds[requestId] = RSVP.defer(); this.deferreds[requestId] = deferred;
return this.deferreds[requestId].promise; Tomahawk.invokeNativeScriptJob(requestId, methodName, encodeParamsToNativeFunctions(params));;
return deferred.promise;
}, },
reportNativeScriptJobResult: function (requestId, result) { reportNativeScriptJobResult: function(requestId, result) {
var deferred = this.deferreds[requestId]; var deferred = this.deferreds[requestId];
if (!deferred) { if (!deferred) {
Tomahawk.log("Deferred object with the given requestId is not present!"); Tomahawk.log("Deferred object with the given requestId is not present!");
} }
deferred.resolve(result); deferred.resolve(result);
delete this.deferreds[requestId];
},
reportNativeScriptJobError: function(requestId, error) {
var deferred = this.deferreds[requestId];
if (!deferred) {
console.log("Deferred object with the given requestId is not present!");
}
deferred.reject(error);
delete this.deferreds[requestId];
} }
}; };
@@ -1215,8 +1106,6 @@ Tomahawk.Country = {
LatinAmericaAndTheCaribbean: 246 LatinAmericaAndTheCaribbean: 246
}; };
Tomahawk.collections = [];
Tomahawk.Collection = { Tomahawk.Collection = {
BrowseCapability: { BrowseCapability: {
Artists: 1, Artists: 1,
@@ -1664,6 +1553,14 @@ Tomahawk.Collection = {
}); });
}, },
_adapter_resolve: function (params) {
return RSVP.Promise.resolve(this.resolve(params)).then(function (results) {
return {
'tracks': results
};
});
},
resolve: function (params) { resolve: function (params) {
var resultIds = Tomahawk.resolveFromFuzzyIndex(params.artist, params.album, params.track); var resultIds = Tomahawk.resolveFromFuzzyIndex(params.artist, params.album, params.track);
return this._fuzzyIndexIdsToTracks(resultIds); return this._fuzzyIndexIdsToTracks(resultIds);
@@ -1671,7 +1568,12 @@ Tomahawk.Collection = {
search: function (params) { search: function (params) {
var resultIds = Tomahawk.searchFuzzyIndex(params.query); var resultIds = Tomahawk.searchFuzzyIndex(params.query);
return this._fuzzyIndexIdsToTracks(resultIds);
return this._fuzzyIndexIdsToTracks(resultIds).then(function(tracks) {
return {
tracks: tracks
};
});
}, },
tracks: function (params, where) { tracks: function (params, where) {
@@ -1715,7 +1617,7 @@ Tomahawk.Collection = {
); );
return t.execDeferredStatements(); return t.execDeferredStatements();
}).then(function (results) { }).then(function (results) {
return {results: Tomahawk.resolver.instance._convertUrls(results[0])}; return {tracks: results[0]};
}); });
}, },
@@ -1889,71 +1791,13 @@ Tomahawk.Collection = {
Tomahawk.Collection.BrowseCapability.Albums, Tomahawk.Collection.BrowseCapability.Albums,
Tomahawk.Collection.BrowseCapability.Tracks]; Tomahawk.Collection.BrowseCapability.Tracks];
return this.settings; return this.settings;
},
getStreamUrl: function(params) {
if(this.resolver) {
return this.resolver.getStreamUrl(params);
}
return params;
} }
}; };
// Legacy compability for 0.8 and before
Tomahawk.reportCapabilities = function (capabilities) {
if (capabilities & TomahawkResolverCapability.Browsable) {
Tomahawk.PluginManager.registerPlugin("collection", Tomahawk.resolver.instance);
}
Tomahawk.nativeReportCapabilities(capabilities);
};
Tomahawk.addArtistResults = Tomahawk.addAlbumResults = Tomahawk.addAlbumTrackResults
= function (result) {
Tomahawk.PluginManager.resolve[result.qid](result);
delete Tomahawk.PluginManager.resolve[result.qid];
};
Tomahawk.addTrackResults = function (result) {
Tomahawk.PluginManager.resolve[result.qid](result.results);
delete Tomahawk.PluginManager.resolve[result.qid];
};
Tomahawk.reportStreamUrl = function (qid, streamUrl, headers) {
Tomahawk.PluginManager.resolve[qid]({
url: streamUrl,
headers: headers
});
delete Tomahawk.PluginManager.resolve[qid];
};
Tomahawk.addUrlResult = function (url, result) {
/* Merge the whole mess into one consistent result which is independent of type
var cleanResult = {
type: result.type,
guid: result.guid,
info: result.info,
creator: result.creator,
linkUrl: result.url
};
if (cleanResult.type == "track") {
cleanResult.track = result.title;
cleanResult.artist = result.artist;
} else if (cleanResult.type == "artist") {
cleanResult.artist = result.name;
} else if (cleanResult.type == "album") {
cleanResult.album = result.name;
cleanResult.artist = result.artist;
} else if (cleanResult.type == "playlist") {
cleanResult.title = result.title;
} else if (cleanResult.type == "xspf-url") {
cleanResult.url = result.url;
}
if (result.tracks) {
cleanResult.tracks = [];
var i;
for (i=0;i<result.tracks.length;i++) {
var cleanTrack = {
track: result.tracks[i].title,
artist: result.tracks[i].artist
};
cleanResult.push(cleanTrack)
}
Tomahawk.PluginManager.resolve[url](cleanResult);
*/
Tomahawk.PluginManager.resolve[url](result);
delete Tomahawk.PluginManager.resolve[url];
};

View File

@@ -199,7 +199,6 @@ list(APPEND libSources
MetaPlaylistInterface.cpp MetaPlaylistInterface.cpp
Query.cpp Query.cpp
Result.cpp Result.cpp
ResultProvider.cpp
Source.cpp Source.cpp
Track.cpp Track.cpp
TrackData.cpp TrackData.cpp

View File

@@ -175,7 +175,7 @@ DropJob::acceptsMimeData( const QMimeData* data, DropJob::DropTypes acceptedType
// Check Scriptresolvers // Check Scriptresolvers
foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() ) foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() )
{ {
if ( resolver->canParseUrl( url, ExternalResolver::Playlist ) ) if ( resolver->canParseUrl( url, ExternalResolver::UrlTypePlaylist ) )
return true; return true;
} }
} }
@@ -201,7 +201,7 @@ DropJob::acceptsMimeData( const QMimeData* data, DropJob::DropTypes acceptedType
// Check Scriptresolvers // Check Scriptresolvers
foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() ) foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() )
{ {
if ( resolver->canParseUrl( url, ExternalResolver::Track ) ) if ( resolver->canParseUrl( url, ExternalResolver::UrlTypeTrack ) )
return true; return true;
} }
} }
@@ -218,7 +218,7 @@ DropJob::acceptsMimeData( const QMimeData* data, DropJob::DropTypes acceptedType
// Check Scriptresolvers // Check Scriptresolvers
foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() ) foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() )
{ {
if ( resolver->canParseUrl( url, ExternalResolver::Album ) ) if ( resolver->canParseUrl( url, ExternalResolver::UrlTypeAlbum ) )
return true; return true;
} }
} }
@@ -235,7 +235,7 @@ DropJob::acceptsMimeData( const QMimeData* data, DropJob::DropTypes acceptedType
// Check Scriptresolvers // Check Scriptresolvers
foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() ) foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() )
{ {
if ( resolver->canParseUrl( url, ExternalResolver::Artist ) ) if ( resolver->canParseUrl( url, ExternalResolver::UrlTypeArtist ) )
return true; return true;
} }
} }
@@ -306,7 +306,7 @@ DropJob::isDropType( DropJob::DropType desired, const QMimeData* data )
// Check Scriptresolvers // Check Scriptresolvers
foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() ) foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() )
{ {
if ( resolver->canParseUrl( url, ExternalResolver::Playlist ) ) if ( resolver->canParseUrl( url, ExternalResolver::UrlTypePlaylist ) )
{ {
tLog( LOGVERBOSE ) << Q_FUNC_INFO << "Accepting current drop as a playlist" << resolver->name(); tLog( LOGVERBOSE ) << Q_FUNC_INFO << "Accepting current drop as a playlist" << resolver->name();
return true; return true;
@@ -763,7 +763,7 @@ DropJob::handleTrackUrls( const QString& urls )
{ {
foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() ) foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() )
{ {
if ( resolver->canParseUrl( track, ExternalResolver::Any ) ) if ( resolver->canParseUrl( track, ExternalResolver::UrlTypeAny ) )
{ {
ScriptCommand_LookupUrl* cmd = new ScriptCommand_LookupUrl( resolver, track ); ScriptCommand_LookupUrl* cmd = new ScriptCommand_LookupUrl( resolver, track );
connect( cmd, SIGNAL( information( QString, QSharedPointer<QObject> ) ), this, SLOT( informationForUrl( QString, QSharedPointer<QObject> ) ) ); connect( cmd, SIGNAL( information( QString, QSharedPointer<QObject> ) ), this, SLOT( informationForUrl( QString, QSharedPointer<QObject> ) ) );

View File

@@ -167,7 +167,7 @@ GlobalActionManager::openUrl( const QString& url )
QList< QPointer< ExternalResolver > > possibleResolvers; QList< QPointer< ExternalResolver > > possibleResolvers;
foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() ) foreach ( QPointer<ExternalResolver> resolver, Pipeline::instance()->scriptResolvers() )
{ {
if ( resolver->canParseUrl( url, ExternalResolver::Any ) ) if ( resolver->canParseUrl( url, ExternalResolver::UrlTypeAny ) )
{ {
canParse = true; canParse = true;
possibleResolvers << resolver; possibleResolvers << resolver;

View File

@@ -323,6 +323,13 @@ Pipeline::resolve( QID qid, bool prioritized, bool temporaryQuery )
} }
void
Pipeline::reportError( QID qid, Tomahawk::Resolver* r )
{
reportResults( qid, r, QList< result_ptr>() );
}
void void
Pipeline::reportResults( QID qid, Tomahawk::Resolver* r, const QList< result_ptr >& results ) Pipeline::reportResults( QID qid, Tomahawk::Resolver* r, const QList< result_ptr >& results )
{ {
@@ -333,7 +340,7 @@ Pipeline::reportResults( QID qid, Tomahawk::Resolver* r, const QList< result_ptr
{ {
if ( !results.isEmpty() ) if ( !results.isEmpty() )
{ {
ResultProvider* resolvedBy = results[0]->resolvedBy(); Resolver* resolvedBy = results[0]->resolvedBy();
if ( resolvedBy ) if ( resolvedBy )
{ {
tDebug() << "Result arrived too late for:" << qid << "by" << resolvedBy->name(); tDebug() << "Result arrived too late for:" << qid << "by" << resolvedBy->name();

View File

@@ -54,6 +54,7 @@ public:
unsigned int pendingQueryCount() const; unsigned int pendingQueryCount() const;
unsigned int activeQueryCount() const; unsigned int activeQueryCount() const;
void reportError( QID qid, Tomahawk::Resolver* r );
void reportResults( QID qid, Tomahawk::Resolver* r, const QList< result_ptr >& results ); void reportResults( QID qid, Tomahawk::Resolver* r, const QList< result_ptr >& results );
void reportAlbums( QID qid, const QList< album_ptr >& albums ); void reportAlbums( QID qid, const QList< album_ptr >& albums );
void reportArtists( QID qid, const QList< artist_ptr >& artists ); void reportArtists( QID qid, const QList< artist_ptr >& artists );

View File

@@ -491,7 +491,7 @@ Result::setFileId( unsigned int id )
} }
Tomahawk::ResultProvider* Tomahawk::Resolver*
Result::resolvedBy() const Result::resolvedBy() const
{ {
if ( !m_collection.isNull() ) if ( !m_collection.isNull() )

View File

@@ -21,7 +21,6 @@
#ifndef RESULT_H #ifndef RESULT_H
#define RESULT_H #define RESULT_H
#include "ResultProvider.h"
#include "DownloadJob.h" #include "DownloadJob.h"
#include "utils/TomahawkUtils.h" #include "utils/TomahawkUtils.h"
#include "Typedefs.h" #include "Typedefs.h"
@@ -86,9 +85,9 @@ public:
void setResolvedByResolver( Tomahawk::Resolver* resolver ); void setResolvedByResolver( Tomahawk::Resolver* resolver );
/** /**
* This is very bad. ResultProvider is not a QObject and thus can not be tracked by a qt smart pointer ... :-( * TODO: Make this a smart pointer
*/ */
ResultProvider* resolvedBy() const; Resolver* resolvedBy() const;
RID id() const; RID id() const;
bool isOnline() const; bool isOnline() const;

View File

@@ -1,24 +0,0 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright (C) 2015 Dominik Schmidt <domme@tomahawk-player.org>
*
* Tomahawk 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.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ResultProvider.h"
using namespace Tomahawk;
ResultProvider::~ResultProvider()
{
}

View File

@@ -1,42 +0,0 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright (C) 2015 Dominik Schmidt <domme@tomahawk-player.org>
*
* Tomahawk 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.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#ifndef TOMAHAWK_RESULTPROVIDER_H
#define TOMAHAWK_RESULTPROVIDER_H
#include "DllMacro.h"
class QPixmap;
class QString;
class QSize;
namespace Tomahawk
{
class DLLEXPORT ResultProvider
{
public:
virtual ~ResultProvider();
virtual QString name() const = 0;
virtual QPixmap icon( const QSize& size ) const = 0;
};
}
#endif // TOMAHAWK_RESULTPROVIDER_H

View File

@@ -524,7 +524,7 @@ void ResolverAccount::testConfig()
if ( resolver ) if ( resolver )
{ {
QVariantMap data = resolver->loadDataFromWidgets(); QVariantMap data = resolver->loadDataFromWidgets();
ScriptJob* job = resolver->scriptObject()->invoke( "_testConfig", data ); ScriptJob* job = resolver->scriptObject()->invoke( "testConfig", data );
connect( job, SIGNAL( done( QVariantMap ) ), SLOT( onTestConfig( QVariantMap ) ) ); connect( job, SIGNAL( done( QVariantMap ) ), SLOT( onTestConfig( QVariantMap ) ) );
job->start(); job->start();
} }

View File

@@ -32,6 +32,8 @@
#include "playlist/SingleTrackPlaylistInterface.h" #include "playlist/SingleTrackPlaylistInterface.h"
#include "utils/Closure.h" #include "utils/Closure.h"
#include "utils/Logger.h" #include "utils/Logger.h"
#include "utils/NetworkReply.h"
#include "utils/NetworkAccessManager.h"
#include "Album.h" #include "Album.h"
#include "Artist.h" #include "Artist.h"
@@ -40,6 +42,7 @@
#include "SourceList.h" #include "SourceList.h"
#include "TomahawkSettings.h" #include "TomahawkSettings.h"
#include "UrlHandler.h" #include "UrlHandler.h"
#include "resolvers/ScriptJob.h"
#include <QDir> #include <QDir>
@@ -574,16 +577,64 @@ AudioEngine::loadTrack( const Tomahawk::result_ptr& result )
setCurrentTrack( result ); setCurrentTrack( result );
if ( !TomahawkUtils::isLocalResult( d->currentTrack->url() ) && !TomahawkUtils::isHttpResult( d->currentTrack->url() ) ScriptJob* job = result->resolvedBy()->getStreamUrl( result );
&& !TomahawkUtils::isRtmpResult( d->currentTrack->url() ) ) connect( job, SIGNAL( done( QVariantMap ) ), SLOT( gotStreamUrl( QVariantMap ) ) );
job->setProperty( "result", QVariant::fromValue( result ) );
job->start();
}
void
AudioEngine::gotStreamUrl( const QVariantMap& data )
{
QString streamUrl = data[ "url" ].toString();
QVariantMap headers = data[ "headers" ].toMap();
Tomahawk::result_ptr result = sender()->property( "result" ).value<result_ptr>();
if ( streamUrl.isEmpty() || !( TomahawkUtils::isHttpResult( streamUrl ) || TomahawkUtils::isHttpsResult( streamUrl ) || TomahawkUtils::isRtmpResult( streamUrl ) ) )
{ {
performLoadIODevice( d->currentTrack, d->currentTrack->url() ); // Not an http(s) or RTMP URL, get IO device
QSharedPointer< QIODevice > sp;
performLoadIODevice( result, streamUrl );
} }
else else
{ {
QSharedPointer< QIODevice > io; // TODO: just make this part of the http(s) IoDeviceFactory (?)
performLoadTrack( result, result->url(), io ); QUrl url = QUrl::fromEncoded( streamUrl.toUtf8() );
QNetworkRequest req( url );
QMap<QString, QString> parsedHeaders;
foreach ( const QString& key, headers.keys() )
{
Q_ASSERT_X( headers[key].canConvert( QVariant::String ), Q_FUNC_INFO, "Expected a Map of string for additional headers" );
if ( headers[key].canConvert( QVariant::String ) )
{
parsedHeaders.insert( key, headers[key].toString() );
}
}
foreach ( const QString& key, parsedHeaders.keys() )
{
req.setRawHeader( key.toLatin1(), parsedHeaders[key].toLatin1() );
}
tDebug() << "Creating a QNetworkReply with url:" << req.url().toString();
NetworkReply* reply = new NetworkReply( Tomahawk::Utils::nam()->get( req ) );
NewClosure( reply, SIGNAL( finalUrlReached() ), this, SLOT( gotRedirectedStreamUrl( Tomahawk::result_ptr, NetworkReply* )), result, reply );
} }
sender()->deleteLater();
}
void
AudioEngine::gotRedirectedStreamUrl( const Tomahawk::result_ptr& result, NetworkReply* reply )
{
// std::functions cannot accept temporaries as parameters
QSharedPointer< QIODevice > sp ( reply->reply(), &QObject::deleteLater );
QString url = reply->reply()->url().toString();
reply->disconnectFromReply();
reply->deleteLater();
performLoadTrack( result, url, sp );
} }

View File

@@ -28,6 +28,7 @@
#include "DllMacro.h" #include "DllMacro.h"
class NetworkReply;
class AudioEnginePrivate; class AudioEnginePrivate;
class DLLEXPORT AudioEngine : public QObject class DLLEXPORT AudioEngine : public QObject
@@ -180,6 +181,10 @@ signals:
private slots: private slots:
void loadTrack( const Tomahawk::result_ptr& result ); //async! void loadTrack( const Tomahawk::result_ptr& result ); //async!
void gotStreamUrl( const QVariantMap& data );
void gotRedirectedStreamUrl( const Tomahawk::result_ptr& result, NetworkReply* reply );
void performLoadIODevice( const Tomahawk::result_ptr& result, const QString& url ); //only call from loadTrack kthxbi void performLoadIODevice( const Tomahawk::result_ptr& result, const QString& url ); //only call from loadTrack kthxbi
void performLoadTrack( const Tomahawk::result_ptr result, const QString url, QSharedPointer< QIODevice > io ); //only call from loadTrack or performLoadIODevice kthxbi void performLoadTrack( const Tomahawk::result_ptr result, const QString url, QSharedPointer< QIODevice > io ); //only call from loadTrack or performLoadIODevice kthxbi
void loadPreviousTrack(); void loadPreviousTrack();

View File

@@ -35,7 +35,7 @@ using namespace Tomahawk;
Collection::Collection( const source_ptr& source, const QString& name, QObject* parent ) Collection::Collection( const source_ptr& source, const QString& name, QObject* parent )
: QObject( parent ) : Resolver( parent )
, m_name( name ) , m_name( name )
, m_lastmodified( 0 ) , m_lastmodified( 0 )
, m_changed( false ) , m_changed( false )

View File

@@ -33,7 +33,7 @@
#include "collection/ArtistsRequest.h" #include "collection/ArtistsRequest.h"
#include "collection/AlbumsRequest.h" #include "collection/AlbumsRequest.h"
#include "collection/TracksRequest.h" #include "collection/TracksRequest.h"
#include "../ResultProvider.h" #include "../resolvers/Resolver.h"
#include "DllMacro.h" #include "DllMacro.h"
@@ -46,7 +46,7 @@
namespace Tomahawk namespace Tomahawk
{ {
class DLLEXPORT Collection : public QObject, public ResultProvider class DLLEXPORT Collection : public Resolver
{ {
Q_OBJECT Q_OBJECT

View File

@@ -251,3 +251,32 @@ DatabaseCollection::stationCreated( const source_ptr& source, const QVariantList
} }
/*
* Resolver interface
*
* We implement searching the database in the DatabaseResolver which avoids a n+1 query here.
* We can't simply let ScriptCollection inherit Collection and Resolver because both are QObjects,
* although Resolver doesn't need to be a QObject atm, blocking adding signals/slots to Resolver
* in future seems to me worse than violating Liskov's law here. ~ domme
*/
unsigned int
DatabaseCollection::timeout() const
{
return 0;
}
unsigned int
DatabaseCollection::weight() const
{
return 0;
}
void
DatabaseCollection::resolve( const Tomahawk::query_ptr& query )
{
Q_UNUSED( query );
Q_ASSERT(false);
}

View File

@@ -65,6 +65,11 @@ public:
int trackCount() const override; int trackCount() const override;
QPixmap icon( const QSize& size ) const override; QPixmap icon( const QSize& size ) const override;
// Resolver interface
unsigned int weight() const override;
unsigned int timeout() const override;
void resolve( const Tomahawk::query_ptr& query ) override;
public slots: public slots:
virtual void addTracks( const QList<QVariant>& newitems ); virtual void addTracks( const QList<QVariant>& newitems );
virtual void removeTracks( const QDir& dir ); virtual void removeTracks( const QDir& dir );

View File

@@ -262,8 +262,15 @@ TrackDetailView::onResultsChanged()
resolverLabel->setFont( f ); resolverLabel->setFont( f );
resolverLabel->setStyleSheet( "QLabel { color: rgba( 0, 0, 0, 50% ) }" ); resolverLabel->setStyleSheet( "QLabel { color: rgba( 0, 0, 0, 50% ) }" );
resolverLabel->setText( QString( "%1 - %2" ).arg( result->track()->track() ).arg( result->track()->artist() ) ); resolverLabel->setText( QString( "%1 - %2" ).arg( result->track()->track() ).arg( result->track()->artist() ) );
resolverLabel->setToolTip( QString( "%1 by %2%3" ).arg( result->track()->track() ).arg( result->track()->artist() ) resolverLabel->setToolTip(
.arg( !result->track()->album().isEmpty() ? QString( " " ) + tr( "on %1" ).arg( result->track()->album() ) : QString() ) ); QString( "%1 by %2%3 (%4)" )
.arg( result->track()->track() )
.arg( result->track()->artist() )
.arg( !result->track()->album().isEmpty() ? QString( " " ) + tr( "on %1" ).arg( result->track()->album() ) : QString() )
.arg( result->friendlySource() )
);
;
resolverLabel->setFixedWidth( width() - 32 - 4 ); resolverLabel->setFixedWidth( width() - 32 - 4 );
NewClosure( resolverLabel, SIGNAL( clicked() ), const_cast< AudioEngine* >( AudioEngine::instance() ), NewClosure( resolverLabel, SIGNAL( clicked() ), const_cast< AudioEngine* >( AudioEngine::instance() ),

View File

@@ -68,11 +68,12 @@ public:
enum UrlType enum UrlType
{ {
Any = 0x00, UrlTypeAny = 0x00,
Playlist = 0x01, UrlTypePlaylist = 0x01,
Track = 0x02, UrlTypeTrack = 0x02,
Album = 0x04, UrlTypeAlbum = 0x04,
Artist = 0x08 UrlTypeArtist = 0x08,
UrlTypeXspf = 0x10
}; };
Q_DECLARE_FLAGS( UrlTypes, UrlType ) Q_DECLARE_FLAGS( UrlTypes, UrlType )
Q_FLAGS( UrlTypes ) Q_FLAGS( UrlTypes )

View File

@@ -170,6 +170,41 @@ JSAccount::syncInvoke( const scriptobject_ptr& scriptObject, const QString& meth
return evaluateJavaScriptWithResult( eval ); return evaluateJavaScriptWithResult( eval );
} }
void
JSAccount::reportNativeScriptJobResult( int resultId, const QVariantMap& result )
{
QString eval = QString(
"Tomahawk.NativeScriptJobManager.reportNativeScriptJobResult("
"%1," // requestId
"%2" // results
");"
).arg( resultId )
.arg( serializeQVariantMap( result ) );
// Remove when new scripting api turned out to work reliably
tDebug( LOGVERBOSE ) << Q_FUNC_INFO << eval;
evaluateJavaScript( eval );
}
void
JSAccount::reportNativeScriptJobError( int resultId, const QVariantMap& error )
{
QString eval = QString(
"Tomahawk.NativeScriptJobManager.reportNativeScriptJobError("
"%1," // requestId
"%2" // results
");"
).arg( resultId )
.arg( serializeQVariantMap( error ) );
// Remove when new scripting api turned out to work reliably
tDebug( LOGVERBOSE ) << Q_FUNC_INFO << eval;
evaluateJavaScript( eval );
}
QVariant QVariant
JSAccount::evaluateJavaScriptInternal( const QString& scriptSource ) JSAccount::evaluateJavaScriptInternal( const QString& scriptSource )

View File

@@ -71,6 +71,9 @@ public:
static QString serializeQVariantMap(const QVariantMap& map); static QString serializeQVariantMap(const QVariantMap& map);
void reportNativeScriptJobResult( int resultId, const QVariantMap& result ) override;
void reportNativeScriptJobError( int resultId, const QVariantMap& error ) override;
private: private:
/** /**
* Wrap the pure evaluateJavaScript call in here, while the threadings guards are in public methods * Wrap the pure evaluateJavaScript call in here, while the threadings guards are in public methods

View File

@@ -44,6 +44,13 @@
#include "Track.h" #include "Track.h"
#include "ScriptInfoPlugin.h" #include "ScriptInfoPlugin.h"
#include "JSAccount.h" #include "JSAccount.h"
#include "ScriptJob.h"
// lookupUrl stuff
#include "playlist/PlaylistTemplate.h"
#include "playlist/XspfPlaylistTemplate.h"
#include "database/Database.h"
#include "database/DatabaseImpl.h"
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
@@ -250,12 +257,11 @@ JSResolver::init()
d->scriptAccount->loadScript( filePath() ); d->scriptAccount->loadScript( filePath() );
// HACK: register resolver object // HACK: register resolver object
d->scriptAccount->evaluateJavaScript( "Tomahawk.PluginManager.registerPlugin('resolver', Tomahawk.resolver.instance);" ) d->scriptAccount->evaluateJavaScript( "Tomahawk.PluginManager.registerPlugin('resolver', Tomahawk.resolver.instance);" );
;
// init resolver // init resolver
resolverInit(); scriptObject()->syncInvoke( "init" );
QVariantMap m = resolverSettings(); QVariantMap m = scriptObject()->syncInvoke( "settings" ).toMap();
d->name = m.value( "name" ).toString(); d->name = m.value( "name" ).toString();
d->weight = m.value( "weight", 0 ).toUInt(); d->weight = m.value( "weight", 0 ).toUInt();
d->timeout = m.value( "timeout", 25 ).toUInt() * 1000; d->timeout = m.value( "timeout", 25 ).toUInt() * 1000;
@@ -319,20 +325,13 @@ JSResolver::canParseUrl( const QString& url, UrlType type )
{ {
Q_D( const JSResolver ); Q_D( const JSResolver );
// FIXME: How can we do this?
/*if ( QThread::currentThread() != thread() )
{
QMetaObject::invokeMethod( this, "canParseUrl", Qt::QueuedConnection,
Q_ARG( QString, url ) );
return;
}*/
if ( d->capabilities.testFlag( UrlLookup ) ) if ( d->capabilities.testFlag( UrlLookup ) )
{ {
QString eval = QString( "canParseUrl( '%1', %2 )" ) QVariantMap arguments;
.arg( JSAccount::escape( QString( url ) ) ) arguments["url"] = url;
.arg( (int) type ); arguments["type"] = (int) type;
return callOnResolver( eval ).toBool();
return scriptObject()->syncInvoke( "canParseUrl", arguments ).toBool();
} }
else else
{ {
@@ -345,34 +344,185 @@ JSResolver::canParseUrl( const QString& url, UrlType type )
void void
JSResolver::lookupUrl( const QString& url ) JSResolver::lookupUrl( const QString& url )
{ {
if ( QThread::currentThread() != thread() )
{
QMetaObject::invokeMethod( this, "lookupUrl", Qt::QueuedConnection,
Q_ARG( QString, url ) );
return;
}
Q_D( const JSResolver ); Q_D( const JSResolver );
if ( !d->capabilities.testFlag( UrlLookup ) ) if ( !d->capabilities.testFlag( UrlLookup ) )
{ {
emit informationFound( url, QSharedPointer<QObject>() ); emit informationFound( url, QSharedPointer<QObject>() );
return; return;
} }
QString eval = QString( "lookupUrl( '%1' )" ) QVariantMap arguments;
.arg( JSAccount::escape( QString( url ) ) ); arguments["url"] = url;
Tomahawk::ScriptJob* job = scriptObject()->invoke( "lookupUrl", arguments );
connect( job, SIGNAL( done( QVariantMap ) ), SLOT( onLookupUrlRequestDone( QVariantMap ) ) );
job->setProperty( "url", url );
job->start();
}
QVariantMap m = callOnResolver( eval ).toMap();
if ( m.isEmpty() ) void
JSResolver::onLookupUrlRequestDone( const QVariantMap& result )
{
sender()->deleteLater();
QString url = sender()->property( "url" ).toString();
tLog() << "ON LOOKUP URL REQUEST DONE" << url << result;
// It may seem a bit weird, but currently no slot should do anything
// more as we starting on a new URL and not task are waiting for it yet.
m_pendingUrl = QString();
m_pendingAlbum = album_ptr();
UrlTypes type = (UrlTypes) result.value( "type" ).toInt();
if ( type == UrlTypeArtist )
{ {
// if the resolver doesn't return anything, async api is used QString name = result.value( "name" ).toString();
return; Q_ASSERT( !name.isEmpty() );
emit informationFound( url, Artist::get( name, true ).objectCast<QObject>() );
}
else if ( type == UrlTypeAlbum )
{
QString name = result.value( "name" ).toString();
QString artist = result.value( "artist" ).toString();
album_ptr album = Album::get( Artist::get( artist, true ), name );
m_pendingUrl = url;
m_pendingAlbum = album;
connect( album.data(), SIGNAL( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ),
SLOT( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ) );
if ( !album->tracks().isEmpty() )
{
emit informationFound( url, album.objectCast<QObject>() );
}
}
else if ( type == UrlTypeTrack )
{
Tomahawk::query_ptr query = parseTrack( result );
if ( query.isNull() )
{
// A valid track result shoud have non-empty title and artist.
tLog() << Q_FUNC_INFO << name() << "Got empty track information for " << url;
emit informationFound( url, QSharedPointer<QObject>() );
}
else
{
emit informationFound( url, query.objectCast<QObject>() );
}
}
else if ( type == UrlTypePlaylist )
{
QString guid = result.value( "guid" ).toString();
Q_ASSERT( !guid.isEmpty() );
// Append nodeid to guid to make it globally unique.
guid += instanceUUID();
// Do we already have this playlist loaded?
{
playlist_ptr playlist = Playlist::get( guid );
if ( !playlist.isNull() )
{
emit informationFound( url, playlist.objectCast<QObject>() );
return;
}
}
// Get all information to build a new playlist but do not build it until we know,
// if it is really handled as a playlist and not as a set of tracks.
Tomahawk::source_ptr source = SourceList::instance()->getLocal();
const QString title = result.value( "title" ).toString();
const QString info = result.value( "info" ).toString();
const QString creator = result.value( "creator" ).toString();
QList<query_ptr> queries;
foreach( QVariant track, result.value( "tracks" ).toList() )
{
query_ptr query = parseTrack( track.toMap() );
if ( !query.isNull() )
{
queries << query;
}
}
tLog( LOGVERBOSE ) << Q_FUNC_INFO << name() << "Got playlist for " << url;
playlisttemplate_ptr pltemplate( new PlaylistTemplate( source, guid, title, info, creator, false, queries ) );
emit informationFound( url, pltemplate.objectCast<QObject>() );
}
else if ( type == UrlTypeXspf )
{
QString xspfUrl = result.value( "url" ).toString();
Q_ASSERT( !xspfUrl.isEmpty() );
QString guid = QString( "xspf-%1-%2" ).arg( xspfUrl.toUtf8().toBase64().constData() ).arg( instanceUUID() );
// Do we already have this playlist loaded?
{
playlist_ptr playlist = Playlist::get( guid );
if ( !playlist.isNull() )
{
emit informationFound( url, playlist.objectCast<QObject>() );
return;
}
}
// Get all information to build a new playlist but do not build it until we know,
// if it is really handled as a playlist and not as a set of tracks.
Tomahawk::source_ptr source = SourceList::instance()->getLocal();
QSharedPointer<XspfPlaylistTemplate> pltemplate( new XspfPlaylistTemplate( xspfUrl, source, guid ) );
NewClosure( pltemplate, SIGNAL( tracksLoaded( QList< Tomahawk::query_ptr > ) ),
this, SLOT( pltemplateTracksLoadedForUrl( QString, Tomahawk::playlisttemplate_ptr ) ),
url, pltemplate.objectCast<Tomahawk::PlaylistTemplate>() );
tLog( LOGVERBOSE ) << Q_FUNC_INFO << name() << "Got playlist for " << url;
pltemplate->load();
}
else
{
tLog( LOGVERBOSE ) << Q_FUNC_INFO << name() << "No usable information found for " << url;
emit informationFound( url, QSharedPointer<QObject>() );
}
}
query_ptr
JSResolver::parseTrack( const QVariantMap& track )
{
QString title = track.value( "track" ).toString();
QString artist = track.value( "artist" ).toString();
QString album = track.value( "album" ).toString();
if ( title.isEmpty() || artist.isEmpty() )
{
return query_ptr();
} }
QString errorMessage = tr( "Script Resolver Warning: API call %1 returned data synchronously." ).arg( eval ); Tomahawk::query_ptr query = Tomahawk::Query::get( artist, title, album );
JobStatusView::instance()->model()->addJob( new ErrorStatusMessage( errorMessage ) ); QString resultHint = track.value( "hint" ).toString();
tDebug() << errorMessage << m; if ( !resultHint.isEmpty() )
{
query->setResultHint( resultHint );
query->setSaveHTTPResultHint( true );
}
return query;
}
void
JSResolver::tracksAdded( const QList<query_ptr>&, const ModelMode, const collection_ptr&)
{
// Check if we still are actively waiting
if ( m_pendingAlbum.isNull() || m_pendingUrl.isNull() )
return;
emit informationFound( m_pendingUrl, m_pendingAlbum.objectCast<QObject>() );
m_pendingAlbum = album_ptr();
m_pendingUrl = QString();
}
void
JSResolver::pltemplateTracksLoadedForUrl( const QString& url, const playlisttemplate_ptr& pltemplate )
{
tLog() << Q_FUNC_INFO;
emit informationFound( url, pltemplate.objectCast<QObject>() );
} }
@@ -388,32 +538,44 @@ JSResolver::error() const
void void
JSResolver::resolve( const Tomahawk::query_ptr& query ) JSResolver::resolve( const Tomahawk::query_ptr& query )
{ {
if ( QThread::currentThread() != thread() ) ScriptJob* job = scriptAccount()->resolve( scriptObject(), query );
{
QMetaObject::invokeMethod( this, "resolve", Qt::QueuedConnection, Q_ARG(Tomahawk::query_ptr, query) );
return;
}
QString eval; connect( job, SIGNAL( done( QVariantMap ) ), SLOT( onResolveRequestDone( QVariantMap ) ) );
if ( !query->isFullTextQuery() )
job->start();
}
void
JSResolver::onResolveRequestDone( const QVariantMap& data )
{
Q_ASSERT( QThread::currentThread() == thread() );
Q_D( JSResolver );
ScriptJob* job = qobject_cast< ScriptJob* >( sender() );
QID qid = job->property( "qid" ).toString();
if ( job->error() )
{ {
eval = QString( "resolve( '%1', '%2', '%3', '%4' )" ) Tomahawk::Pipeline::instance()->reportError( qid, this );
.arg( JSAccount::escape( query->id() ) )
.arg( JSAccount::escape( query->queryTrack()->artist() ) )
.arg( JSAccount::escape( query->queryTrack()->album() ) )
.arg( JSAccount::escape( query->queryTrack()->track() ) );
} }
else else
{ {
eval = QString( "search( '%1', '%2' )" )
.arg( JSAccount::escape( query->id() ) ) QList< Tomahawk::result_ptr > results = scriptAccount()->parseResultVariantList( data.value( "tracks" ).toList() );
.arg( JSAccount::escape( query->fullTextQuery() ) );
foreach( const result_ptr& result, results )
{
result->setResolvedByResolver( this );
result->setFriendlySource( name() );
}
Tomahawk::Pipeline::instance()->reportResults( qid, this, results );
} }
QVariantMap m = callOnResolver( eval ).toMap(); sender()->deleteLater();
} }
void void
JSResolver::stop() JSResolver::stop()
{ {
@@ -433,7 +595,7 @@ JSResolver::loadUi()
{ {
Q_D( JSResolver ); Q_D( JSResolver );
QVariantMap m = callOnResolver( "getConfigUi()" ).toMap(); QVariantMap m = scriptObject()->syncInvoke( "getConfigUi" ).toMap();
bool compressed = m.value( "compressed", "false" ).toBool(); bool compressed = m.value( "compressed", "false" ).toBool();
qDebug() << "Resolver has a preferences widget! compressed?" << compressed; qDebug() << "Resolver has a preferences widget! compressed?" << compressed;
@@ -485,7 +647,7 @@ JSResolver::saveConfig()
// qDebug() << Q_FUNC_INFO << saveData; // qDebug() << Q_FUNC_INFO << saveData;
d->resolverHelper->setResolverConfig( saveData.toMap() ); d->resolverHelper->setResolverConfig( saveData.toMap() );
callOnResolver( "saveUserConfig()" ); scriptObject()->syncInvoke( "saveUserConfig" );
} }
@@ -516,39 +678,25 @@ JSResolver::onCapabilitiesChanged( Tomahawk::ExternalResolver::Capabilities capa
} }
QVariantMap
JSResolver::resolverSettings()
{
return callOnResolver( "settings" ).toMap();
}
QVariantMap QVariantMap
JSResolver::resolverUserConfig() JSResolver::resolverUserConfig()
{ {
return callOnResolver( "getUserConfig()" ).toMap(); return scriptObject()->syncInvoke( "getUserConfig" ).toMap();
} }
QVariantMap QString
JSResolver::resolverInit() JSResolver::instanceUUID()
{ {
return callOnResolver( "init()" ).toMap(); return Tomahawk::Database::instance()->impl()->dbid();
} }
QVariant ScriptJob*
JSResolver::callOnResolver( const QString& scriptSource ) JSResolver::getStreamUrl( const result_ptr& result )
{ {
Q_D( JSResolver ); QVariantMap arguments;
arguments["url"] = result->url();
QString propertyName = scriptSource.split('(').first(); return scriptObject()->invoke( "getStreamUrl", arguments );
return d->scriptAccount->evaluateJavaScriptWithResult( QString(
"if(Tomahawk.resolver.instance['_adapter_%1']) {"
" Tomahawk.resolver.instance._adapter_%2;"
"} else {"
" Tomahawk.resolver.instance.%2"
"}"
).arg( propertyName ).arg( scriptSource ) );
} }

View File

@@ -75,6 +75,8 @@ public:
ScriptAccount* scriptAccount() const; ScriptAccount* scriptAccount() const;
ScriptJob* getStreamUrl( const result_ptr& result ) override;
public slots: public slots:
void resolve( const Tomahawk::query_ptr& query ) override; void resolve( const Tomahawk::query_ptr& query ) override;
void stop() override; void stop() override;
@@ -89,6 +91,10 @@ signals:
protected: protected:
QVariant callOnResolver( const QString& scriptSource ); QVariant callOnResolver( const QString& scriptSource );
private slots:
void onResolveRequestDone(const QVariantMap& data);
void onLookupUrlRequestDone(const QVariantMap& data);
private: private:
void init(); void init();
@@ -96,12 +102,20 @@ private:
void onCapabilitiesChanged( Capabilities capabilities ); void onCapabilitiesChanged( Capabilities capabilities );
// encapsulate javascript calls // encapsulate javascript calls
QVariantMap resolverSettings();
QVariantMap resolverUserConfig(); QVariantMap resolverUserConfig();
QVariantMap resolverInit();
Q_DECLARE_PRIVATE( JSResolver ) Q_DECLARE_PRIVATE( JSResolver )
QScopedPointer<JSResolverPrivate> d_ptr; QScopedPointer<JSResolverPrivate> d_ptr;
// TODO: move lookupUrl stuff to its own plugin type
QString instanceUUID();
static Tomahawk::query_ptr parseTrack( const QVariantMap& track );
QString m_pendingUrl;
Tomahawk::album_ptr m_pendingAlbum;
private slots:
void tracksAdded( const QList<Tomahawk::query_ptr>& tracks, const Tomahawk::ModelMode, const Tomahawk::collection_ptr& collection );
void pltemplateTracksLoadedForUrl( const QString& url, const Tomahawk::playlisttemplate_ptr& pltemplate );
}; };
} // ns: Tomahawk } // ns: Tomahawk

View File

@@ -21,10 +21,6 @@
#include "JSResolverHelper.h" #include "JSResolverHelper.h"
#include "database/Database.h"
#include "database/DatabaseImpl.h"
#include "playlist/PlaylistTemplate.h"
#include "playlist/XspfPlaylistTemplate.h"
#include "resolvers/ScriptEngine.h" #include "resolvers/ScriptEngine.h"
#include "network/Servent.h" #include "network/Servent.h"
#include "utils/Closure.h" #include "utils/Closure.h"
@@ -51,6 +47,8 @@
#include <QMap> #include <QMap>
#include <QWebFrame> #include <QWebFrame>
#include <QLocale> #include <QLocale>
#include <QNetworkReply>
#include <taglib/asffile.h> #include <taglib/asffile.h>
#include <taglib/flacfile.h> #include <taglib/flacfile.h>
#include <taglib/id3v2framefactory.h> #include <taglib/id3v2framefactory.h>
@@ -76,7 +74,6 @@ JSResolverHelper::JSResolverHelper( const QString& scriptPath, JSResolver* paren
: QObject( parent ) : QObject( parent )
, m_resolver( parent ) , m_resolver( parent )
, m_scriptPath( scriptPath ) , m_scriptPath( scriptPath )
, m_urlCallbackIsAsync( false )
{ {
} }
@@ -140,55 +137,6 @@ JSResolverHelper::log( const QString& message )
} }
void
JSResolverHelper::addTrackResults( const QVariantMap& results )
{
Q_ASSERT( results["results"].toMap().isEmpty() );
QList< Tomahawk::result_ptr > tracks = m_resolver->scriptAccount()->parseResultVariantList( results.value( "results" ).toList() );
foreach( const result_ptr& track, tracks )
{
track->setResolvedByResolver( m_resolver );
track->setFriendlySource( m_resolver->name() );
}
QString qid = results.value("qid").toString();
Tomahawk::Pipeline::instance()->reportResults( qid, m_resolver, tracks );
}
query_ptr
JSResolverHelper::parseTrack( const QVariantMap& track )
{
QString title = track.value( "title" ).toString();
QString artist = track.value( "artist" ).toString();
QString album = track.value( "album" ).toString();
if ( title.isEmpty() || artist.isEmpty() )
{
return query_ptr();
}
Tomahawk::query_ptr query = Tomahawk::Query::get( artist, title, album );
QString resultHint = track.value( "hint" ).toString();
if ( !resultHint.isEmpty() )
{
query->setResultHint( resultHint );
query->setSaveHTTPResultHint( true );
}
return query;
}
QString
JSResolverHelper::instanceUUID()
{
return Tomahawk::Database::instance()->impl()->dbid();
}
QString QString
JSResolverHelper::uuid() const JSResolverHelper::uuid() const
{ {
@@ -483,120 +431,6 @@ JSResolverHelper::currentCountry() const
} }
void
JSResolverHelper::addUrlResult( const QString& url, const QVariantMap& result )
{
// It may seem a bit weird, but currently no slot should do anything
// more as we starting on a new URL and not task are waiting for it yet.
m_pendingUrl = QString();
m_pendingAlbum = album_ptr();
QString type = result.value( "type" ).toString();
if ( type == "artist" )
{
QString name = result.value( "name" ).toString();
Q_ASSERT( !name.isEmpty() );
emit m_resolver->informationFound( url, Artist::get( name, true ).objectCast<QObject>() );
}
else if ( type == "album" )
{
QString name = result.value( "name" ).toString();
QString artist = result.value( "artist" ).toString();
album_ptr album = Album::get( Artist::get( artist, true ), name );
m_pendingUrl = url;
m_pendingAlbum = album;
connect( album.data(), SIGNAL( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ),
SLOT( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ) );
if ( !album->tracks().isEmpty() )
{
emit m_resolver->informationFound( url, album.objectCast<QObject>() );
}
}
else if ( type == "track" )
{
Tomahawk::query_ptr query = parseTrack( result );
if ( query.isNull() )
{
// A valid track result shoud have non-empty title and artist.
tLog() << Q_FUNC_INFO << m_resolver->name() << "Got empty track information for " << url;
emit m_resolver->informationFound( url, QSharedPointer<QObject>() );
}
else
{
emit m_resolver->informationFound( url, query.objectCast<QObject>() );
}
}
else if ( type == "playlist" )
{
QString guid = result.value( "guid" ).toString();
Q_ASSERT( !guid.isEmpty() );
// Append nodeid to guid to make it globally unique.
guid += instanceUUID();
// Do we already have this playlist loaded?
{
playlist_ptr playlist = Playlist::get( guid );
if ( !playlist.isNull() )
{
emit m_resolver->informationFound( url, playlist.objectCast<QObject>() );
return;
}
}
// Get all information to build a new playlist but do not build it until we know,
// if it is really handled as a playlist and not as a set of tracks.
Tomahawk::source_ptr source = SourceList::instance()->getLocal();
const QString title = result.value( "title" ).toString();
const QString info = result.value( "info" ).toString();
const QString creator = result.value( "creator" ).toString();
QList<query_ptr> queries;
foreach( QVariant track, result.value( "tracks" ).toList() )
{
query_ptr query = parseTrack( track.toMap() );
if ( !query.isNull() )
{
queries << query;
}
}
tLog( LOGVERBOSE ) << Q_FUNC_INFO << m_resolver->name() << "Got playlist for " << url;
playlisttemplate_ptr pltemplate( new PlaylistTemplate( source, guid, title, info, creator, false, queries ) );
emit m_resolver->informationFound( url, pltemplate.objectCast<QObject>() );
}
else if ( type == "xspf-url" )
{
QString xspfUrl = result.value( "url" ).toString();
Q_ASSERT( !xspfUrl.isEmpty() );
QString guid = QString( "xspf-%1-%2" ).arg( xspfUrl.toUtf8().toBase64().constData() ).arg( instanceUUID() );
// Do we already have this playlist loaded?
{
playlist_ptr playlist = Playlist::get( guid );
if ( !playlist.isNull() )
{
emit m_resolver->informationFound( url, playlist.objectCast<QObject>() );
return;
}
}
// Get all information to build a new playlist but do not build it until we know,
// if it is really handled as a playlist and not as a set of tracks.
Tomahawk::source_ptr source = SourceList::instance()->getLocal();
QSharedPointer<XspfPlaylistTemplate> pltemplate( new XspfPlaylistTemplate( xspfUrl, source, guid ) );
NewClosure( pltemplate, SIGNAL( tracksLoaded( QList< Tomahawk::query_ptr > ) ),
this, SLOT( pltemplateTracksLoadedForUrl( QString, Tomahawk::playlisttemplate_ptr ) ),
url, pltemplate.objectCast<Tomahawk::PlaylistTemplate>() );
tLog( LOGVERBOSE ) << Q_FUNC_INFO << m_resolver->name() << "Got playlist for " << url;
pltemplate->load();
}
else
{
tLog( LOGVERBOSE ) << Q_FUNC_INFO << m_resolver->name() << "No usable information found for " << url;
emit m_resolver->informationFound( url, QSharedPointer<QObject>() );
}
}
void void
JSResolverHelper::nativeReportCapabilities( const QVariant& v ) JSResolverHelper::nativeReportCapabilities( const QVariant& v )
{ {
@@ -633,27 +467,6 @@ JSResolverHelper::unregisterScriptPlugin( const QString& type, const QString& ob
} }
void
JSResolverHelper::tracksAdded( const QList<query_ptr>&, const ModelMode, const collection_ptr&)
{
// Check if we still are actively waiting
if ( m_pendingAlbum.isNull() || m_pendingUrl.isNull() )
return;
emit m_resolver->informationFound( m_pendingUrl, m_pendingAlbum.objectCast<QObject>() );
m_pendingAlbum = album_ptr();
m_pendingUrl = QString();
}
void
JSResolverHelper::pltemplateTracksLoadedForUrl( const QString& url, const playlisttemplate_ptr& pltemplate )
{
tLog() << Q_FUNC_INFO;
emit m_resolver->informationFound( url, pltemplate.objectCast<QObject>() );
}
void void
JSResolverHelper::setResolverConfig( const QVariantMap& config ) JSResolverHelper::setResolverConfig( const QVariantMap& config )
{ {
@@ -668,31 +481,6 @@ JSResolverHelper::accountId()
} }
void
JSResolverHelper::addCustomUrlHandler( const QString& protocol,
const QString& callbackFuncName,
const QString& isAsynchronous )
{
m_urlCallbackIsAsync = ( isAsynchronous.toLower() == "true" );
std::function< void( const Tomahawk::result_ptr&, const QString&,
std::function< void( const QString&, QSharedPointer< QIODevice >& ) > )> fac =
std::bind( &JSResolverHelper::customIODeviceFactory, this,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3 );
Tomahawk::UrlHandler::registerIODeviceFactory( protocol, fac );
m_urlCallback = callbackFuncName;
}
void
JSResolverHelper::reportStreamUrl( const QString& qid, const QString& streamUrl )
{
reportStreamUrl( qid, streamUrl, QVariantMap() );
}
void JSResolverHelper::nativeAssert( bool assertion, const QString& message ) void JSResolverHelper::nativeAssert( bool assertion, const QString& message )
{ {
if ( !assertion ) if ( !assertion )
@@ -703,61 +491,6 @@ void JSResolverHelper::nativeAssert( bool assertion, const QString& message )
} }
void
JSResolverHelper::customIODeviceFactory( const Tomahawk::result_ptr&, const QString& url,
std::function< void( const QString&, QSharedPointer< QIODevice >& ) > callback )
{
//can be sync or async
if ( m_urlCallbackIsAsync )
{
QString qid = uuid();
QString getUrl = QString(
"if(Tomahawk.resolver.instance['_adapter_%1']) {"
" Tomahawk.resolver.instance._adapter_%1( {qid: '%2', url: '%3'} );"
"} else {"
" Tomahawk.resolver.instance.%1( {qid: '%2', url: '%3'} );"
"}"
).arg( m_urlCallback )
.arg( qid )
.arg( url );
m_streamCallbacks.insert( qid, callback );
m_resolver->d_func()->scriptAccount->evaluateJavaScript( getUrl );
}
else
{
QString getUrl = QString( "Tomahawk.resolver.instance.%1( '%2' );" ).arg( m_urlCallback )
.arg( url );
QString urlStr = m_resolver->d_func()->scriptAccount->evaluateJavaScriptWithResult( getUrl ).toString();
returnStreamUrl( urlStr, QMap<QString, QString>(), callback );
}
}
void
JSResolverHelper::reportStreamUrl( const QString& qid, const QString& streamUrl, const QVariantMap& headers )
{
if ( !m_streamCallbacks.contains( qid ) )
return;
std::function< void( const QString&, QSharedPointer< QIODevice >& ) > callback = m_streamCallbacks.take( qid );
QMap<QString, QString> parsedHeaders;
foreach ( const QString& key, headers.keys() )
{
Q_ASSERT_X( headers[key].canConvert( QVariant::String ), Q_FUNC_INFO, "Expected a Map of string for additional headers" );
if ( headers[key].canConvert( QVariant::String ) )
{
parsedHeaders.insert( key, headers[key].toString() );
}
}
returnStreamUrl( streamUrl, parsedHeaders, callback );
}
void void
JSResolverHelper::nativeRetrieveMetadata( int metadataId, const QString& url, JSResolverHelper::nativeRetrieveMetadata( int metadataId, const QString& url,
const QString& mime_type, int sizehint, const QString& mime_type, int sizehint,
@@ -896,12 +629,27 @@ JSResolverHelper::nativeRetrieveMetadata( int metadataId, const QString& url,
} }
} }
void
JSResolverHelper::invokeNativeScriptJob( int requestId, const QString& methodName, const QVariantMap& params )
{
if ( methodName == "httpRequest" ) {
nativeAsyncRequest( requestId, params );
} else {
QVariantMap error;
error["message"] = "NativeScriptJob methodName was not found";
error["name"] = "method_was_not_found";
m_resolver->d_func()->scriptAccount->reportNativeScriptJobError( requestId, error );
}
}
void void
JSResolverHelper::nativeAsyncRequest( const int requestId, const QString& url, JSResolverHelper::nativeAsyncRequest( const int requestId, const QVariantMap& options )
const QVariantMap& headers,
const QVariantMap& options )
{ {
QString url = options[ "url" ].toString();
QVariantMap headers = options[ "headers" ].toMap();
QNetworkRequest req( url ); QNetworkRequest req( url );
foreach ( const QString& key, headers.keys() ) foreach ( const QString& key, headers.keys() )
{ {
@@ -957,17 +705,16 @@ JSResolverHelper::nativeAsyncRequestDone( int requestId, NetworkReply* reply )
map["status"] = reply->reply()->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); map["status"] = reply->reply()->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
map["statusText"] = QString("%1 %2").arg( map["status"].toString() ) map["statusText"] = QString("%1 %2").arg( map["status"].toString() )
.arg( reply->reply()->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString() ); .arg( reply->reply()->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString() );
if (reply->reply()->hasRawHeader( "Content-Type" ))
map["contentType"] = reply->reply()->rawHeader( "Content-Type" );
bool ok = false;
QString json = QString::fromUtf8( TomahawkUtils::toJson( map, &ok ) );
Q_ASSERT( ok );
QString javascript = QString( "Tomahawk.nativeAsyncRequestDone( %1, %2 );" ) QVariantMap responseHeaders;
.arg( QString::number( requestId ) ) foreach( const QNetworkReply::RawHeaderPair& pair, reply->reply()->rawHeaderPairs() )
.arg( json ); {
m_resolver->d_func()->scriptAccount->evaluateJavaScript( javascript ); responseHeaders[ pair.first ] = pair.second;
}
map["responseHeaders"] = responseHeaders;
m_resolver->d_func()->scriptAccount->reportNativeScriptJobResult( requestId, map );
} }
@@ -1143,43 +890,3 @@ JSResolverHelper::readdResolver()
Pipeline::instance()->addResolver( m_resolver ); Pipeline::instance()->addResolver( m_resolver );
} }
void
JSResolverHelper::returnStreamUrl( const QString& streamUrl, const QMap<QString, QString>& headers,
std::function< void( const QString&, QSharedPointer< QIODevice >& ) > callback )
{
if ( streamUrl.isEmpty() || !( TomahawkUtils::isHttpResult( streamUrl ) || TomahawkUtils::isHttpsResult( streamUrl ) ) )
{
// Not an https? URL, so let Phonon handle it
QSharedPointer< QIODevice > sp;
callback( streamUrl, sp );
}
else
{
QUrl url = QUrl::fromEncoded( streamUrl.toUtf8() );
QNetworkRequest req( url );
foreach ( const QString& key, headers.keys() )
{
req.setRawHeader( key.toLatin1(), headers[key].toLatin1() );
}
tDebug() << "Creating a QNetworkReply with url:" << req.url().toString();
NetworkReply* reply = new NetworkReply( Tomahawk::Utils::nam()->get( req ) );
NewClosure( reply, SIGNAL( finalUrlReached() ), this, SLOT( gotStreamUrl( IODeviceCallback, NetworkReply* )), callback, reply );
}
}
Q_DECLARE_METATYPE( IODeviceCallback )
void
JSResolverHelper::gotStreamUrl( std::function< void( const QString&, QSharedPointer< QIODevice >& ) > callback, NetworkReply* reply )
{
// std::functions cannot accept temporaries as parameters
QSharedPointer< QIODevice > sp ( reply->reply(), &QObject::deleteLater );
QString url = reply->reply()->url().toString();
reply->disconnectFromReply();
reply->deleteLater();
callback( url, sp );
}

View File

@@ -61,9 +61,6 @@ public:
*/ */
Q_INVOKABLE QString accountId(); Q_INVOKABLE QString accountId();
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 );
/** /**
* Make Tomahawk assert the assertion is true, probably not to be used by resolvers directly * Make Tomahawk assert the assertion is true, probably not to be used by resolvers directly
@@ -89,20 +86,9 @@ public:
int sizehint, int sizehint,
const QVariantMap& options ); const QVariantMap& options );
/** Q_INVOKABLE void invokeNativeScriptJob( int requestId,
* Native handler for asynchronous HTTP requests. const QString& methodName,
* const QVariantMap& params );
* This handler shall only be used if we cannot achieve the request with
* XMLHttpRequest as that would be more efficient.
* Use cases are:
* * Referer header: Stripped on MacOS and the specification says it
* should be stripped
*
* INTERNAL USE ONLY!
*/
Q_INVOKABLE void nativeAsyncRequest( int requestId, const QString& url,
const QVariantMap& headers,
const QVariantMap& options );
/** /**
* Lucene++ indices for JS resolvers * Lucene++ indices for JS resolvers
@@ -122,18 +108,10 @@ public:
Q_INVOKABLE void readdResolver(); Q_INVOKABLE void readdResolver();
/**
* INTERNAL USE ONLY!
*/
void customIODeviceFactory( const Tomahawk::result_ptr&, const QString& url,
std::function< void( const QString&, QSharedPointer< QIODevice >& ) > callback ); // async
public slots: public slots:
QByteArray readRaw( const QString& fileName ); QByteArray readRaw( const QString& fileName );
QString readBase64( const QString& fileName ); QString readBase64( const QString& fileName );
QString readCompressed( const QString& fileName ); QString readCompressed( const QString& fileName );
QString instanceUUID();
QString uuid() const; QString uuid() const;
int currentCountry() const; int currentCountry() const;
QString compress( const QString& data ); QString compress( const QString& data );
@@ -142,10 +120,6 @@ public slots:
void log( const QString& message ); void log( const QString& message );
bool fakeEnv() { return false; } bool fakeEnv() { return false; }
void addTrackResults( const QVariantMap& results );
void addUrlResult( const QString& url, const QVariantMap& result );
void nativeReportCapabilities( const QVariant& capabilities ); void nativeReportCapabilities( const QVariant& capabilities );
void reportScriptJobResults( const QVariantMap& result ); void reportScriptJobResults( const QVariantMap& result );
@@ -154,27 +128,19 @@ public slots:
void unregisterScriptPlugin( const QString& type, const QString& objectId ); void unregisterScriptPlugin( const QString& type, const QString& objectId );
private slots: private slots:
void gotStreamUrl( IODeviceCallback callback, NetworkReply* reply );
void tracksAdded( const QList<Tomahawk::query_ptr>& tracks, const Tomahawk::ModelMode, const Tomahawk::collection_ptr& collection );
void pltemplateTracksLoadedForUrl( const QString& url, const Tomahawk::playlisttemplate_ptr& pltemplate );
void nativeAsyncRequestDone( int requestId, NetworkReply* reply ); void nativeAsyncRequestDone( int requestId, NetworkReply* reply );
private: private:
Tomahawk::query_ptr parseTrack( const QVariantMap& track );
void returnStreamUrl( const QString& streamUrl, const QMap<QString, QString>& headers,
std::function< void( const QString&, QSharedPointer< QIODevice >& ) > callback );
bool indexDataFromVariant( const QVariantMap& map, struct Tomahawk::IndexData& indexData ); bool indexDataFromVariant( const QVariantMap& map, struct Tomahawk::IndexData& indexData );
QVariantList searchInFuzzyIndex( const Tomahawk::query_ptr& query ); QVariantList searchInFuzzyIndex( const Tomahawk::query_ptr& query );
// native script jobs
void nativeAsyncRequest( int requestId, const QVariantMap& options );
QVariantMap m_resolverConfig; QVariantMap m_resolverConfig;
JSResolver* m_resolver; JSResolver* m_resolver;
QString m_scriptPath, m_urlCallback, m_urlTranslator; QString m_scriptPath;
QHash< QString, std::function< void( const QString&, QSharedPointer< QIODevice >& ) > > m_streamCallbacks;
QHash< QString, std::function< void( const QString& ) > > m_translatorCallbacks;
bool m_urlCallbackIsAsync;
QString m_pendingUrl;
Tomahawk::album_ptr m_pendingAlbum;
}; };
} // ns: Tomahawk } // ns: Tomahawk

View File

@@ -17,10 +17,31 @@
*/ */
#include "Resolver.h" #include "Resolver.h"
#include "../resolvers/SyncScriptJob.h"
#include "../Result.h"
#include <QPixmap> #include <QPixmap>
Tomahawk::Resolver::Resolver( QObject* parent )
: QObject( parent )
{
}
QPixmap QPixmap
Tomahawk::Resolver::icon( const QSize& ) const Tomahawk::Resolver::icon( const QSize& ) const
{ {
Q_ASSERT(false);
return QPixmap(); return QPixmap();
} }
Tomahawk::ScriptJob*
Tomahawk::Resolver::getStreamUrl( const result_ptr& result )
{
QVariantMap data;
data[ "url" ] = result->url();
return new SyncScriptJob( data );
}

View File

@@ -19,7 +19,6 @@
#ifndef RESOLVER_H #ifndef RESOLVER_H
#define RESOLVER_H #define RESOLVER_H
#include "../ResultProvider.h"
#include "Typedefs.h" #include "Typedefs.h"
#include "DllMacro.h" #include "DllMacro.h"
@@ -36,21 +35,23 @@
namespace Tomahawk namespace Tomahawk
{ {
class ScriptJob;
class DLLEXPORT Resolver : public QObject, public ResultProvider class DLLEXPORT Resolver : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
Resolver() {} Resolver( QObject* parent = nullptr );
virtual QString name() const = 0;
virtual QPixmap icon( const QSize& size ) const;
virtual unsigned int weight() const = 0; virtual unsigned int weight() const = 0;
virtual unsigned int timeout() const = 0; virtual unsigned int timeout() const = 0;
virtual QPixmap icon( const QSize& size ) const override;
public slots:
virtual void resolve( const Tomahawk::query_ptr& query ) = 0; virtual void resolve( const Tomahawk::query_ptr& query ) = 0;
virtual ScriptJob* getStreamUrl( const result_ptr& result );
}; };
} //ns } //ns

View File

@@ -326,21 +326,6 @@ ScriptAccount::parseResultVariantList( const QVariantList& reslist )
} }
rp->setDownloadFormats( fl ); rp->setDownloadFormats( fl );
// find collection
const QString collectionId = m.value( "collectionId" ).toString();
if ( !collectionId.isEmpty() )
{
if ( scriptCollection( collectionId ).isNull() )
{
tLog() << "Resolver returned invalid collection id";
Q_ASSERT( false );
}
else
{
rp->setResolvedByCollection( scriptCollection( collectionId ) );
}
}
results << rp; results << rp;
} }
@@ -348,8 +333,27 @@ ScriptAccount::parseResultVariantList( const QVariantList& reslist )
} }
QSharedPointer< ScriptCollection > ScriptJob*
ScriptAccount::scriptCollection( const QString& id ) const ScriptAccount::resolve( const scriptobject_ptr& scriptObject, const query_ptr& query )
{ {
return m_collectionFactory->scriptPlugins().value( id ); ScriptJob* job = nullptr;
if ( !query->isFullTextQuery() )
{
QVariantMap arguments;
arguments["artist"] = query->queryTrack()->artist();
arguments["album"] = query->queryTrack()->album();
arguments["track"] = query->queryTrack()->track();
job = scriptObject->invoke( "resolve", arguments );
}
else
{
QVariantMap arguments;
arguments["query"] = query->fullTextQuery();
job = scriptObject->invoke( "search", arguments );
}
job->setProperty( "qid", query->id() );
return job;
} }

View File

@@ -65,18 +65,20 @@ public:
ScriptJob* invoke( const scriptobject_ptr& scriptObject, const QString& methodName, const QVariantMap& arguments ); ScriptJob* invoke( const scriptobject_ptr& scriptObject, const QString& methodName, const QVariantMap& arguments );
virtual QVariant syncInvoke( const scriptobject_ptr& scriptObject, const QString& methodName, const QVariantMap& arguments ) = 0; virtual QVariant syncInvoke( const scriptobject_ptr& scriptObject, const QString& methodName, const QVariantMap& arguments ) = 0;
virtual void startJob( ScriptJob* scriptJob ) = 0; virtual void startJob( ScriptJob* scriptJob ) = 0;
void reportScriptJobResult( const QVariantMap& result ); void reportScriptJobResult( const QVariantMap& result );
void registerScriptPlugin( const QString& type, const QString& objectId ); void registerScriptPlugin( const QString& type, const QString& objectId );
void unregisterScriptPlugin( const QString& type, const QString& objectId ); void unregisterScriptPlugin( const QString& type, const QString& objectId );
virtual void reportNativeScriptJobResult( int resultId, const QVariantMap& result ) = 0;
virtual void reportNativeScriptJobError( int resultId, const QVariantMap& error ) = 0;
virtual void scriptPluginFactory( const QString& type, const scriptobject_ptr& object ); virtual void scriptPluginFactory( const QString& type, const scriptobject_ptr& object );
// helpers
QList< Tomahawk::result_ptr > parseResultVariantList( const QVariantList& reslist ); QList< Tomahawk::result_ptr > parseResultVariantList( const QVariantList& reslist );
ScriptJob* resolve( const scriptobject_ptr& scriptObject, const query_ptr& query );
QSharedPointer< ScriptCollection > scriptCollection( const QString& id ) const;
private slots: private slots:
void onJobDeleted( const QString& jobId ); void onJobDeleted( const QString& jobId );

View File

@@ -26,7 +26,10 @@
#include "resolvers/ScriptCommand_AllArtists.h" #include "resolvers/ScriptCommand_AllArtists.h"
#include "resolvers/ScriptCommand_AllAlbums.h" #include "resolvers/ScriptCommand_AllAlbums.h"
#include "resolvers/ScriptCommand_AllTracks.h" #include "resolvers/ScriptCommand_AllTracks.h"
#include "resolvers/ScriptJob.h"
#include "ScriptAccount.h" #include "ScriptAccount.h"
#include "Result.h"
#include "Pipeline.h"
#include <QImageReader> #include <QImageReader>
#include <QPainter> #include <QPainter>
@@ -223,6 +226,15 @@ void ScriptCollection::parseMetaData()
return parseMetaData( readMetaData() ); return parseMetaData( readMetaData() );
} }
ScriptJob*
ScriptCollection::getStreamUrl( const result_ptr& result )
{
QVariantMap arguments;
arguments["url"] = result->url();
return scriptObject()->invoke( "getStreamUrl", arguments );
}
void void
ScriptCollection::parseMetaData( const QVariantMap& metadata ) ScriptCollection::parseMetaData( const QVariantMap& metadata )
@@ -326,3 +338,58 @@ ScriptCollection::onIconFetched()
reply->deleteLater(); reply->deleteLater();
} }
} }
unsigned int
ScriptCollection::timeout() const
{
return 0;
}
unsigned int
ScriptCollection::weight() const
{
return 0;
}
void
ScriptCollection::resolve( const Tomahawk::query_ptr& query )
{
ScriptJob* job = scriptAccount()->resolve( scriptObject(), query );
connect( job, SIGNAL( done( QVariantMap ) ), SLOT( onResolveRequestDone( QVariantMap ) ) );
job->start();
}
void
ScriptCollection::onResolveRequestDone( const QVariantMap& data )
{
Q_ASSERT( QThread::currentThread() == thread() );
ScriptJob* job = qobject_cast< ScriptJob* >( sender() );
QID qid = job->property( "qid" ).toString();
if ( job->error() )
{
Tomahawk::Pipeline::instance()->reportError( qid, this );
}
else
{
QList< Tomahawk::result_ptr > results = scriptAccount()->parseResultVariantList( data.value( "tracks" ).toList() );
foreach( const result_ptr& result, results )
{
result->setResolvedByCollection( weakRef().toStrongRef() );
result->setFriendlySource( prettyName() );
}
Tomahawk::Pipeline::instance()->reportResults( qid, this, results );
}
sender()->deleteLater();
}

View File

@@ -91,8 +91,15 @@ public:
void parseMetaData(); void parseMetaData();
void parseMetaData( const QVariantMap& metadata ); void parseMetaData( const QVariantMap& metadata );
// Resolver interface
unsigned int weight() const override;
unsigned int timeout() const override;
void resolve( const Tomahawk::query_ptr& query ) override;
ScriptJob* getStreamUrl( const result_ptr& result ) override;
private slots: private slots:
void onIconFetched(); void onIconFetched();
void onResolveRequestDone( const QVariantMap& data );
private: private:
ScriptAccount* m_scriptAccount; ScriptAccount* m_scriptAccount;

View File

@@ -117,7 +117,7 @@ ScriptCommand_AllTracks::onTracksJobDone( const QVariantMap& result )
QSharedPointer< ScriptCollection > collection = m_collection.objectCast< ScriptCollection >(); QSharedPointer< ScriptCollection > collection = m_collection.objectCast< ScriptCollection >();
Q_ASSERT( !collection.isNull() ); Q_ASSERT( !collection.isNull() );
QList< Tomahawk::result_ptr > t = collection->scriptAccount()->parseResultVariantList( result[ "results"].toList() ); QList< Tomahawk::result_ptr > t = collection->scriptAccount()->parseResultVariantList( result[ "tracks" ].toList() );
QList< Tomahawk::query_ptr > queries; QList< Tomahawk::query_ptr > queries;

View File

@@ -19,6 +19,7 @@
#include "SourceList.h" #include "SourceList.h"
#include "../ScriptAccount.h" #include "../ScriptAccount.h"
#include "../../Pipeline.h"
using namespace Tomahawk; using namespace Tomahawk;
@@ -29,12 +30,14 @@ void ScriptCollectionFactory::addPlugin( const QSharedPointer<ScriptCollection>&
collection->setOnline( true ); collection->setOnline( true );
SourceList::instance()->addScriptCollection( collection ); SourceList::instance()->addScriptCollection( collection );
Pipeline::instance()->addResolver( collection.data() );
} }
void ScriptCollectionFactory::removePlugin( const QSharedPointer<ScriptCollection>& collection ) const void ScriptCollectionFactory::removePlugin( const QSharedPointer<ScriptCollection>& collection ) const
{ {
collection->setOnline( false ); collection->setOnline( false );
SourceList::instance()->removeScriptCollection( collection ); SourceList::instance()->removeScriptCollection( collection );
Pipeline::instance()->removeResolver( collection.data() );
} }
QSharedPointer< ScriptCollection > ScriptCollectionFactory::createPlugin( const scriptobject_ptr& object, ScriptAccount* scriptAccount ) QSharedPointer< ScriptCollection > ScriptCollectionFactory::createPlugin( const scriptobject_ptr& object, ScriptAccount* scriptAccount )