diff --git a/data/js/tomahawk.js b/data/js/tomahawk.js index 1b58d716b..eca51743a 100644 --- a/data/js/tomahawk.js +++ b/data/js/tomahawk.js @@ -4,7 +4,7 @@ * Copyright 2011-2012, Leo Franchi * Copyright 2011, Thierry Goeckel * Copyright 2013, Teo Mrnjavac - * Copyright 2013, Uwe L. Korn + * Copyright 2013-2014, Uwe L. Korn * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -277,6 +277,59 @@ Tomahawk.syncRequest = function (url, extraHeaders, options) { } }; +/** + * Internal counter used to identify retrievedMetadata call back from native + * code. + */ +Tomahawk.retrieveMetadataIdCounter = 0; +/** + * Internal map used to map metadataIds to the respective JavaScript callbacks. + */ +Tomahawk.retrieveMetadataCallbacks = {}; + +/** + * Retrieve metadata for a media stream. + * + * @param url String The URL which should be scanned for metadata. + * @param mimetype String The mimetype of the stream, e.g. application/ogg + * @param sizehint Size in bytes if not supplied possibly the whole file needs + * to be downloaded + * @param options Object Map to specify various parameters related to the media + * URL. This includes: + * * headers: Object of HTTP(S) headers that should be set on doing the + * request. + * * method: String HTTP verb to be used (default: GET) + * * username: Username when using authentication + * * password: Password when using authentication + * @param callback Function(Object,String) This function is called on completeion. + * If an error occured, error is set to the corresponding message else + * null. + */ +Tomahawk.retrieveMetadata = function (url, mimetype, sizehint, options, callback) { + var metadataId = Tomahawk.retrieveMetadataIdCounter; + Tomahawk.retrieveMetadataIdCounter++; + Tomahawk.retrieveMetadataCallbacks[metadataId] = callback; + Tomahawk.nativeRetrieveMetadata(metadataId, url, mimetype, sizehint, options); +}; + +/** + * Pass the natively retrieved metadata back to the JavaScript callback. + * + * Internal use only! + */ +Tomahawk.retrievedMetadata = function(metadataId, metadata, error) { + // Check we have a matching callback stored. + if (!Tomahawk.retrieveMetadataCallbacks.hasOwnProperty(metadataId)) { + return; + } + + // Call the real callback + Tomahawk.retrieveMetadataCallbacks[metadataId](metadata, error); + + // Callback are only used once. + delete Tomahawk.retrieveMetadataCallbacks[metadataId]; +}; + /** * Internal counter used to identify asyncRequest callback from native code. */ diff --git a/src/libtomahawk/resolvers/JSResolverHelper.cpp b/src/libtomahawk/resolvers/JSResolverHelper.cpp index 3ceed5dba..af2bceb16 100644 --- a/src/libtomahawk/resolvers/JSResolverHelper.cpp +++ b/src/libtomahawk/resolvers/JSResolverHelper.cpp @@ -28,6 +28,7 @@ #include "resolvers/ScriptEngine.h" #include "network/Servent.h" #include "utils/Closure.h" +#include "utils/Cloudstream.h" #include "utils/Json.h" #include "utils/NetworkAccessManager.h" #include "utils/NetworkReply.h" @@ -45,6 +46,19 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include + +#if defined(TAGLIB_MAJOR_VERSION) && defined(TAGLIB_MINOR_VERSION) +#if TAGLIB_MAJOR_VERSION >= 1 && TAGLIB_MINOR_VERSION >= 9 + #include +#endif +#endif using namespace Tomahawk; @@ -488,6 +502,137 @@ JSResolverHelper::reportStreamUrl( const QString& qid, } +void +JSResolverHelper::nativeRetrieveMetadata( int metadataId, const QString& url, + const QString& mime_type, int sizehint, + const QVariantMap& options ) +{ + if ( sizehint <= 0 ) + { + QString javascript = QString( "Tomahawk.retrievedMetadata( %1, null, 'Supplied size is not (yet) supported');" ) + .arg( metadataId ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); + return; + } + + if ( TomahawkUtils::isHttpResult( url ) || TomahawkUtils::isHttpsResult( url ) ) + { + // TODO: Add heuristic if size is not defined + // TOOD: Support pushing multiple headers + CloudStream stream( url, sizehint, QString("insert headers here"), + Tomahawk::Utils::nam() ); + stream.Precache(); + QScopedPointer tag; + if ( mime_type == "audio/mpeg" ) + { + tag.reset( new TagLib::MPEG::File( &stream, + TagLib::ID3v2::FrameFactory::instance(), + TagLib::AudioProperties::Accurate + )); + } + else if ( mime_type == "audio/mp4" ) + { + tag.reset( new TagLib::MP4::File( &stream, + true, TagLib::AudioProperties::Accurate + )); + } +#if defined(TAGLIB_MAJOR_VERSION) && defined(TAGLIB_MINOR_VERSION) +#if TAGLIB_MAJOR_VERSION >= 1 && TAGLIB_MINOR_VERSION >= 9 + else if ( mime_type == "application/opus" || mime_type == "audio/opus" ) + { + tag.reset( new TagLib::Ogg::Opus::File( &stream, true, + TagLib::AudioProperties::Accurate + )); + } +#endif +#endif + else if ( mime_type == "application/ogg" || mime_type == "audio/ogg" ) + { + tag.reset( new TagLib::Ogg::Vorbis::File( &stream, true, + TagLib::AudioProperties::Accurate + )); + } + else if ( mime_type == "application/x-flac" || mime_type == "audio/flac" || + mime_type == "audio/x-flac" ) + { + tag.reset( new TagLib::FLAC::File( &stream, + TagLib::ID3v2::FrameFactory::instance(), + true, TagLib::AudioProperties::Accurate + )); + } + else if ( mime_type == "audio/x-ms-wma" ) + { + tag.reset( new TagLib::ASF::File( &stream, true, + TagLib::AudioProperties::Accurate + )); + } + else + { + QString javascript = QString( "Tomahawk.retrievedMetadata( %1, null, 'Unknown mime type for tagging: %2');" ) + .arg( metadataId ).arg( mime_type ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); + return; + } + + if ( stream.num_requests() > 2) + { + // Warn if pre-caching failed. + tLog() << "Total requests for file:" << url + << stream.num_requests() << stream.cached_bytes(); + } + + if ( !tag->tag() || tag->tag()->isEmpty() ) + { + QString javascript = QString( "Tomahawk.retrievedMetadata( %1, null, 'Could not read tag information.');" ) + .arg( metadataId ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); + return; + } + + QVariantMap m; + m["url"] = url; + m["track"] = QString( tag->tag()->title().toCString() ).trimmed(); + m["album"] = QString( tag->tag()->album().toCString() ).trimmed(); + m["artist"] = QString( tag->tag()->artist().toCString() ).trimmed(); + + if ( m["track"].toString().isEmpty() ) + { + QString javascript = QString( "Tomahawk.retrievedMetadata( %1, null, 'Empty track returnd');" ) + .arg( metadataId ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); + return; + } + + if ( m["artist"].toString().isEmpty() ) + { + QString javascript = QString( "Tomahawk.retrievedMetadata( %1, null, 'Empty artist returnd');" ) + .arg( metadataId ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); + return; + } + + if ( tag->audioProperties() ) + { + m["bitrate"] = tag->audioProperties()->bitrate(); + m["channels"] = tag->audioProperties()->channels(); + m["duration"] = tag->audioProperties()->length(); + m["samplerate"] = tag->audioProperties()->sampleRate(); + } + + QString javascript = QString( "Tomahawk.retrievedMetadata( %1, %2 );" ) + .arg( metadataId ) + .arg( QString::fromLatin1( TomahawkUtils::toJson( m ) ) ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); + } + else + { + QString javascript = QString( "Tomahawk.retrievedMetadata( %1, null, 'Protocol not supported');" ) + .arg( metadataId ); + m_resolver->d_func()->engine->mainFrame()->evaluateJavaScript( javascript ); + } +} + + void JSResolverHelper::nativeAsyncRequest( const int requestId, const QString& url, const QVariantMap& headers, diff --git a/src/libtomahawk/resolvers/JSResolverHelper.h b/src/libtomahawk/resolvers/JSResolverHelper.h index 2518dc26b..e0a547657 100644 --- a/src/libtomahawk/resolvers/JSResolverHelper.h +++ b/src/libtomahawk/resolvers/JSResolverHelper.h @@ -49,6 +49,23 @@ public: Q_INVOKABLE void reportStreamUrl( const QString& qid, const QString& streamUrl ); Q_INVOKABLE void reportStreamUrl( const QString& qid, const QString& streamUrl, const QVariantMap& headers ); + /** + * Retrieve metadata for a media stream. + * + * Current suported transport protocols are: + * * HTTP + * * HTTPS + * + * This method is asynchronous and will call + * Tomahawk.retrievedMetadata(metadataId, metadata, error) + * on completion. This method is an internal variant, JavaScript resolvers + * are advised to use Tomahawk.retrieveMetadata(url, options, callback). + */ + Q_INVOKABLE void nativeRetrieveMetadata( int metadataId, const QString& url, + const QString& mimetype, + int sizehint, + const QVariantMap& options ); + /** * Native handler for asynchronous HTTP requests. *