Thursday, October 24, 2013

Ruby Photo Sorter

Today, I worked on a personal project in Ruby that would sort my ever expanding library of family photos into a common folder structure. I like the way iTunes handles this by sorting them into sub-folders as /year/month the photo was taken so that's what I went with.
#!/usr/bin/env ruby

# Summary:
# This utility will take each jpg, png, gif in the specified directory, including sub-directories, and 
# copy it to the specified output directory (optional) for the year and month the photo was taken/created. 
# (example:  ./unsorted/image.jpg -> ./sorted/2013/10/image.jpg)
# 
# Code Author: Jason Savage
#------------------------------------------

require 'exifr'
require 'fileutils'

def run()
  
  return if ARGV.length < 1
  
  # get dir argument
  in_dir  = ARGV[0]
  out_dir = ARGV[1] || in_dir
  
  # fix path if on windows
  in_dir = in_dir.gsub(%r{\\}) { "/" }
  out_dir = out_dir.gsub(%r{\\}) { "/" }
  
  puts 'in: ' + in_dir, 'out: ' + out_dir
  
  if Dir.exist? in_dir
    
    # loop through each *.jpg in the folder and move to dir/#{year}/
    Dir.glob( in_dir + '/**/*.{jpg,png,gif}') do |file|
      
      move_file(out_dir, file)
      
    end
    
  end
  
end;

def move_file(dir, file)
  
  date_time = EXIFR::JPEG.new(file).date_time || File.mtime( file )
  
  if date_time != nil && date_time.year != ''
    
    # get path as #{dir}/year/month
    out_dir = File.join(dir, date_time.year.to_s, zero_pad(date_time.month.to_s))
    
    # create #{dir}/year/month if it doesn't exist?
    FileUtils.mkdir_p out_dir unless Dir.exist?(out_dir)
    
    # check if image file name is already used
    i     = 1
    ext   = File.extname(file)
    fname = File.basename(file, ext)
    
    while File.exist? File.join(out_dir, fname + ext) 
      fname = File.basename(file, ext) + '_' + i.to_s
      i += 1
    end
    
    # move file to new directory
    FileUtils.cp( file, File.join(out_dir, fname + ext) )
    
  end
  
end


def zero_pad(str)
  
  if str.to_f < 10
    return '0' + str
  end
  return str
  
end



# run script
run if __FILE__ == 'photo_date_sort.rb'

Wednesday, October 9, 2013

SQL to find distance from latitude/longitude

This is a quick post, mostly so i never forget this. A guy I work with either wrote this or found it on the internet. If you have a database of addresses with lat/long pairs for each entry, this SQL statement will find the distance (I think in miles) from a given lat/long.

SELECT 
    ( 3959 * acos( cos( radians(origin_lat) ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians(origin_long) ) + sin( radians(origin_lat) ) * sin( radians( latitude ) ) ) ) AS distance 
FROM
    zip_codes
HAVING
    distance <= _miles
ORDER BY
    zip;

Monday, September 30, 2013

YouTube Browser-based Uploading with OAuth

YouTube has changed over to OAuth for using their data API (API 3.0)
https://developers.google.com/youtube/v3/

Which is fine but it seems they haven't move all of their code over to the new API or haven't finished the docs for it yet, so this is sort of a missing manual for implementing the old API 2.0 Browser-based Uploading seen here:
https://developers.google.com/youtube/2.0/developers_guide_protocol_browser_based_uploading


Step 1- register your app with Google

If you are creating this for one of your brands you need to login/create a Google account for them. After you logged in, you'll need to register your application by following the instructions found here:
https://developers.google.com/youtube/registering_an_application

- when you create a client ID, set application type to web application
- redirect url can be this: http://localhost/oauth2callback, you only need it once

You will also need to get a developer key from here:
https://code.google.com/apis/youtube/dashboard/

The developer_key will not change so make sure you add this to your config file.


Step 2 - get a refresh_token

https://developers.google.com/youtube/v3/guides/authentication

Now that you have your application registered, you should have an oAuth Client ID and Client Secret. Go ahead and save them into your project as a config variable since they don't change, but you need them to get an access_token. You now need to get an authorization code from the API. This is a one time use token that can be exchanged for an access_token and refresh_token. They call it a refresh_token in the docs, but it's really a long term token that doesn't expire, which is what we want.

