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.