diff --git a/README.md b/README.md
index f30577f2..cde0f863 100644
--- a/README.md
+++ b/README.md
@@ -1,63 +1,63 @@
-#Headphones
-
-###Support & Discuss
-
-You are free to join the HP support community on IRC where you can ask questions, hang around and discuss anything related to HP.
-
-1. Use any IRC client and connect to the Freenode server.
-2. Join #headphones
-
-###Installation and Notes
-
-[Read our Wiki](../../wiki) on how to install and use HeadPhones properly.
-
-**Issues** can be reported on the GitHub issue tracker considering these rules:
-
-1. Analyze your log, you just might find the solution yourself!
-2. You read the wiki and searched existing issues, but this is not solving your problem.
-3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks).
-4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
-
-**Feature requests** can be reported on the GitHub issue tracker too:
-
-1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'.
-2. If a similar Request exists, post a comment (+1, or add a new idea to the existing request), otherwise you can create a new one.
-
-If you **comply with these rules** you can [post your request/issue](http://github.com/rembo10/headphones/issues).
-
-**Support** the project by implementing new features, solving support tickets and provide bug fixes.
-If you change something in the code always make a PR to the developer branch instead of the master branch.
-
-
-###Screenshots
-
-Homepage (Artist Overview)
-
-
-
-One of the many settings pages....
-
-
-
-It might even know you better than you know yourself:
-
-
-
-Import Your Favorite Artists:
-
-
-
-Artist Search Results (also search by album!):
-
-
-
-Artist Page with Bio & Album Overview:
-
-
-
-Album Page with track overview:
-
-
-
-
-This is free software under the GPL v3 open source license - so feel free to do with it what you wish.
+#Headphones
+
+###Support & Discuss
+
+You are free to join the HP support community on IRC where you can ask questions, hang around and discuss anything related to HP.
+
+1. Use any IRC client and connect to the Freenode server.
+2. Join #headphones
+
+###Installation and Notes
+
+[Read our Wiki](../../wiki) on how to install and use HeadPhones properly.
+
+**Issues** can be reported on the GitHub issue tracker considering these rules:
+
+1. Analyze your log, you just might find the solution yourself!
+2. You read the wiki and searched existing issues, but this is not solving your problem.
+3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks).
+4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
+
+**Feature requests** can be reported on the GitHub issue tracker too:
+
+1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'.
+2. If a similar Request exists, post a comment (+1, or add a new idea to the existing request), otherwise you can create a new one.
+
+If you **comply with these rules** you can [post your request/issue](http://github.com/rembo10/headphones/issues).
+
+**Support** the project by implementing new features, solving support tickets and provide bug fixes.
+If you change something in the code always make a PR to the developer branch instead of the master branch.
+
+
+###Screenshots
+
+Homepage (Artist Overview)
+
+
+
+One of the many settings pages....
+
+
+
+It might even know you better than you know yourself:
+
+
+
+Import Your Favorite Artists:
+
+
+
+Artist Search Results (also search by album!):
+
+
+
+Artist Page with Bio & Album Overview:
+
+
+
+Album Page with track overview:
+
+
+
+
+This is free software under the GPL v3 open source license - so feel free to do with it what you wish.
diff --git a/data/interfaces/default/css/config.less b/data/interfaces/default/css/config.less
index 4fdccaee..022a0700 100644
--- a/data/interfaces/default/css/config.less
+++ b/data/interfaces/default/css/config.less
@@ -1,4 +1,4 @@
-/* Variables */
+/* Variables */
@base-font-face: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
@alt-font-face: "Trebuchet MS", Helvetica, Arial, sans-serif;
@base-font-size: 12px;
@@ -10,20 +10,20 @@
@msg-bg: #FFF6A9;
@msg-bg-success: #D3FFD7;
@msg-bg-error: #FFD3D3;
-
-/* Mixins */
-.rounded(@radius: 5px) {
- -moz-border-radius: @radius;
- -webkit-border-radius: @radius;
- border-radius: @radius;
-}
-.roundedTop(@radius: 5px) {
- -moz-border-radius-topleft: @radius;
- -moz-border-radius-topright: @radius;
- -webkit-border-top-right-radius: @radius;
- -webkit-border-top-left-radius: @radius;
- border-top-left-radius: @radius;
- border-top-right-radius: @radius;
+
+/* Mixins */
+.rounded(@radius: 5px) {
+ -moz-border-radius: @radius;
+ -webkit-border-radius: @radius;
+ border-radius: @radius;
+}
+.roundedTop(@radius: 5px) {
+ -moz-border-radius-topleft: @radius;
+ -moz-border-radius-topright: @radius;
+ -webkit-border-top-right-radius: @radius;
+ -webkit-border-top-left-radius: @radius;
+ border-top-left-radius: @radius;
+ border-top-right-radius: @radius;
}
.roundedLeftTop(@radius: 5px) {
-moz-border-radius-topleft: @radius;
@@ -34,14 +34,14 @@
-moz-border-radius-topright: @radius;
-webkit-border-top-right-radius: @radius;
border-top-right-radius: @radius;
-}
-.roundedBottom(@radius: 5px) {
- -moz-border-radius-bottomleft: @radius;
- -moz-border-radius-bottomright: @radius;
- -webkit-border-bottom-right-radius: @radius;
- -webkit-border-bottom-left-radius: @radius;
- border-bottom-left-radius: @radius;
- border-bottom-right-radius: @radius;
+}
+.roundedBottom(@radius: 5px) {
+ -moz-border-radius-bottomleft: @radius;
+ -moz-border-radius-bottomright: @radius;
+ -webkit-border-bottom-right-radius: @radius;
+ -webkit-border-bottom-left-radius: @radius;
+ border-bottom-left-radius: @radius;
+ border-bottom-right-radius: @radius;
}
.roundedLeftBottom(@radius: 5px) {
-moz-border-radius-bottomleft: @radius;
@@ -52,7 +52,7 @@
-moz-border-radius-bottomright: @radius;
-webkit-border-bottom-right-radius: @radius;
border-bottom-right-radius: @radius;
-}
+}
.shadow(@shadow: 0 17px 11px -1px #ced8d9) {
-moz-box-shadow: @shadow;
-webkit-box-shadow: @shadow;
@@ -74,4 +74,4 @@
-o-opacity:@opacity_percent / 100 !important;
opacity:@opacity_percent / 100 !important;
}
-
+
diff --git a/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js b/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js
index be772753..a8520051 100644
--- a/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js
+++ b/data/interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js
@@ -1,1156 +1,1156 @@
-/*
- * FancyBox - jQuery Plugin
- * Simple and fancy lightbox alternative
- *
- * Examples and documentation at: http://fancybox.net
- *
- * Copyright (c) 2008 - 2010 Janis Skarnelis
- * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated.
- *
- * Version: 1.3.4 (11/11/2010)
- * Requires: jQuery v1.3+
- *
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- */
-
-;(function($) {
- var tmp, loading, overlay, wrap, outer, content, close, title, nav_left, nav_right,
-
- selectedIndex = 0, selectedOpts = {}, selectedArray = [], currentIndex = 0, currentOpts = {}, currentArray = [],
-
- ajaxLoader = null, imgPreloader = new Image(), imgRegExp = /\.(jpg|gif|png|bmp|jpeg)(.*)?$/i, swfRegExp = /[^\.]\.(swf)\s*$/i,
-
- loadingTimer, loadingFrame = 1,
-
- titleHeight = 0, titleStr = '', start_pos, final_pos, busy = false, fx = $.extend($('
'),
+ overlay = $(''),
+ wrap = $('')
+ );
+
+ outer = $('')
+ .append('')
+ .appendTo( wrap );
+
+ outer.append(
+ content = $(''),
+ close = $(''),
+ title = $(''),
+
+ nav_left = $(''),
+ nav_right = $('')
+ );
+
+ close.click($.fancybox.close);
+ loading.click($.fancybox.cancel);
+
+ nav_left.click(function(e) {
+ e.preventDefault();
+ $.fancybox.prev();
+ });
+
+ nav_right.click(function(e) {
+ e.preventDefault();
+ $.fancybox.next();
+ });
+
+ if ($.fn.mousewheel) {
+ wrap.bind('mousewheel.fb', function(e, delta) {
+ if (busy) {
+ e.preventDefault();
+
+ } else if ($(e.target).get(0).clientHeight == 0 || $(e.target).get(0).scrollHeight === $(e.target).get(0).clientHeight) {
+ e.preventDefault();
+ $.fancybox[ delta > 0 ? 'prev' : 'next']();
+ }
+ });
+ }
+
+ if (!$.support.opacity) {
+ wrap.addClass('fancybox-ie');
+ }
+
+ if (isIE6) {
+ loading.addClass('fancybox-ie6');
+ wrap.addClass('fancybox-ie6');
+
+ $('').prependTo(outer);
+ }
+ };
+
+ $.fn.fancybox.defaults = {
+ padding : 10,
+ margin : 40,
+ opacity : false,
+ modal : false,
+ cyclic : false,
+ scrolling : 'auto', // 'auto', 'yes' or 'no'
+
+ width : 560,
+ height : 340,
+
+ autoScale : true,
+ autoDimensions : true,
+ centerOnScroll : false,
+
+ ajax : {},
+ swf : { wmode: 'transparent' },
+
+ hideOnOverlayClick : true,
+ hideOnContentClick : false,
+
+ overlayShow : true,
+ overlayOpacity : 0.7,
+ overlayColor : '#777',
+
+ titleShow : true,
+ titlePosition : 'float', // 'float', 'outside', 'inside' or 'over'
+ titleFormat : null,
+ titleFromAlt : false,
+
+ transitionIn : 'fade', // 'elastic', 'fade' or 'none'
+ transitionOut : 'fade', // 'elastic', 'fade' or 'none'
+
+ speedIn : 300,
+ speedOut : 300,
+
+ changeSpeed : 300,
+ changeFade : 'fast',
+
+ easingIn : 'swing',
+ easingOut : 'swing',
+
+ showCloseButton : true,
+ showNavArrows : true,
+ enableEscapeButton : true,
+ enableKeyboardNav : true,
+
+ onStart : function(){},
+ onCancel : function(){},
+ onComplete : function(){},
+ onCleanup : function(){},
+ onClosed : function(){},
+ onError : function(){}
+ };
+
+ $(document).ready(function() {
+ $.fancybox.init();
+ });
+
})(jQuery);
\ No newline at end of file
diff --git a/headphones/classes.py b/headphones/classes.py
index cf6534bb..c6b14055 100644
--- a/headphones/classes.py
+++ b/headphones/classes.py
@@ -1,130 +1,130 @@
-# This file is part of Headphones.
-#
-# Headphones is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Headphones is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Headphones. If not, see .
-
-#########################################
-## Stolen from Sick-Beard's classes.py ##
-#########################################
-
-import headphones
-
-import urllib
-import datetime
-
-from common import USER_AGENT
-
-class HeadphonesURLopener(urllib.FancyURLopener):
- version = USER_AGENT
-
-class AuthURLOpener(HeadphonesURLopener):
- """
- URLOpener class that supports http auth without needing interactive password entry.
- If the provided username/password don't work it simply fails.
-
- user: username to use for HTTP auth
- pw: password to use for HTTP auth
- """
- def __init__(self, user, pw):
- self.username = user
- self.password = pw
-
- # remember if we've tried the username/password before
- self.numTries = 0
-
- # call the base class
- urllib.FancyURLopener.__init__(self)
-
- def prompt_user_passwd(self, host, realm):
- """
- Override this function and instead of prompting just give the
- username/password that were provided when the class was instantiated.
- """
-
- # if this is the first try then provide a username/password
- if self.numTries == 0:
- self.numTries = 1
- return (self.username, self.password)
-
- # if we've tried before then return blank which cancels the request
- else:
- return ('', '')
-
- # this is pretty much just a hack for convenience
- def openit(self, url):
- self.numTries = 0
- return HeadphonesURLopener.open(self, url)
-
-class SearchResult:
- """
- Represents a search result from an indexer.
- """
-
- def __init__(self):
- self.provider = -1
-
- # URL to the NZB/torrent file
- self.url = ""
-
- # used by some providers to store extra info associated with the result
- self.extraInfo = []
-
- # quality of the release
- self.quality = -1
-
- # release name
- self.name = ""
-
- def __str__(self):
-
- if self.provider == None:
- return "Invalid provider, unable to print self"
-
- myString = self.provider.name + " @ " + self.url + "\n"
- myString += "Extra Info:\n"
- for extra in self.extraInfo:
- myString += " " + extra + "\n"
- return myString
-
-class NZBSearchResult(SearchResult):
- """
- Regular NZB result with an URL to the NZB
- """
- resultType = "nzb"
-
-class NZBDataSearchResult(SearchResult):
- """
- NZB result where the actual NZB XML data is stored in the extraInfo
- """
- resultType = "nzbdata"
-
-class TorrentSearchResult(SearchResult):
- """
- Torrent result with an URL to the torrent
- """
- resultType = "torrent"
-
-class Proper:
- def __init__(self, name, url, date):
- self.name = name
- self.url = url
- self.date = date
- self.provider = None
- self.quality = -1
-
- self.tvdbid = -1
- self.season = -1
- self.episode = -1
-
- def __str__(self):
- return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid)
+# This file is part of Headphones.
+#
+# Headphones is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Headphones is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Headphones. If not, see .
+
+#########################################
+## Stolen from Sick-Beard's classes.py ##
+#########################################
+
+import headphones
+
+import urllib
+import datetime
+
+from common import USER_AGENT
+
+class HeadphonesURLopener(urllib.FancyURLopener):
+ version = USER_AGENT
+
+class AuthURLOpener(HeadphonesURLopener):
+ """
+ URLOpener class that supports http auth without needing interactive password entry.
+ If the provided username/password don't work it simply fails.
+
+ user: username to use for HTTP auth
+ pw: password to use for HTTP auth
+ """
+ def __init__(self, user, pw):
+ self.username = user
+ self.password = pw
+
+ # remember if we've tried the username/password before
+ self.numTries = 0
+
+ # call the base class
+ urllib.FancyURLopener.__init__(self)
+
+ def prompt_user_passwd(self, host, realm):
+ """
+ Override this function and instead of prompting just give the
+ username/password that were provided when the class was instantiated.
+ """
+
+ # if this is the first try then provide a username/password
+ if self.numTries == 0:
+ self.numTries = 1
+ return (self.username, self.password)
+
+ # if we've tried before then return blank which cancels the request
+ else:
+ return ('', '')
+
+ # this is pretty much just a hack for convenience
+ def openit(self, url):
+ self.numTries = 0
+ return HeadphonesURLopener.open(self, url)
+
+class SearchResult:
+ """
+ Represents a search result from an indexer.
+ """
+
+ def __init__(self):
+ self.provider = -1
+
+ # URL to the NZB/torrent file
+ self.url = ""
+
+ # used by some providers to store extra info associated with the result
+ self.extraInfo = []
+
+ # quality of the release
+ self.quality = -1
+
+ # release name
+ self.name = ""
+
+ def __str__(self):
+
+ if self.provider == None:
+ return "Invalid provider, unable to print self"
+
+ myString = self.provider.name + " @ " + self.url + "\n"
+ myString += "Extra Info:\n"
+ for extra in self.extraInfo:
+ myString += " " + extra + "\n"
+ return myString
+
+class NZBSearchResult(SearchResult):
+ """
+ Regular NZB result with an URL to the NZB
+ """
+ resultType = "nzb"
+
+class NZBDataSearchResult(SearchResult):
+ """
+ NZB result where the actual NZB XML data is stored in the extraInfo
+ """
+ resultType = "nzbdata"
+
+class TorrentSearchResult(SearchResult):
+ """
+ Torrent result with an URL to the torrent
+ """
+ resultType = "torrent"
+
+class Proper:
+ def __init__(self, name, url, date):
+ self.name = name
+ self.url = url
+ self.date = date
+ self.provider = None
+ self.quality = -1
+
+ self.tvdbid = -1
+ self.season = -1
+ self.episode = -1
+
+ def __str__(self):
+ return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid)
diff --git a/headphones/common.py b/headphones/common.py
index c195855f..74aa1db8 100644
--- a/headphones/common.py
+++ b/headphones/common.py
@@ -1,177 +1,177 @@
-# This file is part of Headphones.
-#
-# Headphones is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Headphones is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Headphones. If not, see .
-
-'''
-Created on Aug 1, 2011
-
-@author: Michael
-'''
-import platform, operator, os, re
-
-from headphones import version
-
-#Identify Our Application
-USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')'
-
-### Notification Types
-NOTIFY_SNATCH = 1
-NOTIFY_DOWNLOAD = 2
-
-notifyStrings = {}
-notifyStrings[NOTIFY_SNATCH] = "Started Download"
-notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
-
-### Release statuses
-UNKNOWN = -1 # should never happen
-UNAIRED = 1 # releases that haven't dropped yet
-SNATCHED = 2 # qualified with quality
-WANTED = 3 # releases we don't have but want to get
-DOWNLOADED = 4 # qualified with quality
-SKIPPED = 5 # releases we don't want
-ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
-IGNORED = 7 # releases that you don't want included in your download stats
-SNATCHED_PROPER = 9 # qualified with quality
-
-class Quality:
-
- NONE = 0
- B192 = 1<<1 # 2
- VBR = 1<<2 # 4
- B256 = 1<<3 # 8
- B320 = 1<<4 #16
- FLAC = 1<<5 #32
-
- # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
- UNKNOWN = 1<<15
-
- qualityStrings = {NONE: "N/A",
- UNKNOWN: "Unknown",
- B192: "MP3 192",
- VBR: "MP3 VBR",
- B256: "MP3 256",
- B320: "MP3 320",
- FLAC: "Flac"}
-
- statusPrefixes = {DOWNLOADED: "Downloaded",
- SNATCHED: "Snatched"}
-
- @staticmethod
- def _getStatusStrings(status):
- toReturn = {}
- for x in Quality.qualityStrings.keys():
- toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")"
- return toReturn
-
- @staticmethod
- def combineQualities(anyQualities, bestQualities):
- anyQuality = 0
- bestQuality = 0
- if anyQualities:
- anyQuality = reduce(operator.or_, anyQualities)
- if bestQualities:
- bestQuality = reduce(operator.or_, bestQualities)
- return anyQuality | (bestQuality<<16)
-
- @staticmethod
- def splitQuality(quality):
- anyQualities = []
- bestQualities = []
- for curQual in Quality.qualityStrings.keys():
- if curQual & quality:
- anyQualities.append(curQual)
- if curQual<<16 & quality:
- bestQualities.append(curQual)
-
- return (anyQualities, bestQualities)
-
- @staticmethod
- def nameQuality(name):
-
- name = os.path.basename(name)
-
- # if we have our exact text then assume we put it there
- for x in Quality.qualityStrings:
- if x == Quality.UNKNOWN:
- continue
-
- regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W'
- regex_match = re.search(regex, name, re.I)
- if regex_match:
- return x
-
- checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
-
- #TODO: fix quality checking here
- if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
- return Quality.B192
- elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
- return Quality.B256
- elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
- return Quality.VBR
- elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
- return Quality.B320
- else:
- return Quality.UNKNOWN
-
- @staticmethod
- def assumeQuality(name):
-
- if name.lower().endswith(".mp3"):
- return Quality.MP3
- elif name.lower().endswith(".flac"):
- return Quality.LOSSLESS
- else:
- return Quality.UNKNOWN
-
- @staticmethod
- def compositeStatus(status, quality):
- return status + 100 * quality
-
- @staticmethod
- def qualityDownloaded(status):
- return (status - DOWNLOADED) / 100
-
- @staticmethod
- def splitCompositeStatus(status):
- """Returns a tuple containing (status, quality)"""
- for x in sorted(Quality.qualityStrings.keys(), reverse=True):
- if status > x*100:
- return (status-x*100, x)
-
- return (Quality.NONE, status)
-
- @staticmethod
- def statusFromName(name, assume=True):
- quality = Quality.nameQuality(name)
- if assume and quality == Quality.UNKNOWN:
- quality = Quality.assumeQuality(name)
- return Quality.compositeStatus(DOWNLOADED, quality)
-
- DOWNLOADED = None
- SNATCHED = None
- SNATCHED_PROPER = None
-
-Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()]
-Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()]
-Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()]
-
-MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
-LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
-ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
-
-qualityPresets = (MP3, LOSSLESS, ANY)
-qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)",
- LOSSLESS: "Lossless (flac)",
- ANY: "Any"}
+# This file is part of Headphones.
+#
+# Headphones is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Headphones is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Headphones. If not, see .
+
+'''
+Created on Aug 1, 2011
+
+@author: Michael
+'''
+import platform, operator, os, re
+
+from headphones import version
+
+#Identify Our Application
+USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')'
+
+### Notification Types
+NOTIFY_SNATCH = 1
+NOTIFY_DOWNLOAD = 2
+
+notifyStrings = {}
+notifyStrings[NOTIFY_SNATCH] = "Started Download"
+notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
+
+### Release statuses
+UNKNOWN = -1 # should never happen
+UNAIRED = 1 # releases that haven't dropped yet
+SNATCHED = 2 # qualified with quality
+WANTED = 3 # releases we don't have but want to get
+DOWNLOADED = 4 # qualified with quality
+SKIPPED = 5 # releases we don't want
+ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
+IGNORED = 7 # releases that you don't want included in your download stats
+SNATCHED_PROPER = 9 # qualified with quality
+
+class Quality:
+
+ NONE = 0
+ B192 = 1<<1 # 2
+ VBR = 1<<2 # 4
+ B256 = 1<<3 # 8
+ B320 = 1<<4 #16
+ FLAC = 1<<5 #32
+
+ # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
+ UNKNOWN = 1<<15
+
+ qualityStrings = {NONE: "N/A",
+ UNKNOWN: "Unknown",
+ B192: "MP3 192",
+ VBR: "MP3 VBR",
+ B256: "MP3 256",
+ B320: "MP3 320",
+ FLAC: "Flac"}
+
+ statusPrefixes = {DOWNLOADED: "Downloaded",
+ SNATCHED: "Snatched"}
+
+ @staticmethod
+ def _getStatusStrings(status):
+ toReturn = {}
+ for x in Quality.qualityStrings.keys():
+ toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")"
+ return toReturn
+
+ @staticmethod
+ def combineQualities(anyQualities, bestQualities):
+ anyQuality = 0
+ bestQuality = 0
+ if anyQualities:
+ anyQuality = reduce(operator.or_, anyQualities)
+ if bestQualities:
+ bestQuality = reduce(operator.or_, bestQualities)
+ return anyQuality | (bestQuality<<16)
+
+ @staticmethod
+ def splitQuality(quality):
+ anyQualities = []
+ bestQualities = []
+ for curQual in Quality.qualityStrings.keys():
+ if curQual & quality:
+ anyQualities.append(curQual)
+ if curQual<<16 & quality:
+ bestQualities.append(curQual)
+
+ return (anyQualities, bestQualities)
+
+ @staticmethod
+ def nameQuality(name):
+
+ name = os.path.basename(name)
+
+ # if we have our exact text then assume we put it there
+ for x in Quality.qualityStrings:
+ if x == Quality.UNKNOWN:
+ continue
+
+ regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W'
+ regex_match = re.search(regex, name, re.I)
+ if regex_match:
+ return x
+
+ checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
+
+ #TODO: fix quality checking here
+ if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
+ return Quality.B192
+ elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
+ return Quality.B256
+ elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
+ return Quality.VBR
+ elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
+ return Quality.B320
+ else:
+ return Quality.UNKNOWN
+
+ @staticmethod
+ def assumeQuality(name):
+
+ if name.lower().endswith(".mp3"):
+ return Quality.MP3
+ elif name.lower().endswith(".flac"):
+ return Quality.LOSSLESS
+ else:
+ return Quality.UNKNOWN
+
+ @staticmethod
+ def compositeStatus(status, quality):
+ return status + 100 * quality
+
+ @staticmethod
+ def qualityDownloaded(status):
+ return (status - DOWNLOADED) / 100
+
+ @staticmethod
+ def splitCompositeStatus(status):
+ """Returns a tuple containing (status, quality)"""
+ for x in sorted(Quality.qualityStrings.keys(), reverse=True):
+ if status > x*100:
+ return (status-x*100, x)
+
+ return (Quality.NONE, status)
+
+ @staticmethod
+ def statusFromName(name, assume=True):
+ quality = Quality.nameQuality(name)
+ if assume and quality == Quality.UNKNOWN:
+ quality = Quality.assumeQuality(name)
+ return Quality.compositeStatus(DOWNLOADED, quality)
+
+ DOWNLOADED = None
+ SNATCHED = None
+ SNATCHED_PROPER = None
+
+Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()]
+Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()]
+Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()]
+
+MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
+LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
+ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
+
+qualityPresets = (MP3, LOSSLESS, ANY)
+qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)",
+ LOSSLESS: "Lossless (flac)",
+ ANY: "Any"}
diff --git a/headphones/exceptions.py b/headphones/exceptions.py
index 47de6d99..ecbaa035 100644
--- a/headphones/exceptions.py
+++ b/headphones/exceptions.py
@@ -1,41 +1,41 @@
-# This file is part of Headphones.
-#
-# Headphones is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Headphones is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Headphones. If not, see .
-
-def ex(e):
- """
- Returns a string from the exception text if it exists.
- """
-
- # sanity check
- if not e.args or not e.args[0]:
- return ""
-
- e_message = e.args[0]
-
- # if fixStupidEncodings doesn't fix it then maybe it's not a string, in which case we'll try printing it anyway
- if not e_message:
- try:
- e_message = str(e.args[0])
- except:
- e_message = ""
-
- return e_message
-
-
-class HeadphonesException(Exception):
- "Generic Headphones Exception - should never be thrown, only subclassed"
-
-class NewzbinAPIThrottled(HeadphonesException):
- "Newzbin has throttled us, deal with it"
+# This file is part of Headphones.
+#
+# Headphones is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Headphones is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Headphones. If not, see .
+
+def ex(e):
+ """
+ Returns a string from the exception text if it exists.
+ """
+
+ # sanity check
+ if not e.args or not e.args[0]:
+ return ""
+
+ e_message = e.args[0]
+
+ # if fixStupidEncodings doesn't fix it then maybe it's not a string, in which case we'll try printing it anyway
+ if not e_message:
+ try:
+ e_message = str(e.args[0])
+ except:
+ e_message = ""
+
+ return e_message
+
+
+class HeadphonesException(Exception):
+ "Generic Headphones Exception - should never be thrown, only subclassed"
+
+class NewzbinAPIThrottled(HeadphonesException):
+ "Newzbin has throttled us, deal with it"
diff --git a/headphones/sab.py b/headphones/sab.py
index a577f020..5249e914 100644
--- a/headphones/sab.py
+++ b/headphones/sab.py
@@ -1,162 +1,162 @@
-# This file is part of Headphones.
-#
-# Headphones is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Headphones is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Headphones. If not, see .
-
-#####################################
-## Stolen from Sick-Beard's sab.py ##
-#####################################
-
-import urllib, httplib
-import datetime
-
-import headphones
-
-from lib import MultipartPostHandler
-import urllib2, cookielib
-import ast
-
-from headphones.common import USER_AGENT
-from headphones import logger
-from headphones import notifiers, helpers
-
-def sendNZB(nzb):
-
- params = {}
-
- if headphones.SAB_USERNAME:
- params['ma_username'] = headphones.SAB_USERNAME
- if headphones.SAB_PASSWORD:
- params['ma_password'] = headphones.SAB_PASSWORD
- if headphones.SAB_APIKEY:
- params['apikey'] = headphones.SAB_APIKEY
- if headphones.SAB_CATEGORY:
- params['cat'] = headphones.SAB_CATEGORY
-
- # if it's a normal result we just pass SAB the URL
- if nzb.resultType == "nzb":
- # for newzbin results send the ID to sab specifically
- if nzb.provider.getID() == 'newzbin':
- id = nzb.provider.getIDFromURL(nzb.url)
- if not id:
- logger.info("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url))
- return False
- params['mode'] = 'addid'
- params['name'] = id
- else:
- params['mode'] = 'addurl'
- params['name'] = nzb.url
-
- # if we get a raw data result we want to upload it to SAB
- elif nzb.resultType == "nzbdata":
- # Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler
- nzbdata = helpers.latinToAscii(nzb.extraInfo[0])
- params['mode'] = 'addfile'
- multiPartParams = {"nzbfile": (nzb.name+".nzb", nzbdata)}
-
- if not headphones.SAB_HOST.startswith('http'):
- headphones.SAB_HOST = 'http://' + headphones.SAB_HOST
-
- if headphones.SAB_HOST.endswith('/'):
- headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1]
-
- url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
-
- try:
-
- if nzb.resultType == "nzb":
- f = urllib.urlopen(url)
- elif nzb.resultType == "nzbdata":
- cookies = cookielib.CookieJar()
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies),
- MultipartPostHandler.MultipartPostHandler)
-
- req = urllib2.Request(url,
- multiPartParams,
- headers={'User-Agent': USER_AGENT})
-
- f = opener.open(req)
-
- except (EOFError, IOError), e:
- logger.error(u"Unable to connect to SAB with URL: %s" % url)
- return False
-
- except httplib.InvalidURL, e:
- logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.SAB_HOST)
- return False
-
- except Exception, e:
- logger.error(u"Error: " + str(e))
- return False
-
- if f == None:
- logger.info(u"No data returned from SABnzbd, NZB not sent")
- return False
-
- try:
- result = f.readlines()
- except Exception, e:
- logger.info(u"Error trying to get result from SAB, NZB not sent: ")
- return False
-
- if len(result) == 0:
- logger.info(u"No data returned from SABnzbd, NZB not sent")
- return False
-
- sabText = result[0].strip()
-
- logger.info(u"Result text from SAB: " + sabText)
-
- if sabText == "ok":
- logger.info(u"NZB sent to SAB successfully")
- return True
- elif sabText == "Missing authentication":
- logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
- return False
- else:
- logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText)
- return False
-
-def checkConfig():
-
- params = { 'mode' : 'get_config',
- 'section' : 'misc'
- }
-
- if headphones.SAB_USERNAME:
- params['ma_username'] = headphones.SAB_USERNAME
- if headphones.SAB_PASSWORD:
- params['ma_password'] = headphones.SAB_PASSWORD
- if headphones.SAB_APIKEY:
- params['apikey'] = headphones.SAB_APIKEY
-
- if not headphones.SAB_HOST.startswith('http'):
- headphones.SAB_HOST = 'http://' + headphones.SAB_HOST
-
- if headphones.SAB_HOST.endswith('/'):
- headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1]
-
- url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
-
- try:
- f = urllib.urlopen(url).read()
- except Exception, e:
- logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
- return (0, 0)
-
- config_options = ast.literal_eval(f)
-
- replace_spaces = config_options['misc']['replace_spaces']
- replace_dots = config_options['misc']['replace_dots']
-
- return (replace_spaces, replace_dots)
+# This file is part of Headphones.
+#
+# Headphones is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Headphones is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Headphones. If not, see .
+
+#####################################
+## Stolen from Sick-Beard's sab.py ##
+#####################################
+
+import urllib, httplib
+import datetime
+
+import headphones
+
+from lib import MultipartPostHandler
+import urllib2, cookielib
+import ast
+
+from headphones.common import USER_AGENT
+from headphones import logger
+from headphones import notifiers, helpers
+
+def sendNZB(nzb):
+
+ params = {}
+
+ if headphones.SAB_USERNAME:
+ params['ma_username'] = headphones.SAB_USERNAME
+ if headphones.SAB_PASSWORD:
+ params['ma_password'] = headphones.SAB_PASSWORD
+ if headphones.SAB_APIKEY:
+ params['apikey'] = headphones.SAB_APIKEY
+ if headphones.SAB_CATEGORY:
+ params['cat'] = headphones.SAB_CATEGORY
+
+ # if it's a normal result we just pass SAB the URL
+ if nzb.resultType == "nzb":
+ # for newzbin results send the ID to sab specifically
+ if nzb.provider.getID() == 'newzbin':
+ id = nzb.provider.getIDFromURL(nzb.url)
+ if not id:
+ logger.info("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url))
+ return False
+ params['mode'] = 'addid'
+ params['name'] = id
+ else:
+ params['mode'] = 'addurl'
+ params['name'] = nzb.url
+
+ # if we get a raw data result we want to upload it to SAB
+ elif nzb.resultType == "nzbdata":
+ # Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler
+ nzbdata = helpers.latinToAscii(nzb.extraInfo[0])
+ params['mode'] = 'addfile'
+ multiPartParams = {"nzbfile": (nzb.name+".nzb", nzbdata)}
+
+ if not headphones.SAB_HOST.startswith('http'):
+ headphones.SAB_HOST = 'http://' + headphones.SAB_HOST
+
+ if headphones.SAB_HOST.endswith('/'):
+ headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1]
+
+ url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
+
+ try:
+
+ if nzb.resultType == "nzb":
+ f = urllib.urlopen(url)
+ elif nzb.resultType == "nzbdata":
+ cookies = cookielib.CookieJar()
+ opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies),
+ MultipartPostHandler.MultipartPostHandler)
+
+ req = urllib2.Request(url,
+ multiPartParams,
+ headers={'User-Agent': USER_AGENT})
+
+ f = opener.open(req)
+
+ except (EOFError, IOError), e:
+ logger.error(u"Unable to connect to SAB with URL: %s" % url)
+ return False
+
+ except httplib.InvalidURL, e:
+ logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.SAB_HOST)
+ return False
+
+ except Exception, e:
+ logger.error(u"Error: " + str(e))
+ return False
+
+ if f == None:
+ logger.info(u"No data returned from SABnzbd, NZB not sent")
+ return False
+
+ try:
+ result = f.readlines()
+ except Exception, e:
+ logger.info(u"Error trying to get result from SAB, NZB not sent: ")
+ return False
+
+ if len(result) == 0:
+ logger.info(u"No data returned from SABnzbd, NZB not sent")
+ return False
+
+ sabText = result[0].strip()
+
+ logger.info(u"Result text from SAB: " + sabText)
+
+ if sabText == "ok":
+ logger.info(u"NZB sent to SAB successfully")
+ return True
+ elif sabText == "Missing authentication":
+ logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
+ return False
+ else:
+ logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText)
+ return False
+
+def checkConfig():
+
+ params = { 'mode' : 'get_config',
+ 'section' : 'misc'
+ }
+
+ if headphones.SAB_USERNAME:
+ params['ma_username'] = headphones.SAB_USERNAME
+ if headphones.SAB_PASSWORD:
+ params['ma_password'] = headphones.SAB_PASSWORD
+ if headphones.SAB_APIKEY:
+ params['apikey'] = headphones.SAB_APIKEY
+
+ if not headphones.SAB_HOST.startswith('http'):
+ headphones.SAB_HOST = 'http://' + headphones.SAB_HOST
+
+ if headphones.SAB_HOST.endswith('/'):
+ headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1]
+
+ url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
+
+ try:
+ f = urllib.urlopen(url).read()
+ except Exception, e:
+ logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
+ return (0, 0)
+
+ config_options = ast.literal_eval(f)
+
+ replace_spaces = config_options['misc']['replace_spaces']
+ replace_dots = config_options['misc']['replace_dots']
+
+ return (replace_spaces, replace_dots)
diff --git a/lib/MultipartPostHandler.py b/lib/MultipartPostHandler.py
index 82fa59c6..ef63afa2 100644
--- a/lib/MultipartPostHandler.py
+++ b/lib/MultipartPostHandler.py
@@ -1,88 +1,88 @@
-#!/usr/bin/python
-
-####
-# 06/2010 Nic Wolfe
-# 02/2006 Will Holcomb
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-
-import urllib
-import urllib2
-import mimetools, mimetypes
-import os, sys
-
-# Controls how sequences are uncoded. If true, elements may be given multiple values by
-# assigning a sequence.
-doseq = 1
-
-class MultipartPostHandler(urllib2.BaseHandler):
- handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first
-
- def http_request(self, request):
- data = request.get_data()
- if data is not None and type(data) != str:
- v_files = []
- v_vars = []
- try:
- for(key, value) in data.items():
- if type(value) in (file, list, tuple):
- v_files.append((key, value))
- else:
- v_vars.append((key, value))
- except TypeError:
- systype, value, traceback = sys.exc_info()
- raise TypeError, "not a valid non-string sequence or mapping object", traceback
-
- if len(v_files) == 0:
- data = urllib.urlencode(v_vars, doseq)
- else:
- boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files)
- contenttype = 'multipart/form-data; boundary=%s' % boundary
- if(request.has_header('Content-Type')
- and request.get_header('Content-Type').find('multipart/form-data') != 0):
- print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data')
- request.add_unredirected_header('Content-Type', contenttype)
-
- request.add_data(data)
- return request
-
- @staticmethod
- def multipart_encode(vars, files, boundary = None, buffer = None):
- if boundary is None:
- boundary = mimetools.choose_boundary()
- if buffer is None:
- buffer = ''
- for(key, value) in vars:
- buffer += '--%s\r\n' % boundary
- buffer += 'Content-Disposition: form-data; name="%s"' % key
- buffer += '\r\n\r\n' + value + '\r\n'
- for(key, fd) in files:
-
- # allow them to pass in a file or a tuple with name & data
- if type(fd) == file:
- name_in = fd.name
- fd.seek(0)
- data_in = fd.read()
- elif type(fd) in (tuple, list):
- name_in, data_in = fd
-
- filename = os.path.basename(name_in)
- contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
- buffer += '--%s\r\n' % boundary
- buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename)
- buffer += 'Content-Type: %s\r\n' % contenttype
- # buffer += 'Content-Length: %s\r\n' % file_size
- buffer += '\r\n' + data_in + '\r\n'
- buffer += '--%s--\r\n\r\n' % boundary
- return boundary, buffer
-
+#!/usr/bin/python
+
+####
+# 06/2010 Nic Wolfe
+# 02/2006 Will Holcomb
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+
+import urllib
+import urllib2
+import mimetools, mimetypes
+import os, sys
+
+# Controls how sequences are uncoded. If true, elements may be given multiple values by
+# assigning a sequence.
+doseq = 1
+
+class MultipartPostHandler(urllib2.BaseHandler):
+ handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first
+
+ def http_request(self, request):
+ data = request.get_data()
+ if data is not None and type(data) != str:
+ v_files = []
+ v_vars = []
+ try:
+ for(key, value) in data.items():
+ if type(value) in (file, list, tuple):
+ v_files.append((key, value))
+ else:
+ v_vars.append((key, value))
+ except TypeError:
+ systype, value, traceback = sys.exc_info()
+ raise TypeError, "not a valid non-string sequence or mapping object", traceback
+
+ if len(v_files) == 0:
+ data = urllib.urlencode(v_vars, doseq)
+ else:
+ boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files)
+ contenttype = 'multipart/form-data; boundary=%s' % boundary
+ if(request.has_header('Content-Type')
+ and request.get_header('Content-Type').find('multipart/form-data') != 0):
+ print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data')
+ request.add_unredirected_header('Content-Type', contenttype)
+
+ request.add_data(data)
+ return request
+
+ @staticmethod
+ def multipart_encode(vars, files, boundary = None, buffer = None):
+ if boundary is None:
+ boundary = mimetools.choose_boundary()
+ if buffer is None:
+ buffer = ''
+ for(key, value) in vars:
+ buffer += '--%s\r\n' % boundary
+ buffer += 'Content-Disposition: form-data; name="%s"' % key
+ buffer += '\r\n\r\n' + value + '\r\n'
+ for(key, fd) in files:
+
+ # allow them to pass in a file or a tuple with name & data
+ if type(fd) == file:
+ name_in = fd.name
+ fd.seek(0)
+ data_in = fd.read()
+ elif type(fd) in (tuple, list):
+ name_in, data_in = fd
+
+ filename = os.path.basename(name_in)
+ contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+ buffer += '--%s\r\n' % boundary
+ buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename)
+ buffer += 'Content-Type: %s\r\n' % contenttype
+ # buffer += 'Content-Length: %s\r\n' % file_size
+ buffer += '\r\n' + data_in + '\r\n'
+ buffer += '--%s--\r\n\r\n' % boundary
+ return boundary, buffer
+
https_request = http_request
\ No newline at end of file
diff --git a/lib/__init__.py b/lib/__init__.py
index d3f5a12f..8b137891 100755
--- a/lib/__init__.py
+++ b/lib/__init__.py
@@ -1 +1 @@
-
+
diff --git a/lib/cherrypy/LICENSE.txt b/lib/cherrypy/LICENSE.txt
index 8db13fb2..3816f933 100644
--- a/lib/cherrypy/LICENSE.txt
+++ b/lib/cherrypy/LICENSE.txt
@@ -1,25 +1,25 @@
-Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org)
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- * Neither the name of the CherryPy Team nor the names of its contributors
- may be used to endorse or promote products derived from this software
- without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the CherryPy Team nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/ordereddict.py b/lib/ordereddict.py
index 5b0303f5..7242b506 100644
--- a/lib/ordereddict.py
+++ b/lib/ordereddict.py
@@ -1,127 +1,127 @@
-# Copyright (c) 2009 Raymond Hettinger
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation files
-# (the "Software"), to deal in the Software without restriction,
-# including without limitation the rights to use, copy, modify, merge,
-# publish, distribute, sublicense, and/or sell copies of the Software,
-# and to permit persons to whom the Software is furnished to do so,
-# subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-# OTHER DEALINGS IN THE SOFTWARE.
-
-from UserDict import DictMixin
-
-class OrderedDict(dict, DictMixin):
-
- def __init__(self, *args, **kwds):
- if len(args) > 1:
- raise TypeError('expected at most 1 arguments, got %d' % len(args))
- try:
- self.__end
- except AttributeError:
- self.clear()
- self.update(*args, **kwds)
-
- def clear(self):
- self.__end = end = []
- end += [None, end, end] # sentinel node for doubly linked list
- self.__map = {} # key --> [key, prev, next]
- dict.clear(self)
-
- def __setitem__(self, key, value):
- if key not in self:
- end = self.__end
- curr = end[1]
- curr[2] = end[1] = self.__map[key] = [key, curr, end]
- dict.__setitem__(self, key, value)
-
- def __delitem__(self, key):
- dict.__delitem__(self, key)
- key, prev, next = self.__map.pop(key)
- prev[2] = next
- next[1] = prev
-
- def __iter__(self):
- end = self.__end
- curr = end[2]
- while curr is not end:
- yield curr[0]
- curr = curr[2]
-
- def __reversed__(self):
- end = self.__end
- curr = end[1]
- while curr is not end:
- yield curr[0]
- curr = curr[1]
-
- def popitem(self, last=True):
- if not self:
- raise KeyError('dictionary is empty')
- if last:
- key = reversed(self).next()
- else:
- key = iter(self).next()
- value = self.pop(key)
- return key, value
-
- def __reduce__(self):
- items = [[k, self[k]] for k in self]
- tmp = self.__map, self.__end
- del self.__map, self.__end
- inst_dict = vars(self).copy()
- self.__map, self.__end = tmp
- if inst_dict:
- return (self.__class__, (items,), inst_dict)
- return self.__class__, (items,)
-
- def keys(self):
- return list(self)
-
- setdefault = DictMixin.setdefault
- update = DictMixin.update
- pop = DictMixin.pop
- values = DictMixin.values
- items = DictMixin.items
- iterkeys = DictMixin.iterkeys
- itervalues = DictMixin.itervalues
- iteritems = DictMixin.iteritems
-
- def __repr__(self):
- if not self:
- return '%s()' % (self.__class__.__name__,)
- return '%s(%r)' % (self.__class__.__name__, self.items())
-
- def copy(self):
- return self.__class__(self)
-
- @classmethod
- def fromkeys(cls, iterable, value=None):
- d = cls()
- for key in iterable:
- d[key] = value
- return d
-
- def __eq__(self, other):
- if isinstance(other, OrderedDict):
- if len(self) != len(other):
- return False
- for p, q in zip(self.items(), other.items()):
- if p != q:
- return False
- return True
- return dict.__eq__(self, other)
-
- def __ne__(self, other):
- return not self == other
+# Copyright (c) 2009 Raymond Hettinger
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+
+from UserDict import DictMixin
+
+class OrderedDict(dict, DictMixin):
+
+ def __init__(self, *args, **kwds):
+ if len(args) > 1:
+ raise TypeError('expected at most 1 arguments, got %d' % len(args))
+ try:
+ self.__end
+ except AttributeError:
+ self.clear()
+ self.update(*args, **kwds)
+
+ def clear(self):
+ self.__end = end = []
+ end += [None, end, end] # sentinel node for doubly linked list
+ self.__map = {} # key --> [key, prev, next]
+ dict.clear(self)
+
+ def __setitem__(self, key, value):
+ if key not in self:
+ end = self.__end
+ curr = end[1]
+ curr[2] = end[1] = self.__map[key] = [key, curr, end]
+ dict.__setitem__(self, key, value)
+
+ def __delitem__(self, key):
+ dict.__delitem__(self, key)
+ key, prev, next = self.__map.pop(key)
+ prev[2] = next
+ next[1] = prev
+
+ def __iter__(self):
+ end = self.__end
+ curr = end[2]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[2]
+
+ def __reversed__(self):
+ end = self.__end
+ curr = end[1]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[1]
+
+ def popitem(self, last=True):
+ if not self:
+ raise KeyError('dictionary is empty')
+ if last:
+ key = reversed(self).next()
+ else:
+ key = iter(self).next()
+ value = self.pop(key)
+ return key, value
+
+ def __reduce__(self):
+ items = [[k, self[k]] for k in self]
+ tmp = self.__map, self.__end
+ del self.__map, self.__end
+ inst_dict = vars(self).copy()
+ self.__map, self.__end = tmp
+ if inst_dict:
+ return (self.__class__, (items,), inst_dict)
+ return self.__class__, (items,)
+
+ def keys(self):
+ return list(self)
+
+ setdefault = DictMixin.setdefault
+ update = DictMixin.update
+ pop = DictMixin.pop
+ values = DictMixin.values
+ items = DictMixin.items
+ iterkeys = DictMixin.iterkeys
+ itervalues = DictMixin.itervalues
+ iteritems = DictMixin.iteritems
+
+ def __repr__(self):
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, self.items())
+
+ def copy(self):
+ return self.__class__(self)
+
+ @classmethod
+ def fromkeys(cls, iterable, value=None):
+ d = cls()
+ for key in iterable:
+ d[key] = value
+ return d
+
+ def __eq__(self, other):
+ if isinstance(other, OrderedDict):
+ if len(self) != len(other):
+ return False
+ for p, q in zip(self.items(), other.items()):
+ if p != q:
+ return False
+ return True
+ return dict.__eq__(self, other)
+
+ def __ne__(self, other):
+ return not self == other