The code url will be this:
$url = 'https://accounts.google.com/o/oauth2/auth?';  
$url .= 'client_id='   . {{insert_your_client_id_here}}  
$url .= '&redirect_uri=' . urlencode('http://localhost/oauth2callback'); //this is whatever url you set in the "create client id dialog"  
$url .= '&scope='    . urlencode('https://gdata.youtube.com');  
$url .= '&response_type=code';  
$url .= '&access_type=offline';  
$url .= '&approval_prompt=force';  


After you call this url you will be taken to a page and asked if your application is allowed to access this Google account. Click accept and you will be redirected to the url you specified earlier with the get variable code=... added to the end of the url. This is your one time use token so save it into your text editor while you create the next request.

To make a request to get the refresh_token, which is another one time thing, you can use PHP cURL code below or the command line.

 //using PHP cUrl  
 $curl = curl_init( 'https://accounts.google.com/o/oauth2/token' );  
 $post_fields = array(  
   'code'     => '{{insert_the_code_you_just_got_here}}',  
   'client_id'   => '{{insert_your_client_id_here}}',  
   'client_secret' => '{{insert_your_client_secret_here}}',  
   'redirect_uri' => 'http://localhost/oauth2callback', //this doesn't do anything, but it's validated so i needs to match what you've been using  
   'grant_type'  => 'authorization_code'  
 );  

 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);  
 curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);  
 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);  
 curl_setopt($curl, CURLOPT_POST, 1);  
 curl_setopt($curl, CURLOPT_HEADER, 0);  
 curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($post_fields));
  
 curl_setopt($curl, CURLOPT_HTTPHEADER, array(  
   'Content-Type: application/x-www-form-urlencoded'  
 )); 
 
 //send request  
 $response = curl_exec($curl); 
 
 print_r($response);  


If everything worked, you should get something like this back:

{  
  "access_token" : "ya29.AHES6ZTtm7SuokEB-RGtbBty9IIlNiP9-eNMMQKtXdMP3sfjL1Fc",  
  "token_type" : "Bearer",  
  "expires_in" : 3600,  
  "refresh_token" : "1/HKSmLFXzqP0leUihZp2xUt3-5wkU7Gmu2Os_eBnzw74"  
 }  

Bingo!, copy and pasted the refresh_token into your config file. Like I said before, it doesn't expire like the access_token will. You will use it to get a new access_token each time your form loads. Using a token to get a token seems strange but it's Google so I guess they know what they're doing.


Step 3 - post url and upload token

We are back to this guide:
https://developers.google.com/youtube/2.0/developers_guide_protocol_browser_based_uploading

You need to create a video object on the YouTube server to upload a video to. To do this, you'll need to get an upload_token and a post_url from the YouTube API.
http://gdata.youtube.com/action/GetUploadToken

To create a video object, a video title and description along with some keywords and a category are required before can you get the post_url so, in my case, I just created temporary ones and updated them later.

The category definitions can be found here:
http://gdata.youtube.com/schemas/2007/categories.cat

public function get_video_upload_info()  
{  
   //get youtube access token  
   $access_token = $this->get_access_token();  

   //create a video obj with temp info  
   $video_title  = 'Video Temp Title ' . rand(1, 9999);  
   $video_desc   = 'Temp Desc';  
   $video_keywords = 'facebook, contest';  

   //setup request body as xml  
   $xml_str = implode('', array(  
     '<?xml version="1.0"?>',  
     '<entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">',  
       '<media:group>',  
         '<media:title type="plain">' . $video_title . '</media:title>',  
         '<media:description type="plain">' . $video_desc . '</media:description>',  
         '<media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">Animals</media:category>',  
         '<media:keywords>' . $video_keywords . '</media:keywords>',  
         //'<yt:private/>',  
         '<yt:accessControl action="list" permission="denied"/>', //causes the video to be unlisted  
       '</media:group>',  
     '</entry>'));
  
   //use curl to call youtube api  
   $ch = curl_init( 'http://gdata.youtube.com/action/GetUploadToken' );  
   curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);  
   curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);  
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  
   curl_setopt($ch, CURLOPT_HEADER, 0);
  
   curl_setopt($ch, CURLOPT_HTTPHEADER, array(  
     'Authorization: Bearer ' . $access_token,  
     'GData-Version: 2',  
     'X-GData-Key: key=' . {{your_developer_key}}],  
     'Content-Type: application/atom+xml; charset=UTF-8'  
   ));
  
   curl_setopt($ch, CURLOPT_POSTFIELDS, $xml_str);  

   //send request  
   $xml_response = curl_exec($ch);  

   //close connection  
   curl_close($ch); 
  
   $result = simplexml_load_string( $xml_response );  
   if( $result->getName() === 'errors' )  
   {  
     return array('post_url' => 'broke', 'upload_token' => 'not_a_real_token');     
   }  
   return array('post_url' => (string) $result->url, 'upload_token' => (string) $result->token);  
 }  


