rm-r
Contributor
- Joined
- Jan 7, 2013
- Messages
- 166
Hi,
So after several weeks of hunting around and investigation I finally got Sickbeard to work getting episodes down automatically while on a FreeNAS host.
Please note, as far as I can tell this is only happening on a FreeNAS sickbeard host.
Firstly many many thanks to SteelWolf for helping me out so much, with great patience. I am making this post so others can find this info all in one place - maybe worth a sticky?
The issue;
On a FreeNAS host, sickbeard double escapes the download address, as per below
The solution (again many thanks to SteelWolf)
Inserted at line 162 of btn.py (in sickbeard/providers folder)
so the total section should look like
The steps required;
we will assume sickbeard is installed and functioning, and your torrent client is setup to watch the torrent blackhole.
(see here for further info if required https://broadcasthe.net/forums.php?page=1&action=viewthread&threadid=11208)
[*] SSH into the Freenas, navigate to the sickbeard folder in your jail (for windows gui users - check out winSCP http://winscp.net)
[*] Locate the Sickbeard/providers folder
[*] Backup (make copy) of the original btn.py to btn orig.py.bak
[*] Open the original btn.py file and make the edit (or delete it all and paste in the complete contents from the pastebin link below)
[*] Save the edited btn.py
[*] Delete btn.pyc
[*] Login to the sickbeard gui and restart sickbeard
[*] Away you go! - You can use the magnifying glass next to an episode to test if you don’t want to wait for the latest episode.
###############################################################################################################
References;
Steelwolfs original post
http://sickbeard.com/forums/viewtopic.php?t=5130&p=26135
Bug report to the sickbeard team
http://code.google.com/p/sickbeard/...Status Priority Milestone Owner Summary Stars
Full btn.py file paste (again thanks to SteelWolf)
http://pastebin.com/VjDRXQUR
Full "patched" btn.py
So after several weeks of hunting around and investigation I finally got Sickbeard to work getting episodes down automatically while on a FreeNAS host.
Please note, as far as I can tell this is only happening on a FreeNAS sickbeard host.
Firstly many many thanks to SteelWolf for helping me out so much, with great patience. I am making this post so others can find this info all in one place - maybe worth a sticky?
The issue;
On a FreeNAS host, sickbeard double escapes the download address, as per below
Code:
BTN URL: https:\/\/broadcasthe.net\/torrents.php?action=download&id=189660&authkey=redacted&torrent_pass=zb9l41e8y5j8fpxn0jgvookv5vmg9479
The solution (again many thanks to SteelWolf)
Inserted at line 162 of btn.py (in sickbeard/providers folder)
Code:
url = url.replace("\\", "")
so the total section should look like
Code:
if 'DownloadURL' in search_result:
url = search_result['DownloadURL']
url = url.replace("\\", "")
The steps required;
we will assume sickbeard is installed and functioning, and your torrent client is setup to watch the torrent blackhole.
(see here for further info if required https://broadcasthe.net/forums.php?page=1&action=viewthread&threadid=11208)
[*] SSH into the Freenas, navigate to the sickbeard folder in your jail (for windows gui users - check out winSCP http://winscp.net)
[*] Locate the Sickbeard/providers folder
[*] Backup (make copy) of the original btn.py to btn orig.py.bak
[*] Open the original btn.py file and make the edit (or delete it all and paste in the complete contents from the pastebin link below)
[*] Save the edited btn.py
[*] Delete btn.pyc
[*] Login to the sickbeard gui and restart sickbeard
[*] Away you go! - You can use the magnifying glass next to an episode to test if you don’t want to wait for the latest episode.
###############################################################################################################
References;
Steelwolfs original post
http://sickbeard.com/forums/viewtopic.php?t=5130&p=26135
Bug report to the sickbeard team
http://code.google.com/p/sickbeard/...Status Priority Milestone Owner Summary Stars
Full btn.py file paste (again thanks to SteelWolf)
http://pastebin.com/VjDRXQUR
Full "patched" btn.py
Code:
# coding=utf-8
# Author: Daniël Heimans
# 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 sickbeard
import generic
from sickbeard import scene_exceptions
from sickbeard import logger
from sickbeard import tvcache
from sickbeard.helpers import sanitizeSceneName
from sickbeard.common import Quality
from sickbeard.exceptions import ex, AuthException
from lib import jsonrpclib
import datetime
import time
import socket
import math
import pprint
class BTNProvider(generic.TorrentProvider):
def __init__(self):
generic.TorrentProvider.__init__(self, "BTN")
self.supportsBacklog = True
self.cache = BTNCache(self)
self.url = "http://broadcasthe.net"
def isEnabled(self):
return sickbeard.BTN
def imageName(self):
return 'btn.png'
def checkAuthFromData(self, data):
result = True
if 'api-error' in data:
logger.log("Error in sickbeard data retrieval: " + data['api-error'], logger.ERROR)
result = False
return result
def _doSearch(self, search_params, show=None):
params = {}
apikey = sickbeard.BTN_API_KEY
if search_params:
params.update(search_params)
search_results = self._api_call(apikey, params)
if not search_results:
return []
if 'torrents' in search_results:
found_torrents = search_results['torrents']
else:
found_torrents = {}
# We got something, we know the API sends max 1000 results at a time.
# See if there are more than 1000 results for our query, if not we
# keep requesting until we've got everything.
# max 150 requests per minute so limit at that
max_pages = 150
results_per_page = 1000.0
if 'results' in search_results and search_results['results'] >= results_per_page:
pages_needed = int(math.ceil(int(search_results['results']) / results_per_page))
if pages_needed > max_pages:
pages_needed = max_pages
# +1 because range(1,4) = 1, 2, 3
for page in range(1,pages_needed+1):
search_results = self._api_call(apikey, params, results_per_page, page * results_per_page)
# Note that this these are individual requests and might time out individually. This would result in 'gaps'
# in the results. There is no way to fix this though.
if 'torrents' in search_results:
found_torrents.update(search_results['torrents'])
results = []
for torrentid, torrent_info in found_torrents.iteritems():
(title, url) = self._get_title_and_url(torrent_info)
if not title or not url:
logger.log(u"The BTN provider did not return both a valid title and URL for search parameters: " + str(params) + " but returned " + str(torrent_info), logger.WARNING)
results.append(torrent_info)
# Disabled this because it overspammed the debug log a bit too much
# logger.log(u'BTN provider returning the following results for search parameters: ' + str(params), logger.DEBUG)
# for result in results:
# (title, result) = self._get_title_and_url(result)
# logger.log(title, logger.DEBUG)
return results
def _api_call(self, apikey, params={}, results_per_page=1000, offset=0):
server = jsonrpclib.Server('http://api.btnapps.net')
search_results ={}
try:
search_results = server.getTorrentsSearch(apikey, params, int(results_per_page), int(offset))
except jsonrpclib.jsonrpc.ProtocolError, error:
logger.log(u"JSON-RPC protocol error while accessing BTN API: " + ex(error), logger.ERROR)
search_results = {'api-error': ex(error)}
return search_results
except socket.timeout:
logger.log(u"Timeout while accessing BTN API", logger.WARNING)
except socket.error, error:
# Note that sometimes timeouts are thrown as socket errors
logger.log(u"Socket error while accessing BTN API: " + error[1], logger.ERROR)
except Exception, error:
errorstring = str(error)
if(errorstring.startswith('<') and errorstring.endswith('>')):
errorstring = errorstring[1:-1]
logger.log(u"Unknown error while accessing BTN API: " + errorstring, logger.ERROR)
return search_results
def _get_title_and_url(self, search_result):
# The BTN API gives a lot of information in response,
# however Sick Beard is built mostly around Scene or
# release names, which is why we are using them here.
if 'ReleaseName' in search_result and search_result['ReleaseName']:
title = search_result['ReleaseName']
else:
# If we don't have a release name we need to get creative
title = u''
if 'Series' in search_result:
title += search_result['Series']
if 'GroupName' in search_result:
title += '.' + search_result['GroupName'] if title else search_result['GroupName']
if 'Resolution' in search_result:
title += '.' + search_result['Resolution'] if title else search_result['Resolution']
if 'Source' in search_result:
title += '.' + search_result['Source'] if title else search_result['Source']
if 'Codec' in search_result:
title += '.' + search_result['Codec'] if title else search_result['Codec']
if 'DownloadURL' in search_result:
url = search_result['DownloadURL']
url = url.replace("\\", "")
else:
url = None
return (title, url)
def _get_season_search_strings(self, show, season=None):
if not show:
return [{}]
search_params = []
name_exceptions = scene_exceptions.get_scene_exceptions(show.tvdbid) + [show.name]
for name in name_exceptions:
current_params = {}
if show.tvdbid:
current_params['tvdb'] = show.tvdbid
elif show.tvrid:
current_params['tvrage'] = show.tvrid
else:
# Search by name if we don't have tvdb or tvrage id
current_params['series'] = sanitizeSceneName(name)
if season != None:
whole_season_params = current_params.copy()
partial_season_params = current_params.copy()
# Search for entire seasons: no need to do special things for air by date shows
whole_season_params['category'] = 'Season'
whole_season_params['name'] = 'Season ' + str(season)
search_params.append(whole_season_params)
# Search for episodes in the season
partial_season_params['category'] = 'Episode'
if show.air_by_date:
# Search for the year of the air by date show
partial_season_params['name'] = str(season.split('-')[0])
else:
# Search for any result which has Sxx in the name
partial_season_params['name'] = 'S%02d' % int(season)
search_params.append(partial_season_params)
else:
search_params.append(current_params)
return search_params
def _get_episode_search_strings(self, ep_obj):
if not ep_obj:
return [{}]
search_params = {'category':'Episode'}
if ep_obj.show.tvdbid:
search_params['tvdb'] = ep_obj.show.tvdbid
elif ep_obj.show.tvrid:
search_params['tvrage'] = ep_obj.show.rid
else:
search_params['series'] = sanitizeSceneName(ep_obj.show_name)
if ep_obj.show.air_by_date:
date_str = str(ep_obj.airdate)
# BTN uses dots in dates, we just search for the date since that
# combined with the series identifier should result in just one episode
search_params['name'] = date_str.replace('-','.')
else:
# Do a general name search for the episode, formatted like SXXEYY
search_params['name'] = "S%02dE%02d" % (ep_obj.season,ep_obj.episode)
to_return = [search_params]
# only do scene exceptions if we are searching by name
if 'series' in search_params:
# add new query string for every exception
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
# copy all other parameters before setting the show name for this exception
cur_return = search_params.copy()
cur_return['series'] = sanitizeSceneName(cur_exception)
to_return.append(cur_return)
return to_return
def getQuality(self, item):
quality = None
(title,url) = self._get_title_and_url(item)
quality = Quality.nameQuality(title)
return quality
def _doGeneralSearch(self, search_string):
# 'search' looks as broad is it can find. Can contain episode overview and title for example,
# use with caution!
return self._doSearch({'search': search_string})
class BTNCache(tvcache.TVCache):
def __init__(self, provider):
tvcache.TVCache.__init__(self, provider)
# At least 15 minutes between queries
self.minTime = 15
def updateCache(self):
if not self.shouldUpdate():
return
data = self._getRSSData()
# As long as we got something from the provider we count it as an update
if data:
self.setLastUpdate()
else:
return []
logger.log(u"Clearing "+self.provider.name+" cache and updating with new information")
self._clearCache()
if not self._checkAuth(data):
raise AuthException("Your authentication info for "+self.provider.name+" is incorrect, check your config")
# By now we know we've got data and no auth errors, all we need to do is put it in the database
for item in data:
self._parseItem(item)
def _getRSSData(self):
# Get the torrents uploaded since last check.
seconds_since_last_update = math.ceil(time.time() - time.mktime(self._getLastUpdate().timetuple()))
# default to 15 minutes
if seconds_since_last_update < 15*60:
seconds_since_last_update = 15*60
# Set maximum to 24 hours of "RSS" data search, older things will need to be done through backlog
if seconds_since_last_update > 24*60*60:
logger.log(u"The last known successful \"RSS\" update on the BTN API was more than 24 hours ago (%i hours to be precise), only trying to fetch the last 24 hours!" %(int(seconds_since_last_update)//(60*60)), logger.WARNING)
seconds_since_last_update = 24*60*60
age_string = "<=%i" % seconds_since_last_update
search_params={'age': age_string}
data = self.provider._doSearch(search_params)
return data
def _parseItem(self, item):
(title, url) = self.provider._get_title_and_url(item)
if not title or not url:
logger.log(u"The result returned from the BTN regular search is incomplete, this result is unusable", logger.ERROR)
return
logger.log(u"Adding item from regular BTN search to cache: " + title, logger.DEBUG)
self._addCacheEntry(title, url)
def _checkAuth(self, data):
return self.provider.checkAuthFromData(data)
provider = BTNProvider()