Trying to get old newzbin back for rembo...

This commit is contained in:
sbuser
2011-08-01 16:47:54 -05:00
parent 8f70eb9b03
commit b00c7535b4
4 changed files with 504 additions and 0 deletions

131
headphones/classes.py Normal file
View File

@@ -0,0 +1,131 @@
# Author: Nic Wolfe <nic@wolfeden.ca>
# URL: http://code.google.com/p/sickbeard/
#
# This file is part of Sick Beard.
#
# Sick Beard 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.
#
# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>.
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)

162
headphones/common.py Normal file
View File

@@ -0,0 +1,162 @@
'''
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"}

123
headphones/sab.py Normal file
View File

@@ -0,0 +1,123 @@
# This file is part of Sick Beard.
#
# Sick Beard 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.
#
# Sick Beard 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 Sick Beard. If not, see <http://www.gnu.org/licenses/>.
import urllib, httplib
import datetime
import headphones
from lib import MultipartPostHandler
import urllib2, cookielib
from headphones.common import USER_AGENT
from headphones import logger
def sendNZB(nzb):
params = {}
if headphones.SAB_USERNAME != None:
params['ma_username'] = headphones.SAB_USERNAME
if headphones.SAB_PASSWORD != None:
params['ma_password'] = headphones.SAB_PASSWORD
if headphones.SAB_APIKEY != None:
params['apikey'] = headphones.SAB_APIKEY
if headphones.SAB_CATEGORY != None:
params['cat'] = headphones.SAB_CATEGORY
# # if released recently make it high priority
# for curEp in nzb.episodes:
# if datetime.date.today() - curEp.airdate <= datetime.timedelta(days=7):
# params['priority'] = 1
# 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":
params['mode'] = 'addfile'
multiPartParams = {"nzbfile": (nzb.name+".nzb", nzb.extraInfo[0])}
url = "http://" + headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
logger.info(u"Sending NZB to SABnzbd")
logger.info(u"URL: " + url)
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.info(u"Unable to connect to SAB: ")
return False
except httplib.InvalidURL, e:
logger.info(u"Invalid SAB host, check your config: ")
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

View File

@@ -0,0 +1,88 @@
#!/usr/bin/python
####
# 06/2010 Nic Wolfe <nic@wolfeden.ca>
# 02/2006 Will Holcomb <wholcomb@gmail.com>
#
# 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