In the code above, the method '''get_access_token();''' uses the refreash_token to make a cURL request to the YouTube API to get a valid access_token.

private function get_access_token()  
 {  
   $ch = curl_init( 'https://accounts.google.com/o/oauth2/token' );  
   $post_fields = array(  
     'client_id'   => {{insert_your_client_id_here}},  
     'client_secret' => {{insert_your_client_secret_here}},  
     'refresh_token' => {{insert_your_refresh_token_here}},  
     'grant_type'  => 'refresh_token'  
   );  

   curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);  
   curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);  
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  
   curl_setopt($ch, CURLOPT_POST, 1);  
   curl_setopt($ch, CURLOPT_HEADER, 0);  
   curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields)); 
 
   curl_setopt($ch, CURLOPT_HTTPHEADER, array(  
     'Content-Type: application/x-www-form-urlencoded'  
   ));  

   //send request  
   $response = json_decode( curl_exec($ch) );  

   curl_close($ch);  

   return $response->access_token;  
 }  



Step 4 - upload the video to YouTube

Using the post_url we can now display the form to complete this whole process. The post_url you will use in your form will need to have the GET variable next=... added to it. This will be the url that the form will redirect to after the video has finished uploading.

The form needs to have the 2 fields 'token' and 'file', which the API is expecting:

<form method="post" action="{{post_url}}?next={{next_url}}" enctype="multipart/form-data">  
   <input name="token" type="hidden" value="{{upload_token}}"/>   
   <input type='file' id='file' name='file' accept="video/*" />  
 </form>   

When video is finished uploading, the form will redirect to the url you set for next=... along with some extra GET variables from YouTube. The variable status=... is always returned, so you can use that to check if it was successful, which the value would be 200.

If the upload was a success, you will also get the variable id=... which will have the video's new id ( http://www.youtube.com/watch?v={{id}} ).

If the status wasn't 200, then an error occured and you will get the variable code=... which will describe the error.



That should be it. Easy huh? after 3 days I finally got this working so I figured it needed documenting and like i said, it's probably going to change soon so who knows how long this will work.

Google also has some code libraries you can use:
https://developers.google.com/youtube/v3/code_samples/php

I looked into them, but was having trouble fitting the code into my project, so maybe if I started with all this in mind, I would've been able to use them.

Monday, June 10, 2013

The Selected() Plugin

Part of the code I write requires a sort of radio button or toggle button functionality. The core ability is to be able to set an item as selected (or checked, active, clicked, etc.). I ran into this a lot and found myself writing the same simple code over and over again until I created the selected plugin.

Now i can simply write this with jQuery:
$("a").selected(true);
var isSelected = $("a").selected();
Of course this led to being able to have a group of buttons with only one selected at a time:
$("a.tab").selectedGroup();
I have the plugin up on GitHub with a little more explanation and a demo:
https://github.com/jasonsavage2/jquery.selected

Let me know what you think.

Thursday, June 6, 2013

Minimal jQuery.slideshow()

Yesterday, I worked on a barebones jQuery slideshow that would be able to be easily customized for our websites. Why create another slideshow plugin when we have this wonderful plugin: http://jquery.malsup.com/cycle/? I don't know, waste some time at work, ha!

I only wanted the basic functionality of a slideshow. It needs to select a group of items and cycle through them at a set speed. There should be public methods like: next(), prev(), gotoSlide(), pause(), play() for manually switching slides. While building this slideshow, I figured that it doesn't actually need to switch the slides, just dispatches "change" event when it's time.

In the end i did break down and add a bit of code to auto generate previous button, next button, and/or a menu if needed, but other than that it is very small and ment to be extended to your needs.
//default settings (w menu)
$(".slideshow").slideshow({
    nav : "menu",
    delay : 700,
    slidesSelector : "ul.slides > li",
    onChange : [function (event, index, $slides, $slideshow)]
});
Now it's up on Git Hub (which I'm a bit of a noob at). you can download it here:
https://github.com/jasonsavage2/jquery.slideshow

Let me know what you think, or if you have any ideas on how it could be better.

Wednesday, June 5, 2013

Day 1

So I started this blog because it seems like everyone who is in the web development biz has some kind of blog to show what they figure out from day to day while building websites. We'll see how it goes, I'm not that great of a writer...