diff --git a/.gitignore b/.gitignore
index 8841fd4166c10f411d50e8b5aae5f62ce0c761b8..abd2a00c7a0448d27debeddea37fd02cd0fc6648 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,18 @@
-*.pyc
+#  SB User Related   #
+######################
 cache/*
 cache.db*
 config.ini
 Logs/*
 sickbeard.db*
 autoProcessTV/autoProcessTV.cfg
+
+#  Compiled source   #
+######################
+*.py[co]
+
+#  IDE specific      #
+######################
 *.bak
 dist/*
 build/*
@@ -14,4 +22,20 @@ SickBeard-win32-*.zip
 *.exe
 gc.ini
 CHANGELOG.txt
+*.tmp
+*.wpr
+*.project
+*.cproject
+*.tmproj
+*.tmproject
+
+# OS generated files #
+######################
+.Spotlight-V100
+.Trashes
 .DS_Store
+desktop.ini
+ehthumbs.db
+Thumbs.db
+.directory
+*~
\ No newline at end of file
diff --git a/data/css/browser.css b/data/css/browser.css
index 482d9915edb11cc18784857a6704e5202c0ad0a6..f3a177cd36da2438604c0d377c59dffb9bfc6e55 100644
--- a/data/css/browser.css
+++ b/data/css/browser.css
@@ -27,4 +27,28 @@
 .browserDialog.busy .ui-dialog-buttonpane {
   background: url("/images/loading.gif") 10px 50% no-repeat;
 }
-*/
\ No newline at end of file
+*/
+
+/* jquery ui autocomplete overrides to make it look more like the old autocomplete */
+.ui-autocomplete {
+  max-height: 180px;
+  overflow-y: auto;
+  /* prevent horizontal scrollbar */
+  overflow-x: hidden;
+  /* add padding to account for vertical scrollbar */
+  padding-right: 20px;
+}
+* html .ui-autocomplete {
+  height: 180px;
+}
+.ui-menu .ui-menu-item {
+  background-color: #eeeeee;
+}
+.ui-menu .ui-menu-item-alternate{
+  background-color: #ffffff;
+}
+.ui-menu a.ui-state-hover{
+  background: none;
+  background-color: #0A246A;
+  color: #ffffff;
+}
diff --git a/data/css/comingEpisodes.css b/data/css/comingEpisodes.css
index ad9b6fd37ca1ce2ecb2b80d9ec78752ef7f58fa2..ed38f2dae0b6fa061d80c9fc6877b2a32c1d7e0c 100644
--- a/data/css/comingEpisodes.css
+++ b/data/css/comingEpisodes.css
@@ -18,10 +18,10 @@
 .tvshowTitle a {
     color: #FFFFFF;
     float: left; 
-    padding-top: 5px; 
+    padding-top: 3px; 
     padding-left: 8px; 
-    line-height: 17px;
-    font-size: 16px;
+    line-height: 1.2em;
+    font-size: 1.1em;
     text-shadow: -1px -1px 0 rgba(0,0,0,0.3);
 }
 
diff --git a/data/css/config.css b/data/css/config.css
index 3eccac221aa57263595af1de4daf2f776f3e85a8..8c259d704caf0a218a51a8e6fdb11dfae5ebec83 100644
--- a/data/css/config.css
+++ b/data/css/config.css
@@ -54,6 +54,7 @@
 .infoTable {border-collapse: collapse;}
 .infoTableHeader, .infoTableCell {padding: 5px;}
 .infoTableHeader{font-weight:700;}
