Commit c4682ccf authored by echel0n's avatar echel0n
Browse files

Merge branch 'release/10.0.39'

parents 44a0b2bb 5e7bfa31
...@@ -2,8 +2,12 @@ ...@@ -2,8 +2,12 @@
   
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
   
#### [10.0.39](https://git.sickrage.ca/SiCKRAGE/sickrage/compare/10.0.38...10.0.39)
#### [10.0.38](https://git.sickrage.ca/SiCKRAGE/sickrage/compare/10.0.37...10.0.38) #### [10.0.38](https://git.sickrage.ca/SiCKRAGE/sickrage/compare/10.0.37...10.0.38)
   
> 6 September 2021
#### [10.0.37](https://git.sickrage.ca/SiCKRAGE/sickrage/compare/10.0.36...10.0.37) #### [10.0.37](https://git.sickrage.ca/SiCKRAGE/sickrage/compare/10.0.36...10.0.37)
   
> 3 September 2021 > 3 September 2021
......
{ {
"name": "sickrage", "name": "sickrage",
"version": "10.0.38", "version": "10.0.39",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
......
[bumpversion] [bumpversion]
current_version = 10.0.38 current_version = 10.0.39
commit = False commit = False
tag = False tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<dev>\d+))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<dev>\d+))?
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
# along with SiCKRAGE. If not, see <http://www.gnu.org/licenses/>. # along with SiCKRAGE. If not, see <http://www.gnu.org/licenses/>.
# ############################################################################## # ##############################################################################
__version__ = "10.0.38" __version__ = "10.0.39"
__install_type__ = "" __install_type__ = ""
import argparse import argparse
......
This diff is collapsed.
...@@ -49,12 +49,6 @@ class AMQPBase(object): ...@@ -49,12 +49,6 @@ class AMQPBase(object):
IOLoop.current().call_later(5, self.reconnect) IOLoop.current().call_later(5, self.reconnect)
return return
# refresh api token if needed
if sickrage.app.api.token_time_remaining < (int(sickrage.app.api.token['expires_in']) / 2):
if not sickrage.app.api.refresh_token():
IOLoop.current().call_later(5, self.reconnect)
return
# declare server amqp queue # declare server amqp queue
if not sickrage.app.api.server.declare_amqp_queue(sickrage.app.config.general.server_id): if not sickrage.app.api.server.declare_amqp_queue(sickrage.app.config.general.server_id):
IOLoop.current().call_later(5, self.reconnect) IOLoop.current().call_later(5, self.reconnect)
......
...@@ -8,13 +8,11 @@ import oauthlib.oauth2 ...@@ -8,13 +8,11 @@ import oauthlib.oauth2
import requests import requests
import requests.exceptions import requests.exceptions
from jose import ExpiredSignatureError from jose import ExpiredSignatureError
from oauthlib.oauth2 import MissingTokenError, InvalidGrantError, InvalidClientIdError from keycloak.exceptions import KeycloakClientError
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from sqlalchemy import orm
import sickrage import sickrage
from sickrage.core.api.exceptions import APIError from sickrage.core.api.exceptions import APIError
from sickrage.core.databases.cache import CacheDB
class API(object): class API(object):
...@@ -22,11 +20,7 @@ class API(object): ...@@ -22,11 +20,7 @@ class API(object):
self.name = 'SR-API' self.name = 'SR-API'
self.api_base = 'https://www.sickrage.ca/api/' self.api_base = 'https://www.sickrage.ca/api/'
self.api_version = 'v6' self.api_version = 'v6'
self._session = None self._token = {}
@property
def is_enabled(self):
return self.token
@property @property
def imdb(self): def imdb(self):
...@@ -37,8 +31,12 @@ class API(object): ...@@ -37,8 +31,12 @@ class API(object):
return self.ServerAPI(self) return self.ServerAPI(self)
@property @property
def provider(self): def search_provider(self):
return self.ProviderAPI(self) return self.SearchProviderAPI(self)
@property
def series_provider(self):
return self.SeriesProviderAPI(self)
@property @property
def announcement(self): def announcement(self):
...@@ -62,63 +60,36 @@ class API(object): ...@@ -62,63 +60,36 @@ class API(object):
@property @property
def session(self): def session(self):
extra = { if not self.token_url:
'client_id': sickrage.app.auth_server.client_id, return
}
if not self._session and self.token_url:
self._session = OAuth2Session(token=self.token, auto_refresh_kwargs=extra, auto_refresh_url=self.token_url, token_updater=self.token_updater)
return self._session return OAuth2Session(
token=self.token,
auto_refresh_kwargs={'client_id': sickrage.app.auth_server.client_id},
auto_refresh_url=self.token_url,
token_updater=self.token_updater
)
@property @property
def token(self): def token(self):
session = sickrage.app.cache_db.session() if not self._token:
self.login()
try: elif self.token_time_remaining < (int(self._token.get('expires_in')) / 2):
token = session.query(CacheDB.OAuth2Token).one() self.refresh_token()
return token.as_dict()
except orm.exc.NoResultFound:
return {}
@token.setter
def token(self, value):
new_token = {
'access_token': value.get('access_token'),
'refresh_token': value.get('refresh_token'),
'expires_in': value.get('expires_in'),
'session_state': value.get('session_state'),
'token_type': value.get('token_type'),
'expires_at': value.get('expires_at', int(time.time() + value.get('expires_in'))),
'scope': value.scope if isinstance(value, oauthlib.oauth2.OAuth2Token) else value.get('scope'),
}
session = sickrage.app.cache_db.session() return self._token
try:
token = session.query(CacheDB.OAuth2Token).one()
token.update(**new_token)
except orm.exc.NoResultFound:
session.add(CacheDB.OAuth2Token(**new_token))
finally:
session.commit()
self._session = None
@token.deleter
def token(self):
session = sickrage.app.cache_db.session()
session.query(CacheDB.OAuth2Token).delete()
session.commit()
@property @property
def token_expiration(self): def token_expiration(self):
try: try:
if not self._token:
return time.time()
certs = sickrage.app.auth_server.certs() certs = sickrage.app.auth_server.certs()
if not certs: if not certs:
return time.time() return time.time()
decoded_token = sickrage.app.auth_server.decode_token(self.token['access_token'], certs) decoded_token = sickrage.app.auth_server.decode_token(self._token.get('access_token'), certs)
return decoded_token.get('exp', time.time()) return decoded_token.get('exp', time.time())
except ExpiredSignatureError: except ExpiredSignatureError:
return time.time() return time.time()
...@@ -158,31 +129,49 @@ class API(object): ...@@ -158,31 +129,49 @@ class API(object):
return self.request('GET', 'userinfo') return self.request('GET', 'userinfo')
def token_updater(self, value): def token_updater(self, value):
self.token = value self._token = value
def logout(self): def login(self):
sickrage.app.auth_server.logout(self.token.get('refresh_token')) if not self.health:
return False
def refresh_token(self): if not self.token_url:
extra = { return False
session = requests.session()
data = {
'client_id': sickrage.app.auth_server.client_id, 'client_id': sickrage.app.auth_server.client_id,
'grant_type': 'password',
'apikey': sickrage.app.config.general.sso_api_key
} }
if self.token_url: try:
resp = session.post(self.token_url, data)
resp.raise_for_status()
self._token = resp.json()
except requests.exceptions.RequestException:
return False
return True
def logout(self):
if self._token:
try: try:
client = OAuth2Session(sickrage.app.auth_server.client_id, token=self.token) sickrage.app.auth_server.logout(self._token.get('refresh_token'))
self.token = client.refresh_token(self.token_url, **extra) except KeycloakClientError:
return True pass
except (InvalidGrantError, MissingTokenError, InvalidClientIdError, requests.exceptions.RequestException):
return False def refresh_token(self):
try:
if not self._token:
return self.login()
return False self._token = sickrage.app.auth_server.refresh_token(self._token.get('refresh_token'))
except KeycloakClientError:
return self.login()
def exchange_token(self, access_token, scope='offline_access'): return True
exchange = {'scope': scope, 'subject_token': access_token}
exchanged_token = sickrage.app.auth_server.token_exchange(**exchange)
if exchanged_token:
self.token = exchanged_token
def allowed_usernames(self): def allowed_usernames(self):
return self.request('GET', 'allowed-usernames') return self.request('GET', 'allowed-usernames')
...@@ -197,7 +186,7 @@ class API(object): ...@@ -197,7 +186,7 @@ class API(object):
return self.request('GET', 'network-timezones') return self.request('GET', 'network-timezones')
def request(self, method, url, timeout=120, **kwargs): def request(self, method, url, timeout=120, **kwargs):
if not self.is_enabled or not self.session: if not self.session:
return return
url = urljoin(self.api_base, "/".join([self.api_version, url])) url = urljoin(self.api_base, "/".join([self.api_version, url]))
...@@ -211,10 +200,6 @@ class API(object): ...@@ -211,10 +200,6 @@ class API(object):
return None return None
continue continue
if self.token_time_remaining < (int(self.token['expires_in']) / 2):
if not self.refresh_token():
continue
resp = self.session.request(method, url, timeout=timeout, verify=False, hooks={'response': self.throttle_hook}, **kwargs) resp = self.session.request(method, url, timeout=timeout, verify=False, hooks={'response': self.throttle_hook}, **kwargs)
resp.raise_for_status() resp.raise_for_status()
...@@ -225,12 +210,12 @@ class API(object): ...@@ -225,12 +210,12 @@ class API(object):
return resp.json() return resp.json()
except ValueError: except ValueError:
return resp.content return resp.content
except oauthlib.oauth2.TokenExpiredError: except (oauthlib.oauth2.TokenExpiredError, oauthlib.oauth2.InvalidGrantError):
self.refresh_token()
time.sleep(1)
except (oauthlib.oauth2.InvalidClientIdError, oauthlib.oauth2.MissingTokenError) as e:
self.refresh_token() self.refresh_token()
time.sleep(1) time.sleep(1)
except (oauthlib.oauth2.InvalidClientIdError, oauthlib.oauth2.MissingTokenError, oauthlib.oauth2.InvalidGrantError) as e:
sickrage.app.log.warning("Invalid token error, please re-link your SiCKRAGE account from `settings->general->advanced->sickrage api`")
return
except requests.exceptions.ReadTimeout as e: except requests.exceptions.ReadTimeout as e:
if i > 3: if i > 3:
sickrage.app.log.debug(f'Error connecting to url {url} Error: {e}') sickrage.app.log.debug(f'Error connecting to url {url} Error: {e}')
...@@ -346,21 +331,21 @@ class API(object): ...@@ -346,21 +331,21 @@ class API(object):
def get_announcements(self): def get_announcements(self):
return self.api.request('GET', 'announcements') return self.api.request('GET', 'announcements')
class ProviderAPI: class SearchProviderAPI:
def __init__(self, api): def __init__(self, api):
self.api = api self.api = api
def get_urls(self, provider): def get_urls(self, provider):
query = f'provider/{provider}/urls' endpoint = f'provider/{provider}/urls'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def get_status(self, provider): def get_status(self, provider):
query = f'provider/{provider}/status' endpoint = f'provider/{provider}/status'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def get_search_result(self, provider, series_id, season, episode): def get_search_result(self, provider, series_id, season, episode):
query = f'provider/{provider}/series-id/{series_id}/season/{season}/episode/{episode}' endpoint = f'provider/{provider}/series-id/{series_id}/season/{season}/episode/{episode}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def add_search_result(self, provider, data): def add_search_result(self, provider, data):
return self.api.request('POST', f'provider/{provider}', json=data) return self.api.request('POST', f'provider/{provider}', json=data)
...@@ -370,12 +355,12 @@ class API(object): ...@@ -370,12 +355,12 @@ class API(object):
self.api = api self.api = api
def get_trackers(self): def get_trackers(self):
query = f'torrent/trackers' endpoint = f'torrent/trackers'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def get_torrent(self, hash): def get_torrent(self, hash):
query = f'torrent/{hash}' endpoint = f'torrent/{hash}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def add_torrent(self, url): def add_torrent(self, url):
return self.api.request('POST', 'torrent', json={'url': url}) return self.api.request('POST', 'torrent', json={'url': url})
...@@ -385,56 +370,56 @@ class API(object): ...@@ -385,56 +370,56 @@ class API(object):
self.api = api self.api = api
def search_by_imdb_title(self, title): def search_by_imdb_title(self, title):
query = f'imdb/search-by-title/{title}' endpoint = f'imdb/search-by-title/{title}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def search_by_imdb_id(self, imdb_id): def search_by_imdb_id(self, imdb_id):
query = f'imdb/search-by-id/{imdb_id}' endpoint = f'imdb/search-by-id/{imdb_id}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
class GoogleDriveAPI: class GoogleDriveAPI:
def __init__(self, api): def __init__(self, api):
self.api = api self.api = api
def is_connected(self): def is_connected(self):
query = 'google-drive/is-connected' endpoint = 'google-drive/is-connected'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def upload(self, file, folder): def upload(self, file, folder):
query = 'google-drive/upload' endpoint = 'google-drive/upload'
return self.api.request('POST', query, files={'file': open(file, 'rb')}, params={'folder': folder}) return self.api.request('POST', endpoint, files={'file': open(file, 'rb')}, params={'folder': folder})
def download(self, id): def download(self, id):
query = f'google-drive/download/{id}' endpoint = f'google-drive/download/{id}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def delete(self, id): def delete(self, id):
query = f'google-drive/delete/{id}' endpoint = f'google-drive/delete/{id}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def search_files(self, id, term): def search_files(self, id, term):
query = f'google-drive/search-files/{id}/{term}' endpoint = f'google-drive/search-files/{id}/{term}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def list_files(self, id): def list_files(self, id):
query = f'google-drive/list-files/{id}' endpoint = f'google-drive/list-files/{id}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def clear_folder(self, id): def clear_folder(self, id):
query = f'google-drive/clear-folder/{id}' endpoint = f'google-drive/clear-folder/{id}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
class SceneExceptions: class SceneExceptions:
def __init__(self, api): def __init__(self, api):
self.api = api self.api = api
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
query = 'scene-exceptions' endpoint = 'scene-exceptions'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
def search_by_id(self, series_id): def search_by_id(self, series_id):
query = f'scene-exceptions/search-by-id/{series_id}' endpoint = f'scene-exceptions/search-by-id/{series_id}'
return self.api.request('GET', query) return self.api.request('GET', endpoint)
class AlexaAPI: class AlexaAPI:
def __init__(self, api): def __init__(self, api):
...@@ -442,3 +427,27 @@ class API(object): ...@@ -442,3 +427,27 @@ class API(object):
def send_notification(self, message): def send_notification(self, message):
return self.api.request('POST', 'alexa/notification', json={'message': message}) return self.api.request('POST', 'alexa/notification', json={'message': message})
class SeriesProviderAPI:
def __init__(self, api):
self.api = api
def search(self, provider, query, language='eng'):
endpoint = f'series-provider/{provider}/search/{query}/{language}'
return self.api.request('GET', endpoint)
def get_series_info(self, provider, series_id, language='eng'):
endpoint = f'series-provider/{provider}/series/{series_id}/{language}'
return self.api.request('GET', endpoint)
def get_episodes_info(self, provider, series_id, season_type='default', language='eng'):
endpoint = f'series-provider/{provider}/series/{series_id}/episodes/{season_type}/{language}'
return self.api.request('GET', endpoint)
def languages(self, provider):
endpoint = f'series-provider/{provider}/languages'
return self.api.request('GET', endpoint)
def updates(self, provider, since):
endpoint = f'series-provider/{provider}/updates/{since}'
return self.api.request('GET', endpoint)
...@@ -36,9 +36,9 @@ class ImageCache(object): ...@@ -36,9 +36,9 @@ class ImageCache(object):
FANART_THUMB = 6 FANART_THUMB = 6
IMAGE_TYPES = { IMAGE_TYPES = {
BANNER: 'series', BANNER: 'banner',
POSTER: 'poster', POSTER: 'poster',
BANNER_THUMB: 'series_thumb', BANNER_THUMB: 'banner_thumb',
POSTER_THUMB: 'poster_thumb', POSTER_THUMB: 'poster_thumb',
FANART: 'fanart', FANART: 'fanart',
FANART_THUMB: 'fanart_thumb' FANART_THUMB: 'fanart_thumb'
...@@ -266,6 +266,9 @@ class ImageCache(object): ...@@ -266,6 +266,9 @@ class ImageCache(object):
# retrieve the image from a series provider using the generic metadata class # retrieve the image from a series provider using the generic metadata class
metadata_generator = MetadataProvider() metadata_generator = MetadataProvider()
img_data = metadata_generator._retrieve_show_image(self.IMAGE_TYPES[img_type], show_obj) img_data = metadata_generator._retrieve_show_image(self.IMAGE_TYPES[img_type], show_obj)
if not img_data:
return False
result = metadata_generator._write_image(img_data, dest_path, force) result = metadata_generator._write_image(img_data, dest_path, force)
return result return result
......
...@@ -257,7 +257,7 @@ class TVCache(object): ...@@ -257,7 +257,7 @@ class TVCache(object):
from sickrage.search_providers import SearchProviderType from sickrage.search_providers import SearchProviderType
if not self.provider.private and self.provider.provider_type in [SearchProviderType.NZB, SearchProviderType.TORRENT]: if not self.provider.private and self.provider.provider_type in [SearchProviderType.NZB, SearchProviderType.TORRENT]:
try: try:
sickrage.app.api.provider.add_search_result(provider=self.providerID, data=dbData) sickrage.app.api.search_provider.add_search_result(provider=self.providerID, data=dbData)
except Exception as e: except Exception as e:
pass pass
except (InvalidShowException, InvalidNameException): except (InvalidShowException, InvalidNameException):
...@@ -269,7 +269,7 @@ class TVCache(object): ...@@ -269,7 +269,7 @@ class TVCache(object):
# get data from external database