During the summer, I stumbled upon an article on Ars Technica about using PyGTK to create a URL shortener. I thought that was pretty interesting, but I don’t use PyGTK. So at that point, I decided to write my own using wxPython and use the article’s code to do the shortening. I hacked together something pretty quickly and then put this article on the back burner and kind of forgot about it. Today, I decided to go ahead and finish it and also create an application that can shorten URLs using other popular URL shorteners.

Aping Ars

First off, I’ll show you my original program that basically just apes the Ars Technica one.

import re
import urllib
import urllib2
import wx
 
class ArsShortener(wx.Frame):
 
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          'wxArsShortner', size=(300,70))
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
 
        self.txt = wx.TextCtrl(panel, wx.ID_ANY, "", size=(300, -1))
        self.txt.Bind(wx.EVT_TEXT, self.onTextChange)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.txt, 0, wx.EXPAND, 5)
        panel.SetSizer(sizer)
 
    #----------------------------------------------------------------------
    def onTextChange(self, event):
        """"""
        text = self.txt.GetValue()
        textLength = len(text)
        if re.match("^https?://[^ ]+", text) and textLength > 20:
            apiurl = "http://is.gd/api.php?" + urllib.urlencode(dict(longurl=text))
            shorturl = urllib2.urlopen(apiurl).read() 
            self.txt.SetValue(shorturl)
 
if __name__ == '__main__':
    app = wx.PySimpleApp()
    frame = ArsShortener()
    frame.Show()
    app.MainLoop()

As you can see, almost half the code is wxPython related while the other half is taken from the Ars article. To get started, we import the Python regular expressions module, re, for validation purposes. It checks the pasted URL to make sure it is in the right format for a URL and not just junk (see the onTextChange method). The urllib modules are used for accessing the is.gd website and passing it the URL that we want shortened. Once we get the result back, we call the text control’s SetValue method to display it to the user. The conditional statement also checks to see if the length of the URL is long enough to warrant shortening it.

You will notice that I have three lines commented out. I was making it so that the application could be resized and have the text control grow along with the frame, but then thought that might be lame. Thus, I commented it out. However, I thought you, dear reader, might find it useful to know how to accomplish this.

Shortening URLs with Other Websites

Now we will look at my expanded example. It can use is.gd, bit.ly or the venerable tinyurl to shorten urls. For this part of the article, you will need the following modules in addition to wx:

I’m sure there are ways to just use the urllib modules for these websites too, but I wanted to see if these third party modules simplified things. Please note that if you want to use bit.ly, they require you to sign up for an API key. I won’t be publishing mine, but I will show you how to use it in my program. So, without further ado, on with the show:

import re
import urllib
import urllib2
import wx
 
bitlyFlag = True
tinyurlFlag = True
 
try:
    import bitly
except ImportError:
    bitlyFlag = False
 
try:
    import tinyurl
except ImportError:
    tinyurlFlag = False
 
########################################################################
class MainPanel(wx.Panel):
    """
    """
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
        self.frame = parent
 
        # create the widgets
        self.createLayout()
 
    #----------------------------------------------------------------------
    def createLayout(self):
        """
        Create widgets and lay them out
        """
        choices = ["is.gd"]
        if bitlyFlag:
            choices.append("bit.ly")
        if tinyurlFlag:
            choices.append("tinyurl")
        choices.sort()
 
        # create the widgets
        self.urlCbo = wx.ComboBox(self, wx.ID_ANY, "is.gd", 
                                  choices=choices,
                                  size=wx.DefaultSize,
                                  style=wx.CB_DROPDOWN)
        self.inputUrlTxt = wx.TextCtrl(self, value="Paste long url here")
        self.inputUrlTxt.Bind(wx.EVT_SET_FOCUS, self.onFocus)
        self.outputUrlTxt = wx.TextCtrl(self, style=wx.TE_READONLY)
 
        shortenBtn = wx.Button(self, label="Shorten URL")
        shortenBtn.Bind(wx.EVT_BUTTON, self.onShorten)
        copyBtn = wx.Button(self, label="Copy to Clipboard")
        copyBtn.Bind(wx.EVT_BUTTON, self.onCopy)
 
        # create the sizers
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
 
        # layout the widgets
        mainSizer.Add(self.urlCbo, 0, wx.ALL, 5)
        mainSizer.Add(self.inputUrlTxt, 0, 
                      wx.ALL|wx.EXPAND, 5)
        mainSizer.Add(self.outputUrlTxt, 0, 
                      wx.ALL|wx.EXPAND, 5)
        btnSizer.Add(shortenBtn, 0, wx.ALL|wx.CENTER, 5)
        btnSizer.Add(copyBtn, 0, wx.ALL|wx.CENTER, 5)
        mainSizer.Add(btnSizer, 0, wx.ALL|wx.CENTER, 5)
        self.SetSizer(mainSizer)
 
    #----------------------------------------------------------------------
    def onCopy(self, event):
        """
        Copies data to the clipboard or displays an error
        dialog if the clipboard is inaccessible.
        """
        text = self.outputUrlTxt.GetValue()
        self.do = wx.TextDataObject()
        self.do.SetText(text)
        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(self.do)
            wx.TheClipboard.Close()
            status = "Copied %s to clipboard" % text
            self.frame.statusbar.SetStatusText(status)
        else:
            wx.MessageBox("Unable to open the clipboard", "Error")
 
    #----------------------------------------------------------------------
    def onFocus(self, event):
        """
        When control is given the focus, it is cleared
        """
        self.inputUrlTxt.SetValue("")
 
    #----------------------------------------------------------------------
    def onShorten(self, event):
        """
        Shortens a url using the service specified.
        Then sets the text control to the new url.
        """
        text = self.inputUrlTxt.GetValue()
        textLength = len(text)
 
        if re.match("^https?://[^ ]+", text) and textLength > 20:
            pass
        else:
            wx.MessageBox("URL is already tiny!", "Error")
            return
 
        url = self.urlCbo.GetValue()
        if url == "is.gd":
            self.shortenWithIsGd(text)
        elif url == "bit.ly":
            self.shortenWithBitly(text)
        elif url == "tinyurl":
            self.shortenWithTinyurl(text)
 
    #----------------------------------------------------------------------
    def shortenWithBitly(self, text):
        """
        Shortens the URL in the text control using bit.ly
 
        Requires a bit.ly account and API key
        """
        bitly.API_LOGIN = "username"
        bitly.API_KEY = "api_key"
        url = bitly.shorten(text)
        self.outputUrlTxt.SetValue(url)
 
    #----------------------------------------------------------------------
    def shortenWithIsGd(self, text):
        """
        Shortens the URL with is.gd using urllib and urllib2
        """
 
        apiurl = "http://is.gd/api.php?" + urllib.urlencode(dict(longurl=text))
        shorturl = urllib2.urlopen(apiurl).read() 
        self.outputUrlTxt.SetValue(shorturl)
 
    #----------------------------------------------------------------------
    def shortenWithTinyurl(self, text):
        """
        Shortens the URL with tinyurl
        """
        print "in tinyurl"
        url = tinyurl.create_one(text)
        self.outputUrlTxt.SetValue(url)
 