+.infoTableSeperator { border-top: 1px dotted #666666; }
 
 #config div.testNotification {border: 1px dotted #CCCCCC; padding: 5px; margin-bottom: 10px; line-height:20px;}
 
diff --git a/data/css/default.css b/data/css/default.css
index d564ea07e2a48e649f148c2bf686f2a7af063c04..a3234a9a4115f107f55cbf5bf228f4d2bc0cd5fd 100644
--- a/data/css/default.css
+++ b/data/css/default.css
@@ -1,4 +1,5 @@
 * { outline: 0; }
+*:focus { outline: none; }
 img { border: 0; vertical-align: middle;}
 
 body {
@@ -18,9 +19,9 @@ padding:0;
 }
 
 /* these are for inc_top.tmpl */
-#upgrade-notification{line-height:1;color:#57442b;font-size:130%;font-weight:700;height:0;left:0;text-align:center;top:0;width:100%;z-index:100;margin:0;padding:0;}
+#upgrade-notification{line-height:0.5em;color:#57442b;font-size:1em;font-weight:700;height:0px;text-align:center;width:100%;z-index:100;margin:0;padding:0;}
 #upgrade-notification div{background-color:#c6b695;border-bottom:1px solid #af986b;padding:7px 0;}
-#header-fix{*margin-bottom: -31px; /* IE fix */height:21px;padding:0;}
+#header-fix{*margin-bottom: -31px; /* IE fix */height:21px;padding:0;} 
 
 #header {
 background-color:#fff;
@@ -178,6 +179,7 @@ background-color:#fff;
 padding:0;
 }
 tr.seasonheader h2 {
+display:inline;
 font-size:22px;
 line-height:20px;
 letter-spacing:1px;
@@ -329,8 +331,10 @@ div#addShowPortal  button div.button img{ position: absolute; display: block; to
 div#addShowPortal  button .buttontext { position: relative; display: block; padding: 0.1em 0.4em 0.1em 4.4em; text-align: left; }
 
 #rootDirs, #rootDirsControls { width: 50%; min-width: 400px; }
+
+td.tvShow { font-weight: bold; }
 
-.hover { background-color: #cfcfcf !important; cursor: pointer; }
+td.tvShow:hover { background-color: #cfcfcf !important; cursor: pointer; }
 .navShow { display: inline; cursor: pointer; vertical-align: top; }
 
 /* for manage_massEdit */
@@ -350,3 +354,34 @@ a.whitelink { color: white; }
     font-size: 1em;
 }
 div.ui-pnotify { min-width: 340px; max-width: 550px; width: auto !important;}
+
+span.quality {
+    font: bold 1em/1.2em verdana, sans-serif;
+    background: none repeat scroll 0 0 #999999;
+    color: #FFFFFF;
+    display: inline-block;
+    padding: 2px 4px;
+    text-align: center;
+    -webkit-border-radius: 4px;
+    -moz-border-radius: 4px;
+    border-radius: 4px;
+}
+span.Custom {
+    background: none repeat scroll 0 0 #444499; /* blue */
+}
+span.HD {
+    background: none repeat scroll 0 0 #449944; /* green */
+}
+span.SD {
+    background: none repeat scroll 0 0 #994444; /* red */
+}
+span.Any {
+    background: none repeat scroll 0 0 #444444; /* black */
+}
+
+span.false {
+    color: #993333; /* red */
+}
+span.true {
+    color: #669966; /* green */
+}
\ No newline at end of file
diff --git a/data/css/jquery.autocomplete.css b/data/css/jquery.autocomplete.css
deleted file mode 100644
index 40359ee187ac0b27f2f404bf75fa14c14184d7ef..0000000000000000000000000000000000000000
--- a/data/css/jquery.autocomplete.css
+++ /dev/null
@@ -1,48 +0,0 @@
-.ac_results {
-	padding: 0px;
-	border: 1px solid black;
-	background-color: white;
-	overflow: hidden;
-	z-index: 99999;
-}
-
-.ac_results ul {
-	width: 100%;
-	list-style-position: outside;
-	list-style: none;
-	padding: 0;
-	margin: 0;
-}
-
-.ac_results li {
-	margin: 0px;
-	padding: 2px 5px;
-	cursor: default;
-	display: block;
-	/* 
-	if width will be 100% horizontal scrollbar will apear 
-	when scroll mode will be used
-	*/
-	/*width: 100%;*/
-	font: menu;
-	font-size: 12px;
-	/* 
-	it is very important, if line-height not setted or setted 
-	in relative units scroll will be broken in firefox
-	*/
-	line-height: 16px;
-	overflow: hidden;
-}
-/*
-.ac_loading {
-	background: white url('../images/loading16.gif') right center no-repeat;
-}
-*/
-.ac_odd {
-	background-color: #eee;
-}
-
-.ac_over {
-	background-color: #0A246A;
-	color: white;
-}
diff --git a/data/css/smooth-grinder/jquery-ui-1.8.13.custom.css b/data/css/smooth-grinder/jquery-ui-1.8.13.custom.css
index b7322f72da85111a8f1e63265276d3a78ef686c7..4721799f0eff0fc3c4a41a7b0883f3a7e842a753 100644
--- a/data/css/smooth-grinder/jquery-ui-1.8.13.custom.css
+++ b/data/css/smooth-grinder/jquery-ui-1.8.13.custom.css
@@ -50,7 +50,7 @@
  *
  * http://docs.jquery.com/UI/Theming/API
  *
- * To view and modify this theme, visit http://jqueryui.com/themeroller/?ctl=themeroller&ctl=themeroller&ctl=themeroller&ffDefault=Verdana,Arial,sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=ffffff&bgTextureHeader=01_flat.png&bgImgOpacityHeader=0&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=dcdcdc&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=efefef&bgTextureDefault=03_highlight_soft.png&bgImgOpacityDefault=75&borderColorDefault=aaaaaa&fcDefault=222222&iconColorDefault=8c291d&bgColorHover=dddddd&bgTextureHover=03_highlight_soft.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=222222&iconColorHover=222222&bgColorActive=dfdfdf&bgTextureActive=05_inset_soft.png&bgImgOpacityActive=75&borderColorActive=aaaaaa&fcActive=140f06&iconColorActive=8c291d&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=aaaaaa&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=aaaaaa&fcError=8c291d&iconColorError=cd0a0a&bgColorOverlay=6e4f1c&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=35&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=35&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ctl=themeroller&ctl=themeroller&ctl=themeroller&ctl=themeroller&ffDefault=Verdana,Arial,sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=ffffff&bgTextureHeader=01_flat.png&bgImgOpacityHeader=0&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=dcdcdc&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=efefef&bgTextureDefault=03_highlight_soft.png&bgImgOpacityDefault=75&borderColorDefault=aaaaaa&fcDefault=222222&iconColorDefault=8c291d&bgColorHover=dddddd&bgTextureHover=03_highlight_soft.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=222222&iconColorHover=222222&bgColorActive=dfdfdf&bgTextureActive=05_inset_soft.png&bgImgOpacityActive=75&borderColorActive=aaaaaa&fcActive=140f06&iconColorActive=8c291d&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=aaaaaa&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=aaaaaa&fcError=8c291d&iconColorError=cd0a0a&bgColorOverlay=6e4f1c&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=35&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=35&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
  */
 
 
@@ -350,6 +350,58 @@
 .ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
 .ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
 .ui-accordion .ui-accordion-content-active { display: block; }
+/*
+ * jQuery UI Autocomplete 1.8.13
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete { position: absolute; cursor: default; }
+
+/* workarounds */
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
+/*
+ * jQuery UI Menu 1.8.13
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu {
+	list-style:none;
+	padding: 2px;
+	margin: 0;
+	display:block;
+	float: left;
+}
+.ui-menu .ui-menu {
+	margin-top: -3px;
+}
+.ui-menu .ui-menu-item {
+	margin:0;
+	padding: 0;
+	zoom: 1;
+	float: left;
+	clear: left;
+	width: 100%;
+}
+.ui-menu .ui-menu-item a {
+	text-decoration:none;
+	display:block;
+	padding:.2em .4em;
+	line-height:1.5;
+	zoom:1;
+}
+.ui-menu .ui-menu-item a.ui-state-hover,
+.ui-menu .ui-menu-item a.ui-state-active {
+	font-weight: normal;
+	margin: -1px;
+}
 /*
  * jQuery UI Button 1.8.13
  *
diff --git a/data/images/thetvdb16.png b/data/images/thetvdb16.png
new file mode 100644
index 0000000000000000000000000000000000000000..cef1f96d89c5687c193caaa96fbad947cd6f9203
Binary files /dev/null and b/data/images/thetvdb16.png differ
diff --git a/data/interfaces/default/comingEpisodes.tmpl b/data/interfaces/default/comingEpisodes.tmpl
index 0f09f6fe04be8be58f400509e85ea8db22597882..e49977e75784149d14c881863211a743c88b12f0 100644
--- a/data/interfaces/default/comingEpisodes.tmpl
+++ b/data/interfaces/default/comingEpisodes.tmpl
@@ -119,7 +119,7 @@
 
 <br/>
     <table id="showListTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
-    <thead><tr><th class="nowrap">Next Ep</th><th>Next Ep Name</th><th>Airdate</th><th>Show</th><th>Network</th><th>Quality</th><th>tvDB</th><th>Force</th></tr></thead>
+    <thead><tr><th class="nowrap">Next Ep</th><th>Next Ep Name</th><th>Airdate</th><th>Show</th><th>Network</th><th>Quality</th><th>tvDB</th><th>Search</th></tr></thead>
     <tbody>
 
     #for $cur_result in $sql_results:
@@ -151,7 +151,7 @@
         </td>
         <td>$cur_result["name"]</td>
         <td align="center" class="nowrap">$datetime.date.fromordinal(int($cur_result["airdate"]))</td>
-        <td><a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}">$cur_result["show_name"]</a>
+        <td class="tvShow"><a href="$sbRoot/home/displayShow?show=${cur_result["showid"]}">$cur_result["show_name"]</a>
             #if int($cur_result["paused"]):
             <span class="pause">[paused]</span>
             #end if
@@ -164,9 +164,9 @@ $qualityPresetStrings[int($cur_result["quality"])]
 Custom
 #end if
         </td>
-        <td align="center"><a href="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}"><img alt="[info]" height="16" width="16" src="$sbRoot/images/search32.png" /></a></td>
+        <td align="center"><a href="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}"><img alt="[info]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" /></a></td>
         <td align="center">
-        <a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&amp;season=$cur_result["season"]&amp;episode=$cur_result["episode"]" title="Force Update" id="forceUpdate-${cur_result["showid"]}" class="forceUpdate epSearch"><img alt="[update]" height="16" width="16" src="$sbRoot/images/forceUpdate32.png" id="forceUpdateImage-${cur_result["showid"]}" /></a>
+        <a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&amp;season=$cur_result["season"]&amp;episode=$cur_result["episode"]" title="Manual Search" id="forceUpdate-${cur_result["showid"]}" class="forceUpdate epSearch"><img alt="[search]" height="16" width="16" src="$sbRoot/images/search32.png" id="forceUpdateImage-${cur_result["showid"]}" /></a>
         </td>
       </tr>
     <!-- end $cur_result["show_name"] //-->
@@ -179,7 +179,7 @@ Custom
 <script type="text/javascript" charset="utf-8">
 <!--
 \$(document).ready(function(){ 
-  \$('#sbRoot').ajaxEpSearch({'size': 20, 'loadingImage': 'loading16_333333.gif'});
+  \$('#sbRoot').ajaxEpSearch({'size': 16, 'loadingImage': 'loading16_333333.gif'});
   \$(".ep_summary").hide();
   \$(".ep_summaryTrigger").click(function() {
     \$(this).next(".ep_summary").slideToggle('normal', function() {
@@ -260,8 +260,8 @@ Custom
             #end if            
             </a></span>
             <span class="tvshowTitleIcons">
-                <a href="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}"><img alt="[info]" height="20" width="20" src="$sbRoot/images/search32.png" /></a>
-                <span><a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&amp;season=$cur_result["season"]&amp;episode=$cur_result["episode"]" title="Force Update" id="forceUpdate-${cur_result["showid"]}" class="epSearch forceUpdate"><img alt="[update]" height="20" width="20" src="$sbRoot/images/forceUpdate32.png" id="forceUpdateImage-${cur_result["showid"]}" /></a></span>
+                <a href="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}" onclick="window.open(this.href, '_blank'); return false;" title="http://www.thetvdb.com/?tab=series&amp;id=${cur_result["showid"]}"><img alt="[tvdb]" height="16" width="16" src="$sbRoot/images/thetvdb16.png" /></a>
+                <span><a href="$sbRoot/home/searchEpisode?show=${cur_result["showid"]}&amp;season=$cur_result["season"]&amp;episode=$cur_result["episode"]" title="Manual Search" id="forceUpdate-${cur_result["showid"]}" class="epSearch forceUpdate"><img alt="[search]" height="16" width="16" src="$sbRoot/images/search32.png" id="forceUpdateImage-${cur_result["showid"]}" /></a></span>
             </span>
           </th>
         </tr>
diff --git a/data/interfaces/default/config.tmpl b/data/interfaces/default/config.tmpl
index cbf8318e21d5bb4ef4259412650e5d8598f60a64..1f4af4a909d8b2821bfe703d7882071891504613 100644
--- a/data/interfaces/default/config.tmpl
+++ b/data/interfaces/default/config.tmpl
@@ -17,7 +17,7 @@
     <tr><td class="infoTableHeader">SB Arguments: </td><td class="infoTableCell">$sickbeard.MY_ARGS</td></tr>
     <tr><td class="infoTableHeader">SB Web Root: </td><td class="infoTableCell">$sickbeard.WEB_ROOT</td></tr>
     <tr><td class="infoTableHeader">Python Version: </td><td class="infoTableCell">$sys.version[:120]</td></tr>
-    <tr style="border-top: 1px dotted #666666;"><td class="infoTableHeader">Homepage </td><td class="infoTableCell"><a href="http://www.sickbeard.com/">http://www.sickbeard.com/</a></td></tr>
+    <tr class="infoTableSeperator"><td class="infoTableHeader">Homepage </td><td class="infoTableCell"><a href="http://www.sickbeard.com/">http://www.sickbeard.com/</a></td></tr>
     <tr><td class="infoTableHeader">Forums </td><td class="infoTableCell"><a href="http://sickbeard.com/forums/">http://sickbeard.com/forums/</a></td></tr>
     <tr><td class="infoTableHeader">Source </td><td class="infoTableCell"><a href="https://github.com/midgetspy/Sick-Beard/">https://github.com/midgetspy/Sick-Beard/</a></td></tr>
     <tr><td class="infoTableHeader">Bug Tracker &amp;<br/> Windows Builds </td><td class="infoTableCell"><a href="http://code.google.com/p/sickbeard/">http://code.google.com/p/sickbeard/</a></td></tr>
diff --git a/data/interfaces/default/displayShow.tmpl b/data/interfaces/default/displayShow.tmpl
index ac47fb7e371ef8af8e791e91123b7754a41eddcd..d7c3dc6aa7e29820363e62df509c9268afdd4e92 100644
--- a/data/interfaces/default/displayShow.tmpl
+++ b/data/interfaces/default/displayShow.tmpl
@@ -132,7 +132,7 @@ Change selected episodes to
         <h2>#if int($epResult["season"]) == 0 then "Specials" else "Season "+str($epResult["season"])#</h2>
     </td>
   </tr>
-  <tr id="season-$epResult["season"]-cols"><th width="1%"><input type="checkbox" class="seasonCheck" id="$epResult["season"]" /></th><th>NFO</th><th>TBN</th><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th><th>Status</th><th>Action</th></tr>
+  <tr id="season-$epResult["season"]-cols"><th width="1%"><input type="checkbox" class="seasonCheck" id="$epResult["season"]" /></th><th>NFO</th><th>TBN</th><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th><th>Filename</th><th>Status</th><th>Search</th></tr>
         #set $curSeason = int($epResult["season"])
     #end if    
 
diff --git a/data/interfaces/default/home.tmpl b/data/interfaces/default/home.tmpl
index b71b642981d92300a12d9da62e922c8d587291d4..e2ec818c274b3f727edaa7b8e89f4d05edcf79b3 100644
--- a/data/interfaces/default/home.tmpl
+++ b/data/interfaces/default/home.tmpl
@@ -198,12 +198,12 @@ $myShowList.sort(lambda x, y: cmp(x.name, y.name))
 
   <tr>
     <td align="center">#if len($curEp) != 0 then $curEp[0].airdate else ""#</td>
-    <td><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td>
+    <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td>
     <td>$curShow.network</td>
 #if $curShow.quality in $qualityPresets:
-    <td align="center">$qualityPresetStrings[$curShow.quality]</td>
+    <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td>
 #else:
-    <td align="center">Custom</td>
+    <td align="center"><span class="quality Custom">Custom</span></td>
 #end if
     <td align="center"><!--$dlStat--><div id="progressbar$curShow.tvdbid" style="position:relative;"></div>
         <script type="text/javascript">
diff --git a/data/interfaces/default/inc_top.tmpl b/data/interfaces/default/inc_top.tmpl
index 36caa7e1830222d1ad10a2ad8d3b45b5626f085d..5ba87f6b7c6d4ff07bdd1975d84451e074eb310d 100644
--- a/data/interfaces/default/inc_top.tmpl
+++ b/data/interfaces/default/inc_top.tmpl
@@ -15,7 +15,6 @@
     <link rel="stylesheet" type="text/css" href="$sbRoot/css/comingEpisodes.css" />
     <link rel="stylesheet" type="text/css" href="$sbRoot/css/config.css" />
     <link rel="stylesheet" type="text/css" href="$sbRoot/css/jquery.pnotify.default.css" />
-    <link rel="stylesheet" type="text/css" href="$sbRoot/css/jquery.autocomplete.css" />
     <link rel="stylesheet" type="text/css" href="$sbRoot/css/smooth-grinder/jquery-ui-1.8.13.custom.css" />
     <link rel="stylesheet" type="text/css" href="$sbRoot/css/superfish.css" />
     <link rel="stylesheet" type="text/css" href="$sbRoot/css/tablesorter.css"/>
@@ -25,14 +24,14 @@
 <!--
 #contentWrapper { background: url("$sbRoot/images/bg.gif") repeat scroll 0 0 transparent; }
 
-.ac_loading { background: white url("$sbRoot/images/loading16.gif") right center no-repeat; }
 .sf-sub-indicator { background: url("$sbRoot/images/arrows.png") no-repeat -10px -100px; }
 .sf-shadow ul { background: url("$sbRoot/images/shadow.png") no-repeat bottom right; }
 table.tablesorter thead tr .header { background-image: url("$sbRoot/images/tablesorter/bg.gif"); }
 table.tablesorter thead tr .headerSortUp { background-image: url("$sbRoot/images/tablesorter/asc.gif"); }
 table.tablesorter thead tr .headerSortDown { background-image: url("$sbRoot/images/tablesorter/desc.gif"); }
-.browserDialog.busy .ui-dialog-buttonpane { background: url("$sbRoot/images/loading.gif") 10px 50% no-repeat; }
 
+.ui-autocomplete-loading { background: white url("$sbRoot/images/loading16.gif") right center no-repeat; }
+.browserDialog.busy .ui-dialog-buttonpane { background: url("$sbRoot/images/loading.gif") 10px 50% no-repeat !important; }
 .ui-dialog, .ui-dialog-buttonpane { background: #eceadf url("$sbRoot/css/smooth-grinder/images/ui-bg_fine-grain_10_eceadf_60x60.png") 50% 50% repeat !important; }
 .ui-accordion-content, .ui-tabs-panel { background: #ededed !important; background-image: none !important; }
 
@@ -64,7 +63,6 @@ table.tablesorter thead tr .headerSortDown { background-image: url("$sbRoot/imag
     <script type="text/javascript" src="$sbRoot/js/jquery-ui-1.8.13.custom.min.js"></script>
     <script type="text/javascript" src="$sbRoot/js/superfish-1.4.8.js"></script>
     <script type="text/javascript" src="$sbRoot/js/supersubs-0.2b.js"></script>
-    <script type="text/javascript" src="$sbRoot/js/jquery.autocomplete.min.js"></script>
     <script type="text/javascript" src="$sbRoot/js/jquery.cookie.js"></script>
     <script type="text/javascript" src="$sbRoot/js/jquery.cookiejar.js"></script>
     <script type="text/javascript" src="$sbRoot/js/jquery.json-2.2.min.js"></script>
@@ -122,17 +120,15 @@ table.tablesorter thead tr .headerSortDown { background-image: url("$sbRoot/imag
 
         \$("#NAV$topmenu").addClass("current");
 
-        \$("a.confirm").bind("click",function() {
+        \$("a.confirm").bind("click",function(e) {
+            e.preventDefault();
             var target = \$( this ).attr('href');
-            if ( confirm("Are you sure you want to " + \$(this).attr('text') + "?") )
+            if ( confirm("Are you sure you want to " + \$(this).prop('text') + "?") )
                 location.href = target;
+            \$('#MainMenu.sf-menu').hideSuperfishUl();
             return false;
         });
 
-    \$.pnotify.defaults.pnotify_width = "340px";
-    \$.pnotify.defaults.pnotify_history = false;
-    \$.pnotify.defaults.pnotify_delay = 4000;
-
     });
 //-->
 </script>
diff --git a/data/interfaces/default/manage.tmpl b/data/interfaces/default/manage.tmpl
index 1aafd88e9be5a5613742b45978de113f69aa9662..6e940a9c5acb67f3235109bcc0eadcc85c1e6c1f 100644
--- a/data/interfaces/default/manage.tmpl
+++ b/data/interfaces/default/manage.tmpl
@@ -114,11 +114,11 @@ $myShowList.sort(lambda x, y: cmp(x.name, y.name))
 
   <tr>
     <td align="center"><input type="checkbox" class="editCheck" id="edit-$curShow.tvdbid" /></td>
-    <td><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td>
+    <td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></td>
 #if $curShow.quality in $qualityPresets:
-    <td align="center">$qualityPresetStrings[$curShow.quality]</td>
+    <td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td>
 #else:
-    <td align="center">Custom</td>
+    <td align="center"><span class="quality Custom">Custom</span></td>
 #end if 
     <td align="center"><img src="$sbRoot/images/#if int($curShow.seasonfolders) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td>
     <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td>
diff --git a/data/interfaces/default/manage_backlogOverview.tmpl b/data/interfaces/default/manage_backlogOverview.tmpl
index 5a3bf9069e19febc8a419ca4f80f90c4c9bd71ca..3731776d55dc87dd1a8a3aa750a5ca5cf9c4a0a3 100644
--- a/data/interfaces/default/manage_backlogOverview.tmpl
+++ b/data/interfaces/default/manage_backlogOverview.tmpl
@@ -31,8 +31,8 @@
 #end if
 
   <tr class="seasonheader">
-    <td colspan="3">
-        <br/><h2 style="display: inline; position:absolute;"><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></h2>
+    <td colspan="3" class="align-left">
+        <br/><h2><a href="$sbRoot/home/displayShow?show=$curShow.tvdbid">$curShow.name</a></h2>
         <div class="float-right">
             <span class="wanted nowrap">Wanted: <b>$showCounts[$curShow.tvdbid][$Overview.WANTED]</b></span>
             <span class="qual nowrap">Low Quality: <b>$showCounts[$curShow.tvdbid][$Overview.QUAL]</b></span>
diff --git a/data/js/ajaxNotifications.js b/data/js/ajaxNotifications.js
index 3082035a908d9603ed0f97df0dfd956f4b5fb04f..8e34df3114f344c8e98a1b7c80aa85859118064b 100644
--- a/data/js/ajaxNotifications.js
+++ b/data/js/ajaxNotifications.js
@@ -1,4 +1,7 @@
 var message_url = sbRoot + '/ui/get_messages';
+$.pnotify.defaults.pnotify_width = "340px";
+$.pnotify.defaults.pnotify_history = false;
+$.pnotify.defaults.pnotify_delay = 4000;
 
 function check_notifications() {
     $.getJSON(message_url, function(data){
diff --git a/data/js/browser.js b/data/js/browser.js
index 241f62a5cc57bfbafad04148fd12a51ee4a1c433..62b9c856a56a0b4e3921ccfbc031405b7c7d1163 100644
--- a/data/js/browser.js
+++ b/data/js/browser.js
@@ -103,9 +103,16 @@
         // text field used for the result
         options.field = $(this);
         
-        if(options.field.autocomplete && options.autocompleteURL)
-            options.field.autocomplete(options.autocompleteURL, { matchCase: true });
-        
+        if(options.field.autocomplete && options.autocompleteURL) {
+            options.field.autocomplete({ 
+                source: options.autocompleteURL,
+                open: function(event, ui) { 
+                    $(".ui-autocomplete li.ui-menu-item a").removeClass("ui-corner-all");
+                    $(".ui-autocomplete li.ui-menu-item:odd a").addClass("ui-menu-item-alternate");
+                }
+            });
+        }
+
         // if the text field is empty and we're given a key then populate it with the last browsed value from a cookie
         if(options.key && options.field.val().length == 0 && (path = $.cookie('fileBrowser-' + options.key)))
             options.field.val(path);
diff --git a/data/js/configProviders.js b/data/js/configProviders.js
index 272fd5cee7f20e00eb4171453bd3c3cb86c02592..6b85ceac94da47c11b7db3adcc99fc789d28b642 100644
--- a/data/js/configProviders.js
+++ b/data/js/configProviders.js
@@ -4,12 +4,12 @@ $(document).ready(function(){
         $('.providerDiv').each(function(){
             var providerName = $(this).attr('id');
             var selectedProvider = $('#editAProvider :selected').val();
-            
+
             if (selectedProvider+'Div' == providerName)
                 $(this).show();
             else
                 $(this).hide();
-            
+
         });
     } 
 
@@ -23,8 +23,8 @@ $(document).ready(function(){
 
         if (!isDefault)
         {
-	        $('#editANewznabProvider').addOption(id, name);
-	        $(this).populateNewznabSection();
+            $('#editANewznabProvider').addOption(id, name);
+            $(this).populateNewznabSection();
         }
 
         if ($('#provider_order_list > #'+id).length == 0 && showProvider != false) {
@@ -33,9 +33,9 @@ $(document).ready(function(){
             $('#provider_order_list').append(toAdd);
             $('#provider_order_list').sortable("refresh");
         }
-        
+
         $(this).makeNewznabProviderString();
-    
+
     }
 
     $.fn.updateProvider = function (id, url, key) {
@@ -58,13 +58,13 @@ $(document).ready(function(){
         $('#provider_order_list > #'+id).remove();
 
         $(this).makeNewznabProviderString();
-    
+
     }
 
     $.fn.populateNewznabSection = function() {
-    
+
         var selectedProvider = $('#editANewznabProvider :selected').val();
-    
+
         if (selectedProvider == 'addNewznab') {
             var data = ['','',''];
             var isDefault = 0;
@@ -76,7 +76,7 @@ $(document).ready(function(){
             $('#newznab_add_div').hide();
             $('#newznab_update_div').show();
         }
-        
+
         $('#newznab_name').val(data[0]);
         $('#newznab_url').val(data[1]);
         $('#newznab_key').val(data[2]);
@@ -87,7 +87,7 @@ $(document).ready(function(){
         } else {
 
             $('#newznab_name').attr("disabled", "disabled");
-            
+
             if (isDefault) {
                 $('#newznab_url').attr("disabled", "disabled");
                 $('#newznab_delete').attr("disabled", "disabled");
@@ -96,19 +96,19 @@ $(document).ready(function(){
                 $('#newznab_delete').removeAttr("disabled");
             }
         }
-            
+
     }
     
     $.fn.makeNewznabProviderString = function() {
-        
+
         var provStrings = new Array();
         
         for (var id in newznabProviders) {
             provStrings.push(newznabProviders[id][1].join('|'));
         }
-        
+
         $('#newznab_string').val(provStrings.join('!!!'))
-        
+
     }
     
     $.fn.refreshProviderList = function() {
@@ -126,19 +126,19 @@ $(document).ready(function(){
 
     $('.newznab_key').change(function(){
 
-    	var provider_id = $(this).attr('id');
-    	provider_id = provider_id.substring(0, provider_id.length-'_hash'.length);
-    	
-    	var url = $('#'+provider_id+'_url').val();
-    	var key = $(this).val();
+        var provider_id = $(this).attr('id');
+        provider_id = provider_id.substring(0, provider_id.length-'_hash'.length);
+
+        var url = $('#'+provider_id+'_url').val();
+        var key = $(this).val();
+
+        $(this).updateProvider(provider_id, url, key);
 
-    	$(this).updateProvider(provider_id, url, key);
-    	
     });
     
     $('#newznab_key').change(function(){
         
-    	var selectedProvider = $('#editANewznabProvider :selected').val();
+        var selectedProvider = $('#editANewznabProvider :selected').val();
 
         var url = $('#newznab_url').val();
         var key = $('#newznab_key').val();
@@ -180,8 +180,8 @@ $(document).ready(function(){
 
                 $(this).addProvider(data.success, name, url, key, 0);
         });
-        
-        
+
+
     });
 
     $('.newznab_delete').click(function(){
@@ -202,7 +202,7 @@ $(document).ready(function(){
             $(this).refreshProviderList();
         }
     });
-    
+
     $("#provider_order_list").disableSelection();
 
 });
\ No newline at end of file
diff --git a/data/js/jquery-ui-1.8.13.custom.min.js b/data/js/jquery-ui-1.8.13.custom.min.js
index 6d9b092232adc7ad150f347394d495cd32276543..30a4d7588c8cd1e441cad19cd5d23433848f2ad4 100644
--- a/data/js/jquery-ui-1.8.13.custom.min.js
+++ b/data/js/jquery-ui-1.8.13.custom.min.js
@@ -299,6 +299,38 @@ e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedD
 animations:{slide:function(a,b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),h=0,f={},g={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){g[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/);
 f[i]={value:j[1],unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(g,{step:function(j,i){if(i.prop=="height")h=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=h*f[i.prop].value+f[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide",
 paddingTop:"hide",paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery);
+;/*
+ * jQuery UI Autocomplete 1.8.13
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.position.js
+ */
+(function(d){var e=0;d.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:false,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var a=this,b=this.element[0].ownerDocument,g;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!(a.options.disabled||a.element.attr("readonly"))){g=
+false;var f=d.ui.keyCode;switch(c.keyCode){case f.PAGE_UP:a._move("previousPage",c);break;case f.PAGE_DOWN:a._move("nextPage",c);break;case f.UP:a._move("previous",c);c.preventDefault();break;case f.DOWN:a._move("next",c);c.preventDefault();break;case f.ENTER:case f.NUMPAD_ENTER:if(a.menu.active){g=true;c.preventDefault()}case f.TAB:if(!a.menu.active)return;a.menu.select(c);break;case f.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=
+a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);break}}}).bind("keypress.autocomplete",function(c){if(g){g=false;c.preventDefault()}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};
+this.menu=d("<ul></ul>").addClass("ui-autocomplete").appendTo(d(this.options.appendTo||"body",b)[0]).mousedown(function(c){var f=a.menu.element[0];d(c.target).closest(".ui-menu-item").length||setTimeout(function(){d(document).one("mousedown",function(h){h.target!==a.element[0]&&h.target!==f&&!d.ui.contains(f,h.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,f){f=f.item.data("item.autocomplete");false!==a._trigger("focus",c,{item:f})&&/^key/.test(c.originalEvent.type)&&
+a.element.val(f.value)},selected:function(c,f){var h=f.item.data("item.autocomplete"),i=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();a.previous=i;setTimeout(function(){a.previous=i;a.selectedItem=h},1)}false!==a._trigger("select",c,{item:h})&&a.element.val(h.value);a.term=a.element.val();a.close(c);a.selectedItem=h},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");
+d.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");this.menu.element.remove();d.Widget.prototype.destroy.call(this)},_setOption:function(a,b){d.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(d(b||"body",this.element[0].ownerDocument)[0]);a==="disabled"&&
+b&&this.xhr&&this.xhr.abort()},_initSource:function(){var a=this,b,g;if(d.isArray(this.options.source)){b=this.options.source;this.source=function(c,f){f(d.ui.autocomplete.filter(b,c.term))}}else if(typeof this.options.source==="string"){g=this.options.source;this.source=function(c,f){a.xhr&&a.xhr.abort();a.xhr=d.ajax({url:g,data:c,dataType:"json",autocompleteRequest:++e,success:function(h){this.autocompleteRequest===e&&f(h)},error:function(){this.autocompleteRequest===e&&f([])}})}}else this.source=
+this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search",b)!==false)return this._search(a)},_search:function(a){this.pending++;this.element.addClass("ui-autocomplete-loading");this.source({term:a},this.response)},_response:function(a){if(!this.options.disabled&&a&&a.length){a=this._normalize(a);this._suggest(a);this._trigger("open")}else this.close();
+this.pending--;this.pending||this.element.removeClass("ui-autocomplete-loading")},close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){this.menu.element.hide();this.menu.deactivate();this._trigger("close",a)}},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(a){if(a.length&&a[0].label&&a[0].value)return a;return d.map(a,function(b){if(typeof b==="string")return{label:b,value:b};return d.extend({label:b.label||
+b.value,value:b.value||b.label},b)})},_suggest:function(a){var b=this.menu.element.empty().zIndex(this.element.zIndex()+1);this._renderMenu(b,a);this.menu.deactivate();this.menu.refresh();b.show();this._resizeMenu();b.position(d.extend({of:this.element},this.options.position));this.options.autoFocus&&this.menu.next(new d.Event("mouseover"))},_resizeMenu:function(){var a=this.menu.element;a.outerWidth(Math.max(a.width("").outerWidth(),this.element.outerWidth()))},_renderMenu:function(a,b){var g=this;
+d.each(b,function(c,f){g._renderItem(a,f)})},_renderItem:function(a,b){return d("<li></li>").data("item.autocomplete",b).append(d("<a></a>").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});d.extend(d.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,
+"\\$&")},filter:function(a,b){var g=new RegExp(d.ui.autocomplete.escapeRegex(b),"i");return d.grep(a,function(c){return g.test(c.label||c.value||c)})}})})(jQuery);
+(function(d){d.widget("ui.menu",{_create:function(){var e=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(a){if(d(a.target).closest(".ui-menu-item a").length){a.preventDefault();e.select(a)}});this.refresh()},refresh:function(){var e=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex",
+-1).mouseenter(function(a){e.activate(a,d(this).parent())}).mouseleave(function(){e.deactivate()})},activate:function(e,a){this.deactivate();if(this.hasScroll()){var b=a.offset().top-this.element.offset().top,g=this.element.scrollTop(),c=this.element.height();if(b<0)this.element.scrollTop(g+b);else b>=c&&this.element.scrollTop(g+b-c+a.height())}this.active=a.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",e,{item:a})},deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");
+this._trigger("blur");this.active=null}},next:function(e){this.move("next",".ui-menu-item:first",e)},previous:function(e){this.move("prev",".ui-menu-item:last",e)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(e,a,b){if(this.active){e=this.active[e+"All"](".ui-menu-item").eq(0);e.length?this.activate(b,e):this.activate(b,this.element.children(a))}else this.activate(b,
+this.element.children(a))},nextPage:function(e){if(this.hasScroll())if(!this.active||this.last())this.activate(e,this.element.children(".ui-menu-item:first"));else{var a=this.active.offset().top,b=this.element.height(),g=this.element.children(".ui-menu-item").filter(function(){var c=d(this).offset().top-a-b+d(this).height();return c<10&&c>-10});g.length||(g=this.element.children(".ui-menu-item:last"));this.activate(e,g)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active||
+this.last()?":first":":last"))},previousPage:function(e){if(this.hasScroll())if(!this.active||this.first())this.activate(e,this.element.children(".ui-menu-item:last"));else{var a=this.active.offset().top,b=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var g=d(this).offset().top-a+b-d(this).height();return g<10&&g>-10});result.length||(result=this.element.children(".ui-menu-item:first"));this.activate(e,result)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active||
+this.first()?":last":":first"))},hasScroll:function(){return this.element.height()<this.element[d.fn.prop?"prop":"attr"]("scrollHeight")},select:function(e){this._trigger("selected",e,{item:this.active})}})})(jQuery);
 ;/*
  * jQuery UI Button 1.8.13
  *
diff --git a/data/js/jquery.autocomplete.min.js b/data/js/jquery.autocomplete.min.js
deleted file mode 100644
index d6eb959cbff3f3d10b46cf5345ee1dc191f94e8d..0000000000000000000000000000000000000000
--- a/data/js/jquery.autocomplete.min.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * jQuery Autocomplete plugin 1.2
- *
- * Copyright (c) 2009 Jörn Zaefferer
- *
- * Dual licensed under the MIT and GPL licenses:
- *   http://www.opensource.org/licenses/mit-license.php
- *   http://www.gnu.org/licenses/gpl.html
- *
- * With a small modifications by Alfonso Gómez-Arzola.
- * See changelog for details.
- *
- */;(function($){$.fn.extend({autocomplete:function(urlOrData,options){var isUrl=typeof urlOrData=="string";options=$.extend({},$.Autocompleter.defaults,{url:isUrl?urlOrData:null,data:isUrl?null:urlOrData,delay:isUrl?$.Autocompleter.defaults.delay:10,max:options&&!options.scroll?100:150},options);options.highlight=options.highlight||function(value){return value;};options.formatMatch=options.formatMatch||options.formatItem;return this.each(function(){new $.Autocompleter(this,options);});},result:function(handler){return this.bind("result",handler);},search:function(handler){return this.trigger("search",[handler]);},flushCache:function(){return this.trigger("flushCache");},setOptions:function(options){return this.trigger("setOptions",[options]);},unautocomplete:function(){return this.trigger("unautocomplete");}});$.Autocompleter=function(input,options){var KEY={UP:38,DOWN:40,DEL:46,TAB:9,RETURN:13,ESC:27,COMMA:188,PAGEUP:33,PAGEDOWN:34,BACKSPACE:8};var $input=$(input).attr("autocomplete","off").addClass(options.inputClass);var timeout;var previousValue="";var cache=$.Autocompleter.Cache(options);var hasFocus=0;var lastKeyPressCode;var config={mouseDownOnSelect:false};var select=$.Autocompleter.Select(options,input,selectCurrent,config);var blockSubmit;$.browser.opera&&$(input.form).bind("submit.autocomplete",function(){if(blockSubmit){blockSubmit=false;return false;}});$input.bind(($.browser.opera?"keypress":"keydown")+".autocomplete",function(event){hasFocus=1;lastKeyPressCode=event.keyCode;switch(event.keyCode){case KEY.UP:if(select.visible()){event.preventDefault();select.prev();}else{onChange(0,true);}break;case KEY.DOWN:if(select.visible()){event.preventDefault();select.next();}else{onChange(0,true);}break;case KEY.PAGEUP:event.preventDefault();if(select.visible()){select.pageUp();}else{onChange(0,true);}break;case KEY.PAGEDOWN:event.preventDefault();if(select.visible()){select.pageDown();}else{onChange(0,true);}break;case options.multiple&&$.trim(options.multipleSeparator)==","&&KEY.COMMA:case KEY.TAB:case KEY.RETURN:if(selectCurrent()){event.preventDefault();blockSubmit=true;return false;}break;case KEY.ESC:select.hide();break;default:clearTimeout(timeout);timeout=setTimeout(onChange,options.delay);break;}}).focus(function(){hasFocus++;}).blur(function(){hasFocus=0;if(!config.mouseDownOnSelect){hideResults();}}).click(function(){if(hasFocus++>1&&!select.visible()){onChange(0,true);}}).bind("search",function(){var fn=(arguments.length>1)?arguments[1]:null;function findValueCallback(q,data){var result;if(data&&data.length){for(var i=0;i<data.length;i++){if(data[i].result.toLowerCase()==q.toLowerCase()){result=data[i];break;}}}if(typeof fn=="function")fn(result);else $input.trigger("result",result&&[result.data,result.value]);}$.each(trimWords($input.val()),function(i,value){request(value,findValueCallback,findValueCallback);});}).bind("flushCache",function(){cache.flush();}).bind("setOptions",function(){$.extend(true,options,arguments[1]);if("data"in arguments[1])cache.populate();}).bind("unautocomplete",function(){select.unbind();$input.unbind();$(input.form).unbind(".autocomplete");});function selectCurrent(){var selected=select.selected();if(!selected)return false;var v=selected.result;previousValue=v;if(options.multiple){var words=trimWords($input.val());if(words.length>1){var seperator=options.multipleSeparator.length;var cursorAt=$(input).selection().start;var wordAt,progress=0;$.each(words,function(i,word){progress+=word.length;if(cursorAt<=progress){wordAt=i;return false;}progress+=seperator;});words[wordAt]=v;v=words.join(options.multipleSeparator);}v+=options.multipleSeparator;}$input.val(v);hideResultsNow();$input.trigger("result",[selected.data,selected.value]);return true;}function onChange(crap,skipPrevCheck){if(lastKeyPressCode==KEY.DEL){select.hide();return;}var currentValue=$input.val();if(!skipPrevCheck&&currentValue==previousValue)return;previousValue=currentValue;currentValue=lastWord(currentValue);if(currentValue.length>=options.minChars){$input.addClass(options.loadingClass);if(!options.matchCase)currentValue=currentValue.toLowerCase();request(currentValue,receiveData,hideResultsNow);}else{stopLoading();select.hide();}};function trimWords(value){if(!value)return[""];if(!options.multiple)return[$.trim(value)];return $.map(value.split(options.multipleSeparator),function(word){return $.trim(value).length?$.trim(word):null;});}function lastWord(value){if(!options.multiple)return value;var words=trimWords(value);if(words.length==1)return words[0];var cursorAt=$(input).selection().start;if(cursorAt==value.length){words=trimWords(value)}else{words=trimWords(value.replace(value.substring(cursorAt),""));}return words[words.length-1];}function autoFill(q,sValue){if(options.autoFill&&(lastWord($input.val()).toLowerCase()==q.toLowerCase())&&lastKeyPressCode!=KEY.BACKSPACE){$input.val($input.val()+sValue.substring(lastWord(previousValue).length));$(input).selection(previousValue.length,previousValue.length+sValue.length);}};function hideResults(){clearTimeout(timeout);timeout=setTimeout(hideResultsNow,200);};function hideResultsNow(){var wasVisible=select.visible();select.hide();clearTimeout(timeout);stopLoading();if(options.mustMatch){$input.search(function(result){if(!result){if(options.multiple){var words=trimWords($input.val()).slice(0,-1);$input.val(words.join(options.multipleSeparator)+(words.length?options.multipleSeparator:""));}else{$input.val("");$input.trigger("result",null);}}});}};function receiveData(q,data){if(data&&data.length&&hasFocus){stopLoading();select.display(data,q);autoFill(q,data[0].value);select.show();}else{hideResultsNow();}};function request(term,success,failure){if(!options.matchCase)term=term.toLowerCase();var data=cache.load(term);if(data&&data.length){success(term,data);}else if((typeof options.url=="string")&&(options.url.length>0)){var extraParams={timestamp:+new Date()};$.each(options.extraParams,function(key,param){extraParams[key]=typeof param=="function"?param():param;});$.ajax({mode:"abort",port:"autocomplete"+input.name,dataType:options.dataType,url:options.url,data:$.extend({q:lastWord(term),limit:options.max},extraParams),success:function(data){var parsed=options.parse&&options.parse(data)||parse(data);cache.add(term,parsed);success(term,parsed);}});}else{select.emptyList();failure(term);}};function parse(data){var parsed=[];var rows=data.split("\n");for(var i=0;i<rows.length;i++){var row=$.trim(rows[i]);if(row){row=row.split("|");parsed[parsed.length]={data:row,value:row[0],result:options.formatResult&&options.formatResult(row,row[0])||row[0]};}}return parsed;};function stopLoading(){$input.removeClass(options.loadingClass);};};$.Autocompleter.defaults={inputClass:"ac_input",resultsClass:"ac_results",loadingClass:"ac_loading",minChars:1,delay:400,matchCase:false,matchSubset:true,matchContains:false,cacheLength:10,max:100,mustMatch:false,extraParams:{},selectFirst:true,formatItem:function(row){return row[0];},formatMatch:null,autoFill:false,width:0,multiple:false,multipleSeparator:" ",highlight:function(value,term){return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("+term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,"\\$1")+")(?![^<>]*>)(?![^&;]+;)","gi"),"<strong>$1</strong>");},scroll:true,scrollHeight:180};$.Autocompleter.Cache=function(options){var data={};var length=0;function matchSubset(s,sub){if(!options.matchCase)s=s.toLowerCase();var i=s.indexOf(sub);if(options.matchContains=="word"){i=s.toLowerCase().search("\\b"+sub.toLowerCase());}if(i==-1)return false;return i==0||options.matchContains;};function add(q,value){if(length>options.cacheLength){flush();}if(!data[q]){length++;}data[q]=value;}function populate(){if(!options.data)return false;var stMatchSets={},nullData=0;if(!options.url)options.cacheLength=1;stMatchSets[""]=[];for(var i=0,ol=options.data.length;i<ol;i++){var rawValue=options.data[i];rawValue=(typeof rawValue=="string")?[rawValue]:rawValue;var value=options.formatMatch(rawValue,i+1,options.data.length);if(value===false)continue;var firstChar=value.charAt(0).toLowerCase();if(!stMatchSets[firstChar])stMatchSets[firstChar]=[];var row={value:value,data:rawValue,result:options.formatResult&&options.formatResult(rawValue)||value};stMatchSets[firstChar].push(row);if(nullData++<options.max){stMatchSets[""].push(row);}};$.each(stMatchSets,function(i,value){options.cacheLength++;add(i,value);});}setTimeout(populate,25);function flush(){data={};length=0;}return{flush:flush,add:add,populate:populate,load:function(q){if(!options.cacheLength||!length)return null;if(!options.url&&options.matchContains){var csub=[];for(var k in data){if(k.length>0){var c=data[k];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub.push(x);}});}}return csub;}else
-if(data[q]){return data[q];}else
-if(options.matchSubset){for(var i=q.length-1;i>=options.minChars;i--){var c=data[q.substr(0,i)];if(c){var csub=[];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub[csub.length]=x;}});return csub;}}}return null;}};};$.Autocompleter.Select=function(options,input,select,config){var CLASSES={ACTIVE:"ac_over"};var listItems,active=-1,data,term="",needsInit=true,element,list;function init(){if(!needsInit)return;element=$("<div/>").hide().addClass(options.resultsClass).css("position","absolute").appendTo(document.body);list=$("<ul/>").appendTo(element).mouseover(function(event){if(target(event).nodeName&&target(event).nodeName.toUpperCase()=='LI'){active=$("li",list).removeClass(CLASSES.ACTIVE).index(target(event));$(target(event)).addClass(CLASSES.ACTIVE);}}).click(function(event){$(target(event)).addClass(CLASSES.ACTIVE);select();input.focus();return false;}).mousedown(function(){config.mouseDownOnSelect=true;}).mouseup(function(){config.mouseDownOnSelect=false;});if(options.width>0)element.css("width",options.width);needsInit=false;}function target(event){var element=event.target;while(element&&element.tagName!="LI")element=element.parentNode;if(!element)return[];return element;}function moveSelect(step){listItems.slice(active,active+1).removeClass(CLASSES.ACTIVE);movePosition(step);var activeItem=listItems.slice(active,active+1).addClass(CLASSES.ACTIVE);if(options.scroll){var offset=0;listItems.slice(0,active).each(function(){offset+=this.offsetHeight;});if((offset+activeItem[0].offsetHeight-list.scrollTop())>list[0].clientHeight){list.scrollTop(offset+activeItem[0].offsetHeight-list.innerHeight());}else if(offset<list.scrollTop()){list.scrollTop(offset);}}};function movePosition(step){active+=step;if(active<0){active=listItems.size()-1;}else if(active>=listItems.size()){active=0;}}function limitNumberOfItems(available){return options.max&&options.max<available?options.max:available;}function fillList(){list.empty();var max=limitNumberOfItems(data.length);for(var i=0;i<max;i++){if(!data[i])continue;var formatted=options.formatItem(data[i].data,i+1,max,data[i].value,term);if(formatted===false)continue;var li=$("<li/>").html(options.highlight(formatted,term)).addClass(i%2==0?"ac_even":"ac_odd").appendTo(list)[0];$.data(li,"ac_data",data[i]);}listItems=list.find("li");if(options.selectFirst){listItems.slice(0,1).addClass(CLASSES.ACTIVE);active=0;}if($.fn.bgiframe)list.bgiframe();}return{display:function(d,q){init();data=d;term=q;fillList();},next:function(){moveSelect(1);},prev:function(){moveSelect(-1);},pageUp:function(){if(active!=0&&active-8<0){moveSelect(-active);}else{moveSelect(-8);}},pageDown:function(){if(active!=listItems.size()-1&&active+8>listItems.size()){moveSelect(listItems.size()-1-active);}else{moveSelect(8);}},hide:function(){element&&element.hide();listItems&&listItems.removeClass(CLASSES.ACTIVE);active=-1;},visible:function(){return element&&element.is(":visible");},current:function(){return this.visible()&&(listItems.filter("."+CLASSES.ACTIVE)[0]||options.selectFirst&&listItems[0]);},show:function(){var offset=$(input).offset();element.css({width:typeof options.width=="string"||options.width>0?options.width:$(input).width(),top:offset.top+input.offsetHeight,left:offset.left}).show();if(options.scroll){list.scrollTop(0);list.css({maxHeight:options.scrollHeight,overflow:'auto'});if($.browser.msie&&typeof document.body.style.maxHeight==="undefined"){var listHeight=0;listItems.each(function(){listHeight+=this.offsetHeight;});var scrollbarsVisible=listHeight>options.scrollHeight;list.css('height',scrollbarsVisible?options.scrollHeight:listHeight);if(!scrollbarsVisible){listItems.width(list.width()-parseInt(listItems.css("padding-left"))-parseInt(listItems.css("padding-right")));}}}},selected:function(){var selected=listItems&&listItems.filter("."+CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);return selected&&selected.length&&$.data(selected[0],"ac_data");},emptyList:function(){list&&list.empty();},unbind:function(){element&&element.remove();}};};$.fn.selection=function(start,end){if(start!==undefined){return this.each(function(){if(this.createTextRange){var selRange=this.createTextRange();if(end===undefined||start==end){selRange.move("character",start);selRange.select();}else{selRange.collapse(true);selRange.moveStart("character",start);selRange.moveEnd("character",end);selRange.select();}}else if(this.setSelectionRange){this.setSelectionRange(start,end);}else if(this.selectionStart){this.selectionStart=start;this.selectionEnd=end;}});}var field=this[0];if(field.createTextRange){var range=document.selection.createRange(),orig=field.value,teststring="<->",textLength=range.text.length;range.text=teststring;var caretAt=field.value.indexOf(teststring);field.value=orig;this.selection(caretAt,caretAt+textLength);return{start:caretAt,end:caretAt+textLength}}else if(field.selectionStart!==undefined){return{start:field.selectionStart,end:field.selectionEnd}}};})(jQuery);
\ No newline at end of file
diff --git a/data/js/tableClick.js b/data/js/tableClick.js
index 8cab4aa69853e3111394bddc8a7840f6dfe0beae..25c9de34338b30c64efdddc26dd710e0dfd30e15 100644
--- a/data/js/tableClick.js
+++ b/data/js/tableClick.js
@@ -1,13 +1,12 @@
 $(document).ready(function(){
 
-   $("table.sickbeardTable td").hover( 
-	       function() { $(this).find("a").parent().addClass("hover"); }, 
-	       function() { $(this).find("a").parent().removeClass("hover");
-   } );
-
-   $("table.sickbeardTable td").click( function() {
-        var href = $(this).find("a").attr("href");
-        if(href) { window.location = href; }
-   });
+    $("table.sickbeardTable td.tvShow").live('click', function(e) {
+        if( (!$.browser.msie && e.button == 0) || ($.browser.msie && e.button == 1) ) {
+            if(!e.shiftKey) {
+                var href = $(this).find("a").attr("href");
+                if(href) { window.location = href; }
+            }
+        }
+    });
 
 });
