Hackviking He killed Chuck Norris, he ruled dancing so he took up a new hobby…

12Feb/160

Google Speech API – returns no result

google-chome-devine-mai-interactiv-download-chrome-beta_size1

Google Speech API takes a flac audio file and converts it to text. This API is aimed at Chromium and Android developers first and for most so the documentation for other plattforms isn't that good. This article contains C# code but the principal is the same for other languages. I haven't looked at this before today but got a question from a developer that was stuck only receiving {"result":[]} in return from the API even though the HTTP response was 200 OK. I was pretty impressed with the whole thing so here is what I found today and a simple example to get you started.

Get access

To get access to the API you need to obtain an API key. Simply logging into Google Cloud Console, creating a new project and adding the API will not work. First you need to join the Chromium-dev group on Google groups with the same Google account used in the cloud console.

Then you can head over to Google Cloud Console and:

  1. Create a new project.
  2. Go into API and search for Speech API and add it.
  3. Create credentials for the API, select Server Key.
  4. Put in your public IP and click Create.
  5. Save your API key generated somewhere.

Note: The API only allows you 50 requests a day!

Test is out

You can use the same flac file I used or get/create your own. Just make sure its 16-bit PCM and mono. That was the issue causing the {"result":[]} result.

I used a simple C# windows application with one button running this code:

string api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
string path = @"C:\temp\good-morning-google.flac";

byte[] bytes = System.IO.File.ReadAllBytes(path);

WebClient client = new WebClient();
client.Headers.Add("Content-Type", "audio/x-flac; rate=44100");
byte[] result = client.UploadData(string.Format(
				"https://www.google.com/speech-api/v2/recognize?client=chromium&lang=en-us&key={0}", api_key), "POST", bytes);

string s = client.Encoding.GetString(result);

Then you should get this result back:

{"result":[{"alternative":[{"transcript":"good morning Google how are you feeling today","confidence":0.987629}],"final":true}],"result_index":0}

Important about the flac file

For this to work you need to have a 16-bit PCM mono flac file. The issue that the developer I helped with this ran into was that his file was 32-bit float and stereo. If your not sure download Audacity and check/convert your file.

audacity google speech api

11Feb/160

AWS EC2 Linux: Enable SSH password logon

ec2_logo

Amazon AWS EC2 instances are by default secured with ssh certificates. This is great for security until you need to provide a UAT duplicate for an external user or developer. You don't want to share your certificate with them and setting up a new one is more work than this quick fix. The security isn't as important on a UAT or test system as it is on a production system so for this use case we can go for lower security.

To enable users to access we first need to set a password on the ec2-user account. It's highly recommended that you select a strong password!

sudo passwd ec2-user

Then we need to allow password only connections. We edit the ssh settings, find the line PasswordAuthentication no and change it to PasswordAuthentication yes.

sudo nano /etc/ssh/sshd_config

Then we need to restart the ssh service.

sudo service sshd restart

Now you can login to you Amazon AWS EC2 instance with only a password. To secure the server again just change the PasswordAuthentication line back to no.

9Feb/160

Use UNC path in Filezilla server

380px-FileZilla_logo.svg

Filezilla is widely used for ftp servers, it's open source and easy to setup. It also support SSL encrypted FTP connections which is nice for data security. In one of my setups the need for sharing UNC paths came up. Filezilla actually supports it even though the UI doesn't. So in a few easy steps we can set it up. Remember that the service account running the Filezilla service needs access to the share.

  1. Setup the account in the UI. Point the directory to "c:\temp" or similar.
  2. Open up the "FileZilla Server.xml" located in the Filezilla install directory.
  3. Find the corresponding user node "<User Name="{username}">
  4. Under "<Permissions>" you will have an entry for each folder setup. Change the <Permission Dir="C:\temp"> to <Permission Dir="\\server\share">
  5. Recycle the Filezilla service and you are good to go!

 

You can then change permissions from the UI if you like. So this work around is just needed for creating the link to the share. Once again the FileZilla service account need access to the share. Running it under "System" or "Network Service" will not work in most cases!

8Feb/160

AngularJS: $digest loop when filtering array of objects

AngularJS