########################################################################
class UrlFrame(wx.Frame):
    """
    wx.Frame class
    """
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        title = "URL Shortener"
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          title=title, size=(650, 220))
        panel = MainPanel(self)
        self.statusbar = self.CreateStatusBar()
        self.SetMinSize((650, 220))       
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = UrlFrame()
    frame.Show()
    app.MainLoop()

This piece of code is quite a bit longer than my simple example, but it has a lot more logic built into it. Right off the bat, I have some exception handling implemented in case the programmer doesn’t have the bitly or the tinyurl modules installed. If they do not, then a flag is set that prevents those options from being added. You’ll see this in action in the MainPanel class’s createLayout method. That is where we add the options to the choices list which our combobox will use. Depending on what you have installed, you will see one to three options in the drop-down list.

The next interesting bit is where the input url text control is bound to a focus event. We use this to clear the text control when we paste a url into it. Also take note that the output text control is set to read-only mode. This prevents the user from messing up the new url. Finally, we reach our last two widgets: the Shorten URL and the Copy to Clipboard buttons.

Let’s take a quick look at what happens in the onCopy method since it’s next:

def onCopy(self, event):
    """
    Copies data to the clipboard or displays an error
    dialog if the clipboard is inaccessible.
    """
    text = self.outputUrlTxt.GetValue()
    self.do = wx.TextDataObject()
    self.do.SetText(text)
    if wx.TheClipboard.Open():
        wx.TheClipboard.SetData(self.do)
        wx.TheClipboard.Close()
        status = "Copied %s to clipboard" % text
        self.frame.statusbar.SetStatusText(status)
    else:
        wx.MessageBox("Unable to open the clipboard", "Error")

As you can see, this grabs the current text from the input text control and creates a TextDataObject out of it. Then we attempt to open the clipboard and if we’re successful, we put the TextDataObject into it using the clipboard’s SetData method. Finally, we alert the user to what we have done by changing the frame’s statusbar text.

In the onShorten method, I re-use the regular expression from the Ars program to check if the user has pasted a valid url and we also check the length to see if the url really needs shortening. We get the shortener url type from the combobox and then use a conditional that passes the url we want shortened to the appropriate shortening method. The shortenWithIsGd method is basically the same as the first example, so we’ll skip that one. The shortenWithBitly method shows that we need to set the LOGIN and API_KEY properties before we can shorten the url. Once we’ve done that, we just call bitly’s shorten method. In the shortenWithTinyurl method, it’s even simpler: all you need to do here is call the tinyurl’s create_one method.

Wrapping Up

Now you know the basics for shortening your long URLs via several methods. Feel free to add your own features or other shortening APIs to improve the application for your own use. Note that there’s another Python bitly module. While its home page says that all it needs is the simplejson module, if you actually try to use this module, you’ll receive an error if you don’t also have django installed. They seem to be aware of the issue, but for this reason, I chose to use Opie’s bitly module instead. Have fun coding!

Note: This code was tested on Windows Vista using wxPython 2.8.10.1 (unicode) with Python 2.5.

Downloads

Print Friendly