diff --git a/init.fedora b/init.fedora
index d763c373f58c58f7e2f37612d49448e60c6f170b..53da732e4e019ec43632253e5169063fd0264f1e 100755
--- a/init.fedora
+++ b/init.fedora
@@ -23,12 +23,13 @@ lockfile=/var/lock/subsys/$prog
 ## the defaults
 username=${SB_USER-sickbeard}
 homedir=${SB_HOME-/opt/sickbeard}
+datadir=${SB_DATA-~/.sickbeard}
 pidfile=${SB_PIDFILE-/var/run/sickbeard/sickbeard.pid}
 nice=${SB_NICE-}
 ##
 
 pidpath=`dirname ${pidfile}`
-options=" --daemon --pidfile=${pidfile}"
+options=" --daemon --pidfile=${pidfile} --datadir=${datadir}"
 
 # create PID directory if not exist and ensure the SickBeard user can write to it
 if [ ! -d $pidpath ]; then
@@ -36,6 +37,11 @@ if [ ! -d $pidpath ]; then
 	chown $username $pidpath
 fi
 
+if [ ! -d $datadir ]; then
+	mkdir -p $datadir
+	chown $username $datadir
+fi
+
 start() {
         # Start daemon.
         echo -n $"Starting $prog: "
diff --git a/init.freebsd b/init.freebsd
new file mode 100755
index 0000000000000000000000000000000000000000..e6edc880810459f8120be80021985c058187ed7d
--- /dev/null
+++ b/init.freebsd
@@ -0,0 +1,83 @@
+#!/bin/sh
+#
+# PROVIDE: sickbeard
+# REQUIRE: sabnzbd
+# KEYWORD: shutdown
+#
+# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
+# to enable this service:
+#
+# sickbeard_enable (bool):	Set to NO by default.
+#			Set it to YES to enable it.
+# sickbeard_user:  The user account Sick Beard daemon runs as what
+#			you want it to be. It uses '_sabnzbd' user by
+#			default. Do not sets it as empty or it will run
+#			as root.
+# sickbeard_dir:	Directory where Sick Beard lives.
+#			Default: /usr/local/sickbeard
+# sickbeard_chdir:  Change to this directory before running Sick Beard.
+#     Default is same as sickbeard_dir.
+# sickbeard_pid:  The name of the pidfile to create.
+#     Default is sickbeard.pid in sickbeard_dir.
+
+. /etc/rc.subr
+
+name="sickbeard"
+rcvar=${name}_enable
+
+load_rc_config ${name}
+
+: ${sickbeard_enable:="NO"}
+: ${sickbeard_user:="_sabnzbd"}
+: ${sickbeard_dir:="/usr/local/sickbeard"}
+: ${sickbeard_chdir:="${sickbeard_dir}"}
+: ${sickbeard_pid:="${sickbeard_dir}/sickbeard.pid"}
+
+WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown Sick Beard.
+HOST="127.0.0.1" # Set Sick Beard address here.
+PORT="8081" # Set Sick Beard port here.
+SBUSR="" # Set Sick Beard username (if you use one) here.
+SBPWD="" # Set Sick Beard password (if you use one) here.
+
+status_cmd="${name}_status"
+stop_cmd="${name}_stop"
+
+command="/usr/sbin/daemon"
+command_args="-f -p ${sickbeard_pid} python ${sickbeard_dir}/SickBeard.py ${sickbeard_flags} --quiet"
+
+# Check for wget and refuse to start without it.
+if [ ! -x "${WGET}" ]; then
+  warn "Sickbeard not started: You need wget to safely shut down Sick Beard."
+  exit 1
+fi
+
+# Ensure user is root when running this script.
+if [ `id -u` != "0" ]; then
+  echo "Oops, you should be root before running this!"
+  exit 1
+fi
+
+verify_sickbeard_pid() {
+    # Make sure the pid corresponds to the Sick Beard process.
+    pid=`cat ${sickbeard_pid} 2>/dev/null`
+    ps -p ${pid} | grep -q "python ${sickbeard_dir}/SickBeard.py"
+    return $?
+}
+
+# Try to stop Sick Beard cleanly by calling shutdown over http.
+sickbeard_stop() {
+    echo "Stopping $name"
+    verify_sickbeard_pid
+    ${WGET} -O - -q --user=${SBUSR} --password=${SBPWD} "http://${HOST}:${PORT}/home/shutdown/" >/dev/null
+    if [ -n "${pid}" ]; then
+      wait_for_pids ${pid}
+      echo "Stopped"
+    fi
+}
+
+sickbeard_status() {
+    verify_sickbeard_pid && echo "$name is running as ${pid}" || echo "$name is not running"
+}
+
+run_rc_command "$1"
+
diff --git a/init.ubuntu b/init.ubuntu
index 6509898c04075f82b3f45777978ece232ec26398..d1560d5383f537c4ffd088331cf2e173b803840f 100755
--- a/init.ubuntu
+++ b/init.ubuntu
@@ -21,9 +21,6 @@ DAEMON=/usr/bin/python
 PID_FILE=/var/run/sickbeard/sickbeard.pid
 PID_PATH=`dirname $PID_FILE`
 
-# startup args
-DAEMON_OPTS=" SickBeard.py -q --daemon --pidfile=${PID_FILE}"
-
 # script name
 NAME=sickbeard
 
@@ -33,6 +30,12 @@ DESC=SickBeard
 # user
 RUN_AS=SICKBEARD_USER
 
+# data directory
+DATA_DIR=~/.sickbeard
+
+# startup args
+DAEMON_OPTS=" SickBeard.py -q --daemon --pidfile=${PID_FILE} --datadir=${DATA_DIR}"
+
 ############### END EDIT ME ##################
 
 test -x $DAEMON || exit 0
@@ -40,10 +43,16 @@ test -x $DAEMON || exit 0
 set -e
 
 if [ ! -d $PID_PATH ]; then
-        mkdir -p $PID_PATH
-        chown $RUN_AS $PID_PATH
+    mkdir -p $PID_PATH
+    chown $RUN_AS $PID_PATH
+fi
+
+if [ ! -d $DATA_DIR ]; then
+    mkdir -p $DATA_DIR
+    chown $RUN_AS $DATA_DIR
 fi
 
+
 case "$1" in
   start)
         echo "Starting $DESC"
@@ -51,13 +60,12 @@ case "$1" in
         ;;
   stop)
         echo "Stopping $DESC"
-        start-stop-daemon --stop --pidfile $PID_FILE
+        start-stop-daemon --stop --pidfile $PID_FILE --retry 15
         ;;
 
   restart|force-reload)
         echo "Restarting $DESC"
-        start-stop-daemon --stop --pidfile $PID_FILE
-        sleep 15
+        start-stop-daemon --stop --pidfile $PID_FILE --retry 15
         start-stop-daemon -d $APP_PATH -c $RUN_AS --start --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
         ;;
   *)