Ran into trouble trying to use a filter to sort an array of objects in AngularJS. Basic issue is because the array of objects is passed to the filter by reference. This is not unique to AngularJS, javascript always passed arrays by reference. Since AngularJS watches for changes in the array the sort will fire a digest cycle for every change. This will end up with an error similar to this:

Error: $rootScope:infdig
Infinite $digest Loop

10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);
return b(f,c,e)}","newVal":139,"oldVal":137},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":141,"oldVal":139}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":141,"oldVal":139},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":143,"oldVal":141}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":143,"oldVal":141},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":145,"oldVal":143}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":145,"oldVal":143},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":147,"oldVal":145}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":147,"oldVal":145},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":149,"oldVal":147}]]

Background

So the data structure looks something like this:

{
"feed": {
    "entry": [
    {
        "gphoto$id": {
            "$t": "1000000482404626"
            },
        "updated": {
            "$t": "2016-01-31T19:15:32.739Z"
        },
        "title": {
            "$t": "Auto Backup",
            "type": "text"
        },
        "gphoto$numphotos": {
            "$t": 4223
        },
        "published": {
            "$t": "2016-01-31T17:44:14.000Z"
        }
    },
    {
        "gphoto$id": {
            "$t": "6056987966618281201"
        },
        "updated": {
            "$t": "2015-09-04T15:47:26.370Z"
        },
        "title": {
            "$t": "Norge 2014-09",
            "type": "text"
        },
        "gphoto$numphotos": {
            "$t": 368
        },
        "published": {
            "$t": "2014-09-09T08:46:46.000Z"
        }
    },
    {
        "gphoto$id": {
            "$t": "5980880175059657089"
        },
        "updated": {
            "$t": "2016-01-23T02:50:47.003Z"
        },
        "title": {
            "$t": "Testalbum2",
            "type": "text"
        },
        "gphoto$numphotos": {
            "$t": 3
        },
        "published": {
            "$t": "2014-02-16T06:29:40.000Z"
        }
    }],
    "xmlns": "http://www.w3.org/2005/Atom",
    "xmlns$gphoto": "http://schemas.google.com/photos/2007"
},
"version": "1.0",
"encoding": "UTF-8"
}

We store this array in a $scope variable called List. When we want to run through this array in a simple ng-repeat like

ng-repeat="album in List | BasicFilter:Field:Reverse track by album.gphoto$id.$t"

It will call the filter every time the List variable is changed. If we use this filter:

.filter('BasicFilter', function() {
    return function(items, field, reverse) {
        items.sort(function(a, b){
            var aValue = a[field].$t.toLowerCase();
            var bValue = b[field].$t.toLowerCase();
            
            if(reverse)
            {
                return ((aValue &amp;gt; bValue) ? -1 : ((aValue &amp;lt; bValue) ? 1 : 0));
            } else {
                return ((aValue &amp;lt; bValue) ? -1 : ((aValue &amp;gt; bValue) ? 1 : 0));
            }
        });
        
        return items;
    };
})

Why?

This filter takes in the array as items and sort it. Without going in to to much detail the function supplied to .sort will fire a lot of times. Since the array items is passed by reference it will end up updating the $scope.List that Angular watches and fire a digest each time. There for the fail safe kicks in and you get the error from before.

Solution

So how do we get around this? We need to sort the array without updating the original to prevent the digest from running each time. So we modify our filter to:

.filter('SlicedFilter', function() {
    return function(input, field, reverse) {
      var items = input.slice();
        items.sort(function(a, b){
            var aValue = a[field].$t.toLowerCase();
            var bValue = b[field].$t.toLowerCase();
            
            if(reverse)
            {
                return ((aValue > bValue) ? -1 : ((aValue < bValue) ? 1 : 0));
            } else {
                return ((aValue < bValue) ? -1 : ((aValue > bValue) ? 1 : 0));
            }
        });
        
        return items;
    };
})

By renaming the items variable to input and then create items as an internal variable and copy the original array with .slice() we will get around this issue. Slice will create a shallow copy of the object array. That means that all simple types like strings and numbers will be copied while objects inside the array will be by reference. This means that if any of the objects is updated in the original array the changes will reflect in our copy as well. Then we sort our new copy and when we are done, running the function passed to sort many times, we return the array. When the filter finally return the sorted copy that will trigger the digest and DOM update. So instead of triggering it to many times we only trigger it when we are done with our sorting returning the final product.

