I am creating application which can use google fit api.
I want to get all the activities(Movements) available in the google fit. Here the list of activities in google fit Reference.
Edited
I know the way how to get the activities which performed by user, But i want complete list of activities which available in the google fit API (Not only the activity which performed by user, need whole list of activities) like the list available in the above link.
The Google Fit activities are listed in the FitnessActivities class.
You can programmatically get a list of all these fields using:
FitnessActivities.class.getFields()
Had similar problem when started playing with Google Fit API on Android.
There are videos with code samples as well as more detailed API documentation on Google Fit website.
It helped me a lot -- https://developers.google.com/fit/android/get-started
Check both videos and later how to save and get data types:
https://developers.google.com/fit/android/data-types
To have some data available install Google Fit app on your android phone. Use it for a while and then you will have some real data in Google Fit database available.
EDIT:
If I get your edited question correctly, then you need something like the following code.
Please note that I use this in my own app that lists activities recorded by Google Fit Andorid app.
I'm not sure if it will list other activities, for example custom data types recorded by other apps.
Request "activites" (like STILL, RUNNING, WALKING) from Google Fit:
DataReadRequest readRequest = new DataReadRequest.Builder()
.read(DataType.TYPE_ACTIVITY_SEGMENT)
// maybe you want to limit data to specific time range?
//.setTimeRange(today.startTime, today.endTime, TimeUnit.MILLISECONDS)
.build();
Then parse the response. While parsing there will be activity time available:
Fitness.HistoryApi.readData(mClient, readRequest).setResultCallback(new ResultCallback<DataReadResult>() {
#Override
public void onResult(DataReadResult dataReadResult) {
for (DataSet dataSet : dataReadResult.getDataSets()) {
for (DataPoint dataPoint : dataSet.getDataPoints()) {
DataType dataType = dataPoint.getDataType();
if (dataType.equals(DataType.TYPE_ACTIVITY_SEGMENT)) {
String activity = FitnessActivities.getValue(dataPoint);
/* process as needed */
/* the `activitity' string contains values as described here:
* https://developer.android.com/reference/com/google/android/gms/fitness/FitnessActivities.html
*/
}
}
}
}
});
Like I said it works for me -- in my own app I list activities (and their type, ie. walking, running, etc) recorded by Google Fit app for Android.
Hope this can help others...
List<Session> sessions = sessionReadResponse.getSessions();
for (Session session : sessions) {
dumpSession(session);
Log.i(TAG, "Activity Name: "+sessions.get(position).getActivity());
position++;
List<DataSet> dataSets = sessionReadResponse.getDataSet(session);
for (DataSet dataSet : dataSets) {
dumpDataSet(dataSet);
}
}
Related
I would like to offer the opportunity to view output from the same data, in a spreadsheet, TBA sidebar and, ideally another type of HTML window for output created, for example, with a JavaScript Library like THREE.
The non Google version I made is a web page with iframes that can be resized, dragged and opened/closed and, most importantly, their content shares the same record object in the top window. So, I believe, perhaps naively, something similar could be made an option inside this established and popular application.
At the very least, the TBA trial has shown me it useful to view and manipulate information from either sheet or TBA. The facility to navigate large building projects, clone rooms and floors, and combine JSON records (stored in depositories like myjson) for collaborative work is particularly inspiring for me.
I have tried using the sidebar for different HTML files, but the fact only one stays open is not very useful, and frankly, sharing record objects is still beyond me. So that is the main question. Whether Google people would consider an extra window type is probably a bit ambitious, but I think worth asking.
You can't maintain a global variable across calls to HtmlService. When you fire off an HtmlService instance, which runs in the browser, the server side code that launched it exits.
From that point control is client side, in the HtmlService code. If you then launch a server side function (using google.script.run from client side), a new instance of the server side script is launched, with no memory of the previous instance - which means that any global variables are re-initialized.
There are a number of techniques for peristing values across calls.
The simplest one of course is to pass it to the htmlservice in the first place, then to pass it back to server side as an argument to google.script.run.
Another is to use property service to hold your values, and they will still be there when you go back, but there is a 9k maximum entry size
If you need more space, then the cache service can hold 100k in a single entry and you can use that in the same way (although there is a slight chance it will be cleaned away -- although it's never happened for me)
If you need even more space, there are techniques for compressing and/or spreading a single object across several cache entries - as documented here http://ramblings.mcpher.com/Home/excelquirks/gassnips/squuezer. This same method supports Google Drive, or Google cloud storage if you need to persist data even longer
Of course you can't pass non-stringifiable objects like functions and so on, but you can postpone their evaluation and allow the initialized server side script to evaulate them, and even share the same code between server, client or across projects.
Some techniques for that are described in these articles
http://ramblings.mcpher.com/Home/excelquirks/gassnips/nonstringify
http://ramblings.mcpher.com/Home/excelquirks/gassnips/htmltemplateresuse
However in your specific example, it seems that the global data you want is fetched from an external api call. Why not just retrieve it client side in any case ? If you need to do something with it server side, then pass it to the server using google.script.run.
window.open and window.postMessage() solved both the problems I described above.
I hope you will be assured from the screenshot and code that the usefulness of Google sheets can be extended for the common good. At the core is the two methods for inputting, copying and reviewing textual data - spreadsheet for a slice through a set of data, and TBA for navigation of associations in the Trail (x axis) and Branches (y axis), and for working on Aspects (z axis) of the current selection that require attention, in collaborations, from different interests.
So, for example, a nurse would find TBA useful for recording many aspects of an examination of a patient, whereas a pharmacist might find a spreadsheet more useful for stock control. Both record their data in a common object I call 'nset' (hierarchy of named sets), saved in the cloud and available for distribution in collaborative activities.
TBA is also useful for cloning large sets of records. For example, one room, complete with furniture can be replicated on one floor, then that floor, complete with rooms can be replicated for a complete tower.
Being able to maintain parallel nset objects in multiple monitor windows by postMessage means unrivalled opportunities to display the same data in different forms of multimedia, including interactive animation, augmented reality, CNC machine instruction, IOT controls ...
Here is the related code:
From the TBA in sidebar:
window.addEventListener("message", receiveMessage, false);
function openMonitor(nset){
var params = [
'height=400',
'width=400'
].join(',');
let file = 'http://glasier.hk/blazer/model.html';
popup = window.open(file,'popup_window', params);
popup.moveTo(100,100);
}
var popup;
function receiveMessage(event) {
let ed,nb;
ed = event.data;
nb = typeof ed === "string"? ed : nb[0];
switch(nb){
case "Post":
console.log("Post");
popup.postMessage(["Refreshing nset",nset], "http:glasier.hk");
break;
}
}
function importNset(){
google.script.run
.withSuccessHandler(function (code) {
root = '1grsin';
trial = 'msm4r';
orig = 'ozs29';
code = orig;
path = "https://api.myjson.com/bins/"+code;
$.get(path)
.done((data, textStatus, jqXHR) => {
nset = data;
openMonitor(nset);
cfig = nset.cfig;
start();
})
})
.sendCode();
}
From the popup window:
$(document).ready(function(){
name = $(window).attr("name");
if(name === "workshop"){
tgt = opener.location.href;
}
else{
tgt = "https://n-rxnikgfd6bqtnglngjmbaz3j2p7cbcqce3dihry-0lu-script.googleusercontent.com"
}
$("#notice").html(tgt);
opener.postMessage("Post",tgt);
$(window).on("resize",function(){
location.reload();
})
})
}
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
let ed,nb;
ed = event.data;
nb = typeof ed === "string"? ed : ed[0];
switch(nb){
case "Post": popup.postMessage(["nset" +nset], "*"); break;
default :
src = event.origin;
notice = [ed[0]," from ",src ];
console.log(notice);
// $("#notice").html(notice).show();
nset = ed[1];
cfig = nset.cfig;
reloader(src);
}
}
I should explain that the html part of the sidebar was built on a localhost workshop, with all styles and scripts compiled into a single file for pasting in a sidebar html file. The workshop also is available online. The Google target is provided by event.origin in postMessage. This would have to be issued to anyone wishing to make different monitors. For now I have just made the 3D modelling monitor with Three.js.
I think, after much research and questioning around here, this should be the proper answer.
The best way to implement global variables in GAS is through userproperties or script properties.https://developers.google.com/apps-script/reference/properties/properties-service. If you'd rather deal with just one, write them to an object and then json.stringify the object (and json.parse to get it back).
The Chromeosdevices API relies on the deviceID parameter to find devices on the back end. That has caused a bunch of confusion and frustration on my end. Initially, I thought the deviceID was the serial # of the device. We typically do all searches for device on Google's Admin console using the serial #'s, so it just made sense. I realize now that the deviceID is not the serial #.
Is there a way, then, to translate serial # to deviceId? I'm thinking I may need to export out the entire directory in some table, and then do look ups using the serial # as the reference key. But, it would be nice to figure out a programmatic way to do it. I tried searching stack overflow to no avail.
Thanks,
Figured this out. What a headache. First, if you're developing an Apps Script, there is no need to access the Admin Directory API via UrlFetchApp and trying to get it to work with Oauth2 libraries. All you need is the direct API, which I wish was illustrated in Google's API document and API explorer. Instead, use Admin SDK directly. You can create a script file with a bunch of helper functions to call from your main script:
function getDeviceId(serialnum) {
var optionalArgs = {
projection: 'BASIC',
query: serialnum,};
var chromebook = (AdminDirectory.Chromeosdevices.list("my_customer", optionalArgs)) ;
var chromebookDevID = chromebook.chromeosdevices[0].deviceId;
return chromebookDevID ;
}
For example, you call this function, pass in the serial number, and it will return the chromebook's deviceId as such
var deviceId = getDeviceId('5CD81072C4');
From this, you can also have helper functions to enable / disable chromebooks:
function disableChromeBook(deviceId) {
AdminDirectory.Chromeosdevices.action({"action": "disable"}, "my_customer", deviceId) ;
}
function enableChromeBook(deviceId) {
AdminDirectory.Chromeosdevices.action({"action": "reenable"}, "my_customer", deviceId)
}
I found the Google API document to be really obtuse. I hope this helps someone else out. Took me awhile.
I am trying to obtain the list of places the user has saved on Google Maps. Now I know there isnt an API for this (for whatever reason), but I saw here:
"My Places" Google Maps API
That apparently there used to be a way to obtain the URL, but it does not seem to work with my list of places.
E.g.
https://www.google.com/maps/#46.889424,0.1194148,6z/data=!4m3!11m2!2s1KbZtik1IdXyNhwfXEb3P9vaZvzU!3e3
Does not seem to work if I append &output=kml or &output=json
I created this list on Google Maps, then hit share and obtained that link.
I even tried parsing the resulting HTML but it seems everything is handled by some Javascript Engine and I can't find any reference to Google Ids there --- I dont even know how they handle clicks!
Any help? There must be a way to retrieve this information programmatically!
EDIT:
I managed to get something working by visiting the shared link, then processing the html and storing the window.APP_INITIALIZATION_STATE variable. I then convert it to an javascript array and loop over it. Deep inside the array/map structure, I managed to get the google name and google place id out of that array. That seems to work a bit, but when trying with lists over 20 items long, google only gets the first 20 and is waiting for the user to 'scroll down' to get the next 20. That seems to trigger another call to get the next 20 results and looks a bit like:
https://www.google.com/search?tbm=map&fp=1&authuser=0&hl=en&gl=nl&pb=!4m8!1m3!1d54065472.4384380........
I can see the original feature id being included at the end of the url, but have no idea how to construct this url in full though to get the next 20 items.... Any ideas?
Your saved places list actually has what you call a feature ID attribute, this isn't a common practice and Google frowns upon this technique but take a look at this URL:
https://www.google.com/maps/preview/entity?authuser=0&hl=en&gl=us&pb=!1m10!1s0x0%3A0x3743ae09a161976b!3m8!1m3!1d14318.72623152007!2d-98.2296425!3d26.2070353!3m2!1i1024!2i768!4f13.1!12m3!2m2!1i392!2i106!13m57!2m2!1i203!2i100!3m2!2i4!5b1!6m6!1m2!1i86!2i86!1m2!1i408!2i200!7m42!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e9!2b1!3e2!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!2b1!4b1!9b0!14m3!1snyc5W-WeHY3r5gLwkoRI!7e81!15i10112!15m19!2b1!5m4!2b1!3b1!5b1!6b1!10m1!8e3!14m1!3b1!17b1!24b1!25b1!26b1!30m1!2b1!36b1!52b1!53b1!21m28!1m6!1m2!1i0!2i0!2m2!1i458!2i768!1m6!1m2!1i974!2i0!2m2!1i1024!2i768!1m6!1m2!1i0!2i0!2m2!1i1024!2i20!1m6!1m2!1i0!2i748!2m2!1i1024!2i768!22m1!1e81!29m0!30m1!3b1
Highlighted is the feature ID from the link you posted:
https://www.google.com/maps/#46.889424,0.1194148,6z/data=!4m3!11m2!2s1KbZtik1IdXyNhwfXEb3P9vaZvzU!3e3
Along with other maps parameters; when you hit that link you're actually manually triggering the same callback that Google's own scripts in maps use to parse the data to feed back to the maps UI; if you look at array item 2, or {c:..} you'll find a stringified array with the contents of your list, now depending on the program language you're using all it takes is a little tweaking (find/replace, loop through, lint and trim, etc.) to this array and you can pull your results; the cool thing is if you add or remove a place the next time you hit that end point it's updated in real-time.
Some people may call it a "hack"; but it gets the job done. :)
Hope I pointed you to a direction in the event you haven't found a solution; give this a shot.
Note the URL has to be pasted in its entirety, SO truncated the hyperlink; copy and paste the whole thing in one shot and a text file from Google with the arrays will be produced; in my case I curl the URLs I need and parse the returned strings as needed to pull data from Google where their API has limitations. Just a tip. :)
Also check Joel's Answer who did some research and refined some of the following information.
Pagination
You can use this tool to decrypt the pb-parameter. PB stands for protocol buffer (protobuf) and Google uses its own kind of it for maps. You can find different decoders for this by googling it.
In my case, the pagination was done via one parameter (8iX0). It seems, that it always comes with another similar parameter (7i20) but I don't know that it does. I can't yet confirm that this is always the case, but from my experience you're basically looking for two integers that are 20/40/60 etc. apart.
Here's what this looks like for me:
page 2 (7i20, 8i20)
page 3 (7i20, 8i40)
page 4 (7i20, 8i60)
From this information, I tried 7i20 8i00 for page 1, that seemed to work. For lists with >100 items, it just continues like that (8i120, 8i140 etc.)
Here's a code snippet in python (quick & dirty). Make sure to add (long) delays if your list has many pages as you will get rate-limited by captchas eventually if you don't. Notice the 8i%s0 in the url, make sure to put the %s back when you paste your pb-block.
url = "https://www.google.com:443/search?tbm=map&pb=!7i20!8i%s0!..."
headers = {"Referer": "https://www.google.com/"}
def fetch_stops_from_maps():
new_results = -1
page = 0
results = []
while new_results != 0:
new_results = 0
x = requests.get(url % page, headers=headers)
txt = html.unescape(x.text)
txt = txt.split("\n")[1]
results = re.findall(r"\[null,null,[0-9]{1,2}\.[0-9]{4,15},[0-9]{1,2}\.[0-9]{4,15}]", txt)
print(len(results))
for cord in results:
# curr = the description you can manually type in when saving
curr = txt.split(cord)[1].split("\"]]")[0]
curr = curr[curr.rindex(",\"") + 2:]
cords = str(cord).split(",")
lat = cords[2]
lon = cords[3][:-1]
results.append(s)
new_results += 1
page += 2
Actually getting the correct url
Getting the correct url currently seems to be the hardest part when doing this and I have not fully figured this out aswell. However, for my use-case this is not really important, so I extracted the correct pb-block once and called it a day.
As explained in the other answers, the id of the list is visible in the basic url (here, the 2sXX...) when you navigate to the list in your browser. It seems to usually be 24-32 (?) characters long.
.../maps/<coords>/data=!4m3!11m2!2sXXXX...XXXX!3e3
If you have this id, you can put it into an existing protobuf-block and it may work (I only tested this with 3 different lists, which were all created by the same account, so this theory is far from proven).
Now, how do you get the block? I would just share the one I have, but because I only understand parts of what it does, I fear that it may contain some personal info. Instead, I will share my process of getting it. For this I use Burpsuite. It's a program mainly used for web-security testing and has a free community edition, however for our use-case it is the perfect tool, because with it you can easily tinker with requests, change small parts in the request, send it again and immediately see if your changes changed the response. However for extracting the pb-block, one should also be able to use any program that can intercept browser traffic.
Heres the basic rundown with burp:
From GMaps, share a list that has >20 items (this is important) and copy the public link
In Burp, go to the tab "Proxy", make sure "Intercept" is off and click "Open browser" to open the integrated chromium browser
There, paste the link and wait until maps loaded completely
In Burp, turn "Intercept" on, then in google maps, scroll down in the list, until it starts loading new results (always blocks of 20)
Burp now intercepted all requests the browser made since you turned intercepting on. Click "Forward" and go through all requests, until you see a request in the format
GET /search?tbm=map&authuser=0&hl=de&gl=de&pb=!7i20....
This is what you're looking for.
Optionally, you can now right click into the request-text and click "send to repeater", then switch to the repeater-tab. Here you can edit the request and then send it again, being able to see the response immediately. For example, removing the authuser, hl, gl, q, ech, psi url parameters, the request still works flawlessly. If you remove the tch=1 parameter, the response you get will be in a more human readable format.
In the request-text you should now be able to just search for the list-id you got from the link previously and replace it with the id of another list (search bar is at the bottom in burp). As I said, this worked for me, but it may be possible that the pb-block contains some additional metadata that makes lists from different google-accounts or different types of lists incompatible with specific pb-blocks. Just a theory though. Let me know how it goes!
Further automating
I have theorised that one could automate getting the pb-block using requests-html because it can load html-sites fully but it doesn't get updated anymore. Another option (probably the better one) is Selenium Wire, as you should be able to load the page and intercept the requests, like we did in burp. Seems like a whole lot of work tho :D
This was the only API was able to find was this:
https://www.google.com/bookmarks/?output=xml
Used in a browser you would have to first log in through Google's OAuth. It would then return your saved places. Not sure at the moment how you would embedded the authentication to do this programmatically, but this might send you in the right direction.
I was able to extract the data I needed from my google maps list. Below are some comments that expand on some of the other comments here, along with a script that extracts all of the relevant data points from the network response.
Obtaining the underlying URL
You can easily find this URL by just opening the devtools on your browser, going to the network tab, and refreshing the webpage or scrolling down on the list until it loads new results (the list must be larger than 20 results). You should be able to find the network request that starts with https://www.google.com/search?tbm=map&pb... and go from there.
Increase the results size
I was able to increase the number of results returned from the request by changing the value of the 7i20 parameter. From what I can tell, the 71XX parameter is the size of the page, and the 8iXX parameter is the starting point. I haven't tested how large you can make the page limit, but I tested 100 and it seemed to work fine. This should make dealing with larger lists much easier.
Parsing out the data
Instead of using regex to parse out the relevant data from the response, I found that the response is basically just a massive JSON object and I was able to identify the indexes for specific types of data, such as the name of the place, location, notes, etc. See the script below.
If you look at the buildResults function in the script below, you can see the exact indexes used to extract specific pieces of information. This of course may change over time if the network response changes format at all, so use these as a starting point in the case where the specific values aren't at those indexes anymore. Hopefully they would be close to those locations
Script to parse the data (javascript / node.js)
// Insert the raw text content from the network response from the
// https://www.google.com/search?tbm=map&pb... url below.
const rawInput = null
function prepare(input) {
// There are 5 random characters before the JSON object we need to remove
// Also I found that the newlines were messing up the JSON parsing,
// so I removed those and it worked.
const preparedForParsing = input.substring(5).replace(/\n/g, '')
const json = JSON.parse(preparedForParsing)
const results = json[0][1].map(array => array[14])
return results
}
function prepareLookup(data) {
// this function takes a list of indexes as arguments
// constructs them into a line of code and then
// execs the retrieval in a try/catch to handle data not being present
return function lookup(...indexes) {
const indexesWithBrackets = indexes.reduce((acc, cur) => `${acc}[${cur}]`, '')
const cmd = `data${indexesWithBrackets}`
try {
const result = eval(cmd)
return result
} catch(e) {
return null
}
}
}
function buildResults(preparedData) {
const results = []
for (const place of preparedData) {
const lookup = prepareLookup(place)
// Use the indexes below to extract certain pieces of data
// or as a starting point of exploring the data response.
const result = {
address: {
street_address: lookup(183, 1, 2),
city: lookup(183, 1, 3),
zip: lookup(183, 1, 4),
state: lookup(183, 1, 5),
country_code: lookup(183, 1, 6),
},
name: lookup(11),
tags: lookup(13),
notes: lookup(25,15,0,2),
placeId: lookup(78),
phone: lookup(178,0,0),
coordinates: {
long: lookup(208,0,2),
lat: lookup(208,0,3)
}
}
results.push(result)
}
return results
}
const preparedData = prepare(rawInput)
const listResults = buildResults(preparedData)
console.log(listResults)
https://graph.microsoft.com/v1.0/me/drive/recent
only returns created time and last modified time from fileSystemInfo in the response as below:
"fileSystemInfo": {
"createdDateTime": "2018-05-14T10:15:21Z",
"lastModifiedDateTime": "2018-05-14T10:15:00Z"
}
But not the lastAccessedDateTime as documented.
Does Microsoft Graph Recent API capture and include in its response a read-only file open event? Or is it limited to only the file modified or uploaded events?
I can see that FileSystemInfo class has a property called LastAccessedDateTime. If Recent API does not return this, what is the alternative way to query and retrieve it?
The lastAccessedDateTime property only applies to consumer OneDrive, not SharePoint or OneDrive for Business. From the documentation:
lastAccessedDateTime is not available for items in SharePoint online or OneDrive for Business.
I am trying to import a list of files from Google Drive to YouTube. The meta-data and the URL to the file are in a Google spreadsheet, so I wrote some code using Google Apps Script that does the following
Get the selected rows
Retrieve title, description, Google Drive URL
Load the file from Google Drive via DriveApp.getFileById
Upload the blob to YouTube using the title and description via YouTube.Videos.insert
Update the selected row with the YouTube video id from the response
The upload looks something like this
var blob = DriveApp.getFileById(id).getBlob();
var resource = {
snippet: {
title: 'The title',
description: 'A long description ...',
defaultLanguage: 'de',
categoryId: 17,
tags: [ 'Sport', 'Fitness' ],
},
status: {
privacyStatus: 'unlisted'
}
}
try {
var result = YouTube.Videos.insert(resource, "snippet,status", blob);
return result.id;
} catch (err) {
console.log({message: 'Error ' + err.message, error: err});
}
This code has already worked about a year ago. I have adapted it slightly, but now I do not get a response from the YouTube.Videos.insert call. The following is logged inside the catch:
message: Error Empty response
error: Exception: Empty response
Not very helpful.
Before uploading, I do a YouTube.Channels.list
to get a target channel in case there are multiple channels available. For this request, I have to permit access to my data and I am only asked on the first invocation. I also see the script in the list of applications for my Google account. I assume permissions are ok.
Any suggestions on how I can get more information on the issue, or is there something I should do differently?
Regarding the target channel (and this might be a different question), I cannot really use this, as it seems I can only upload to a specific channel, if I am a YouTube content partner (see parameters onBehalfOfContentOwner and onBehalfOfContentOwnerChannel):
Note: This parameter is intended exclusively for YouTube content partners.
I had same problem in my project and here's what I have figured out: if your video file size is more than 10 Mb, you will get Empty response error.
Probably (can't say officialy because no documentation mentions it) this is happening because Google Apps Script's YouTube.Videos.insert (and all other available built-in services) uses UrlFetchApp under the hood, which have restriction of 10 Mb per call: https://developers.google.com/apps-script/guides/services/quotas#current_limitations. You can check it yourself using your sample code: if file is under 10 Mb, it will be uploaded successfully.
As possible workaround, you can use idea from this answer: https://stackoverflow.com/a/44853845/555121
Basically, you will need to open modal window using SpreadsheetApp.getUi().showModalDialog and then perform upload to YouTube via plain JavaScript inside modal dialog, which have no restrictions on transferred data size. Here's good example of YouTube resumable upload implementation: https://github.com/sangnvus/2015SUMJS01/blob/master/WIP/Sources/FlyAwayPlus/FlyAwayPlus/Scripts/youtube-upload.js