diff --git a/lib/dateutil/__init__.py b/lib/dateutil/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..290814cf9ca518f66bc23d1219c63772e22dbd40
--- /dev/null
+++ b/lib/dateutil/__init__.py
@@ -0,0 +1,9 @@
+"""
+Copyright (c) 2003-2010  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+__version__ = "1.5"
diff --git a/lib/dateutil/easter.py b/lib/dateutil/easter.py
new file mode 100644
index 0000000000000000000000000000000000000000..d7944104beb186bceda1584c98d431a7e2624371
--- /dev/null
+++ b/lib/dateutil/easter.py
@@ -0,0 +1,92 @@
+"""
+Copyright (c) 2003-2007  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import datetime
+
+__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
+
+EASTER_JULIAN   = 1
+EASTER_ORTHODOX = 2
+EASTER_WESTERN  = 3
+
+def easter(year, method=EASTER_WESTERN):
+    """
+    This method was ported from the work done by GM Arts,
+    on top of the algorithm by Claus Tondering, which was
+    based in part on the algorithm of Ouding (1940), as
+    quoted in "Explanatory Supplement to the Astronomical
+    Almanac", P.  Kenneth Seidelmann, editor.
+
+    This algorithm implements three different easter
+    calculation methods:
+    
+    1 - Original calculation in Julian calendar, valid in
+        dates after 326 AD
+    2 - Original method, with date converted to Gregorian
+        calendar, valid in years 1583 to 4099
+    3 - Revised method, in Gregorian calendar, valid in
+        years 1583 to 4099 as well
+
+    These methods are represented by the constants:
+
+    EASTER_JULIAN   = 1
+    EASTER_ORTHODOX = 2
+    EASTER_WESTERN  = 3
+
+    The default method is method 3.
+    
+    More about the algorithm may be found at:
+
+    http://users.chariot.net.au/~gmarts/eastalg.htm
+
+    and
+
+    http://www.tondering.dk/claus/calendar.html
+
+    """
+
+    if not (1 <= method <= 3):
+        raise ValueError, "invalid method"
+
+    # g - Golden year - 1
+    # c - Century
+    # h - (23 - Epact) mod 30
+    # i - Number of days from March 21 to Paschal Full Moon
+    # j - Weekday for PFM (0=Sunday, etc)
+    # p - Number of days from March 21 to Sunday on or before PFM
+    #     (-6 to 28 methods 1 & 3, to 56 for method 2)
+    # e - Extra days to add for method 2 (converting Julian
+    #     date to Gregorian date)
+
+    y = year
+    g = y % 19
+    e = 0
+    if method < 3:
+        # Old method
+        i = (19*g+15)%30
+        j = (y+y//4+i)%7
+        if method == 2:
+            # Extra dates to convert Julian to Gregorian date
+            e = 10
+            if y > 1600:
+                e = e+y//100-16-(y//100-16)//4
+    else:
+        # New method
+        c = y//100
+        h = (c-c//4-(8*c+13)//25+19*g+15)%30
+        i = h-(h//28)*(1-(h//28)*(29//(h+1))*((21-g)//11))
+        j = (y+y//4+i+2-c+c//4)%7
+
+    # p can be from -6 to 56 corresponding to dates 22 March to 23 May
+    # (later dates apply to method 2, although 23 May never actually occurs)
+    p = i-j+e
+    d = 1+(p+27+(p+6)//40)%31
+    m = 3+(p+26)//30
+    return datetime.date(int(y),int(m),int(d))
+
diff --git a/lib/dateutil/parser.py b/lib/dateutil/parser.py
new file mode 100644
index 0000000000000000000000000000000000000000..5d824e411f32949c95de0a512bfa061a12c68bc1
--- /dev/null
+++ b/lib/dateutil/parser.py
@@ -0,0 +1,886 @@
+# -*- coding:iso-8859-1 -*-
+"""
+Copyright (c) 2003-2007  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import datetime
+import string
+import time
+import sys
+import os
+
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+import relativedelta
+import tz
+
+
+__all__ = ["parse", "parserinfo"]
+
+
+# Some pointers:
+#
+# http://www.cl.cam.ac.uk/~mgk25/iso-time.html
+# http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html
+# http://www.w3.org/TR/NOTE-datetime
+# http://ringmaster.arc.nasa.gov/tools/time_formats.html
+# http://search.cpan.org/author/MUIR/Time-modules-2003.0211/lib/Time/ParseDate.pm
+# http://stein.cshl.org/jade/distrib/docs/java.text.SimpleDateFormat.html
+
+
+class _timelex(object):
+
+    def __init__(self, instream):
+        if isinstance(instream, basestring):
+            instream = StringIO(instream)
+        self.instream = instream
+        self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
+                          'ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
+                          '��������������������������������'
+                          '������������������������������')
+        self.numchars = '0123456789'
+        self.whitespace = ' \t\r\n'
+        self.charstack = []
+        self.tokenstack = []
+        self.eof = False
+
+    def get_token(self):
+        if self.tokenstack:
+            return self.tokenstack.pop(0)
+        seenletters = False
+        token = None
+        state = None
+        wordchars = self.wordchars
+        numchars = self.numchars
+        whitespace = self.whitespace
+        while not self.eof:
+            if self.charstack:
+                nextchar = self.charstack.pop(0)
+            else:
+                nextchar = self.instream.read(1)
+                while nextchar == '\x00':
+                    nextchar = self.instream.read(1)
+            if not nextchar:
+                self.eof = True
+                break
+            elif not state:
+                token = nextchar
+                if nextchar in wordchars:
+                    state = 'a'
+                elif nextchar in numchars:
+                    state = '0'
+                elif nextchar in whitespace:
+                    token = ' '
+                    break # emit token
+                else:
+                    break # emit token
+            elif state == 'a':
+                seenletters = True
+                if nextchar in wordchars:
+                    token += nextchar
+                elif nextchar == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+            elif state == '0':
+                if nextchar in numchars:
+                    token += nextchar
+                elif nextchar == '.':
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+            elif state == 'a.':
+                seenletters = True
+                if nextchar == '.' or nextchar in wordchars:
+                    token += nextchar
+                elif nextchar in numchars and token[-1] == '.':
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+            elif state == '0.':
+                if nextchar == '.' or nextchar in numchars:
+                    token += nextchar
+                elif nextchar in wordchars and token[-1] == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+        if (state in ('a.', '0.') and
+            (seenletters or token.count('.') > 1 or token[-1] == '.')):
+            l = token.split('.')
+            token = l[0]
+            for tok in l[1:]:
+                self.tokenstack.append('.')
+                if tok:
+                    self.tokenstack.append(tok)
+        return token
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        token = self.get_token()
+        if token is None:
+            raise StopIteration
+        return token
+
+    def split(cls, s):
+        return list(cls(s))
+    split = classmethod(split)
+
+
+class _resultbase(object):
+
+    def __init__(self):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+
+    def _repr(self, classname):
+        l = []
+        for attr in self.__slots__:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, `value`))
+        return "%s(%s)" % (classname, ", ".join(l))
+
+    def __repr__(self):
+        return self._repr(self.__class__.__name__)
+
+
+class parserinfo(object):
+
+    # m from a.m/p.m, t from ISO T separator
+    JUMP = [" ", ".", ",", ";", "-", "/", "'",
+            "at", "on", "and", "ad", "m", "t", "of",
+            "st", "nd", "rd", "th"] 
+
+    WEEKDAYS = [("Mon", "Monday"),
+                ("Tue", "Tuesday"),
+                ("Wed", "Wednesday"),
+                ("Thu", "Thursday"),
+                ("Fri", "Friday"),
+                ("Sat", "Saturday"),
+                ("Sun", "Sunday")]
+    MONTHS   = [("Jan", "January"),
+                ("Feb", "February"),
+                ("Mar", "March"),
+                ("Apr", "April"),
+                ("May", "May"),
+                ("Jun", "June"),
+                ("Jul", "July"),
+                ("Aug", "August"),
+                ("Sep", "September"),
+                ("Oct", "October"),
+                ("Nov", "November"),
+                ("Dec", "December")]
+    HMS = [("h", "hour", "hours"),
+           ("m", "minute", "minutes"),
+           ("s", "second", "seconds")]
+    AMPM = [("am", "a"),
+            ("pm", "p")]
+    UTCZONE = ["UTC", "GMT", "Z"]
+    PERTAIN = ["of"]
+    TZOFFSET = {}
+
+    def __init__(self, dayfirst=False, yearfirst=False):
+        self._jump = self._convert(self.JUMP)
+        self._weekdays = self._convert(self.WEEKDAYS)
+        self._months = self._convert(self.MONTHS)
+        self._hms = self._convert(self.HMS)
+        self._ampm = self._convert(self.AMPM)
+        self._utczone = self._convert(self.UTCZONE)
+        self._pertain = self._convert(self.PERTAIN)
+
+        self.dayfirst = dayfirst
+        self.yearfirst = yearfirst
+
+        self._year = time.localtime().tm_year
+        self._century = self._year//100*100
+
+    def _convert(self, lst):
+        dct = {}
+        for i in range(len(lst)):
+            v = lst[i]
+            if isinstance(v, tuple):
+                for v in v:
+                    dct[v.lower()] = i
+            else:
+                dct[v.lower()] = i
+        return dct
+
+    def jump(self, name):
+        return name.lower() in self._jump
+
+    def weekday(self, name):
+        if len(name) >= 3:
+            try:
+                return self._weekdays[name.lower()]
+            except KeyError:
+                pass
+        return None
+
+    def month(self, name):
+        if len(name) >= 3:
+            try:
+                return self._months[name.lower()]+1
+            except KeyError:
+                pass
+        return None
+
+    def hms(self, name):
+        try:
+            return self._hms[name.lower()]
+        except KeyError:
+            return None
+
+    def ampm(self, name):
+        try:
+            return self._ampm[name.lower()]
+        except KeyError:
+            return None
+
+    def pertain(self, name):
+        return name.lower() in self._pertain
+
+    def utczone(self, name):
+        return name.lower() in self._utczone
+
+    def tzoffset(self, name):
+        if name in self._utczone:
+            return 0
+        return self.TZOFFSET.get(name)
+
+    def convertyear(self, year):
+        if year < 100:
+            year += self._century
+            if abs(year-self._year) >= 50:
+                if year < self._year:
+                    year += 100
+                else:
+                    year -= 100
+        return year
+
+    def validate(self, res):
+        # move to info
+        if res.year is not None:
+            res.year = self.convertyear(res.year)
+        if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z':
+            res.tzname = "UTC"
+            res.tzoffset = 0
+        elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
+            res.tzoffset = 0
+        return True
+
+
+class parser(object):
+
+    def __init__(self, info=None):
+        self.info = info or parserinfo()
+
+    def parse(self, timestr, default=None,
+                    ignoretz=False, tzinfos=None,
+                    **kwargs):
+        if not default:
+            default = datetime.datetime.now().replace(hour=0, minute=0,
+                                                      second=0, microsecond=0)
+        res = self._parse(timestr, **kwargs)
+        if res is None:
+            raise ValueError, "unknown string format"
+        repl = {}
+        for attr in ["year", "month", "day", "hour",
+                     "minute", "second", "microsecond"]:
+            value = getattr(res, attr)
+            if value is not None:
+                repl[attr] = value
+        ret = default.replace(**repl)
+        if res.weekday is not None and not res.day:
+            ret = ret+relativedelta.relativedelta(weekday=res.weekday)
+        if not ignoretz:
+            if callable(tzinfos) or tzinfos and res.tzname in tzinfos:
+                if callable(tzinfos):
+                    tzdata = tzinfos(res.tzname, res.tzoffset)
+                else:
+                    tzdata = tzinfos.get(res.tzname)
+                if isinstance(tzdata, datetime.tzinfo):
+                    tzinfo = tzdata
+                elif isinstance(tzdata, basestring):
+                    tzinfo = tz.tzstr(tzdata)
+                elif isinstance(tzdata, int):
+                    tzinfo = tz.tzoffset(res.tzname, tzdata)
+                else:
+                    raise ValueError, "offset must be tzinfo subclass, " \
+                                      "tz string, or int offset"
+                ret = ret.replace(tzinfo=tzinfo)
+            elif res.tzname and res.tzname in time.tzname:
+                ret = ret.replace(tzinfo=tz.tzlocal())
+            elif res.tzoffset == 0:
+                ret = ret.replace(tzinfo=tz.tzutc())
+            elif res.tzoffset:
+                ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
+        return ret
+
+    class _result(_resultbase):
+        __slots__ = ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond",
+                     "tzname", "tzoffset"]
+
+    def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False):
+        info = self.info
+        if dayfirst is None:
+            dayfirst = info.dayfirst
+        if yearfirst is None:
+            yearfirst = info.yearfirst
+        res = self._result()
+        l = _timelex.split(timestr)
+        try:
+
+            # year/month/day list
+            ymd = []
+
+            # Index of the month string in ymd
+            mstridx = -1
+
+            len_l = len(l)
+            i = 0
+            while i < len_l:
+
+                # Check if it's a number
+                try:
+                    value_repr = l[i]
+                    value = float(value_repr)
+                except ValueError:
+                    value = None
+
+                if value is not None:
+                    # Token is a number
+                    len_li = len(l[i])
+                    i += 1
+                    if (len(ymd) == 3 and len_li in (2, 4)
+                        and (i >= len_l or (l[i] != ':' and
+                                            info.hms(l[i]) is None))):
+                        # 19990101T23[59]
+                        s = l[i-1]
+                        res.hour = int(s[:2])
+                        if len_li == 4:
+                            res.minute = int(s[2:])
+                    elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6):
+                        # YYMMDD or HHMMSS[.ss]
+                        s = l[i-1] 
+                        if not ymd and l[i-1].find('.') == -1:
+                            ymd.append(info.convertyear(int(s[:2])))
+                            ymd.append(int(s[2:4]))
+                            ymd.append(int(s[4:]))
+                        else:
+                            # 19990101T235959[.59]
+                            res.hour = int(s[:2])
+                            res.minute = int(s[2:4])
+                            res.second, res.microsecond = _parsems(s[4:])
+                    elif len_li == 8:
+                        # YYYYMMDD
+                        s = l[i-1]
+                        ymd.append(int(s[:4]))
+                        ymd.append(int(s[4:6]))
+                        ymd.append(int(s[6:]))
+                    elif len_li in (12, 14):
+                        # YYYYMMDDhhmm[ss]
+                        s = l[i-1]
+                        ymd.append(int(s[:4]))
+                        ymd.append(int(s[4:6]))
+                        ymd.append(int(s[6:8]))
+                        res.hour = int(s[8:10])
+                        res.minute = int(s[10:12])
+                        if len_li == 14:
+                            res.second = int(s[12:])
+                    elif ((i < len_l and info.hms(l[i]) is not None) or
+                          (i+1 < len_l and l[i] == ' ' and
+                           info.hms(l[i+1]) is not None)):
+                        # HH[ ]h or MM[ ]m or SS[.ss][ ]s
+                        if l[i] == ' ':
+                            i += 1
+                        idx = info.hms(l[i])
+                        while True:
+                            if idx == 0:
+                                res.hour = int(value)
+                                if value%1:
+                                    res.minute = int(60*(value%1))
+                            elif idx == 1:
+                                res.minute = int(value)
+                                if value%1:
+                                    res.second = int(60*(value%1))
+                            elif idx == 2:
+                                res.second, res.microsecond = \
+                                    _parsems(value_repr)
+                            i += 1
+                            if i >= len_l or idx == 2:
+                                break
+                            # 12h00
+                            try:
+                                value_repr = l[i]
+                                value = float(value_repr)
+                            except ValueError:
+                                break
+                            else:
+                                i += 1
+                                idx += 1
+                                if i < len_l:
+                                    newidx = info.hms(l[i])
+                                    if newidx is not None:
+                                        idx = newidx
+                    elif i+1 < len_l and l[i] == ':':
+                        # HH:MM[:SS[.ss]]
+                        res.hour = int(value)
+                        i += 1
+                        value = float(l[i])
+                        res.minute = int(value)
+                        if value%1:
+                            res.second = int(60*(value%1))
+                        i += 1
+                        if i < len_l and l[i] == ':':
+                            res.second, res.microsecond = _parsems(l[i+1])
+                            i += 2
+                    elif i < len_l and l[i] in ('-', '/', '.'):
+                        sep = l[i]
+                        ymd.append(int(value))
+                        i += 1
+                        if i < len_l and not info.jump(l[i]):
+                            try:
+                                # 01-01[-01]
+                                ymd.append(int(l[i]))
+                            except ValueError:
+                                # 01-Jan[-01]
+                                value = info.month(l[i])
+                                if value is not None:
+                                    ymd.append(value)
+                                    assert mstridx == -1
+                                    mstridx = len(ymd)-1
+                                else:
+                                    return None
+                            i += 1
+                            if i < len_l and l[i] == sep:
+                                # We have three members
+                                i += 1
+                                value = info.month(l[i])
+                                if value is not None:
+                                    ymd.append(value)
+                                    mstridx = len(ymd)-1
+                                    assert mstridx == -1
+                                else:
+                                    ymd.append(int(l[i]))
+                                i += 1
+                    elif i >= len_l or info.jump(l[i]):
+                        if i+1 < len_l and info.ampm(l[i+1]) is not None:
+                            # 12 am
+                            res.hour = int(value)
+                            if res.hour < 12 and info.ampm(l[i+1]) == 1:
+                                res.hour += 12
+                            elif res.hour == 12 and info.ampm(l[i+1]) == 0:
+                                res.hour = 0
+                            i += 1
+                        else:
+                            # Year, month or day
+                            ymd.append(int(value))
+                        i += 1
+                    elif info.ampm(l[i]) is not None:
+                        # 12am
+                        res.hour = int(value)
+                        if res.hour < 12 and info.ampm(l[i]) == 1:
+                            res.hour += 12
+                        elif res.hour == 12 and info.ampm(l[i]) == 0:
+                            res.hour = 0
+                        i += 1
+                    elif not fuzzy:
+                        return None
+                    else:
+                        i += 1
+                    continue
+
+                # Check weekday
+                value = info.weekday(l[i])
+                if value is not None:
+                    res.weekday = value
+                    i += 1
+                    continue
+
+                # Check month name
+                value = info.month(l[i])
+                if value is not None:
+                    ymd.append(value)
+                    assert mstridx == -1
+                    mstridx = len(ymd)-1
+                    i += 1
+                    if i < len_l:
+                        if l[i] in ('-', '/'):
+                            # Jan-01[-99]
+                            sep = l[i]
+                            i += 1
+                            ymd.append(int(l[i]))
+                            i += 1
+                            if i < len_l and l[i] == sep:
+                                # Jan-01-99
+                                i += 1
+                                ymd.append(int(l[i]))
+                                i += 1
+                        elif (i+3 < len_l and l[i] == l[i+2] == ' '
+                              and info.pertain(l[i+1])):
+                            # Jan of 01
+                            # In this case, 01 is clearly year
+                            try:
+                                value = int(l[i+3])
+                            except ValueError:
+                                # Wrong guess
+                                pass
+                            else:
+                                # Convert it here to become unambiguous
+                                ymd.append(info.convertyear(value))
+                            i += 4
+                    continue
+
+                # Check am/pm
+                value = info.ampm(l[i])
+                if value is not None:
+                    if value == 1 and res.hour < 12:
+                        res.hour += 12
+                    elif value == 0 and res.hour == 12:
+                        res.hour = 0
+                    i += 1
+                    continue
+
+                # Check for a timezone name
+                if (res.hour is not None and len(l[i]) <= 5 and
+                    res.tzname is None and res.tzoffset is None and
+                    not [x for x in l[i] if x not in string.ascii_uppercase]):
+                    res.tzname = l[i]
+                    res.tzoffset = info.tzoffset(res.tzname)
+                    i += 1
+
+                    # Check for something like GMT+3, or BRST+3. Notice
+                    # that it doesn't mean "I am 3 hours after GMT", but
+                    # "my time +3 is GMT". If found, we reverse the
+                    # logic so that timezone parsing code will get it
+                    # right.
+                    if i < len_l and l[i] in ('+', '-'):
+                        l[i] = ('+', '-')[l[i] == '+']
+                        res.tzoffset = None
+                        if info.utczone(res.tzname):
+                            # With something like GMT+3, the timezone
+                            # is *not* GMT.
+                            res.tzname = None
+
+                    continue
+
+                # Check for a numbered timezone
+                if res.hour is not None and l[i] in ('+', '-'):
+                    signal = (-1,1)[l[i] == '+']
+                    i += 1
+                    len_li = len(l[i])
+                    if len_li == 4:
+                        # -0300
+                        res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60
+                    elif i+1 < len_l and l[i+1] == ':':
+                        # -03:00
+                        res.tzoffset = int(l[i])*3600+int(l[i+2])*60
+                        i += 2
+                    elif len_li <= 2:
+                        # -[0]3
+                        res.tzoffset = int(l[i][:2])*3600
+                    else:
+                        return None
+                    i += 1
+                    res.tzoffset *= signal
+
+                    # Look for a timezone name between parenthesis
+                    if (i+3 < len_l and
+                        info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and
+                        3 <= len(l[i+2]) <= 5 and
+                        not [x for x in l[i+2]
+                                if x not in string.ascii_uppercase]):
+                        # -0300 (BRST)
+                        res.tzname = l[i+2]
+                        i += 4
+                    continue
+
+                # Check jumps
+                if not (info.jump(l[i]) or fuzzy):
+                    return None
+
+                i += 1
+
+            # Process year/month/day
+            len_ymd = len(ymd)
+            if len_ymd > 3:
+                # More than three members!?
+                return None
+            elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2):
+                # One member, or two members with a month string
+                if mstridx != -1:
+                    res.month = ymd[mstridx]
+                    del ymd[mstridx]
+                if len_ymd > 1 or mstridx == -1:
+                    if ymd[0] > 31:
+                        res.year = ymd[0]
+                    else:
+                        res.day = ymd[0]
+            elif len_ymd == 2:
+                # Two members with numbers
+                if ymd[0] > 31:
+                    # 99-01
+                    res.year, res.month = ymd
+                elif ymd[1] > 31:
+                    # 01-99
+                    res.month, res.year = ymd
+                elif dayfirst and ymd[1] <= 12:
+                    # 13-01
+                    res.day, res.month = ymd
+                else:
+                    # 01-13
+                    res.month, res.day = ymd
+            if len_ymd == 3:
+                # Three members
+                if mstridx == 0:
+                    res.month, res.day, res.year = ymd
+                elif mstridx == 1:
+                    if ymd[0] > 31 or (yearfirst and ymd[2] <= 31):
+                        # 99-Jan-01
+                        res.year, res.month, res.day = ymd
+                    else:
+                        # 01-Jan-01
+                        # Give precendence to day-first, since
+                        # two-digit years is usually hand-written.
+                        res.day, res.month, res.year = ymd
+                elif mstridx == 2:
+                    # WTF!?
+                    if ymd[1] > 31:
+                        # 01-99-Jan
+                        res.day, res.year, res.month = ymd
+                    else:
+                        # 99-01-Jan
+                        res.year, res.day, res.month = ymd
+                else:
+                    if ymd[0] > 31 or \
+                       (yearfirst and ymd[1] <= 12 and ymd[2] <= 31):
+                        # 99-01-01
+                        res.year, res.month, res.day = ymd
+                    elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12):
+                        # 13-01-01
+                        res.day, res.month, res.year = ymd
+                    else:
+                        # 01-13-01
+                        res.month, res.day, res.year = ymd
+
+        except (IndexError, ValueError, AssertionError):
+            return None
+
+        if not info.validate(res):
+            return None
+        return res
+
+DEFAULTPARSER = parser()
+def parse(timestr, parserinfo=None, **kwargs):
+    if parserinfo:
+        return parser(parserinfo).parse(timestr, **kwargs)
+    else:
+        return DEFAULTPARSER.parse(timestr, **kwargs)
+
+
+class _tzparser(object):
+
+    class _result(_resultbase):
+
+        __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
+                     "start", "end"]
+
+        class _attr(_resultbase):
+            __slots__ = ["month", "week", "weekday",
+                         "yday", "jyday", "day", "time"]
+
+        def __repr__(self):
+            return self._repr("")
+
+        def __init__(self):
+            _resultbase.__init__(self)
+            self.start = self._attr()
+            self.end = self._attr()
+
+    def parse(self, tzstr):
+        res = self._result()
+        l = _timelex.split(tzstr)
+        try:
+
+            len_l = len(l)
+
+            i = 0
+            while i < len_l:
+                # BRST+3[BRDT[+2]]
+                j = i
+                while j < len_l and not [x for x in l[j]
+                                            if x in "0123456789:,-+"]:
+                    j += 1
+                if j != i:
+                    if not res.stdabbr:
+                        offattr = "stdoffset"
+                        res.stdabbr = "".join(l[i:j])
+                    else:
+                        offattr = "dstoffset"
+                        res.dstabbr = "".join(l[i:j])
+                    i = j
+                    if (i < len_l and
+                        (l[i] in ('+', '-') or l[i][0] in "0123456789")):
+                        if l[i] in ('+', '-'):
+                            # Yes, that's right.  See the TZ variable
+                            # documentation.
+                            signal = (1,-1)[l[i] == '+']
+                            i += 1
+                        else:
+                            signal = -1
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            setattr(res, offattr,
+                                    (int(l[i][:2])*3600+int(l[i][2:])*60)*signal)
+                        elif i+1 < len_l and l[i+1] == ':':
+                            # -03:00
+                            setattr(res, offattr,
+                                    (int(l[i])*3600+int(l[i+2])*60)*signal)
+                            i += 2
+                        elif len_li <= 2:
+                            # -[0]3
+                            setattr(res, offattr,
+                                    int(l[i][:2])*3600*signal)
+                        else:
+                            return None
+                        i += 1
+                    if res.dstabbr:
+                        break
+                else:
+                    break
+
+            if i < len_l:
+                for j in range(i, len_l):
+                    if l[j] == ';': l[j] = ','
+
+                assert l[i] == ','
+
+                i += 1
+
+            if i >= len_l:
+                pass
+            elif (8 <= l.count(',') <= 9 and
+                not [y for x in l[i:] if x != ','
+                       for y in x if y not in "0123456789"]):
+                # GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
+                for x in (res.start, res.end):
+                    x.month = int(l[i])
+                    i += 2
+                    if l[i] == '-':
+                        value = int(l[i+1])*-1
+                        i += 1
+                    else:
+                        value = int(l[i])
+                    i += 2
+                    if value:
+                        x.week = value
+                        x.weekday = (int(l[i])-1)%7
+                    else:
+                        x.day = int(l[i])
+                    i += 2
+                    x.time = int(l[i])
+                    i += 2
+                if i < len_l:
+                    if l[i] in ('-','+'):
+                        signal = (-1,1)[l[i] == "+"]
+                        i += 1
+                    else:
+                        signal = 1
+                    res.dstoffset = (res.stdoffset+int(l[i]))*signal
+            elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
+                  not [y for x in l[i:] if x not in (',','/','J','M',
+                                                     '.','-',':')
+                         for y in x if y not in "0123456789"]):
+                for x in (res.start, res.end):
+                    if l[i] == 'J':
+                        # non-leap year day (1 based)
+                        i += 1
+                        x.jyday = int(l[i])
+                    elif l[i] == 'M':
+                        # month[-.]week[-.]weekday
+                        i += 1
+                        x.month = int(l[i])
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        i += 1
+                        x.week = int(l[i])
+                        if x.week == 5:
+                            x.week = -1
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        i += 1
+                        x.weekday = (int(l[i])-1)%7
+                    else:
+                        # year day (zero based)
+                        x.yday = int(l[i])+1
+
+                    i += 1
+
+                    if i < len_l and l[i] == '/':
+                        i += 1
+                        # start time
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            x.time = (int(l[i][:2])*3600+int(l[i][2:])*60)
+                        elif i+1 < len_l and l[i+1] == ':':
+                            # -03:00
+                            x.time = int(l[i])*3600+int(l[i+2])*60
+                            i += 2
+                            if i+1 < len_l and l[i+1] == ':':
+                                i += 2
+                                x.time += int(l[i])
+                        elif len_li <= 2:
+                            # -[0]3
+                            x.time = (int(l[i][:2])*3600)
+                        else:
+                            return None
+                        i += 1
+
+                    assert i == len_l or l[i] == ','
+
+                    i += 1
+
+                assert i >= len_l
+
+        except (IndexError, ValueError, AssertionError):
+            return None
+        
+        return res
+
+
+DEFAULTTZPARSER = _tzparser()
+def _parsetz(tzstr):
+    return DEFAULTTZPARSER.parse(tzstr)
+
+
+def _parsems(value):
+    """Parse a I[.F] seconds value into (seconds, microseconds)."""
+    if "." not in value:
+        return int(value), 0
+    else:
+        i, f = value.split(".")
+        return int(i), int(f.ljust(6, "0")[:6])
+
+
+# vim:ts=4:sw=4:et
diff --git a/lib/dateutil/relativedelta.py b/lib/dateutil/relativedelta.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c72a8180fb707eef4a1072edb171bad42f4d294
--- /dev/null
+++ b/lib/dateutil/relativedelta.py
@@ -0,0 +1,432 @@
+"""
+Copyright (c) 2003-2010  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import datetime
+import calendar
+
+__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+class weekday(object):
+    __slots__ = ["weekday", "n"]
+
+    def __init__(self, weekday, n=None):
+        self.weekday = weekday
+        self.n = n
+
+    def __call__(self, n):
+        if n == self.n:
+            return self
+        else:
+            return self.__class__(self.weekday, n)
+
+    def __eq__(self, other):
+        try:
+            if self.weekday != other.weekday or self.n != other.n:
+                return False
+        except AttributeError:
+            return False
+        return True
+
+    def __repr__(self):
+        s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
+        if not self.n:
+            return s
+        else:
+            return "%s(%+d)" % (s, self.n)
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
+
+class relativedelta:
+    """
+The relativedelta type is based on the specification of the excelent
+work done by M.-A. Lemburg in his mx.DateTime extension. However,
+notice that this type does *NOT* implement the same algorithm as
+his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
+
+There's two different ways to build a relativedelta instance. The
+first one is passing it two date/datetime classes:
+
+    relativedelta(datetime1, datetime2)
+
+And the other way is to use the following keyword arguments:
+
+    year, month, day, hour, minute, second, microsecond:
+        Absolute information.
+
+    years, months, weeks, days, hours, minutes, seconds, microseconds:
+        Relative information, may be negative.
+
+    weekday:
+        One of the weekday instances (MO, TU, etc). These instances may
+        receive a parameter N, specifying the Nth weekday, which could
+        be positive or negative (like MO(+1) or MO(-2). Not specifying
+        it is the same as specifying +1. You can also use an integer,
+        where 0=MO.
+
+    leapdays:
+        Will add given days to the date found, if year is a leap
+        year, and the date found is post 28 of february.
+
+    yearday, nlyearday:
+        Set the yearday or the non-leap year day (jump leap days).
+        These are converted to day/month/leapdays information.
+
+Here is the behavior of operations with relativedelta:
+
+1) Calculate the absolute year, using the 'year' argument, or the
+   original datetime year, if the argument is not present.
+
+2) Add the relative 'years' argument to the absolute year.
+
+3) Do steps 1 and 2 for month/months.
+
+4) Calculate the absolute day, using the 'day' argument, or the
+   original datetime day, if the argument is not present. Then,
+   subtract from the day until it fits in the year and month
+   found after their operations.
+
+5) Add the relative 'days' argument to the absolute day. Notice
+   that the 'weeks' argument is multiplied by 7 and added to
+   'days'.
+
+6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
+   microsecond/microseconds.
+
+7) If the 'weekday' argument is present, calculate the weekday,
+   with the given (wday, nth) tuple. wday is the index of the
+   weekday (0-6, 0=Mon), and nth is the number of weeks to add
+   forward or backward, depending on its signal. Notice that if
+   the calculated date is already Monday, for example, using
+   (0, 1) or (0, -1) won't change the day.
+    """
+
+    def __init__(self, dt1=None, dt2=None,
+                 years=0, months=0, days=0, leapdays=0, weeks=0,
+                 hours=0, minutes=0, seconds=0, microseconds=0,
+                 year=None, month=None, day=None, weekday=None,
+                 yearday=None, nlyearday=None,
+                 hour=None, minute=None, second=None, microsecond=None):
+        if dt1 and dt2:
+            if not isinstance(dt1, datetime.date) or \
+               not isinstance(dt2, datetime.date):
+                raise TypeError, "relativedelta only diffs datetime/date"
+            if type(dt1) is not type(dt2):
+                if not isinstance(dt1, datetime.datetime):
+                    dt1 = datetime.datetime.fromordinal(dt1.toordinal())
+                elif not isinstance(dt2, datetime.datetime):
+                    dt2 = datetime.datetime.fromordinal(dt2.toordinal())
+            self.years = 0
+            self.months = 0
+            self.days = 0
+            self.leapdays = 0
+            self.hours = 0
+            self.minutes = 0
+            self.seconds = 0
+            self.microseconds = 0
+            self.year = None
+            self.month = None
+            self.day = None
+            self.weekday = None
+            self.hour = None
+            self.minute = None
+            self.second = None
+            self.microsecond = None
+            self._has_time = 0
+
+            months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month)
+            self._set_months(months)
+            dtm = self.__radd__(dt2)
+            if dt1 < dt2:
+                while dt1 > dtm:
+                    months += 1
+                    self._set_months(months)
+                    dtm = self.__radd__(dt2)
+            else:
+                while dt1 < dtm:
+                    months -= 1
+                    self._set_months(months)
+                    dtm = self.__radd__(dt2)
+            delta = dt1 - dtm
+            self.seconds = delta.seconds+delta.days*86400
+            self.microseconds = delta.microseconds
+        else:
+            self.years = years
+            self.months = months
+            self.days = days+weeks*7
+            self.leapdays = leapdays
+            self.hours = hours
+            self.minutes = minutes
+            self.seconds = seconds
+            self.microseconds = microseconds
+            self.year = year
+            self.month = month
+            self.day = day
+            self.hour = hour
+            self.minute = minute
+            self.second = second
+            self.microsecond = microsecond
+
+            if type(weekday) is int:
+                self.weekday = weekdays[weekday]
+            else:
+                self.weekday = weekday
+
+            yday = 0
+            if nlyearday:
+                yday = nlyearday
+            elif yearday:
+                yday = yearday
+                if yearday > 59:
+                    self.leapdays = -1
+            if yday:
+                ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366]
+                for idx, ydays in enumerate(ydayidx):
+                    if yday <= ydays:
+                        self.month = idx+1
+                        if idx == 0:
+                            self.day = yday
+                        else:
+                            self.day = yday-ydayidx[idx-1]
+                        break
+                else:
+                    raise ValueError, "invalid year day (%d)" % yday
+
+        self._fix()
+
+    def _fix(self):
+        if abs(self.microseconds) > 999999:
+            s = self.microseconds//abs(self.microseconds)
+            div, mod = divmod(self.microseconds*s, 1000000)
+            self.microseconds = mod*s
+            self.seconds += div*s
+        if abs(self.seconds) > 59:
+            s = self.seconds//abs(self.seconds)
+            div, mod = divmod(self.seconds*s, 60)
+            self.seconds = mod*s
+            self.minutes += div*s
+        if abs(self.minutes) > 59:
+            s = self.minutes//abs(self.minutes)
+            div, mod = divmod(self.minutes*s, 60)
+            self.minutes = mod*s
+            self.hours += div*s
+        if abs(self.hours) > 23:
+            s = self.hours//abs(self.hours)
+            div, mod = divmod(self.hours*s, 24)
+            self.hours = mod*s
+            self.days += div*s
+        if abs(self.months) > 11:
+            s = self.months//abs(self.months)
+            div, mod = divmod(self.months*s, 12)
+            self.months = mod*s
+            self.years += div*s
+        if (self.hours or self.minutes or self.seconds or self.microseconds or
+            self.hour is not None or self.minute is not None or
+            self.second is not None or self.microsecond is not None):
+            self._has_time = 1
+        else:
+            self._has_time = 0
+
+    def _set_months(self, months):
+        self.months = months
+        if abs(self.months) > 11:
+            s = self.months//abs(self.months)
+            div, mod = divmod(self.months*s, 12)
+            self.months = mod*s
+            self.years = div*s
+        else:
+            self.years = 0
+
+    def __radd__(self, other):
+        if not isinstance(other, datetime.date):
+            raise TypeError, "unsupported type for add operation"
+        elif self._has_time and not isinstance(other, datetime.datetime):
+            other = datetime.datetime.fromordinal(other.toordinal())
+        year = (self.year or other.year)+self.years
+        month = self.month or other.month
+        if self.months:
+            assert 1 <= abs(self.months) <= 12
+            month += self.months
+            if month > 12:
+                year += 1
+                month -= 12
+            elif month < 1:
+                year -= 1
+                month += 12
+        day = min(calendar.monthrange(year, month)[1],
+                  self.day or other.day)
+        repl = {"year": year, "month": month, "day": day}
+        for attr in ["hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                repl[attr] = value
+        days = self.days
+        if self.leapdays and month > 2 and calendar.isleap(year):
+            days += self.leapdays
+        ret = (other.replace(**repl)
+               + datetime.timedelta(days=days,
+                                    hours=self.hours,
+                                    minutes=self.minutes,
+                                    seconds=self.seconds,
+                                    microseconds=self.microseconds))
+        if self.weekday:
+            weekday, nth = self.weekday.weekday, self.weekday.n or 1
+            jumpdays = (abs(nth)-1)*7
+            if nth > 0:
+                jumpdays += (7-ret.weekday()+weekday)%7
+            else:
+                jumpdays += (ret.weekday()-weekday)%7
+                jumpdays *= -1
+            ret += datetime.timedelta(days=jumpdays)
+        return ret
+
+    def __rsub__(self, other):
+        return self.__neg__().__radd__(other)
+
+    def __add__(self, other):
+        if not isinstance(other, relativedelta):
+            raise TypeError, "unsupported type for add operation"
+        return relativedelta(years=other.years+self.years,
+                             months=other.months+self.months,
+                             days=other.days+self.days,
+                             hours=other.hours+self.hours,
+                             minutes=other.minutes+self.minutes,
+                             seconds=other.seconds+self.seconds,
+                             microseconds=other.microseconds+self.microseconds,
+                             leapdays=other.leapdays or self.leapdays,
+                             year=other.year or self.year,
+                             month=other.month or self.month,
+                             day=other.day or self.day,
+                             weekday=other.weekday or self.weekday,
+                             hour=other.hour or self.hour,
+                             minute=other.minute or self.minute,
+                             second=other.second or self.second,
+                             microsecond=other.second or self.microsecond)
+
+    def __sub__(self, other):
+        if not isinstance(other, relativedelta):
+            raise TypeError, "unsupported type for sub operation"
+        return relativedelta(years=other.years-self.years,
+                             months=other.months-self.months,
+                             days=other.days-self.days,
+                             hours=other.hours-self.hours,
+                             minutes=other.minutes-self.minutes,
+                             seconds=other.seconds-self.seconds,
+                             microseconds=other.microseconds-self.microseconds,
+                             leapdays=other.leapdays or self.leapdays,
+                             year=other.year or self.year,
+                             month=other.month or self.month,
+                             day=other.day or self.day,
+                             weekday=other.weekday or self.weekday,
+                             hour=other.hour or self.hour,
+                             minute=other.minute or self.minute,
+                             second=other.second or self.second,
+                             microsecond=other.second or self.microsecond)
+
+    def __neg__(self):
+        return relativedelta(years=-self.years,
+                             months=-self.months,
+                             days=-self.days,
+                             hours=-self.hours,
+                             minutes=-self.minutes,
+                             seconds=-self.seconds,
+                             microseconds=-self.microseconds,
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    def __nonzero__(self):
+        return not (not self.years and
+                    not self.months and
+                    not self.days and
+                    not self.hours and
+                    not self.minutes and
+                    not self.seconds and
+                    not self.microseconds and
+                    not self.leapdays and
+                    self.year is None and
+                    self.month is None and
+                    self.day is None and
+                    self.weekday is None and
+                    self.hour is None and
+                    self.minute is None and
+                    self.second is None and
+                    self.microsecond is None)
+
+    def __mul__(self, other):
+        f = float(other)
+        return relativedelta(years=self.years*f,
+                             months=self.months*f,
+                             days=self.days*f,
+                             hours=self.hours*f,
+                             minutes=self.minutes*f,
+                             seconds=self.seconds*f,
+                             microseconds=self.microseconds*f,
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    def __eq__(self, other):
+        if not isinstance(other, relativedelta):
+            return False
+        if self.weekday or other.weekday:
+            if not self.weekday or not other.weekday:
+                return False
+            if self.weekday.weekday != other.weekday.weekday:
+                return False
+            n1, n2 = self.weekday.n, other.weekday.n
+            if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
+                return False
+        return (self.years == other.years and
+                self.months == other.months and
+                self.days == other.days and
+                self.hours == other.hours and
+                self.minutes == other.minutes and
+                self.seconds == other.seconds and
+                self.leapdays == other.leapdays and
+                self.year == other.year and
+                self.month == other.month and
+                self.day == other.day and
+                self.hour == other.hour and
+                self.minute == other.minute and
+                self.second == other.second and
+                self.microsecond == other.microsecond)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __div__(self, other):
+        return self.__mul__(1/float(other))
+
+    def __repr__(self):
+        l = []
+        for attr in ["years", "months", "days", "leapdays",
+                     "hours", "minutes", "seconds", "microseconds"]:
+            value = getattr(self, attr)
+            if value:
+                l.append("%s=%+d" % (attr, value))
+        for attr in ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, `value`))
+        return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
+
+# vim:ts=4:sw=4:et
diff --git a/lib/dateutil/rrule.py b/lib/dateutil/rrule.py
new file mode 100644
index 0000000000000000000000000000000000000000..6bd83cad372262acd26f14cc2936c25823fe7f14
--- /dev/null
+++ b/lib/dateutil/rrule.py
@@ -0,0 +1,1097 @@
+"""
+Copyright (c) 2003-2010  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import itertools
+import datetime
+import calendar
+import thread
+import sys
+
+__all__ = ["rrule", "rruleset", "rrulestr",
+           "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
+           "HOURLY", "MINUTELY", "SECONDLY",
+           "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+# Every mask is 7 days longer to handle cross-year weekly periods.
+M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+
+                 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
+M365MASK = list(M366MASK)
+M29, M30, M31 = range(1,30), range(1,31), range(1,32)
+MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+MDAY365MASK = list(MDAY366MASK)
+M29, M30, M31 = range(-29,0), range(-30,0), range(-31,0)
+NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+NMDAY365MASK = list(NMDAY366MASK)
+M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366)
+M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365)
+WDAYMASK = [0,1,2,3,4,5,6]*55
+del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
+MDAY365MASK = tuple(MDAY365MASK)
+M365MASK = tuple(M365MASK)
+
+(YEARLY,
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ HOURLY,
+ MINUTELY,
+ SECONDLY) = range(7)
+
+# Imported on demand.
+easter = None
+parser = None
+
+class weekday(object):
+    __slots__ = ["weekday", "n"]
+
+    def __init__(self, weekday, n=None):
+        if n == 0:
+            raise ValueError, "Can't create weekday with n == 0"
+        self.weekday = weekday
+        self.n = n
+
+    def __call__(self, n):
+        if n == self.n:
+            return self
+        else:
+            return self.__class__(self.weekday, n)
+
+    def __eq__(self, other):
+        try:
+            if self.weekday != other.weekday or self.n != other.n:
+                return False
+        except AttributeError:
+            return False
+        return True
+
+    def __repr__(self):
+        s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
+        if not self.n:
+            return s
+        else:
+            return "%s(%+d)" % (s, self.n)
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
+
+class rrulebase:
+    def __init__(self, cache=False):
+        if cache:
+            self._cache = []
+            self._cache_lock = thread.allocate_lock()
+            self._cache_gen  = self._iter()
+            self._cache_complete = False
+        else:
+            self._cache = None
+            self._cache_complete = False
+        self._len = None
+
+    def __iter__(self):
+        if self._cache_complete:
+            return iter(self._cache)
+        elif self._cache is None:
+            return self._iter()
+        else:
+            return self._iter_cached()
+
+    def _iter_cached(self):
+        i = 0
+        gen = self._cache_gen
+        cache = self._cache
+        acquire = self._cache_lock.acquire
+        release = self._cache_lock.release
+        while gen:
+            if i == len(cache):
+                acquire()
+                if self._cache_complete:
+                    break
+                try:
+                    for j in range(10):
+                        cache.append(gen.next())
+                except StopIteration:
+                    self._cache_gen = gen = None
+                    self._cache_complete = True
+                    break
+                release()
+            yield cache[i]
+            i += 1
+        while i < self._len:
+            yield cache[i]
+            i += 1
+
+    def __getitem__(self, item):
+        if self._cache_complete:
+            return self._cache[item]
+        elif isinstance(item, slice):
+            if item.step and item.step < 0:
+                return list(iter(self))[item]
+            else:
+                return list(itertools.islice(self,
+                                             item.start or 0,
+                                             item.stop or sys.maxint,
+                                             item.step or 1))
+        elif item >= 0:
+            gen = iter(self)
+            try:
+                for i in range(item+1):
+                    res = gen.next()
+            except StopIteration:
+                raise IndexError
+            return res
+        else:
+            return list(iter(self))[item]
+
+    def __contains__(self, item):
+        if self._cache_complete:
+            return item in self._cache
+        else:
+            for i in self:
+                if i == item:
+                    return True
+                elif i > item:
+                    return False
+        return False
+
+    # __len__() introduces a large performance penality.
+    def count(self):
+        if self._len is None:
+            for x in self: pass
+        return self._len
+
+    def before(self, dt, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        last = None
+        if inc:
+            for i in gen:
+                if i > dt:
+                    break
+                last = i
+        else:
+            for i in gen:
+                if i >= dt:
+                    break
+                last = i
+        return last
+
+    def after(self, dt, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        if inc:
+            for i in gen:
+                if i >= dt:
+                    return i
+        else:
+            for i in gen:
+                if i > dt:
+                    return i
+        return None
+
+    def between(self, after, before, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        started = False
+        l = []
+        if inc:
+            for i in gen:
+                if i > before:
+                    break
+                elif not started:
+                    if i >= after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        else:
+            for i in gen:
+                if i >= before:
+                    break
+                elif not started:
+                    if i > after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        return l
+
+class rrule(rrulebase):
+    def __init__(self, freq, dtstart=None,
+                 interval=1, wkst=None, count=None, until=None, bysetpos=None,
+                 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
+                 byweekno=None, byweekday=None,
+                 byhour=None, byminute=None, bysecond=None,
+                 cache=False):
+        rrulebase.__init__(self, cache)
+        global easter
+        if not dtstart:
+            dtstart = datetime.datetime.now().replace(microsecond=0)
+        elif not isinstance(dtstart, datetime.datetime):
+            dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+        else:
+            dtstart = dtstart.replace(microsecond=0)
+        self._dtstart = dtstart
+        self._tzinfo = dtstart.tzinfo
+        self._freq = freq
+        self._interval = interval
+        self._count = count
+        if until and not isinstance(until, datetime.datetime):
+            until = datetime.datetime.fromordinal(until.toordinal())
+        self._until = until
+        if wkst is None:
+            self._wkst = calendar.firstweekday()
+        elif type(wkst) is int:
+            self._wkst = wkst
+        else:
+            self._wkst = wkst.weekday
+        if bysetpos is None:
+            self._bysetpos = None
+        elif type(bysetpos) is int:
+            if bysetpos == 0 or not (-366 <= bysetpos <= 366):
+                raise ValueError("bysetpos must be between 1 and 366, "
+                                 "or between -366 and -1")
+            self._bysetpos = (bysetpos,)
+        else:
+            self._bysetpos = tuple(bysetpos)
+            for pos in self._bysetpos:
+                if pos == 0 or not (-366 <= pos <= 366):
+                    raise ValueError("bysetpos must be between 1 and 366, "
+                                     "or between -366 and -1")
+        if not (byweekno or byyearday or bymonthday or
+                byweekday is not None or byeaster is not None):
+            if freq == YEARLY:
+                if not bymonth:
+                    bymonth = dtstart.month
+                bymonthday = dtstart.day
+            elif freq == MONTHLY:
+                bymonthday = dtstart.day
+            elif freq == WEEKLY:
+                byweekday = dtstart.weekday()
+        # bymonth
+        if not bymonth:
+            self._bymonth = None
+        elif type(bymonth) is int:
+            self._bymonth = (bymonth,)
+        else:
+            self._bymonth = tuple(bymonth)
+        # byyearday
+        if not byyearday:
+            self._byyearday = None
+        elif type(byyearday) is int:
+            self._byyearday = (byyearday,)
+        else:
+            self._byyearday = tuple(byyearday)
+        # byeaster
+        if byeaster is not None:
+            if not easter:
+                from dateutil import easter
+            if type(byeaster) is int:
+                self._byeaster = (byeaster,)
+            else:
+                self._byeaster = tuple(byeaster)
+        else:
+            self._byeaster = None
+        # bymonthay
+        if not bymonthday:
+            self._bymonthday = ()
+            self._bynmonthday = ()
+        elif type(bymonthday) is int:
+            if bymonthday < 0:
+                self._bynmonthday = (bymonthday,)
+                self._bymonthday = ()
+            else:
+                self._bymonthday = (bymonthday,)
+                self._bynmonthday = ()
+        else:
+            self._bymonthday = tuple([x for x in bymonthday if x > 0])
+            self._bynmonthday = tuple([x for x in bymonthday if x < 0])
+        # byweekno
+        if byweekno is None:
+            self._byweekno = None
+        elif type(byweekno) is int:
+            self._byweekno = (byweekno,)
+        else:
+            self._byweekno = tuple(byweekno)
+        # byweekday / bynweekday
+        if byweekday is None:
+            self._byweekday = None
+            self._bynweekday = None
+        elif type(byweekday) is int:
+            self._byweekday = (byweekday,)
+            self._bynweekday = None
+        elif hasattr(byweekday, "n"):
+            if not byweekday.n or freq > MONTHLY:
+                self._byweekday = (byweekday.weekday,)
+                self._bynweekday = None
+            else:
+                self._bynweekday = ((byweekday.weekday, byweekday.n),)
+                self._byweekday = None
+        else:
+            self._byweekday = []
+            self._bynweekday = []
+            for wday in byweekday:
+                if type(wday) is int:
+                    self._byweekday.append(wday)
+                elif not wday.n or freq > MONTHLY:
+                    self._byweekday.append(wday.weekday)
+                else:
+                    self._bynweekday.append((wday.weekday, wday.n))
+            self._byweekday = tuple(self._byweekday)
+            self._bynweekday = tuple(self._bynweekday)
+            if not self._byweekday:
+                self._byweekday = None
+            elif not self._bynweekday:
+                self._bynweekday = None
+        # byhour
+        if byhour is None:
+            if freq < HOURLY:
+                self._byhour = (dtstart.hour,)
+            else:
+                self._byhour = None
+        elif type(byhour) is int:
+            self._byhour = (byhour,)
+        else:
+            self._byhour = tuple(byhour)
+        # byminute
+        if byminute is None:
+            if freq < MINUTELY:
+                self._byminute = (dtstart.minute,)
+            else:
+                self._byminute = None
+        elif type(byminute) is int:
+            self._byminute = (byminute,)
+        else:
+            self._byminute = tuple(byminute)
+        # bysecond
+        if bysecond is None:
+            if freq < SECONDLY:
+                self._bysecond = (dtstart.second,)
+            else:
+                self._bysecond = None
+        elif type(bysecond) is int:
+            self._bysecond = (bysecond,)
+        else:
+            self._bysecond = tuple(bysecond)
+
+        if self._freq >= HOURLY:
+            self._timeset = None
+        else:
+            self._timeset = []
+            for hour in self._byhour:
+                for minute in self._byminute:
+                    for second in self._bysecond:
+                        self._timeset.append(
+                                datetime.time(hour, minute, second,
+                                                    tzinfo=self._tzinfo))
+            self._timeset.sort()
+            self._timeset = tuple(self._timeset)
+
+    def _iter(self):
+        year, month, day, hour, minute, second, weekday, yearday, _ = \
+            self._dtstart.timetuple()
+
+        # Some local variables to speed things up a bit
+        freq = self._freq
+        interval = self._interval
+        wkst = self._wkst
+        until = self._until
+        bymonth = self._bymonth
+        byweekno = self._byweekno
+        byyearday = self._byyearday
+        byweekday = self._byweekday
+        byeaster = self._byeaster
+        bymonthday = self._bymonthday
+        bynmonthday = self._bynmonthday
+        bysetpos = self._bysetpos
+        byhour = self._byhour
+        byminute = self._byminute
+        bysecond = self._bysecond
+
+        ii = _iterinfo(self)
+        ii.rebuild(year, month)
+
+        getdayset = {YEARLY:ii.ydayset,
+                     MONTHLY:ii.mdayset,
+                     WEEKLY:ii.wdayset,
+                     DAILY:ii.ddayset,
+                     HOURLY:ii.ddayset,
+                     MINUTELY:ii.ddayset,
+                     SECONDLY:ii.ddayset}[freq]
+        
+        if freq < HOURLY:
+            timeset = self._timeset
+        else:
+            gettimeset = {HOURLY:ii.htimeset,
+                          MINUTELY:ii.mtimeset,
+                          SECONDLY:ii.stimeset}[freq]
+            if ((freq >= HOURLY and
+                 self._byhour and hour not in self._byhour) or
+                (freq >= MINUTELY and
+                 self._byminute and minute not in self._byminute) or
+                (freq >= SECONDLY and
+                 self._bysecond and second not in self._bysecond)):
+                timeset = ()
+            else:
+                timeset = gettimeset(hour, minute, second)
+
+        total = 0
+        count = self._count
+        while True:
+            # Get dayset with the right frequency
+            dayset, start, end = getdayset(year, month, day)
+
+            # Do the "hard" work ;-)
+            filtered = False
+            for i in dayset[start:end]:
+                if ((bymonth and ii.mmask[i] not in bymonth) or
+                    (byweekno and not ii.wnomask[i]) or
+                    (byweekday and ii.wdaymask[i] not in byweekday) or
+                    (ii.nwdaymask and not ii.nwdaymask[i]) or
+                    (byeaster and not ii.eastermask[i]) or
+                    ((bymonthday or bynmonthday) and
+                     ii.mdaymask[i] not in bymonthday and
+                     ii.nmdaymask[i] not in bynmonthday) or
+                    (byyearday and
+                     ((i < ii.yearlen and i+1 not in byyearday
+                                      and -ii.yearlen+i not in byyearday) or
+                      (i >= ii.yearlen and i+1-ii.yearlen not in byyearday
+                                       and -ii.nextyearlen+i-ii.yearlen
+                                           not in byyearday)))):
+                    dayset[i] = None
+                    filtered = True
+
+            # Output results
+            if bysetpos and timeset:
+                poslist = []
+                for pos in bysetpos:
+                    if pos < 0:
+                        daypos, timepos = divmod(pos, len(timeset))
+                    else:
+                        daypos, timepos = divmod(pos-1, len(timeset))
+                    try:
+                        i = [x for x in dayset[start:end]
+                                if x is not None][daypos]
+                        time = timeset[timepos]
+                    except IndexError:
+                        pass
+                    else:
+                        date = datetime.date.fromordinal(ii.yearordinal+i)
+                        res = datetime.datetime.combine(date, time)
+                        if res not in poslist:
+                            poslist.append(res)
+                poslist.sort()
+                for res in poslist:
+                    if until and res > until:
+                        self._len = total
+                        return
+                    elif res >= self._dtstart:
+                        total += 1
+                        yield res
+                        if count:
+                            count -= 1
+                            if not count:
+                                self._len = total
+                                return
+            else:
+                for i in dayset[start:end]:
+                    if i is not None:
+                        date = datetime.date.fromordinal(ii.yearordinal+i)
+                        for time in timeset:
+                            res = datetime.datetime.combine(date, time)
+                            if until and res > until:
+                                self._len = total
+                                return
+                            elif res >= self._dtstart:
+                                total += 1
+                                yield res
+                                if count:
+                                    count -= 1
+                                    if not count:
+                                        self._len = total
+                                        return
+
+            # Handle frequency and interval
+            fixday = False
+            if freq == YEARLY:
+                year += interval
+                if year > datetime.MAXYEAR:
+                    self._len = total
+                    return
+                ii.rebuild(year, month)
+            elif freq == MONTHLY:
+                month += interval
+                if month > 12:
+                    div, mod = divmod(month, 12)
+                    month = mod
+                    year += div
+                    if month == 0:
+                        month = 12
+                        year -= 1
+                    if year > datetime.MAXYEAR:
+                        self._len = total
+                        return
+                ii.rebuild(year, month)
+            elif freq == WEEKLY:
+                if wkst > weekday:
+                    day += -(weekday+1+(6-wkst))+self._interval*7
+                else:
+                    day += -(weekday-wkst)+self._interval*7
+                weekday = wkst
+                fixday = True
+            elif freq == DAILY:
+                day += interval
+                fixday = True
+            elif freq == HOURLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    hour += ((23-hour)//interval)*interval
+                while True:
+                    hour += interval
+                    div, mod = divmod(hour, 24)
+                    if div:
+                        hour = mod
+                        day += div
+                        fixday = True
+                    if not byhour or hour in byhour:
+                        break
+                timeset = gettimeset(hour, minute, second)
+            elif freq == MINUTELY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    minute += ((1439-(hour*60+minute))//interval)*interval
+                while True:
+                    minute += interval
+                    div, mod = divmod(minute, 60)
+                    if div:
+                        minute = mod
+                        hour += div
+                        div, mod = divmod(hour, 24)
+                        if div:
+                            hour = mod
+                            day += div
+                            fixday = True
+                            filtered = False
+                    if ((not byhour or hour in byhour) and
+                        (not byminute or minute in byminute)):
+                        break
+                timeset = gettimeset(hour, minute, second)
+            elif freq == SECONDLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    second += (((86399-(hour*3600+minute*60+second))
+                                //interval)*interval)
+                while True:
+                    second += self._interval
+                    div, mod = divmod(second, 60)
+                    if div:
+                        second = mod
+                        minute += div
+                        div, mod = divmod(minute, 60)
+                        if div:
+                            minute = mod
+                            hour += div
+                            div, mod = divmod(hour, 24)
+                            if div:
+                                hour = mod
+                                day += div
+                                fixday = True
+                    if ((not byhour or hour in byhour) and
+                        (not byminute or minute in byminute) and
+                        (not bysecond or second in bysecond)):
+                        break
+                timeset = gettimeset(hour, minute, second)
+
+            if fixday and day > 28:
+                daysinmonth = calendar.monthrange(year, month)[1]
+                if day > daysinmonth:
+                    while day > daysinmonth:
+                        day -= daysinmonth
+                        month += 1
+                        if month == 13:
+                            month = 1
+                            year += 1
+                            if year > datetime.MAXYEAR:
+                                self._len = total
+                                return
+                        daysinmonth = calendar.monthrange(year, month)[1]
+                    ii.rebuild(year, month)
+
+class _iterinfo(object):
+    __slots__ = ["rrule", "lastyear", "lastmonth",
+                 "yearlen", "nextyearlen", "yearordinal", "yearweekday",
+                 "mmask", "mrange", "mdaymask", "nmdaymask",
+                 "wdaymask", "wnomask", "nwdaymask", "eastermask"]
+
+    def __init__(self, rrule):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+        self.rrule = rrule
+
+    def rebuild(self, year, month):
+        # Every mask is 7 days longer to handle cross-year weekly periods.
+        rr = self.rrule
+        if year != self.lastyear:
+            self.yearlen = 365+calendar.isleap(year)
+            self.nextyearlen = 365+calendar.isleap(year+1)
+            firstyday = datetime.date(year, 1, 1)
+            self.yearordinal = firstyday.toordinal()
+            self.yearweekday = firstyday.weekday()
+
+            wday = datetime.date(year, 1, 1).weekday()
+            if self.yearlen == 365:
+                self.mmask = M365MASK
+                self.mdaymask = MDAY365MASK
+                self.nmdaymask = NMDAY365MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M365RANGE
+            else:
+                self.mmask = M366MASK
+                self.mdaymask = MDAY366MASK
+                self.nmdaymask = NMDAY366MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M366RANGE
+
+            if not rr._byweekno:
+                self.wnomask = None
+            else:
+                self.wnomask = [0]*(self.yearlen+7)
+                #no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
+                no1wkst = firstwkst = (7-self.yearweekday+rr._wkst)%7
+                if no1wkst >= 4:
+                    no1wkst = 0
+                    # Number of days in the year, plus the days we got
+                    # from last year.
+                    wyearlen = self.yearlen+(self.yearweekday-rr._wkst)%7
+                else:
+                    # Number of days in the year, minus the days we
+                    # left in last year.
+                    wyearlen = self.yearlen-no1wkst
+                div, mod = divmod(wyearlen, 7)
+                numweeks = div+mod//4
+                for n in rr._byweekno:
+                    if n < 0:
+                        n += numweeks+1
+                    if not (0 < n <= numweeks):
+                        continue
+                    if n > 1:
+                        i = no1wkst+(n-1)*7
+                        if no1wkst != firstwkst:
+                            i -= 7-firstwkst
+                    else:
+                        i = no1wkst
+                    for j in range(7):
+                        self.wnomask[i] = 1
+                        i += 1
+                        if self.wdaymask[i] == rr._wkst:
+                            break
+                if 1 in rr._byweekno:
+                    # Check week number 1 of next year as well
+                    # TODO: Check -numweeks for next year.
+                    i = no1wkst+numweeks*7
+                    if no1wkst != firstwkst:
+                        i -= 7-firstwkst
+                    if i < self.yearlen:
+                        # If week starts in next year, we
+                        # don't care about it.
+                        for j in range(7):
+                            self.wnomask[i] = 1
+                            i += 1
+                            if self.wdaymask[i] == rr._wkst:
+                                break
+                if no1wkst:
+                    # Check last week number of last year as
+                    # well. If no1wkst is 0, either the year
+                    # started on week start, or week number 1
+                    # got days from last year, so there are no
+                    # days from last year's last week number in
+                    # this year.
+                    if -1 not in rr._byweekno:
+                        lyearweekday = datetime.date(year-1,1,1).weekday()
+                        lno1wkst = (7-lyearweekday+rr._wkst)%7
+                        lyearlen = 365+calendar.isleap(year-1)
+                        if lno1wkst >= 4:
+                            lno1wkst = 0
+                            lnumweeks = 52+(lyearlen+
+                                           (lyearweekday-rr._wkst)%7)%7//4
+                        else:
+                            lnumweeks = 52+(self.yearlen-no1wkst)%7//4
+                    else:
+                        lnumweeks = -1
+                    if lnumweeks in rr._byweekno:
+                        for i in range(no1wkst):
+                            self.wnomask[i] = 1
+
+        if (rr._bynweekday and
+            (month != self.lastmonth or year != self.lastyear)):
+            ranges = []
+            if rr._freq == YEARLY:
+                if rr._bymonth:
+                    for month in rr._bymonth:
+                        ranges.append(self.mrange[month-1:month+1])
+                else:
+                    ranges = [(0, self.yearlen)]
+            elif rr._freq == MONTHLY:
+                ranges = [self.mrange[month-1:month+1]]
+            if ranges:
+                # Weekly frequency won't get here, so we may not
+                # care about cross-year weekly periods.
+                self.nwdaymask = [0]*self.yearlen
+                for first, last in ranges:
+                    last -= 1
+                    for wday, n in rr._bynweekday:
+                        if n < 0:
+                            i = last+(n+1)*7
+                            i -= (self.wdaymask[i]-wday)%7
+                        else:
+                            i = first+(n-1)*7
+                            i += (7-self.wdaymask[i]+wday)%7
+                        if first <= i <= last:
+                            self.nwdaymask[i] = 1
+
+        if rr._byeaster:
+            self.eastermask = [0]*(self.yearlen+7)
+            eyday = easter.easter(year).toordinal()-self.yearordinal
+            for offset in rr._byeaster:
+                self.eastermask[eyday+offset] = 1
+
+        self.lastyear = year
+        self.lastmonth = month
+
+    def ydayset(self, year, month, day):
+        return range(self.yearlen), 0, self.yearlen
+
+    def mdayset(self, year, month, day):
+        set = [None]*self.yearlen
+        start, end = self.mrange[month-1:month+1]
+        for i in range(start, end):
+            set[i] = i
+        return set, start, end
+
+    def wdayset(self, year, month, day):
+        # We need to handle cross-year weeks here.
+        set = [None]*(self.yearlen+7)
+        i = datetime.date(year, month, day).toordinal()-self.yearordinal
+        start = i
+        for j in range(7):
+            set[i] = i
+            i += 1
+            #if (not (0 <= i < self.yearlen) or
+            #    self.wdaymask[i] == self.rrule._wkst):
+            # This will cross the year boundary, if necessary.
+            if self.wdaymask[i] == self.rrule._wkst:
+                break
+        return set, start, i
+
+    def ddayset(self, year, month, day):
+        set = [None]*self.yearlen
+        i = datetime.date(year, month, day).toordinal()-self.yearordinal
+        set[i] = i
+        return set, i, i+1
+
+    def htimeset(self, hour, minute, second):
+        set = []
+        rr = self.rrule
+        for minute in rr._byminute:
+            for second in rr._bysecond:
+                set.append(datetime.time(hour, minute, second,
+                                         tzinfo=rr._tzinfo))
+        set.sort()
+        return set
+
+    def mtimeset(self, hour, minute, second):
+        set = []
+        rr = self.rrule
+        for second in rr._bysecond:
+            set.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
+        set.sort()
+        return set
+
+    def stimeset(self, hour, minute, second):
+        return (datetime.time(hour, minute, second,
+                tzinfo=self.rrule._tzinfo),)
+
+
+class rruleset(rrulebase):
+
+    class _genitem:
+        def __init__(self, genlist, gen):
+            try:
+                self.dt = gen()
+                genlist.append(self)
+            except StopIteration:
+                pass
+            self.genlist = genlist
+            self.gen = gen
+
+        def next(self):
+            try:
+                self.dt = self.gen()
+            except StopIteration:
+                self.genlist.remove(self)
+
+        def __cmp__(self, other):
+            return cmp(self.dt, other.dt)
+
+    def __init__(self, cache=False):
+        rrulebase.__init__(self, cache)
+        self._rrule = []
+        self._rdate = []
+        self._exrule = []
+        self._exdate = []
+
+    def rrule(self, rrule):
+        self._rrule.append(rrule)
+    
+    def rdate(self, rdate):
+        self._rdate.append(rdate)
+
+    def exrule(self, exrule):
+        self._exrule.append(exrule)
+
+    def exdate(self, exdate):
+        self._exdate.append(exdate)
+
+    def _iter(self):
+        rlist = []
+        self._rdate.sort()
+        self._genitem(rlist, iter(self._rdate).next)
+        for gen in [iter(x).next for x in self._rrule]:
+            self._genitem(rlist, gen)
+        rlist.sort()
+        exlist = []
+        self._exdate.sort()
+        self._genitem(exlist, iter(self._exdate).next)
+        for gen in [iter(x).next for x in self._exrule]:
+            self._genitem(exlist, gen)
+        exlist.sort()
+        lastdt = None
+        total = 0
+        while rlist:
+            ritem = rlist[0]
+            if not lastdt or lastdt != ritem.dt:
+                while exlist and exlist[0] < ritem:
+                    exlist[0].next()
+                    exlist.sort()
+                if not exlist or ritem != exlist[0]:
+                    total += 1
+                    yield ritem.dt
+                lastdt = ritem.dt
+            ritem.next()
+            rlist.sort()
+        self._len = total
+
+class _rrulestr:
+
+    _freq_map = {"YEARLY": YEARLY,
+                 "MONTHLY": MONTHLY,
+                 "WEEKLY": WEEKLY,
+                 "DAILY": DAILY,
+                 "HOURLY": HOURLY,
+                 "MINUTELY": MINUTELY,
+                 "SECONDLY": SECONDLY}
+
+    _weekday_map = {"MO":0,"TU":1,"WE":2,"TH":3,"FR":4,"SA":5,"SU":6}
+
+    def _handle_int(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = int(value)
+
+    def _handle_int_list(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
+
+    _handle_INTERVAL   = _handle_int
+    _handle_COUNT      = _handle_int
+    _handle_BYSETPOS   = _handle_int_list
+    _handle_BYMONTH    = _handle_int_list
+    _handle_BYMONTHDAY = _handle_int_list
+    _handle_BYYEARDAY  = _handle_int_list
+    _handle_BYEASTER   = _handle_int_list
+    _handle_BYWEEKNO   = _handle_int_list
+    _handle_BYHOUR     = _handle_int_list
+    _handle_BYMINUTE   = _handle_int_list
+    _handle_BYSECOND   = _handle_int_list
+
+    def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["freq"] = self._freq_map[value]
+
+    def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
+        global parser
+        if not parser:
+            from dateutil import parser
+        try:
+            rrkwargs["until"] = parser.parse(value,
+                                           ignoretz=kwargs.get("ignoretz"),
+                                           tzinfos=kwargs.get("tzinfos"))
+        except ValueError:
+            raise ValueError, "invalid until date"
+
+    def _handle_WKST(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["wkst"] = self._weekday_map[value]
+
+    def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg):
+        l = []
+        for wday in value.split(','):
+            for i in range(len(wday)):
+                if wday[i] not in '+-0123456789':
+                    break
+            n = wday[:i] or None
+            w = wday[i:]
+            if n: n = int(n)
+            l.append(weekdays[self._weekday_map[w]](n))
+        rrkwargs["byweekday"] = l
+
+    _handle_BYDAY = _handle_BYWEEKDAY
+
+    def _parse_rfc_rrule(self, line,
+                         dtstart=None,
+                         cache=False,
+                         ignoretz=False,
+                         tzinfos=None):
+        if line.find(':') != -1:
+            name, value = line.split(':')
+            if name != "RRULE":
+                raise ValueError, "unknown parameter name"
+        else:
+            value = line
+        rrkwargs = {}
+        for pair in value.split(';'):
+            name, value = pair.split('=')
+            name = name.upper()
+            value = value.upper()
+            try:
+                getattr(self, "_handle_"+name)(rrkwargs, name, value,
+                                               ignoretz=ignoretz,
+                                               tzinfos=tzinfos)
+            except AttributeError:
+                raise ValueError, "unknown parameter '%s'" % name
+            except (KeyError, ValueError):
+                raise ValueError, "invalid '%s': %s" % (name, value)
+        return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+
+    def _parse_rfc(self, s,
+                   dtstart=None,
+                   cache=False,
+                   unfold=False,
+                   forceset=False,
+                   compatible=False,
+                   ignoretz=False,
+                   tzinfos=None):
+        global parser
+        if compatible:
+            forceset = True
+            unfold = True
+        s = s.upper()
+        if not s.strip():
+            raise ValueError, "empty string"
+        if unfold:
+            lines = s.splitlines()
+            i = 0
+            while i < len(lines):
+                line = lines[i].rstrip()
+                if not line:
+                    del lines[i]
+                elif i > 0 and line[0] == " ":
+                    lines[i-1] += line[1:]
+                    del lines[i]
+                else:
+                    i += 1
+        else:
+            lines = s.split()
+        if (not forceset and len(lines) == 1 and
+            (s.find(':') == -1 or s.startswith('RRULE:'))):
+            return self._parse_rfc_rrule(lines[0], cache=cache,
+                                         dtstart=dtstart, ignoretz=ignoretz,
+                                         tzinfos=tzinfos)
+        else:
+            rrulevals = []
+            rdatevals = []
+            exrulevals = []
+            exdatevals = []
+            for line in lines:
+                if not line:
+                    continue
+                if line.find(':') == -1:
+                    name = "RRULE"
+                    value = line
+                else:
+                    name, value = line.split(':', 1)
+                parms = name.split(';')
+                if not parms:
+                    raise ValueError, "empty property name"
+                name = parms[0]
+                parms = parms[1:]
+                if name == "RRULE":
+                    for parm in parms:
+                        raise ValueError, "unsupported RRULE parm: "+parm
+                    rrulevals.append(value)
+                elif name == "RDATE":
+                    for parm in parms:
+                        if parm != "VALUE=DATE-TIME":
+                            raise ValueError, "unsupported RDATE parm: "+parm
+                    rdatevals.append(value)
+                elif name == "EXRULE":
+                    for parm in parms:
+                        raise ValueError, "unsupported EXRULE parm: "+parm
+                    exrulevals.append(value)
+                elif name == "EXDATE":
+                    for parm in parms:
+                        if parm != "VALUE=DATE-TIME":
+                            raise ValueError, "unsupported RDATE parm: "+parm
+                    exdatevals.append(value)
+                elif name == "DTSTART":
+                    for parm in parms:
+                        raise ValueError, "unsupported DTSTART parm: "+parm
+                    if not parser:
+                        from dateutil import parser
+                    dtstart = parser.parse(value, ignoretz=ignoretz,
+                                           tzinfos=tzinfos)
+                else:
+                    raise ValueError, "unsupported property: "+name
+            if (forceset or len(rrulevals) > 1 or
+                rdatevals or exrulevals or exdatevals):
+                if not parser and (rdatevals or exdatevals):
+                    from dateutil import parser
+                set = rruleset(cache=cache)
+                for value in rrulevals:
+                    set.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                    ignoretz=ignoretz,
+                                                    tzinfos=tzinfos))
+                for value in rdatevals:
+                    for datestr in value.split(','):
+                        set.rdate(parser.parse(datestr,
+                                               ignoretz=ignoretz,
+                                               tzinfos=tzinfos))
+                for value in exrulevals:
+                    set.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                     ignoretz=ignoretz,
+                                                     tzinfos=tzinfos))
+                for value in exdatevals:
+                    for datestr in value.split(','):
+                        set.exdate(parser.parse(datestr,
+                                                ignoretz=ignoretz,
+                                                tzinfos=tzinfos))
+                if compatible and dtstart:
+                    set.rdate(dtstart)
+                return set
+            else:
+                return self._parse_rfc_rrule(rrulevals[0],
+                                             dtstart=dtstart,
+                                             cache=cache,
+                                             ignoretz=ignoretz,
+                                             tzinfos=tzinfos)
+
+    def __call__(self, s, **kwargs):
+        return self._parse_rfc(s, **kwargs)
+
+rrulestr = _rrulestr()
+
+# vim:ts=4:sw=4:et
diff --git a/lib/dateutil/tz.py b/lib/dateutil/tz.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e28d6b3320953a71aace88d4043c80e2b71a4b8
--- /dev/null
+++ b/lib/dateutil/tz.py
@@ -0,0 +1,951 @@
+"""
+Copyright (c) 2003-2007  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import datetime
+import struct
+import time
+import sys
+import os
+
+relativedelta = None
+parser = None
+rrule = None
+
+__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
+           "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"]
+
+try:
+    from dateutil.tzwin import tzwin, tzwinlocal
+except (ImportError, OSError):
+    tzwin, tzwinlocal = None, None
+
+ZERO = datetime.timedelta(0)
+EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
+
+class tzutc(datetime.tzinfo):
+
+    def utcoffset(self, dt):
+        return ZERO
+     
+    def dst(self, dt):
+        return ZERO
+
+    def tzname(self, dt):
+        return "UTC"
+
+    def __eq__(self, other):
+        return (isinstance(other, tzutc) or
+                (isinstance(other, tzoffset) and other._offset == ZERO))
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
+
+class tzoffset(datetime.tzinfo):
+
+    def __init__(self, name, offset):
+        self._name = name
+        self._offset = datetime.timedelta(seconds=offset)
+
+    def utcoffset(self, dt):
+        return self._offset
+
+    def dst(self, dt):
+        return ZERO
+
+    def tzname(self, dt):
+        return self._name
+
+    def __eq__(self, other):
+        return (isinstance(other, tzoffset) and
+                self._offset == other._offset)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return "%s(%s, %s)" % (self.__class__.__name__,
+                               `self._name`,
+                               self._offset.days*86400+self._offset.seconds)
+
+    __reduce__ = object.__reduce__
+
+class tzlocal(datetime.tzinfo):
+
+    _std_offset = datetime.timedelta(seconds=-time.timezone)
+    if time.daylight:
+        _dst_offset = datetime.timedelta(seconds=-time.altzone)
+    else:
+        _dst_offset = _std_offset
+
+    def utcoffset(self, dt):
+        if self._isdst(dt):
+            return self._dst_offset
+        else:
+            return self._std_offset
+
+    def dst(self, dt):
+        if self._isdst(dt):
+            return self._dst_offset-self._std_offset
+        else:
+            return ZERO
+
+    def tzname(self, dt):
+        return time.tzname[self._isdst(dt)]
+
+    def _isdst(self, dt):
+        # We can't use mktime here. It is unstable when deciding if
+        # the hour near to a change is DST or not.
+        # 
+        # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
+        #                         dt.minute, dt.second, dt.weekday(), 0, -1))
+        # return time.localtime(timestamp).tm_isdst
+        #
+        # The code above yields the following result:
+        #
+        #>>> import tz, datetime
+        #>>> t = tz.tzlocal()
+        #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        #'BRDT'
+        #>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
+        #'BRST'
+        #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        #'BRST'
+        #>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
+        #'BRDT'
+        #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
+        #'BRDT'
+        #
+        # Here is a more stable implementation:
+        #
+        timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
+                     + dt.hour * 3600
+                     + dt.minute * 60
+                     + dt.second)
+        return time.localtime(timestamp+time.timezone).tm_isdst
+
+    def __eq__(self, other):
+        if not isinstance(other, tzlocal):
+            return False
+        return (self._std_offset == other._std_offset and
+                self._dst_offset == other._dst_offset)
+        return True
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
+
+class _ttinfo(object):
+    __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
+
+    def __init__(self):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+
+    def __repr__(self):
+        l = []
+        for attr in self.__slots__:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, `value`))
+        return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
+
+    def __eq__(self, other):
+        if not isinstance(other, _ttinfo):
+            return False
+        return (self.offset == other.offset and
+                self.delta == other.delta and
+                self.isdst == other.isdst and
+                self.abbr == other.abbr and
+                self.isstd == other.isstd and
+                self.isgmt == other.isgmt)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __getstate__(self):
+        state = {}
+        for name in self.__slots__:
+            state[name] = getattr(self, name, None)
+        return state
+
+    def __setstate__(self, state):
+        for name in self.__slots__:
+            if name in state:
+                setattr(self, name, state[name])
+
+class tzfile(datetime.tzinfo):
+
+    # http://www.twinsun.com/tz/tz-link.htm
+    # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz
+    
+    def __init__(self, fileobj):
+        if isinstance(fileobj, basestring):
+            self._filename = fileobj
+            fileobj = open(fileobj)
+        elif hasattr(fileobj, "name"):
+            self._filename = fileobj.name
+        else:
+            self._filename = `fileobj`
+
+        # From tzfile(5):
+        #
+        # The time zone information files used by tzset(3)
+        # begin with the magic characters "TZif" to identify
+        # them as time zone information files, followed by
+        # sixteen bytes reserved for future use, followed by
+        # six four-byte values of type long, written in a
+        # ``standard'' byte order (the high-order  byte
+        # of the value is written first).
+
+        if fileobj.read(4) != "TZif":
+            raise ValueError, "magic not found"
+
+        fileobj.read(16)
+
+        (
+         # The number of UTC/local indicators stored in the file.
+         ttisgmtcnt,
+
+         # The number of standard/wall indicators stored in the file.
+         ttisstdcnt,
+         
+         # The number of leap seconds for which data is
+         # stored in the file.
+         leapcnt,
+
+         # The number of "transition times" for which data
+         # is stored in the file.
+         timecnt,
+
+         # The number of "local time types" for which data
+         # is stored in the file (must not be zero).
+         typecnt,
+
+         # The  number  of  characters  of "time zone
+         # abbreviation strings" stored in the file.
+         charcnt,
+
+        ) = struct.unpack(">6l", fileobj.read(24))
+
+        # The above header is followed by tzh_timecnt four-byte
+        # values  of  type long,  sorted  in ascending order.
+        # These values are written in ``standard'' byte order.
+        # Each is used as a transition time (as  returned  by
+        # time(2)) at which the rules for computing local time
+        # change.
+
+        if timecnt:
+            self._trans_list = struct.unpack(">%dl" % timecnt,
+                                             fileobj.read(timecnt*4))
+        else:
+            self._trans_list = []
+
+        # Next come tzh_timecnt one-byte values of type unsigned
+        # char; each one tells which of the different types of
+        # ``local time'' types described in the file is associated
+        # with the same-indexed transition time. These values
+        # serve as indices into an array of ttinfo structures that
+        # appears next in the file.
+        
+        if timecnt:
+            self._trans_idx = struct.unpack(">%dB" % timecnt,
+                                            fileobj.read(timecnt))
+        else:
+            self._trans_idx = []
+        
+        # Each ttinfo structure is written as a four-byte value
+        # for tt_gmtoff  of  type long,  in  a  standard  byte
+        # order, followed  by a one-byte value for tt_isdst
+        # and a one-byte  value  for  tt_abbrind.   In  each
+        # structure, tt_gmtoff  gives  the  number  of
+        # seconds to be added to UTC, tt_isdst tells whether
+        # tm_isdst should be set by  localtime(3),  and
+        # tt_abbrind serves  as an index into the array of
+        # time zone abbreviation characters that follow the
+        # ttinfo structure(s) in the file.
+
+        ttinfo = []
+
+        for i in range(typecnt):
+            ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
+
+        abbr = fileobj.read(charcnt)
+
+        # Then there are tzh_leapcnt pairs of four-byte
+        # values, written in  standard byte  order;  the
+        # first  value  of  each pair gives the time (as
+        # returned by time(2)) at which a leap second
+        # occurs;  the  second  gives the  total  number of
+        # leap seconds to be applied after the given time.
+        # The pairs of values are sorted in ascending order
+        # by time.
+
+        # Not used, for now
+        if leapcnt:
+            leap = struct.unpack(">%dl" % (leapcnt*2),
+                                 fileobj.read(leapcnt*8))
+
+        # Then there are tzh_ttisstdcnt standard/wall
+        # indicators, each stored as a one-byte value;
+        # they tell whether the transition times associated
+        # with local time types were specified as standard
+        # time or wall clock time, and are used when
+        # a time zone file is used in handling POSIX-style
+        # time zone environment variables.
+
+        if ttisstdcnt:
+            isstd = struct.unpack(">%db" % ttisstdcnt,
+                                  fileobj.read(ttisstdcnt))
+
+        # Finally, there are tzh_ttisgmtcnt UTC/local
+        # indicators, each stored as a one-byte value;
+        # they tell whether the transition times associated
+        # with local time types were specified as UTC or
+        # local time, and are used when a time zone file
+        # is used in handling POSIX-style time zone envi-
+        # ronment variables.
+
+        if ttisgmtcnt:
+            isgmt = struct.unpack(">%db" % ttisgmtcnt,
+                                  fileobj.read(ttisgmtcnt))
+
+        # ** Everything has been read **
+
+        # Build ttinfo list
+        self._ttinfo_list = []
+        for i in range(typecnt):
+            gmtoff, isdst, abbrind =  ttinfo[i]
+            # Round to full-minutes if that's not the case. Python's
+            # datetime doesn't accept sub-minute timezones. Check
+            # http://python.org/sf/1447945 for some information.
+            gmtoff = (gmtoff+30)//60*60
+            tti = _ttinfo()
+            tti.offset = gmtoff
+            tti.delta = datetime.timedelta(seconds=gmtoff)
+            tti.isdst = isdst
+            tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
+            tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
+            tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
+            self._ttinfo_list.append(tti)
+
+        # Replace ttinfo indexes for ttinfo objects.
+        trans_idx = []
+        for idx in self._trans_idx:
+            trans_idx.append(self._ttinfo_list[idx])
+        self._trans_idx = tuple(trans_idx)
+
+        # Set standard, dst, and before ttinfos. before will be
+        # used when a given time is before any transitions,
+        # and will be set to the first non-dst ttinfo, or to
+        # the first dst, if all of them are dst.
+        self._ttinfo_std = None
+        self._ttinfo_dst = None
+        self._ttinfo_before = None
+        if self._ttinfo_list:
+            if not self._trans_list:
+                self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
+            else:
+                for i in range(timecnt-1,-1,-1):
+                    tti = self._trans_idx[i]
+                    if not self._ttinfo_std and not tti.isdst:
+                        self._ttinfo_std = tti
+                    elif not self._ttinfo_dst and tti.isdst:
+                        self._ttinfo_dst = tti
+                    if self._ttinfo_std and self._ttinfo_dst:
+                        break
+                else:
+                    if self._ttinfo_dst and not self._ttinfo_std:
+                        self._ttinfo_std = self._ttinfo_dst
+
+                for tti in self._ttinfo_list:
+                    if not tti.isdst:
+                        self._ttinfo_before = tti
+                        break
+                else:
+                    self._ttinfo_before = self._ttinfo_list[0]
+
+        # Now fix transition times to become relative to wall time.
+        #
+        # I'm not sure about this. In my tests, the tz source file
+        # is setup to wall time, and in the binary file isstd and
+        # isgmt are off, so it should be in wall time. OTOH, it's
+        # always in gmt time. Let me know if you have comments
+        # about this.
+        laststdoffset = 0
+        self._trans_list = list(self._trans_list)
+        for i in range(len(self._trans_list)):
+            tti = self._trans_idx[i]
+            if not tti.isdst:
+                # This is std time.
+                self._trans_list[i] += tti.offset
+                laststdoffset = tti.offset
+            else:
+                # This is dst time. Convert to std.
+                self._trans_list[i] += laststdoffset
+        self._trans_list = tuple(self._trans_list)
+
+    def _find_ttinfo(self, dt, laststd=0):
+        timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
+                     + dt.hour * 3600
+                     + dt.minute * 60
+                     + dt.second)
+        idx = 0
+        for trans in self._trans_list:
+            if timestamp < trans:
+                break
+            idx += 1
+        else:
+            return self._ttinfo_std
+        if idx == 0:
+            return self._ttinfo_before
+        if laststd:
+            while idx > 0:
+                tti = self._trans_idx[idx-1]
+                if not tti.isdst:
+                    return tti
+                idx -= 1
+            else:
+                return self._ttinfo_std
+        else:
+            return self._trans_idx[idx-1]
+
+    def utcoffset(self, dt):
+        if not self._ttinfo_std:
+            return ZERO
+        return self._find_ttinfo(dt).delta
+
+    def dst(self, dt):
+        if not self._ttinfo_dst:
+            return ZERO
+        tti = self._find_ttinfo(dt)
+        if not tti.isdst:
+            return ZERO
+
+        # The documentation says that utcoffset()-dst() must
+        # be constant for every dt.
+        return tti.delta-self._find_ttinfo(dt, laststd=1).delta
+
+        # An alternative for that would be:
+        #
+        # return self._ttinfo_dst.offset-self._ttinfo_std.offset
+        #
+        # However, this class stores historical changes in the
+        # dst offset, so I belive that this wouldn't be the right
+        # way to implement this.
+        
+    def tzname(self, dt):
+        if not self._ttinfo_std:
+            return None
+        return self._find_ttinfo(dt).abbr
+
+    def __eq__(self, other):
+        if not isinstance(other, tzfile):
+            return False
+        return (self._trans_list == other._trans_list and
+                self._trans_idx == other._trans_idx and
+                self._ttinfo_list == other._ttinfo_list)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, `self._filename`)
+
+    def __reduce__(self):
+        if not os.path.isfile(self._filename):
+            raise ValueError, "Unpickable %s class" % self.__class__.__name__
+        return (self.__class__, (self._filename,))
+
+class tzrange(datetime.tzinfo):
+
+    def __init__(self, stdabbr, stdoffset=None,
+                 dstabbr=None, dstoffset=None,
+                 start=None, end=None):
+        global relativedelta
+        if not relativedelta:
+            from dateutil import relativedelta
+        self._std_abbr = stdabbr
+        self._dst_abbr = dstabbr
+        if stdoffset is not None:
+            self._std_offset = datetime.timedelta(seconds=stdoffset)
+        else:
+            self._std_offset = ZERO
+        if dstoffset is not None:
+            self._dst_offset = datetime.timedelta(seconds=dstoffset)
+        elif dstabbr and stdoffset is not None:
+            self._dst_offset = self._std_offset+datetime.timedelta(hours=+1)
+        else:
+            self._dst_offset = ZERO
+        if dstabbr and start is None:
+            self._start_delta = relativedelta.relativedelta(
+                    hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
+        else:
+            self._start_delta = start
+        if dstabbr and end is None:
+            self._end_delta = relativedelta.relativedelta(
+                    hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
+        else:
+            self._end_delta = end
+
+    def utcoffset(self, dt):
+        if self._isdst(dt):
+            return self._dst_offset
+        else:
+            return self._std_offset
+
+    def dst(self, dt):
+        if self._isdst(dt):
+            return self._dst_offset-self._std_offset
+        else:
+            return ZERO
+
+    def tzname(self, dt):
+        if self._isdst(dt):
+            return self._dst_abbr
+        else:
+            return self._std_abbr
+
+    def _isdst(self, dt):
+        if not self._start_delta:
+            return False
+        year = datetime.datetime(dt.year,1,1)
+        start = year+self._start_delta
+        end = year+self._end_delta
+        dt = dt.replace(tzinfo=None)
+        if start < end:
+            return dt >= start and dt < end
+        else:
+            return dt >= start or dt < end
+
+    def __eq__(self, other):
+        if not isinstance(other, tzrange):
+            return False
+        return (self._std_abbr == other._std_abbr and
+                self._dst_abbr == other._dst_abbr and
+                self._std_offset == other._std_offset and
+                self._dst_offset == other._dst_offset and
+                self._start_delta == other._start_delta and
+                self._end_delta == other._end_delta)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return "%s(...)" % self.__class__.__name__
+
+    __reduce__ = object.__reduce__
+
+class tzstr(tzrange):
+    
+    def __init__(self, s):
+        global parser
+        if not parser:
+            from dateutil import parser
+        self._s = s
+
+        res = parser._parsetz(s)
+        if res is None:
+            raise ValueError, "unknown string format"
+
+        # Here we break the compatibility with the TZ variable handling.
+        # GMT-3 actually *means* the timezone -3.
+        if res.stdabbr in ("GMT", "UTC"):
+            res.stdoffset *= -1
+
+        # We must initialize it first, since _delta() needs
+        # _std_offset and _dst_offset set. Use False in start/end
+        # to avoid building it two times.
+        tzrange.__init__(self, res.stdabbr, res.stdoffset,
+                         res.dstabbr, res.dstoffset,
+                         start=False, end=False)
+
+        if not res.dstabbr:
+            self._start_delta = None
+            self._end_delta = None
+        else:
+            self._start_delta = self._delta(res.start)
+            if self._start_delta:
+                self._end_delta = self._delta(res.end, isend=1)
+
+    def _delta(self, x, isend=0):
+        kwargs = {}
+        if x.month is not None:
+            kwargs["month"] = x.month
+            if x.weekday is not None:
+                kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
+                if x.week > 0:
+                    kwargs["day"] = 1
+                else:
+                    kwargs["day"] = 31
+            elif x.day:
+                kwargs["day"] = x.day
+        elif x.yday is not None:
+            kwargs["yearday"] = x.yday
+        elif x.jyday is not None:
+            kwargs["nlyearday"] = x.jyday
+        if not kwargs:
+            # Default is to start on first sunday of april, and end
+            # on last sunday of october.
+            if not isend:
+                kwargs["month"] = 4
+                kwargs["day"] = 1
+                kwargs["weekday"] = relativedelta.SU(+1)
+            else:
+                kwargs["month"] = 10
+                kwargs["day"] = 31
+                kwargs["weekday"] = relativedelta.SU(-1)
+        if x.time is not None:
+            kwargs["seconds"] = x.time
+        else:
+            # Default is 2AM.
+            kwargs["seconds"] = 7200
+        if isend:
+            # Convert to standard time, to follow the documented way
+            # of working with the extra hour. See the documentation
+            # of the tzinfo class.
+            delta = self._dst_offset-self._std_offset
+            kwargs["seconds"] -= delta.seconds+delta.days*86400
+        return relativedelta.relativedelta(**kwargs)
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, `self._s`)
+
+class _tzicalvtzcomp:
+    def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
+                       tzname=None, rrule=None):
+        self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
+        self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
+        self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
+        self.isdst = isdst
+        self.tzname = tzname
+        self.rrule = rrule
+
+class _tzicalvtz(datetime.tzinfo):
+    def __init__(self, tzid, comps=[]):
+        self._tzid = tzid
+        self._comps = comps
+        self._cachedate = []
+        self._cachecomp = []
+
+    def _find_comp(self, dt):
+        if len(self._comps) == 1:
+            return self._comps[0]
+        dt = dt.replace(tzinfo=None)
+        try:
+            return self._cachecomp[self._cachedate.index(dt)]
+        except ValueError:
+            pass
+        lastcomp = None
+        lastcompdt = None
+        for comp in self._comps:
+            if not comp.isdst:
+                # Handle the extra hour in DST -> STD
+                compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
+            else:
+                compdt = comp.rrule.before(dt, inc=True)
+            if compdt and (not lastcompdt or lastcompdt < compdt):
+                lastcompdt = compdt
+                lastcomp = comp
+        if not lastcomp:
+            # RFC says nothing about what to do when a given
+            # time is before the first onset date. We'll look for the
+            # first standard component, or the first component, if
+            # none is found.
+            for comp in self._comps:
+                if not comp.isdst:
+                    lastcomp = comp
+                    break
+            else:
+                lastcomp = comp[0]
+        self._cachedate.insert(0, dt)
+        self._cachecomp.insert(0, lastcomp)
+        if len(self._cachedate) > 10:
+            self._cachedate.pop()
+            self._cachecomp.pop()
+        return lastcomp
+
+    def utcoffset(self, dt):
+        return self._find_comp(dt).tzoffsetto
+
+    def dst(self, dt):
+        comp = self._find_comp(dt)
+        if comp.isdst:
+            return comp.tzoffsetdiff
+        else:
+            return ZERO
+
+    def tzname(self, dt):
+        return self._find_comp(dt).tzname
+
+    def __repr__(self):
+        return "<tzicalvtz %s>" % `self._tzid`
+
+    __reduce__ = object.__reduce__
+
+class tzical:
+    def __init__(self, fileobj):
+        global rrule
+        if not rrule:
+            from dateutil import rrule
+
+        if isinstance(fileobj, basestring):
+            self._s = fileobj
+            fileobj = open(fileobj)
+        elif hasattr(fileobj, "name"):
+            self._s = fileobj.name
+        else:
+            self._s = `fileobj`
+
+        self._vtz = {}
+
+        self._parse_rfc(fileobj.read())
+
+    def keys(self):
+        return self._vtz.keys()
+
+    def get(self, tzid=None):
+        if tzid is None:
+            keys = self._vtz.keys()
+            if len(keys) == 0:
+                raise ValueError, "no timezones defined"
+            elif len(keys) > 1:
+                raise ValueError, "more than one timezone available"
+            tzid = keys[0]
+        return self._vtz.get(tzid)
+
+    def _parse_offset(self, s):
+        s = s.strip()
+        if not s:
+            raise ValueError, "empty offset"
+        if s[0] in ('+', '-'):
+            signal = (-1,+1)[s[0]=='+']
+            s = s[1:]
+        else:
+            signal = +1
+        if len(s) == 4:
+            return (int(s[:2])*3600+int(s[2:])*60)*signal
+        elif len(s) == 6:
+            return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
+        else:
+            raise ValueError, "invalid offset: "+s
+
+    def _parse_rfc(self, s):
+        lines = s.splitlines()
+        if not lines:
+            raise ValueError, "empty string"
+
+        # Unfold
+        i = 0
+        while i < len(lines):
+            line = lines[i].rstrip()
+            if not line:
+                del lines[i]
+            elif i > 0 and line[0] == " ":
+                lines[i-1] += line[1:]
+                del lines[i]
+            else:
+                i += 1
+
+        tzid = None
+        comps = []
+        invtz = False
+        comptype = None
+        for line in lines:
+            if not line:
+                continue
+            name, value = line.split(':', 1)
+            parms = name.split(';')
+            if not parms:
+                raise ValueError, "empty property name"
+            name = parms[0].upper()
+            parms = parms[1:]
+            if invtz:
+                if name == "BEGIN":
+                    if value in ("STANDARD", "DAYLIGHT"):
+                        # Process component
+                        pass
+                    else:
+                        raise ValueError, "unknown component: "+value
+                    comptype = value
+                    founddtstart = False
+                    tzoffsetfrom = None
+                    tzoffsetto = None
+                    rrulelines = []
+                    tzname = None
+                elif name == "END":
+                    if value == "VTIMEZONE":
+                        if comptype:
+                            raise ValueError, \
+                                  "component not closed: "+comptype
+                        if not tzid:
+                            raise ValueError, \
+                                  "mandatory TZID not found"
+                        if not comps:
+                            raise ValueError, \
+                                  "at least one component is needed"
+                        # Process vtimezone
+                        self._vtz[tzid] = _tzicalvtz(tzid, comps)
+                        invtz = False
+                    elif value == comptype:
+                        if not founddtstart:
+                            raise ValueError, \
+                                  "mandatory DTSTART not found"
+                        if tzoffsetfrom is None:
+                            raise ValueError, \
+                                  "mandatory TZOFFSETFROM not found"
+                        if tzoffsetto is None:
+                            raise ValueError, \
+                                  "mandatory TZOFFSETFROM not found"
+                        # Process component
+                        rr = None
+                        if rrulelines:
+                            rr = rrule.rrulestr("\n".join(rrulelines),
+                                                compatible=True,
+                                                ignoretz=True,
+                                                cache=True)
+                        comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
+                                              (comptype == "DAYLIGHT"),
+                                              tzname, rr)
+                        comps.append(comp)
+                        comptype = None
+                    else:
+                        raise ValueError, \
+                              "invalid component end: "+value
+                elif comptype:
+                    if name == "DTSTART":
+                        rrulelines.append(line)
+                        founddtstart = True
+                    elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
+                        rrulelines.append(line)
+                    elif name == "TZOFFSETFROM":
+                        if parms:
+                            raise ValueError, \
+                                  "unsupported %s parm: %s "%(name, parms[0])
+                        tzoffsetfrom = self._parse_offset(value)
+                    elif name == "TZOFFSETTO":
+                        if parms:
+                            raise ValueError, \
+                                  "unsupported TZOFFSETTO parm: "+parms[0]
+                        tzoffsetto = self._parse_offset(value)
+                    elif name == "TZNAME":
+                        if parms:
+                            raise ValueError, \
+                                  "unsupported TZNAME parm: "+parms[0]
+                        tzname = value
+                    elif name == "COMMENT":
+                        pass
+                    else:
+                        raise ValueError, "unsupported property: "+name
+                else:
+                    if name == "TZID":
+                        if parms:
+                            raise ValueError, \
+                                  "unsupported TZID parm: "+parms[0]
+                        tzid = value
+                    elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
+                        pass
+                    else:
+                        raise ValueError, "unsupported property: "+name
+            elif name == "BEGIN" and value == "VTIMEZONE":
+                tzid = None
+                comps = []
+                invtz = True
+
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, `self._s`)
+
+if sys.platform != "win32":
+    TZFILES = ["/etc/localtime", "localtime"]
+    TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"]
+else:
+    TZFILES = []
+    TZPATHS = []
+
+def gettz(name=None):
+    tz = None
+    if not name:
+        try:
+            name = os.environ["TZ"]
+        except KeyError:
+            pass
+    if name is None or name == ":":
+        for filepath in TZFILES:
+            if not os.path.isabs(filepath):
+                filename = filepath
+                for path in TZPATHS:
+                    filepath = os.path.join(path, filename)
+                    if os.path.isfile(filepath):
+                        break
+                else:
+                    continue
+            if os.path.isfile(filepath):
+                try:
+                    tz = tzfile(filepath)
+                    break
+                except (IOError, OSError, ValueError):
+                    pass
+        else:
+            tz = tzlocal()
+    else:
+        if name.startswith(":"):
+            name = name[:-1]
+        if os.path.isabs(name):
+            if os.path.isfile(name):
+                tz = tzfile(name)
+            else:
+                tz = None
+        else:
+            for path in TZPATHS:
+                filepath = os.path.join(path, name)
+                if not os.path.isfile(filepath):
+                    filepath = filepath.replace(' ','_')
+                    if not os.path.isfile(filepath):
+                        continue
+                try:
+                    tz = tzfile(filepath)
+                    break
+                except (IOError, OSError, ValueError):
+                    pass
+            else:
+                tz = None
+                if tzwin:
+                    try:
+                        tz = tzwin(name)
+                    except OSError:
+                        pass
+                if not tz:
+                    from dateutil.zoneinfo import gettz
+                    tz = gettz(name)
+                if not tz:
+                    for c in name:
+                        # name must have at least one offset to be a tzstr
+                        if c in "0123456789":
+                            try:
+                                tz = tzstr(name)
+                            except ValueError:
+                                pass
+                            break
+                    else:
+                        if name in ("GMT", "UTC"):
+                            tz = tzutc()
+                        elif name in time.tzname:
+                            tz = tzlocal()
+    return tz
+
+# vim:ts=4:sw=4:et
diff --git a/lib/dateutil/tzwin.py b/lib/dateutil/tzwin.py
new file mode 100644
index 0000000000000000000000000000000000000000..073e0ff68e3fb586af05908407982cfec34599b5
--- /dev/null
+++ b/lib/dateutil/tzwin.py
@@ -0,0 +1,180 @@
+# This code was originally contributed by Jeffrey Harris.
+import datetime
+import struct
+import _winreg
+
+__author__ = "Jeffrey Harris & Gustavo Niemeyer <gustavo@niemeyer.net>"
+
+__all__ = ["tzwin", "tzwinlocal"]
+
+ONEWEEK = datetime.timedelta(7)
+
+TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
+TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+
+def _settzkeyname():
+    global TZKEYNAME
+    handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
+    try:
+        _winreg.OpenKey(handle, TZKEYNAMENT).Close()
+        TZKEYNAME = TZKEYNAMENT
+    except WindowsError:
+        TZKEYNAME = TZKEYNAME9X
+    handle.Close()
+
+_settzkeyname()
+
+class tzwinbase(datetime.tzinfo):
+    """tzinfo class based on win32's timezones available in the registry."""
+
+    def utcoffset(self, dt):
+        if self._isdst(dt):
+            return datetime.timedelta(minutes=self._dstoffset)
+        else:
+            return datetime.timedelta(minutes=self._stdoffset)
+
+    def dst(self, dt):
+        if self._isdst(dt):
+            minutes = self._dstoffset - self._stdoffset
+            return datetime.timedelta(minutes=minutes)
+        else:
+            return datetime.timedelta(0)
+        
+    def tzname(self, dt):
+        if self._isdst(dt):
+            return self._dstname
+        else:
+            return self._stdname
+
+    def list():
+        """Return a list of all time zones known to the system."""
+        handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
+        tzkey = _winreg.OpenKey(handle, TZKEYNAME)
+        result = [_winreg.EnumKey(tzkey, i)
+                  for i in range(_winreg.QueryInfoKey(tzkey)[0])]
+        tzkey.Close()
+        handle.Close()
+        return result
+    list = staticmethod(list)
+
+    def display(self):
+        return self._display
+    
+    def _isdst(self, dt):
+        dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek,
+                               self._dsthour, self._dstminute,
+                               self._dstweeknumber)
+        dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek,
+                                self._stdhour, self._stdminute,
+                                self._stdweeknumber)
+        if dston < dstoff:
+            return dston <= dt.replace(tzinfo=None) < dstoff
+        else:
+            return not dstoff <= dt.replace(tzinfo=None) < dston
+
+
+class tzwin(tzwinbase):
+
+    def __init__(self, name):
+        self._name = name
+
+        handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
+        tzkey = _winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name))
+        keydict = valuestodict(tzkey)
+        tzkey.Close()
+        handle.Close()
+
+        self._stdname = keydict["Std"].encode("iso-8859-1")
+        self._dstname = keydict["Dlt"].encode("iso-8859-1")
+
+        self._display = keydict["Display"]
+        
+        # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+        tup = struct.unpack("=3l16h", keydict["TZI"])
+        self._stdoffset = -tup[0]-tup[1]         # Bias + StandardBias * -1
+        self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1
+        
+        (self._stdmonth,
+         self._stddayofweek,  # Sunday = 0
+         self._stdweeknumber, # Last = 5
+         self._stdhour,
+         self._stdminute) = tup[4:9]
+
+        (self._dstmonth,
+         self._dstdayofweek,  # Sunday = 0
+         self._dstweeknumber, # Last = 5
+         self._dsthour,
+         self._dstminute) = tup[12:17]
+
+    def __repr__(self):
+        return "tzwin(%s)" % repr(self._name)
+
+    def __reduce__(self):
+        return (self.__class__, (self._name,))
+
+
+class tzwinlocal(tzwinbase):
+    
+    def __init__(self):
+
+        handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
+
+        tzlocalkey = _winreg.OpenKey(handle, TZLOCALKEYNAME)
+        keydict = valuestodict(tzlocalkey)
+        tzlocalkey.Close()
+
+        self._stdname = keydict["StandardName"].encode("iso-8859-1")
+        self._dstname = keydict["DaylightName"].encode("iso-8859-1")
+
+        try:
+            tzkey = _winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname))
+            _keydict = valuestodict(tzkey)
+            self._display = _keydict["Display"]
+            tzkey.Close()
+        except OSError:
+            self._display = None
+
+        handle.Close()
+        
+        self._stdoffset = -keydict["Bias"]-keydict["StandardBias"]
+        self._dstoffset = self._stdoffset-keydict["DaylightBias"]
+
+
+        # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+        tup = struct.unpack("=8h", keydict["StandardStart"])
+
+        (self._stdmonth,
+         self._stddayofweek,  # Sunday = 0
+         self._stdweeknumber, # Last = 5
+         self._stdhour,
+         self._stdminute) = tup[1:6]
+
+        tup = struct.unpack("=8h", keydict["DaylightStart"])
+
+        (self._dstmonth,
+         self._dstdayofweek,  # Sunday = 0
+         self._dstweeknumber, # Last = 5
+         self._dsthour,
+         self._dstminute) = tup[1:6]
+
+    def __reduce__(self):
+        return (self.__class__, ())
+
+def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
+    """dayofweek == 0 means Sunday, whichweek 5 means last instance"""
+    first = datetime.datetime(year, month, 1, hour, minute)
+    weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1))
+    for n in xrange(whichweek):
+        dt = weekdayone+(whichweek-n)*ONEWEEK
+        if dt.month == month:
+            return dt
+
+def valuestodict(key):
+    """Convert a registry key's values to a dictionary."""
+    dict = {}
+    size = _winreg.QueryInfoKey(key)[1]
+    for i in range(size):
+        data = _winreg.EnumValue(key, i)
+        dict[data[0]] = data[1]
+    return dict
diff --git a/lib/dateutil/zoneinfo/__init__.py b/lib/dateutil/zoneinfo/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9bed6264c8b97230e787e31d56a2b7318d9f63f9
--- /dev/null
+++ b/lib/dateutil/zoneinfo/__init__.py
@@ -0,0 +1,87 @@
+"""
+Copyright (c) 2003-2005  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+from dateutil.tz import tzfile
+from tarfile import TarFile
+import os
+
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+__all__ = ["setcachesize", "gettz", "rebuild"]
+
+CACHE = []
+CACHESIZE = 10
+
+class tzfile(tzfile):
+    def __reduce__(self):
+        return (gettz, (self._filename,))
+
+def getzoneinfofile():
+    filenames = os.listdir(os.path.join(os.path.dirname(__file__)))
+    filenames.sort()
+    filenames.reverse()
+    for entry in filenames:
+        if entry.startswith("zoneinfo") and ".tar." in entry:
+            return os.path.join(os.path.dirname(__file__), entry)
+    return None
+
+ZONEINFOFILE = getzoneinfofile()
+
+del getzoneinfofile
+
+def setcachesize(size):
+    global CACHESIZE, CACHE
+    CACHESIZE = size
+    del CACHE[size:]
+
+def gettz(name):
+    tzinfo = None
+    if ZONEINFOFILE:
+        for cachedname, tzinfo in CACHE:
+            if cachedname == name:
+                break
+        else:
+            tf = TarFile.open(ZONEINFOFILE)
+            try:
+                zonefile = tf.extractfile(name)
+            except KeyError:
+                tzinfo = None
+            else:
+                tzinfo = tzfile(zonefile)
+            tf.close()
+            CACHE.insert(0, (name, tzinfo))
+            del CACHE[CACHESIZE:]
+    return tzinfo
+
+def rebuild(filename, tag=None, format="gz"):
+    import tempfile, shutil
+    tmpdir = tempfile.mkdtemp()
+    zonedir = os.path.join(tmpdir, "zoneinfo")
+    moduledir = os.path.dirname(__file__)
+    if tag: tag = "-"+tag
+    targetname = "zoneinfo%s.tar.%s" % (tag, format)
+    try:
+        tf = TarFile.open(filename)
+        for name in tf.getnames():
+            if not (name.endswith(".sh") or
+                    name.endswith(".tab") or
+                    name == "leapseconds"):
+                tf.extract(name, tmpdir)
+                filepath = os.path.join(tmpdir, name)
+                os.system("zic -d %s %s" % (zonedir, filepath))
+        tf.close()
+        target = os.path.join(moduledir, targetname)
+        for entry in os.listdir(moduledir):
+            if entry.startswith("zoneinfo") and ".tar." in entry:
+                os.unlink(os.path.join(moduledir, entry))
+        tf = TarFile.open(target, "w:%s" % format)
+        for entry in os.listdir(zonedir):
+            entrypath = os.path.join(zonedir, entry)
+            tf.add(entrypath, entry)
+        tf.close()
+    finally:
+        shutil.rmtree(tmpdir)
diff --git a/lib/dateutil/zoneinfo/zoneinfo-2010g.tar.gz b/lib/dateutil/zoneinfo/zoneinfo-2010g.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..8bd4f96402be50779e4b2749688d077347a6eef0
Binary files /dev/null and b/lib/dateutil/zoneinfo/zoneinfo-2010g.tar.gz differ
diff --git a/readme.md b/readme.md
index 5d7b62f21251714acb8cc7c88f3baa1b3fc3230f..0fae42e6e0317e48f19e989d6f6627f7b62c54f0 100644
--- a/readme.md
+++ b/readme.md
@@ -29,6 +29,7 @@ Sick Beard makes use of the following projects:
 * [jQuery][jquery]
 * [Python GNTP][pythongntp]
 * [SocksiPy][socks]
+* [python-dateutil][dateutil]
 
 ## Dependencies
 
@@ -47,6 +48,7 @@ If you find a bug please report it or it'll never get fixed. Verify that it hasn
 [jquery]: http://jquery.com
 [pythongntp]: http://github.com/kfdm/gntp
 [socks]: http://code.google.com/p/socksipy-branch/
+[dateutil]: http://labix.org/python-dateutil
 [googledownloads]: http://code.google.com/p/sickbeard/downloads/list
 [googleissues]: http://code.google.com/p/sickbeard/issues/list
 [googlenewissue]: http://code.google.com/p/sickbeard/issues/entry
\ No newline at end of file
diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py
index f45986f0552324007ff9b4b37ad7a42d16426c08..a96e595879b7b98f55984587dcd27bd405353253 100755
--- a/sickbeard/__init__.py
+++ b/sickbeard/__init__.py
@@ -915,7 +915,7 @@ def save_config():
     new_config['General']['log_dir'] = LOG_DIR
     new_config['General']['web_port'] = WEB_PORT
     new_config['General']['web_host'] = WEB_HOST
-    new_config['General']['web_ipv6'] = WEB_IPV6
+    new_config['General']['web_ipv6'] = int(WEB_IPV6)
     new_config['General']['web_log'] = int(WEB_LOG)
     new_config['General']['web_root'] = WEB_ROOT
     new_config['General']['web_username'] = WEB_USERNAME
diff --git a/sickbeard/browser.py b/sickbeard/browser.py
index c89728e137ae992b31a6f27b581f7d7e88056202..efbb3c0697a904ea136b588272191d0ba74deb74 100644
--- a/sickbeard/browser.py
+++ b/sickbeard/browser.py
@@ -34,6 +34,7 @@ if os.name == 'nt':
 
 # adapted from http://stackoverflow.com/questions/827371/is-there-a-way-to-list-all-the-available-drive-letters-in-python/827490
 def getWinDrives():
+    """ Return list of detected drives """
     assert os.name == 'nt'
 
     drives = []
@@ -45,10 +46,12 @@ def getWinDrives():
 
     return drives
 
-# Returns a list of dictionaries with the folders contained at the given path
-# Give the empty string as the path to list the contents of the root path
-# (under Unix this means "/", on Windows this will be a list of drive letters)
+
 def foldersAtPath(path, includeParent = False):
+    """ Returns a list of dictionaries with the folders contained at the given path
+        Give the empty string as the path to list the contents of the root path
+        under Unix this means "/", on Windows this will be a list of drive letters)
+    """
     assert os.path.isabs(path) or path == ""
 
     # walk up the tree until we find a valid path
@@ -96,7 +99,7 @@ class WebFileBrowser:
         return json.dumps(foldersAtPath(path, True))
 
     @cherrypy.expose
-    def complete(self, q, limit=30, timestamp=None):
-        cherrypy.response.headers['Content-Type'] = "text/plain"
-        paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(q)) if 'path' in entry]
-        return "\n".join(paths[0:int(limit)])
+    def complete(self, term):
+        cherrypy.response.headers['Content-Type'] = "application/json"
+        paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term)) if 'path' in entry]
+        return json.dumps( paths )
diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py
index 959f0815181afb0d42fa505bb7e804a4cb1c014e..f5aeb5e0f7ba5e2b75e4dab65edf12da3e89f6eb 100644
--- a/sickbeard/helpers.py
+++ b/sickbeard/helpers.py
@@ -103,11 +103,17 @@ def sanitizeFileName (name):
     'abc'
     >>> sanitizeFileName('a"b')
     'ab'
+    >>> sanitizeFileName('.a.b..')
+    'a.b'
     '''
-    for x in "\\/*":
-        name = name.replace(x, "-")
-    for x in ":\"<>|?":
-        name = name.replace(x, "")
+    
+    # remove bad chars from the filename
+    name = re.sub(r'[\\/\*]', '-', name)
+    name = re.sub(r'[:"<>|?]', '', name)
+    
+    # remove leading/trailing periods
+    name = re.sub(r'(^\.+|\.+$)', '', name)
+    
     return name
 
 
diff --git a/sickbeard/metadata/tivo.py b/sickbeard/metadata/tivo.py
index eee65a8a7c83f2f7023309dd9a6302be8f832b9b..bd940103ac8576ac0682e9bb8ca7c6c5c8906641 100644
--- a/sickbeard/metadata/tivo.py
+++ b/sickbeard/metadata/tivo.py
@@ -199,7 +199,15 @@ class TIVOMetadata(generic.GenericMetadata):
             
             
             # Write the synopsis of the video here. 
-            data += ("description : " + curEpToWrite.description + "\n")
+            
+            # Micrsoft Word's smartquotes can die in a fire.
+            sanitizedDescription = curEpToWrite.description
+            # Replace double curly quotes
+            sanitizedDescription = sanitizedDescription.replace(u"\u201c", "\"").replace(u"\u201d", "\"")
+            # Replace single curly quotes
+            sanitizedDescription = sanitizedDescription.replace(u"\u2018", "'").replace(u"\u2019", "'").replace(u"\u02BC", "'")
+
+            data += ("description : " + sanitizedDescription + "\n")
             
             
             # Usually starts with "SH" and followed by 6-8 digits.
@@ -224,7 +232,7 @@ class TIVOMetadata(generic.GenericMetadata):
             if myShow["actors"]:
                 for actor in myShow["actors"].split('|'):
                     if actor:
-                        data += ("vActor : " + str(actor) + "\n")
+                        data += ("vActor : " + actor + "\n")
                     
                
             # This is shown on both the Program screen and the Details screen. It uses a single digit to determine the 
diff --git a/sickbeard/name_parser/parser.py b/sickbeard/name_parser/parser.py
index ed844bcdea1b501b37a8d586d4cfc5d11997ff3a..f44f8656223d4da6d8a1add99736a37af0edd5aa 100644
--- a/sickbeard/name_parser/parser.py
+++ b/sickbeard/name_parser/parser.py
@@ -282,7 +282,10 @@ class ParseResult(object):
         return True
 
     def __str__(self):
-        to_return = str(self.series_name) + ' - '
+        if self.series_name != None:
+            to_return = self.series_name + u' - '
+        else:
+            to_return = u''
         if self.season_number != None:
             to_return += 'S'+str(self.season_number)
         if self.episode_numbers and len(self.episode_numbers):
diff --git a/sickbeard/name_parser/regexes.py b/sickbeard/name_parser/regexes.py
index f3cc29c3e56aece1756a81f9396020d0133a996a..e7b2900d5d90f5aa1a13a6039cd7a87def10eed3 100644
--- a/sickbeard/name_parser/regexes.py
+++ b/sickbeard/name_parser/regexes.py
@@ -95,6 +95,7 @@ ep_regexes = [
                # tpz-abc102
                '''
                (?P<release_group>.+?)-\w+?[\. ]?           # tpz-abc
+               (?!264)                                     # don't count x264
                (?P<season_num>\d{1,2})                     # 1
                (?P<ep_num>\d{2})$                          # 02
                '''),
diff --git a/sickbeard/notifiers/xbmc.py b/sickbeard/notifiers/xbmc.py
index f967e4b88c6a9d0301a1c6c49afbb3b768286883..6adc3d861282df07d538b5d06128574db21f646c 100644
--- a/sickbeard/notifiers/xbmc.py
+++ b/sickbeard/notifiers/xbmc.py
@@ -27,6 +27,7 @@ import sickbeard
 from sickbeard import logger
 from sickbeard import common
 from sickbeard.exceptions import ex
+from sickbeard.encodingKludge import fixStupidEncodings
 
 try:
     import xml.etree.cElementTree as etree
@@ -105,7 +106,7 @@ class XBMCNotifier:
             response = handle.read()
             logger.log(u"response: " + response, logger.DEBUG)
         except IOError, e:
-            logger.log(u"Warning: Couldn't contact XBMC HTTP server at " + host + ": " + ex(e))
+            logger.log(u"Warning: Couldn't contact XBMC HTTP server at " + fixStupidEncodings(host) + ": " + ex(e))
             response = ''
     
         return response
diff --git a/sickbeard/providers/newzbin.py b/sickbeard/providers/newzbin.py
index e995204fdf108b6138b89f23a4645a7bd706b674..f02516226215f035a9b938fea4a1530d88b99199 100644
--- a/sickbeard/providers/newzbin.py
+++ b/sickbeard/providers/newzbin.py
@@ -33,6 +33,7 @@ from sickbeard import classes, logger, helpers, exceptions, show_name_helpers
 from sickbeard import tvcache
 from sickbeard.common import Quality
 from sickbeard.exceptions import ex
+from lib.dateutil.parser import parse as parseDate
 
 class NewzbinDownloader(urllib.FancyURLopener):
 
@@ -287,7 +288,11 @@ class NewzbinProvider(generic.NZBProvider):
                 raise exceptions.AuthException("The feed wouldn't load, probably because of invalid auth info")
             if sickbeard.USENET_RETENTION is not None:
                 try:
-                    post_date = datetime.strptime(cur_item.findtext('{http://www.newzbin.com/DTD/2007/feeds/report/}postdate'), self.NEWZBIN_DATE_FORMAT)
+                    dateString = cur_item.findtext('{http://www.newzbin.com/DTD/2007/feeds/report/}postdate')
+                    # use the parse (imported as parseDate) function from the dateutil lib
+                    # and we have to remove the timezone info from it because the retention_date will not have one
+                    # and a comparison of them is not possible
+                    post_date = parseDate(dateString).replace(tzinfo=None)
                     retention_date = datetime.now() - timedelta(days=sickbeard.USENET_RETENTION)
                     if post_date < retention_date:
                         continue
diff --git a/sickbeard/providers/newznab.py b/sickbeard/providers/newznab.py
index 6950b05ffe5abf9edbe875ebe6d4ba9724ed7120..020f18e6088e3b9f61fd483cae65cef36925a296 100644
--- a/sickbeard/providers/newznab.py
+++ b/sickbeard/providers/newznab.py
@@ -30,6 +30,7 @@ import generic
 
 from sickbeard import classes
 from sickbeard.helpers import sanitizeSceneName
+from sickbeard import scene_exceptions
 from sickbeard import encodingKludge as ek
 
 from sickbeard import exceptions
@@ -69,37 +70,47 @@ class NewznabProvider(generic.NZBProvider):
 
 	def _get_season_search_strings(self, show, season=None):
 
-		params = {}
-
 		if not show:
-			return params
+			return [{}]
 		
-		# search directly by tvrage id
-		if show.tvrid:
-			params['rid'] = show.tvrid
-		# if we can't then fall back on a very basic name search
-		else:
-			params['q'] = sanitizeSceneName(show.name)
-
-		if season != None:
-			# air-by-date means &season=2010&q=2010.03, no other way to do it atm
-			if show.air_by_date:
-				params['season'] = season.split('-')[0]
-				if 'q' in params:
-					params['q'] += '.' + season.replace('-', '.')
-				else:
-					params['q'] = season.replace('-', '.')
+		to_return = []
+
+		# add new query strings for exceptions
+		name_exceptions = scene_exceptions.get_scene_exceptions(show.tvdbid) + [show.name]
+		for cur_exception in name_exceptions:
+		
+			cur_params = {}
+	
+			# search directly by tvrage id
+			if show.tvrid:
+				cur_params['rid'] = show.tvrid
+			# if we can't then fall back on a very basic name search
 			else:
-				params['season'] = season
+				cur_params['q'] = sanitizeSceneName(cur_exception)
+	
+			if season != None:
+				# air-by-date means &season=2010&q=2010.03, no other way to do it atm
+				if show.air_by_date:
+					cur_params['season'] = season.split('-')[0]
+					if 'q' in cur_params:
+						cur_params['q'] += '.' + season.replace('-', '.')
+					else:
+						cur_params['q'] = season.replace('-', '.')
+				else:
+					cur_params['season'] = season
 
-		return [params]
+			# hack to only add a single result if it's a rageid search
+			if not ('rid' in cur_params and to_return):
+				to_return.append(cur_params) 
+
+		return to_return
 
 	def _get_episode_search_strings(self, ep_obj):
 		
 		params = {}
 
 		if not ep_obj:
-			return params
+			return [params]
 		
 		# search directly by tvrage id
 		if ep_obj.show.tvrid:
@@ -117,7 +128,24 @@ class NewznabProvider(generic.NZBProvider):
 			params['season'] = ep_obj.season
 			params['ep'] = ep_obj.episode
 
-		return [params]
+		to_return = [params]
+
+		# only do exceptions if we are searching by name
+		if 'q' in params:
+
+			# add new query strings for exceptions
+			name_exceptions = scene_exceptions.get_scene_exceptions(ep_obj.show.tvdbid)
+			for cur_exception in name_exceptions:
+				
+				# don't add duplicates 
+				if cur_exception == ep_obj.show.name:
+					continue
+
+				cur_return = params.copy()
+				cur_return['q'] = sanitizeSceneName(cur_exception)
+				to_return.append(cur_return)
+
+		return to_return
 
 	def _doGeneralSearch(self, search_string):
 		return self._doSearch({'q': search_string})
diff --git a/sickbeard/providers/nzbmatrix.py b/sickbeard/providers/nzbmatrix.py
index 316f6cad48f1e146476e718fdafc0f41ab3ed723..d84446c79b91b81481c433c23f4c6b682ff11c0a 100644
--- a/sickbeard/providers/nzbmatrix.py
+++ b/sickbeard/providers/nzbmatrix.py
@@ -78,7 +78,7 @@ class NZBMatrixProvider(generic.NZBProvider):
         if show and show.genre and 'documentary' in show.genre.lower():
             params['subcat'] = params['subcat'] + ',53,9' 
 
-        searchURL = "http://rss.nzbmatrix.com/rss.php?" + urllib.urlencode(params)
+        searchURL = "https://rss.nzbmatrix.com/rss.php?" + urllib.urlencode(params)
 
         logger.log(u"Search string: " + searchURL, logger.DEBUG)
 
@@ -150,7 +150,7 @@ class NZBMatrixCache(tvcache.TVCache):
 
     def _getRSSData(self):
         # get all records since the last timestamp
-        url = "http://rss.nzbmatrix.com/rss.php?"
+        url = "https://rss.nzbmatrix.com/rss.php?"
 
         urlArgs = {'page': 'download',
                    'username': sickbeard.NZBMATRIX_USERNAME,
diff --git a/sickbeard/search.py b/sickbeard/search.py
index 79879224a867089f96877fb45bc7690b7f44d4e3..52b1e12352a27ab00baf5dac25b8cf2c44f101f5 100644
--- a/sickbeard/search.py
+++ b/sickbeard/search.py
@@ -85,7 +85,7 @@ def _downloadResult(result):
         return False
 
     if newResult:
-        ui.notifications.message('Episode <b>%s</b> snatched from <b>%s</b>' % (result.name, resProvider.name))
+        ui.notifications.message('Episode snatched','<b>%s</b> snatched from <b>%s</b>' % (result.name, resProvider.name))
 
     return newResult
 
diff --git a/sickbeard/tv.py b/sickbeard/tv.py
index e75ca2ad5103c7a6f58002520155e195c9665400..955060115e9657e5dddc14f04de6b91876c26885 100644
--- a/sickbeard/tv.py
+++ b/sickbeard/tv.py
@@ -231,10 +231,16 @@ class TVShow(object):
 
         for curResult in sqlResults:
 
+            deleteEp = False
+                    
             curSeason = int(curResult["season"])
             curEpisode = int(curResult["episode"])
             if curSeason not in cachedSeasons:
-                cachedSeasons[curSeason] = cachedShow[curSeason]
+                try:
+                    cachedSeasons[curSeason] = cachedShow[curSeason]
+                except tvdb_exceptions.tvdb_seasonnotfound, e:
+                    logger.log(u"Error when trying to load the episode from TVDB: "+e.message, logger.WARNING)
+                    deleteEp = True
 
             if not curSeason in scannedEps:
                 scannedEps[curSeason] = {}
@@ -243,6 +249,11 @@ class TVShow(object):
 
             try:
                 curEp = self.getEpisode(curSeason, curEpisode)
+                
+                # if we found out that the ep is no longer on TVDB then delete it from our database too
+                if deleteEp:
+                    curEp.deleteEpisode()
+                
                 curEp.loadFromDB(curSeason, curEpisode)
                 curEp.loadFromTVDB(tvapi=t, cachedSeason=cachedSeasons[curSeason])
                 scannedEps[curSeason][curEpisode] = True
diff --git a/sickbeard/version.py b/sickbeard/version.py
index 4df29c1bb34eb3599fb4069415d94a14f525cd04..a70a90d39ab5ebc5b75459add4c19482f657445d 100644
--- a/sickbeard/version.py
+++ b/sickbeard/version.py
@@ -1 +1 @@
-SICKBEARD_VERSION = "master"
\ No newline at end of file
+SICKBEARD_VERSION = "build Nonea"
\ No newline at end of file
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index 5cb7e3ff6b14cdfe5fef7f320e8304e9f33afbca..23c508d7da3152cda82d6183270cced744569e3c 100755
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -122,10 +122,10 @@ def _getEpisode(show, season, episode):
     return epObj
 
 ManageMenu = [
-            { 'title': 'Backlog Overview', 'path': 'manage/backlogOverview' },
-            { 'title': 'Manage Searches', 'path': 'manage/manageSearches' },
-            { 'title': 'Episode Status Management', 'path': 'manage/episodeStatuses' },
-            ]
+    { 'title': 'Backlog Overview',          'path': 'manage/backlogOverview' },
+    { 'title': 'Manage Searches',           'path': 'manage/manageSearches'  },
+    { 'title': 'Episode Status Management', 'path': 'manage/episodeStatuses' },
+]
 
 class ManageSearches:
 
@@ -1345,12 +1345,12 @@ def havePLEX():
 
 def HomeMenu():
     return [
-    { 'title': 'Add Shows',               'path': 'home/addShows/',                         },
-    { 'title': 'Manual Post-Processing', 'path': 'home/postprocess/'                        },
-    { 'title': 'Update XBMC',            'path': 'home/updateXBMC/', 'requires': haveXBMC   },
-    { 'title': 'Update Plex',            'path': 'home/updatePLEX/', 'requires': havePLEX   },
-    { 'title': 'Restart',                'path': 'home/restart/?pid='+str(sickbeard.PID)    },
-    { 'title': 'Shutdown',               'path': 'home/shutdown/'                           },
+        { 'title': 'Add Shows',              'path': 'home/addShows/',                                          },
+        { 'title': 'Manual Post-Processing', 'path': 'home/postprocess/'                                        },
+        { 'title': 'Update XBMC',            'path': 'home/updateXBMC/', 'requires': haveXBMC                   },
+        { 'title': 'Update Plex',            'path': 'home/updatePLEX/', 'requires': havePLEX                   },
+        { 'title': 'Restart',                'path': 'home/restart/?pid='+str(sickbeard.PID), 'confirm': True   },
+        { 'title': 'Shutdown',               'path': 'home/shutdown/', 'confirm': True                          },
     ]
 
 class HomePostProcess:
@@ -1986,7 +1986,7 @@ class Home:
         )
 
         t = PageTemplate(file="displayShow.tmpl")
-        t.submenu = [ { 'title': 'Edit',              'path': 'home/editShow?show=%d'%showObj.tvdbid } ]
+        t.submenu = [ { 'title': 'Edit', 'path': 'home/editShow?show=%d'%showObj.tvdbid } ]
 
         try:
             t.showLoc = (showObj.location, True)
@@ -2012,11 +2012,11 @@ class Home:
 
         if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable
             if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable
-                t.submenu.append({ 'title': 'Delete',            'path': 'home/deleteShow?show=%d'%showObj.tvdbid, 'confirm': True })
-                t.submenu.append({ 'title': 'Re-scan files',           'path': 'home/refreshShow?show=%d'%showObj.tvdbid })
-                t.submenu.append({ 'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&amp;force=1'%showObj.tvdbid })
-                t.submenu.append({ 'title': 'Update show in XBMC', 'path': 'home/updateXBMC?showName=%s'%urllib.quote_plus(showObj.name.encode('utf-8')), 'requires': haveXBMC })
-            t.submenu.append({ 'title': 'Rename Episodes',   'path': 'home/fixEpisodeNames?show=%d'%showObj.tvdbid, 'confirm': True })
+                t.submenu.append({ 'title': 'Delete',               'path': 'home/deleteShow?show=%d'%showObj.tvdbid, 'confirm': True })
+                t.submenu.append({ 'title': 'Re-scan files',        'path': 'home/refreshShow?show=%d'%showObj.tvdbid })
+                t.submenu.append({ 'title': 'Force Full Update',    'path': 'home/updateShow?show=%d&amp;force=1'%showObj.tvdbid })
+                t.submenu.append({ 'title': 'Update show in XBMC',  'path': 'home/updateXBMC?showName=%s'%urllib.quote_plus(showObj.name.encode('utf-8')), 'requires': haveXBMC })
+                t.submenu.append({ 'title': 'Rename Episodes',      'path': 'home/fixEpisodeNames?show=%d'%showObj.tvdbid, 'confirm': True })
 
         t.show = showObj
         t.sqlResults = sqlResults
@@ -2455,9 +2455,10 @@ class WebInterface:
                     size = 136, 200
                 else:
                     return cherrypy.lib.static.serve_file(image_file_name, content_type="image/jpeg")
-                im.thumbnail(size, Image.ANTIALIAS)
+                im = im.resize(size, Image.ANTIALIAS)
                 buffer = StringIO()
-                im.save(buffer, 'JPEG')
+                im.save(buffer, 'JPEG', quality=85)
+                cherrypy.response.headers['Content-Type'] = 'image/jpeg'
                 return buffer.getvalue()
         else:
             return cherrypy.lib.static.serve_file(default_image_path, content_type="image/jpeg")
@@ -2528,7 +2529,7 @@ class WebInterface:
                                             'Show': 'setComingEpsSort/?sort=show',
                                             'Network': 'setComingEpsSort/?sort=network',
                                            }},
-                                           
+
             { 'title': 'Layout:', 'path': {'Banner': 'setComingEpsLayout/?layout=banner',
                                            'Poster': 'setComingEpsLayout/?layout=poster',
                                            'List': 'setComingEpsLayout/?layout=list',
diff --git a/tests/name_parser_tests.py b/tests/name_parser_tests.py
index 1e62cf0f2a94a9c1f40f0cbca8fa34b4752b60fd..cadad88db04aff26f003f1fba0749d875c3f95d0 100644
--- a/tests/name_parser_tests.py
+++ b/tests/name_parser_tests.py
@@ -138,6 +138,9 @@ unicode_test_cases = [
                        ),
                       ]
 
+failure_cases = ['7sins-jfcs01e09-720p-bluray-x264']
+
+
 class UnicodeTests(unittest.TestCase):
     
     def _test_unicode(self, name, result):
@@ -150,6 +153,23 @@ class UnicodeTests(unittest.TestCase):
         for (name, result) in unicode_test_cases:
             self._test_unicode(name, result)
 
+class FailureCaseTests(unittest.TestCase):
+    
+    def _test_name(self, name):
+        np = parser.NameParser(True)
+        try:
+            parse_result = np.parse(name)
+        except parser.InvalidNameException:
+            return True
+        
+        if VERBOSE:
+            print 'Actual: ', parse_result.which_regex, parse_result
+        return False
+    
+    def test_failures(self):
+        for name in failure_cases:
+            self.assertTrue(self._test_name(name))
+
 class ComboTests(unittest.TestCase):
     
     def _test_combo(self, name, result, which_regexes):
@@ -308,3 +328,6 @@ if __name__ == '__main__':
 
     suite = unittest.TestLoader().loadTestsFromTestCase(UnicodeTests)
     unittest.TextTestRunner(verbosity=2).run(suite)
+
+    suite = unittest.TestLoader().loadTestsFromTestCase(FailureCaseTests)
+    unittest.TextTestRunner(verbosity=2).run(suite)