But what if the user decides to change back to a sort order we already used? We will sort once again and return it, that's not really efficient! So let's cache the sorted list:

.filter('CachedFilter', function() {
    return _.memoize(function(input, field, reverse) {
      var items = input.slice();
        items.sort(function(a, b){
            var aValue = a[field].$t.toLowerCase();
            var bValue = b[field].$t.toLowerCase();
            
            if(reverse)
            {
                return ((aValue > bValue) ? -1 : ((aValue < bValue) ? 1 : 0));
            } else {
                return ((aValue < bValue) ? -1 : ((aValue > bValue) ? 1 : 0));
            }
        });
        
        return items;
    }, function(items, field, reverse) {
      return items.length + field + reverse;
    });
})

This is a function from underscoreJS that will cache output from whatever function you give it. The second function we pass in calculates the key for the cache. In our case that would be the number of items in the array, field we filtered on and if it's ascending or descending. So the next time we request the same sort we will get the cached version which increases performance.

I put together a demo on Plunker for this issue...

Please share, comment and add suggestions below!

19Oct/150

Implementing Google OAuth in JSFiddle

JSfiddle-blue-w-type.sh

The implementation of Google OAuth for use with there API's are simple with there API Client Library for JavaScript, if you are developing a normal webpage. If you want the functionality in a JSFiddle it's a little more complex for several reasons. I have put together a demo on JSFiddle that shows how it can be accomplished!

Background

I wanted to be able to include Google Oauth for API access in demos I create on JSFiddle. I did a few tests with the API Client Library for JavaScript provided by Google. Ran into several issues with cross-site limitations and sandboxing. Since JSFiddle runs in i sandboxed iframes it limits access to navigation, URL reading and other stuff that the client.js relies on to authenticate the user and get the token back. I found that if the user is already authenticated with Google and you set the immediate parameter to true it will work. That's probably good enough for several demos but it's not a clean solution so I spent a few hours and came up with a solution. If the user isn't already authenticated two new tabs will open up and nothing more will happen.

Pre-requirements

You need to setup a project in the Google Cloud Platform Console and get the clientID. You also need to setup Authorized redirect URIs that matches your JSFiddle. Due to the iframe nature of JSFiddle you cant use the jsfiddle.net address, since the result frame being served from the fiddle.jshell.net domain. For this demo I setup these two redirect URIs:

One address for the case of direct access to the fiddle (someone fining it via Google for example) and one if they navigate to it from my dashboard.

To get this to work I implemented the OAuth flow from the ground up. The demo uses jquery, because it's easier, but it can be done in pure JavaScript if you want to. I'm using the standard Oauth 2 flow were you send the user to Google for login with your client_id, redirect_uri and scope as query string parameters. The user authenticates with Google and get's redirected back to the provided redirect_uri with access_token, token_type and expires_in parameters as query string parameters. Google OAuth supports redirecting the user back to a http address but that's no good from a security standpoint! JSFiddle does support https on all it's fiddles so let's make use of it!

Different then a standard JSFiddle

There are a few things that are different from a regular JSFiddle implementation, to create a safe demo that handles the iframe limitations.

  • Check for HTTPS
    This is just for making the demo safe. When Google returns the user to the site with the token as query string parameter it is possible to intercept if you don't use HTTPS. A simple implementation of location.protocol == 'https:' checks if we are secure or not. This isn't needed since you can send the user from a HTTP site to Google and get the user back on a HTTPS redirect. This is just put there to make a point of the HTTPS implementation.
  • The use of fiddle.jshell.net instead of jsfiddle.net
    Since the iframe with the result, where all the javascript is executed, is served from fiddle.jshell.net all the content needs to be served from there. If not the script cant access the parent frame URL containing the access token returned from Google.

