Creating an Ubuntu scope
Transcription
Creating an Ubuntu scope
Creating an Ubuntu scope Connect to SoundCloud with your first scope In this workshop we are going to create our first scope. It will be able to connect to SoundCloud, we are going to customise it to look nice, it will be able to search songs and we are going to be able to preview them. The nice thing about writing a scope using the Ubuntu SDK is that the default template is a working scope already. That will give us a chance to deconstruct it together and build a new scope from scratch and get a good understanding of the inner workings of scopes in Ubuntu. Let’s get started! Step 1 - Starting a new project What we are doing here: In this step we will get a new scope project started in the Ubuntu SDK. What you need to do: 1. 2. 3. 4. 5. Install the SDK, if necessary. Open the Ubuntu SDK. Create a new project Choose Ubuntu project, “Unity Scope”. Pick “soundcloud-scope” as the project and app name and apart from that: leave the defaults. 6. On the “Kits” page, choose “Desktop”. Run the project and find: Note: To run an app or scope, simply press Ctrl+Rin the SDK. The initial scope the Ubuntu SDK provides us with searches on openweather.org for weather information about London. Step 2 - Deconstructing the default scope What we are doing here: Tearing the default scope template down part by part is going to be quite educational, as we will more easily find our way around afterwards. First we have a look at src/s cope/query.cppwhich is where ● queries from the UI are sent to the client who talks to the web API, ● we transform results into result cards, ● we declare categories that will host these cards and their layout. What you need to do: 1. Remove the definition of WEATHER_TEMPLATEand CITY_TEMPLATE (including comments). Here we define how each result is displayed. 2. In the same file, have a look at the function named Query::run. This is where the query is run: the query text is passed to the API client and we deal with the incoming results. Remove all the code after str ingquery_string=alg ::trim_copy(query.query_string( ) ); up to }catch(domain_error&e){ If you have a look at the remaining code in the file, there should be no mention of types, classes or variables with the names city, weather or current. Run the project and find: The scope still builds and runs, but typing in a query returns nothing. Not really surprising, isn’t it? :-) Step 3 - Data sanitation in the API client What we are doing here: We are still on our mission to deconstruct the weather scope. Now we are having a look at the API client. It provides the separation between the scope code and HTTP API access. Right now it talks openweathermap.org. We are going to change this in a bit. What you need to do: Find the following piece of code and remove it: //O penweathermapAPIerrorcodecaneitherbeastringorint QVar iantcod=root.toVariant( ).toMap()["cod"]; if( (cod.canConvert<QString>()&&cod.toString()!="200") ||(cod.canConvert<uns ignedint>()&&cod.toUInt()!=200)){ throwdomain_error(root.to Variant().toMap()["message"].toString().toStdString() ); } We won’t able to use it in our SoundCloud scope, but it illustrates quite well how you can treat and sanitise data which comes in through external sites. Run the project and find: Still the same, an empty scope, no results. Step 4 - Checking out the data class definitions and the API client What we are doing here: The last traces of the weather scope are in front of us now, what we will have left afterwards is the functional skeleton of a scope, the bare essentials of being able to build and run it. What you need to do: 1. We stay in src/api/client.cppfor now and remove the function Cl ie nt::Cu rrentClient : :weather () entirely. It’s the place where a URI is built and the result (current weather) stored into the variable weather. 2. Next remove the function Cl ie nt::Fo recastClient ::forecast_daily () It’s another nice example of how a URI is built, you can also see how we iterate through data (here the daily forecast data) and store it in the variable result. 3. Now let’s have a look at include/api/client.h. It’s where the C++ headers live, more specifically the class definition of our API client. Here let’s remove the structs and typedefs Forecast, Current, We at herLis t, W eather , Tempand City. 4. Now also remove the following: vi rt ualCu rrentweather (conststd::string&query); vi rt ualFo recastforecast_daily (conststd ::s tring&query,unsig ned in tdays=7); Remove the accompanying comments as well. What we have now is a completely weather-free scope. Step 5 - Setting the essential API client information What we are doing here: In src/api/config.hthe scope defines its user-agent and the API root. They are simple strings, but have a big effect on the behaviour of the API client. What you need to do: 1. Open src/api/config.hand replace the content of apirootto be "h tt ps://a pi.soundcloud.com ". 2. Optional: If you want, set user_agentto be something like "s ou ndclou d-scope0.1;(training)" . Run the project and find: Still the same, an empty scope, no results. Step 6 - Defining our data structures What we are doing here: As we saw in the weather scope, the definition of data structures happens in include/api/client.h. We are going to keep the scope very simple, so we will just define Artist, Track, TrackListand TrackRes(to store a list of track results). If you are unfamiliar with C++’s common data types, you might want to have a look at this article on cplusplus.com. What you need to do: 1. Add the following below cl as sClient{ pu bl ic: to define what Artistwill mean in the context of our scope. / ** *OurArtistobject. * / s tructArtist{ unsignedintid; std ::stringusername ; std ::stringavatar_url ; } ; 2. Next add / ** *Trackinfo,includingtheartist. * / s tructTrack{ unsignedintid; std ::stringtitle ; std ::stringuri; std ::stringartwork_url ; std ::stringstream_url ; std ::stringdescription ; std ::stringgenre ; Artistartist ; } ; 3. Next add / ** *AlistofTrackobjects. * / t ypedefstd::deque <T rack>TrackList ; / ** *Trackresults. * / s tructTrackRes{ TrackListtracks ; } ; With this done, we can now proceed to implementing the query. Step 7 - Implementing the query What we are doing here: Have a look at the results of a search for “ubuntu” using the SoundCloud API. The results are in JSON format, now we are going to extract the relevant information using the data structures we defined in the step before. What you need to do: 1. In include/api/client.hbelow v irtual~Client ()=default ; add: / ** *Getthetracklistforaquery */ v irtualTrackRestracks (conststd:: string&query); 2. Now let’s move over to src/api/client.cpp and before ht tp ::Requ est::Progress ::NextClient::progress_report( add Clie nt::TrackResClient::track s(conststring&query){ Q JsonDocumentroot; g et({"tracks.json"},{{"client_id","apigee"},{"q",query}},root); T rackResresult; Q VariantListvariant=root .toVariant().toList(); f or(constQVariant&i:va riant){ QVariantMapitem=i.to Map(); QVariantMapuser=item ["user"].toMap(); stringart=item["artw ork_url"].toString().toStdString(); //Weaddeachresultt oourlist result.tracks.emplace_b ack( Track{ item["id"].toUI nt(),item["title"].toString().toStdString(), item["uri"].toS tring().toStdString(),art, item["stream_ur l"].toString().toStdString(), item["descripti on"].toString().toStdString(), item["genre"].t oString().toStdString(), Artist{ user["id"]. toUInt(), user["usern ame"].toString().toStdString(), user["avata r_url"].toString().toStdString() } } ); } r eturnresult; } Step 8 - Defining the layout of the scope What we are doing here: Each result needs to be displayed inside a category. In terms of UI, a category can provide a header title to a list of results and a specific layout for both the way results are positioned and the way they look. This will display a simple list of results, it’s a category style used in many scopes, working well with many types of content. What you need to do: 1. Add the following code to src/scope/query.cpp above the definition of the Qu er y::Que ry() function: /* * *De finet helayoutforthetracksresults */ co ns tstat icstringTRACKS_TEMPLATE= R "( { "schema-version" :1 , "template" :{ "category-layout" :"grid" , "card-layout" :"horizontal" , "card-size" :"large" }, "components" :{ "title" :"title" , "art":{ "field" :"art" }, "subtitle" :"artist" } } ) "; 2. Learn more. Review the options for scope layout in the CategoryRenderer API docs. Find out which other values in the te mp latesection would have made sense as well. Step 9 - Stringing everything together What we are doing here: We pass the query_string to our API client, iterate over the results, set a few attributes (some are mandatory like the URI and title) and push the result cards. What you need to do: 1. In src/scope/query.cppadd below str ingquery_string=alg ::trim_copy(query.query_string( ) ); the following code: Client::TrackRestrackslist; trackslist=client_.tr acks(query_string); //Registeracategoryfortracks autotracks_cat=reply ->register_category("tracks","","", sc::CategoryRendere r(TRACKS_TEMPLATE)); for(constauto&track:trackslist.tracks){ //Iterateoverthetra ckslist sc::CategorisedResu ltres(tracks_cat); res.set_uri(track.u ri); res.set_title(track .title); //Settherestoftheattributes,art,artist,etc res.set_art(track.a rtwork_url); res["artist"]=tra ck.artist.username; res["stream"]=tra ck.stream_url; //Pushtheresult if(!reply->push(re s)){ return; } } Run the project and find: All the bits are connected to each other now and we can query SoundCloud and find sounds. Let’s move on to the finishing touches. Step 10 - Customising the scope What we are doing here: We are going to make the scope look more like the SoundCloud site itself. What you need to do: Let’s first change the colour theming of the scope. Open the file with the ending .iniin the data/directory of the project and replace the content with the following: [S co peConf ig] _D is playNa me=Soun dC loudScope _D es cripti on=Finda rtistsandsoundsonSoundCloud Ar t= screen shot.pn g Au th or=Fir stnameLa stname Ic on =icon. png [A pp earanc e] Pa ge Header .Logo=l og o.png Pa ge Header .backgr ou nd=color:///#FFFFFF Pa ge Header .Foregr ou ndColor=#F8500F Ba ck ground Color=# FFFFFF Pa ge Header .Divide rC olor=#F8500F Pr ev iewBut tonColo r=#F8500F Run the project and find: Note how the colours look a lot more like the SoundCloud page. Let’s change the logo now. Step 11 - Changing the logo What we are doing here: This is the most simple step. We simply replace the default logo by one we download from the internet. What you need to do: Download this logo and save it under data/logo.png. Run the project and find: The logo is replaced with a nice SoundCloud logo. That’s how we like it. Step 12 - Your first challenge What we are doing here: It’s nice if a scope shows some results already when the user first launches it. If the scope is launched, the search query is empty, so it makes sense to show some default content, ie: do a query with query text we provide. What you need to do: 1. Find out where in the code this is noted down, where exactly we submit the query string to the API client. 2. A good test for if a string is empty is: if(my_string.empty()){ Run the project and find: The scope coming up with the default content you decided it to have. Step 13 - Challenge number 2 What we are doing here: Did you notice that some tracks don’t have artwork shown? Sometimes this happens when an artist didn’t submit artwork for a particular track. A good fallback could be to just use the avatar of the artist instead. What you need to do: 1. Find out where exactly we instantiate the Trackand Artiststructs with the data we got from the API client. 2. Use the artist’s avatar if the track’s artwork is empty ( ==""). Run the project and find: Whenever you search the amount of tracks without artwork should be close to zero. Next steps ● Review our entire scope section on http://developer.ubuntu.com. ● Get in touch with our App Developer Community and get involved. Thanks a lot for your interest and keep up the good work! :-) Solution to all challenges: ● In step 12, something like this should work: if (query_string.empty()) { // If the string is empty, provide a specific one trackslist = client_.tracks("blur cover"); } else { // otherwise, use the query string trackslist = client_.tracks(query_string); } ● In step 13, try this: stri ngart; //I fthetracka rtworkisempty,weusetheartistpictu re if( item["artwork_url" ].toString().toStdString()=="" ){ art=user[ "avatar_url" ].toString().toStdString(); }el se{ art=item[ "artwork_url" ].toString().toStdString(); } instead of stri ngart=item[ "artwork_url" ].toString().toStdString() ; If you had trouble with any of the challenges or this tutorial in general, check out the source code of the app by branching the code from Launchpad. Just run the following command in a terminal: bzrbranchlp:~ubuntu-sdk-tutorials-d ev/ubuntu-sdk-tutorials/soundcloud-scope-training Simply check out the code and compare. Browse the revisions (you can use the b zr-explorerpackage for that), to confirm each of the steps we took during this workshop.