Points of interest in the code

  • Check for HTTPS
    As above this isn't really needed since I can set the redirect_uri to whatever address I want as long as it's specified in the cloud console.
  • Create the authentication link
    For this to work properly it needs to be a target="_blank" link. Navigation inside the iframe is blocked outside the jsfiddle domains. This is the reason why the demo needs to open a new tab to be able to return with the access token. The parameters at the top will set the basic information for building the link:

    var oAuthEndPoint = "https://accounts.google.com/o/oauth2/auth";
    var oAuthClientID = "702841265150-iaravt3jmu0c67k892pivj9kgkb8dbco.apps.googleusercontent.com";
    var oAuthScope = "https://www.googleapis.com/auth/userinfo.profile";
    

    Then we build the redirect uri from the current JSFiddle address. I implemented it this way to make it easy to fork it to create other demos based on this implementation. I use the a element for easy manipulation of the domain and path. Replacing the protocol and domain with https://fiddle.jshell.net:

    var a = document.createElement('a');
    a.href = document.referrer;
    var redirect_uri = ['https://fiddle.jshell.net', a.pathname].join('');
    a = '';
    

    document.referrer is used to get the actual address of the fiddle from the parent frame, even if the domains doesn't match at this point. An easy way to get around the same origin limitations of the iframe.

  • User clicks the authentication link/button
    The user is sent to Google to authenticate and allow the app access to the user information. The only scope requested in this demo is basic profile information.
  • User is returned to JSFiddle
    When the user is returned the fiddle will load all over again. So each time it loads it will check if HTTPS is used, if so check for the URL parameter access_token. This is why we need to use the fiddle.jshell.net domain, it will make all the iframes of the fiddle to load from the same domain giving our script access to it's parent. We need that access to grab the URL and extract the parameters. This function grabs different parameters from the URL of the parent frame:

    function GetURLParameter(sParam) {
        var sPageURL = window.parent.location.hash.substring(1);
        var sURLVariables = sPageURL.split('&amp;amp;amp;amp;amp;');
        
        for (var i = 0; i &amp;amp;amp;amp;lt; sURLVariables.length; i++) {
            var sParameterName = sURLVariables[i].split('=');
            if (sParameterName[0] == sParam) {
                return sParameterName[1];
            }
        }
    }
    
  • Use the token for API access
    As soon as we have the token we can use that for accessing the API with a simple ajax request via this simple function:

    function LoadUserInfo(access_token, expires_in) {
        $.ajax({
            url: 'https://www.googleapis.com/userinfo/v2/me',
            type: 'GET',
            dataType: 'json',
            headers: {
                'Authorization': 'Bearer ' + access_token
            },
            success: function (data) {
                // Hide login
                $("#login").hide();
    
                // Populate demo, img and name
                $("#user_pic").attr("src", data.picture);
                $("#user_name").attr("href", data.link);
                $("#user_name").text(data.name);
    
                // Show raw data
                for (var property in data) {
                    if (data.hasOwnProperty(property)) {
                        $("#raw_data").append("<b>" + property + "</b>" + data[property] + "<br/>");
                    }
                }
    
                // Display demo
                $("#demo").show();
            },
            error: function () {
                $('#demo').html('<p>An error has occurred</p>');
            }
        });
    }
    

Limitations

So there are a few limitations with this approach. You get the token that is valid for 3600 seconds (one hour) and implementing code for renewing that with out losing state is not possible. So if the user have started creating something and the token expires renewing it will end up in a reload of the fiddle. The other, smaller limitation, is that there always will be a second tab opened to authenticate the user.

You also need to set the code as base in the fiddle at all times, redirects to a version number like /3 will make the redirect back from Google to fail!

Any thoughts, questions or suggestions? Please share in the comments below!

Complete JSFiddle demo

12Oct/150

Embed image in HTML code

base64

You can embed images straight into HTML code without loading a separate image file. This is called data URL and is a base 64 encoded representation of the actual image file. You can more or less embed what ever data you like this way but the only implementations I have seen so far is with images. So why do this? There are probably hundreds of examples where this can be useful, I have used it for:

  • For taglines on services like JSFiddle, where external hosting of files can be a mess. If I delete the file or my server is down my JSFiddle demos will be broken.
  • E-mail signatures, same thing here no dependence on external servers for serving images in my mail signature. Some e-mail clients out there doesn't display data URL's so be careful with this one.
  • Screenshots in documentation distributed with software. Since the file is loaded from the users local hard drive the bigger size (see below) isn't an issue for any image and I only have to distribute on HTML file with the complete documentation.

So why isn't every image put into HTML this way? The answer is simple, the size increases! For small images, usually part of the layout, can be loaded faster even though the size is increased. This is due to the lack of overhead from an additional http request to get the image file. Every time someone opens an HTML page over the internet the browser then open new http connections for each resource referenced in the HTML like images, css and javascript files. So by cutting down on the number of request the page can load faster.

Dataurl.net by Sveinbjorn Thordarson is a really good resource to check what files on your site that would be suitable for data url embeds.

Simple example of a base 64 encoded data url image, my tagline image:

<img class="brand-img" alt="Kristofer Källsbo aka Hackviking"    src="data:image/jpeg;base64,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">

This code will result in this:
Kristofer Källsbo aka Hackviking

I put together a JSFiddle with some example code for converting images to base 64 encoded data url's using HTML5 components and plain javascript:

17Sep/150

Win32 Disk Imager

Win32 Disk Imager main window

Reading and writing images to SD cards made easy! I more or less us it every day to write images to SD cards for my Raspberry Pi projects or for doing a backup of them. Win32 Disk Imager has received some bad press because it some times breaks SD cards. Every time that happen to me it was the image that was bad, so I can not really agree with the bad comments. If the card becomes unreadable it's easily fixed with SD Formatter. Win32 Disk Imager can be downloaded from SourceForge!

16Sep/150

SD Formatter

SD Formatter main window

SD cards some times seem to shrink or just get unreadable. Most of the time there is nothing wrong with them at all. They have just been written to in a bad way that messes up the partition table or breaks it all together. Most of the time you can fix it with disk management tools but it's more work then needed. I usually get problems with my SD cards from bad images for my Raspberry Pi but even my GoPro camera messed up a card ones. I also use a Denver action cam that usually formats the SD card with a smaller partition then the actual card size. SD Formatter from SD Association fixes the cards every time. Just run it against the card and turn on "format size adjustment" and it will come out just fine.

15Sep/150

Raspberry Pi as a torrent server

transmission

A Raspberry Pi is a great for creating an always on torrent box that can take care of all your downloading and seeding. If you combine it with a NAS and a Raspberry Pi Kodi media center you will have a really sweat setup. The Raspberry Pi has a low power consumption, I run my of the USB port on my NAS. It also have no fans so it's quiet! In this guide we setup Transmission on a Raspberry Pi which includes both a web gui and third party apps for IOS and Android.

I presume that you have some basic knowledge of Linux and the Raspberry Pi. If not you might need to check out the installation guide for Raspberry Pi. When you have your Raspberry Pi up and running just follow the guide below. Use an image and not NOOBs it will come back and haunt you!
Continue reading...

15Sep/155

Raspbian: fstab doesn’t mount NFS on boot

NFS-Raspberry

Ran out of disc space in one of my Raspberry Pi projects last night. Of course I did a quick and dirty install with NOOBs so cloning to a larger SD-card felt like a drag. So I decided it was time to upgrade from a 4GB SD to a 16GB SD as well as the latest version  4.1.6+. Installation went like a charm until I went to edit my /ect/fstab. I added the same NFS line as I used before:

192.168.0.5:/nfs/Download /mnt/download nfs rsize=8192,wsize=8192,timeo=14,intr 0 0

sudo mount -a work just fine but the share wasn't mounted after reboot. Googled the issue and found a lot of different suggestions, many related to USB drives. The number one suggestion was adding rootdelay=10 or rootdelay=5 to /boot/cmdline.txt. That would probably solve the issue for USB drives because the system are unable to identify the drive that early in the boot. Same suggestion was given for NFS failures as well but will not work. Tried a lot of suggestions, even found scripts to run mount -a after boot. That is not a solution just a work around!

Suggestion for adding x-systemd.automount,noauto to the mount options failed as well. Tried a lot of different configurations with one thing in common, no error in /var/log/syslog.

Finally I realized that the network was not ready! I checked the /etc/network/interfaces settings for eth0.

iface eth0 inet manual

It will still get a DHCP address but that will happen later in the boot process. So when the fstab entries are processed there is no network connection and therefore the disc will not mount. So if you change it to:

iface eth0 inet dhcp

Then the NFS drive will mount just fine after a reboot.