2021-05-08 14:39:00 +00:00
|
|
|
# coding=utf-8
|
|
|
|
|
|
|
|
import logging
|
2021-05-18 16:02:01 +00:00
|
|
|
import json
|
2021-10-21 00:46:22 +00:00
|
|
|
import time
|
2022-08-21 12:22:56 +00:00
|
|
|
import threading
|
2022-05-01 12:00:20 +00:00
|
|
|
|
2021-05-08 14:39:00 +00:00
|
|
|
from requests import Session
|
|
|
|
from signalr import Connection
|
|
|
|
from requests.exceptions import ConnectionError
|
|
|
|
from signalrcore.hub_connection_builder import HubConnectionBuilder
|
2022-08-21 12:22:56 +00:00
|
|
|
from collections import deque
|
|
|
|
from time import sleep
|
2022-11-09 17:29:14 +00:00
|
|
|
from websocket._exceptions import WebSocketBadStatusException
|
2021-05-08 14:39:00 +00:00
|
|
|
|
2022-05-01 12:00:20 +00:00
|
|
|
from constants import headers
|
2022-10-06 02:51:54 +00:00
|
|
|
from app.event_handler import event_stream
|
2022-05-01 12:00:20 +00:00
|
|
|
from sonarr.sync.episodes import sync_episodes, sync_one_episode
|
|
|
|
from sonarr.sync.series import update_series, update_one_series
|
|
|
|
from radarr.sync.movies import update_movies, update_one_movie
|
|
|
|
from sonarr.info import get_sonarr_info, url_sonarr
|
|
|
|
from radarr.info import url_radarr
|
2022-09-25 12:38:27 +00:00
|
|
|
from .database import TableShows
|
2022-11-09 14:44:47 +00:00
|
|
|
from .event_handler import event_stream
|
2021-05-08 14:39:00 +00:00
|
|
|
|
2022-05-01 12:00:20 +00:00
|
|
|
from .config import settings
|
|
|
|
from .scheduler import scheduler
|
|
|
|
from .get_args import args
|
2021-06-01 02:01:35 +00:00
|
|
|
|
|
|
|
|
2022-08-21 12:22:56 +00:00
|
|
|
sonarr_queue = deque()
|
|
|
|
radarr_queue = deque()
|
|
|
|
|
|
|
|
last_event_data = None
|
|
|
|
|
|
|
|
|
2021-12-01 02:44:33 +00:00
|
|
|
class SonarrSignalrClientLegacy:
|
2021-05-08 14:39:00 +00:00
|
|
|
def __init__(self):
|
2021-12-01 02:44:33 +00:00
|
|
|
super(SonarrSignalrClientLegacy, self).__init__()
|
2021-05-08 14:39:00 +00:00
|
|
|
self.apikey_sonarr = None
|
|
|
|
self.session = Session()
|
2021-06-01 02:01:35 +00:00
|
|
|
self.session.timeout = 60
|
|
|
|
self.session.verify = False
|
|
|
|
self.session.headers = headers
|
2021-05-08 14:39:00 +00:00
|
|
|
self.connection = None
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = False
|
2021-05-08 14:39:00 +00:00
|
|
|
|
2021-05-15 13:41:39 +00:00
|
|
|
def start(self):
|
2021-08-04 19:29:37 +00:00
|
|
|
if get_sonarr_info.is_legacy():
|
2021-05-08 14:39:00 +00:00
|
|
|
logging.warning('BAZARR can only sync from Sonarr v3 SignalR feed to get real-time update. You should '
|
2021-08-04 19:29:37 +00:00
|
|
|
'consider upgrading your version({}).'.format(get_sonarr_info.version()))
|
2021-07-22 18:09:39 +00:00
|
|
|
else:
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = False
|
|
|
|
event_stream(type='badges')
|
2021-07-22 18:09:39 +00:00
|
|
|
logging.info('BAZARR trying to connect to Sonarr SignalR feed...')
|
|
|
|
self.configure()
|
|
|
|
while not self.connection.started:
|
|
|
|
try:
|
|
|
|
self.connection.start()
|
|
|
|
except ConnectionError:
|
2021-10-21 00:46:22 +00:00
|
|
|
time.sleep(5)
|
2021-07-22 18:09:39 +00:00
|
|
|
except json.decoder.JSONDecodeError:
|
2021-08-09 20:15:38 +00:00
|
|
|
logging.error("BAZARR cannot parse JSON returned by SignalR feed. This is caused by a permissions "
|
2022-02-17 20:53:53 +00:00
|
|
|
"issue when Sonarr try to access its /config/.config directory."
|
2022-08-21 12:22:56 +00:00
|
|
|
"Typically permissions are too permissive - only the user and group Sonarr runs as "
|
|
|
|
"should have Read/Write permissions (e.g. files 664 / folders 775). You should fix "
|
|
|
|
"permissions on that directory and restart Sonarr. Also, if you're a Docker image "
|
2021-08-09 20:15:38 +00:00
|
|
|
"user, you should make sure you properly defined PUID/PGID environment variables. "
|
|
|
|
"Otherwise, please contact Sonarr support.")
|
2022-11-09 17:29:14 +00:00
|
|
|
except WebSocketBadStatusException:
|
|
|
|
logging.debug("BAZARR cannot connect to Sonarr SignalR feed using websocket. We'll fall back to "
|
|
|
|
"SSE.")
|
|
|
|
self.configure(force_sse=True)
|
|
|
|
self.restart()
|
2021-07-22 18:09:39 +00:00
|
|
|
else:
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = True
|
|
|
|
event_stream(type='badges')
|
2021-07-22 18:09:39 +00:00
|
|
|
logging.info('BAZARR SignalR client for Sonarr is connected and waiting for events.')
|
|
|
|
if not args.dev:
|
|
|
|
scheduler.add_job(update_series, kwargs={'send_event': True}, max_instances=1)
|
|
|
|
scheduler.add_job(sync_episodes, kwargs={'send_event': True}, max_instances=1)
|
2021-05-15 13:41:39 +00:00
|
|
|
|
|
|
|
def stop(self, log=True):
|
|
|
|
try:
|
|
|
|
self.connection.close()
|
2022-01-03 03:59:30 +00:00
|
|
|
except Exception:
|
2022-08-21 12:22:56 +00:00
|
|
|
self.connection.started = False
|
2021-05-15 13:41:39 +00:00
|
|
|
if log:
|
|
|
|
logging.info('BAZARR SignalR client for Sonarr is now disconnected.')
|
|
|
|
|
|
|
|
def restart(self):
|
2021-05-26 20:47:14 +00:00
|
|
|
if self.connection:
|
2021-06-01 18:42:52 +00:00
|
|
|
if self.connection.started:
|
2022-08-21 12:22:56 +00:00
|
|
|
self.stop(log=False)
|
2021-05-15 13:41:39 +00:00
|
|
|
if settings.general.getboolean('use_sonarr'):
|
|
|
|
self.start()
|
|
|
|
|
2022-08-21 12:22:56 +00:00
|
|
|
def exception_handler(self):
|
|
|
|
sonarr_queue.clear()
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = False
|
|
|
|
event_stream(type='badges')
|
2021-06-01 18:42:52 +00:00
|
|
|
logging.error('BAZARR connection to Sonarr SignalR feed has been lost.')
|
2021-05-15 13:41:39 +00:00
|
|
|
self.restart()
|
|
|
|
|
2022-11-09 17:29:14 +00:00
|
|
|
def configure(self, force_sse=False):
|
2021-05-08 14:39:00 +00:00
|
|
|
self.apikey_sonarr = settings.sonarr.apikey
|
2022-11-09 17:29:14 +00:00
|
|
|
self.connection = Connection(url_sonarr() + "/signalr", self.session, force_sse=force_sse)
|
2021-05-08 14:39:00 +00:00
|
|
|
self.connection.qs = {'apikey': self.apikey_sonarr}
|
|
|
|
sonarr_hub = self.connection.register_hub('') # Sonarr doesn't use named hub
|
|
|
|
|
|
|
|
sonarr_method = ['series', 'episode']
|
|
|
|
for item in sonarr_method:
|
2022-08-21 12:22:56 +00:00
|
|
|
sonarr_hub.client.on(item, feed_queue)
|
2021-05-08 14:39:00 +00:00
|
|
|
|
2021-05-15 13:41:39 +00:00
|
|
|
self.connection.exception += self.exception_handler
|
2021-05-08 14:39:00 +00:00
|
|
|
|
|
|
|
|
2021-12-01 02:44:33 +00:00
|
|
|
class SonarrSignalrClient:
|
|
|
|
def __init__(self):
|
|
|
|
super(SonarrSignalrClient, self).__init__()
|
|
|
|
self.apikey_sonarr = None
|
|
|
|
self.connection = None
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = False
|
2021-12-01 02:44:33 +00:00
|
|
|
|
|
|
|
def start(self):
|
|
|
|
self.configure()
|
|
|
|
logging.info('BAZARR trying to connect to Sonarr SignalR feed...')
|
|
|
|
while self.connection.transport.state.value not in [0, 1, 2]:
|
|
|
|
try:
|
|
|
|
self.connection.start()
|
|
|
|
except ConnectionError:
|
|
|
|
time.sleep(5)
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
logging.info('BAZARR SignalR client for Sonarr is now disconnected.')
|
|
|
|
self.connection.stop()
|
|
|
|
|
|
|
|
def restart(self):
|
|
|
|
if self.connection:
|
|
|
|
if self.connection.transport.state.value in [0, 1, 2]:
|
|
|
|
self.stop()
|
|
|
|
if settings.general.getboolean('use_sonarr'):
|
|
|
|
self.start()
|
|
|
|
|
|
|
|
def exception_handler(self):
|
2022-08-21 12:22:56 +00:00
|
|
|
sonarr_queue.clear()
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = False
|
|
|
|
event_stream(type='badges')
|
2021-12-01 02:44:33 +00:00
|
|
|
logging.error("BAZARR connection to Sonarr SignalR feed has failed. We'll try to reconnect.")
|
|
|
|
self.restart()
|
|
|
|
|
2022-11-09 14:44:47 +00:00
|
|
|
def on_connect_handler(self):
|
|
|
|
self.connected = True
|
|
|
|
event_stream(type='badges')
|
2021-12-01 02:44:33 +00:00
|
|
|
logging.info('BAZARR SignalR client for Sonarr is connected and waiting for events.')
|
|
|
|
if not args.dev:
|
|
|
|
scheduler.add_job(update_series, kwargs={'send_event': True}, max_instances=1)
|
|
|
|
scheduler.add_job(sync_episodes, kwargs={'send_event': True}, max_instances=1)
|
|
|
|
|
2022-11-09 14:44:47 +00:00
|
|
|
def on_reconnect_handler(self):
|
|
|
|
self.connected = False
|
|
|
|
event_stream(type='badges')
|
|
|
|
logging.error('BAZARR SignalR client for Sonarr connection as been lost. Trying to reconnect...')
|
|
|
|
|
2021-12-01 02:44:33 +00:00
|
|
|
def configure(self):
|
|
|
|
self.apikey_sonarr = settings.sonarr.apikey
|
|
|
|
self.connection = HubConnectionBuilder() \
|
|
|
|
.with_url(url_sonarr() + "/signalr/messages?access_token={}".format(self.apikey_sonarr),
|
|
|
|
options={
|
|
|
|
"verify_ssl": False,
|
|
|
|
"headers": headers
|
|
|
|
}) \
|
|
|
|
.with_automatic_reconnect({
|
|
|
|
"type": "raw",
|
|
|
|
"keep_alive_interval": 5,
|
|
|
|
"reconnect_interval": 180,
|
|
|
|
"max_attempts": None
|
|
|
|
}).build()
|
|
|
|
self.connection.on_open(self.on_connect_handler)
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connection.on_reconnect(self.on_reconnect_handler)
|
2021-12-01 02:44:33 +00:00
|
|
|
self.connection.on_close(lambda: logging.debug('BAZARR SignalR client for Sonarr is disconnected.'))
|
|
|
|
self.connection.on_error(self.exception_handler)
|
2022-08-21 12:22:56 +00:00
|
|
|
self.connection.on("receiveMessage", feed_queue)
|
2021-12-01 02:44:33 +00:00
|
|
|
|
|
|
|
|
2021-05-17 23:39:09 +00:00
|
|
|
class RadarrSignalrClient:
|
2021-05-08 14:39:00 +00:00
|
|
|
def __init__(self):
|
|
|
|
super(RadarrSignalrClient, self).__init__()
|
|
|
|
self.apikey_radarr = None
|
|
|
|
self.connection = None
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = False
|
2021-05-08 14:39:00 +00:00
|
|
|
|
2021-05-11 20:24:02 +00:00
|
|
|
def start(self):
|
2021-05-12 12:57:53 +00:00
|
|
|
self.configure()
|
2021-06-01 18:42:52 +00:00
|
|
|
logging.info('BAZARR trying to connect to Radarr SignalR feed...')
|
|
|
|
while self.connection.transport.state.value not in [0, 1, 2]:
|
|
|
|
try:
|
|
|
|
self.connection.start()
|
|
|
|
except ConnectionError:
|
2021-10-21 00:46:22 +00:00
|
|
|
time.sleep(5)
|
2021-05-11 20:24:02 +00:00
|
|
|
|
2021-05-08 14:39:00 +00:00
|
|
|
def stop(self):
|
|
|
|
logging.info('BAZARR SignalR client for Radarr is now disconnected.')
|
2021-05-11 20:24:02 +00:00
|
|
|
self.connection.stop()
|
2021-05-08 14:39:00 +00:00
|
|
|
|
|
|
|
def restart(self):
|
2021-05-26 20:47:14 +00:00
|
|
|
if self.connection:
|
|
|
|
if self.connection.transport.state.value in [0, 1, 2]:
|
|
|
|
self.stop()
|
2021-05-08 14:39:00 +00:00
|
|
|
if settings.general.getboolean('use_radarr'):
|
2021-05-11 20:24:02 +00:00
|
|
|
self.start()
|
2021-05-08 14:39:00 +00:00
|
|
|
|
2021-05-17 23:39:09 +00:00
|
|
|
def exception_handler(self):
|
2022-08-21 12:22:56 +00:00
|
|
|
radarr_queue.clear()
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connected = False
|
|
|
|
event_stream(type='badges')
|
2021-05-17 23:39:09 +00:00
|
|
|
logging.error("BAZARR connection to Radarr SignalR feed has failed. We'll try to reconnect.")
|
|
|
|
self.restart()
|
|
|
|
|
2022-11-09 14:44:47 +00:00
|
|
|
def on_connect_handler(self):
|
|
|
|
self.connected = True
|
|
|
|
event_stream(type='badges')
|
2021-05-18 03:57:42 +00:00
|
|
|
logging.info('BAZARR SignalR client for Radarr is connected and waiting for events.')
|
|
|
|
if not args.dev:
|
2021-06-16 23:14:21 +00:00
|
|
|
scheduler.add_job(update_movies, kwargs={'send_event': True}, max_instances=1)
|
2021-05-18 03:57:42 +00:00
|
|
|
|
2022-11-09 14:44:47 +00:00
|
|
|
def on_reconnect_handler(self):
|
|
|
|
self.connected = False
|
|
|
|
event_stream(type='badges')
|
|
|
|
logging.error('BAZARR SignalR client for Radarr connection as been lost. Trying to reconnect...')
|
|
|
|
|
2021-05-11 20:24:02 +00:00
|
|
|
def configure(self):
|
2021-05-08 14:39:00 +00:00
|
|
|
self.apikey_radarr = settings.radarr.apikey
|
|
|
|
self.connection = HubConnectionBuilder() \
|
|
|
|
.with_url(url_radarr() + "/signalr/messages?access_token={}".format(self.apikey_radarr),
|
|
|
|
options={
|
2021-06-01 02:01:35 +00:00
|
|
|
"verify_ssl": False,
|
|
|
|
"headers": headers
|
2021-05-11 20:24:02 +00:00
|
|
|
}) \
|
|
|
|
.with_automatic_reconnect({
|
|
|
|
"type": "raw",
|
2021-05-28 17:21:53 +00:00
|
|
|
"keep_alive_interval": 5,
|
2021-05-19 03:05:23 +00:00
|
|
|
"reconnect_interval": 180,
|
2021-05-11 20:24:02 +00:00
|
|
|
"max_attempts": None
|
|
|
|
}).build()
|
2021-05-18 03:57:42 +00:00
|
|
|
self.connection.on_open(self.on_connect_handler)
|
2022-11-09 14:44:47 +00:00
|
|
|
self.connection.on_reconnect(self.on_reconnect_handler)
|
2021-05-11 20:24:02 +00:00
|
|
|
self.connection.on_close(lambda: logging.debug('BAZARR SignalR client for Radarr is disconnected.'))
|
2021-05-17 23:39:09 +00:00
|
|
|
self.connection.on_error(self.exception_handler)
|
2022-08-21 12:22:56 +00:00
|
|
|
self.connection.on("receiveMessage", feed_queue)
|
2021-05-08 14:39:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
def dispatcher(data):
|
2021-05-26 20:47:14 +00:00
|
|
|
try:
|
2022-08-21 12:22:56 +00:00
|
|
|
series_title = series_year = episode_title = season_number = episode_number = movie_title = movie_year = None
|
|
|
|
|
|
|
|
#
|
|
|
|
try:
|
|
|
|
episodesChanged = False
|
2021-05-26 20:47:14 +00:00
|
|
|
topic = data['name']
|
2022-08-21 12:22:56 +00:00
|
|
|
|
|
|
|
media_id = data['body']['resource']['id']
|
|
|
|
action = data['body']['action']
|
|
|
|
if topic == 'series':
|
2021-05-26 20:47:14 +00:00
|
|
|
if 'episodesChanged' in data['body']['resource']:
|
|
|
|
episodesChanged = data['body']['resource']['episodesChanged']
|
2022-08-21 12:22:56 +00:00
|
|
|
series_title = data['body']['resource']['title']
|
|
|
|
series_year = data['body']['resource']['year']
|
|
|
|
elif topic == 'episode':
|
2022-09-25 12:38:27 +00:00
|
|
|
if 'series' in data['body']['resource']:
|
|
|
|
series_title = data['body']['resource']['series']['title']
|
|
|
|
series_year = data['body']['resource']['series']['year']
|
|
|
|
else:
|
|
|
|
series_metadata = TableShows.select(TableShows.title, TableShows.year)\
|
|
|
|
.where(TableShows.sonarrSeriesId == data['body']['resource']['seriesId'])\
|
|
|
|
.dicts()\
|
|
|
|
.get_or_none()
|
|
|
|
if series_metadata:
|
|
|
|
series_title = series_metadata['title']
|
|
|
|
series_year = series_metadata['year']
|
2022-08-21 12:22:56 +00:00
|
|
|
episode_title = data['body']['resource']['title']
|
|
|
|
season_number = data['body']['resource']['seasonNumber']
|
|
|
|
episode_number = data['body']['resource']['episodeNumber']
|
|
|
|
elif topic == 'movie':
|
|
|
|
movie_title = data['body']['resource']['title']
|
|
|
|
movie_year = data['body']['resource']['year']
|
|
|
|
except KeyError:
|
|
|
|
return
|
2021-05-26 20:47:14 +00:00
|
|
|
|
|
|
|
if topic == 'series':
|
2022-08-21 12:22:56 +00:00
|
|
|
logging.debug(f'Event received from Sonarr for series: {series_title} ({series_year})')
|
2021-05-26 20:47:14 +00:00
|
|
|
update_one_series(series_id=media_id, action=action)
|
|
|
|
if episodesChanged:
|
2021-05-28 17:49:57 +00:00
|
|
|
# this will happen if a season monitored status is changed.
|
2021-05-26 20:47:14 +00:00
|
|
|
sync_episodes(series_id=media_id, send_event=True)
|
|
|
|
elif topic == 'episode':
|
2022-08-21 12:22:56 +00:00
|
|
|
logging.debug(f'Event received from Sonarr for episode: {series_title} ({series_year}) - '
|
|
|
|
f'S{season_number:0>2}E{episode_number:0>2} - {episode_title}')
|
2022-03-22 02:14:44 +00:00
|
|
|
sync_one_episode(episode_id=media_id, defer_search=settings.sonarr.getboolean('defer_search_signalr'))
|
2021-05-26 20:47:14 +00:00
|
|
|
elif topic == 'movie':
|
2022-08-21 12:22:56 +00:00
|
|
|
logging.debug(f'Event received from Radarr for movie: {movie_title} ({movie_year})')
|
2022-03-22 02:14:44 +00:00
|
|
|
update_one_movie(movie_id=media_id, action=action,
|
|
|
|
defer_search=settings.radarr.getboolean('defer_search_signalr'))
|
2021-05-26 20:47:14 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.debug('BAZARR an exception occurred while parsing SignalR feed: {}'.format(repr(e)))
|
|
|
|
finally:
|
2022-10-06 02:51:54 +00:00
|
|
|
event_stream(type='badges')
|
2021-05-08 14:39:00 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
|
2022-08-21 12:22:56 +00:00
|
|
|
def feed_queue(data):
|
|
|
|
# check if event is duplicate from the previous one
|
|
|
|
global last_event_data
|
|
|
|
if data == last_event_data:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
last_event_data = data
|
|
|
|
|
|
|
|
# some sonarr version send event as a list of a single dict, we make it a dict
|
|
|
|
if isinstance(data, list) and len(data):
|
|
|
|
data = data[0]
|
|
|
|
|
|
|
|
# if data is a dict and contain an event for series, episode or movie, we add it to the event queue
|
|
|
|
if isinstance(data, dict) and 'name' in data:
|
|
|
|
if data['name'] in ['series', 'episode']:
|
|
|
|
sonarr_queue.append(data)
|
|
|
|
elif data['name'] == 'movie':
|
|
|
|
radarr_queue.append(data)
|
|
|
|
|
|
|
|
|
|
|
|
def consume_queue(queue):
|
|
|
|
# get events data from queue one at a time and dispatch it
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
data = queue.popleft()
|
|
|
|
except IndexError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
dispatcher(data)
|
|
|
|
sleep(0.1)
|
|
|
|
|
|
|
|
|
|
|
|
# start both queue consuming threads
|
|
|
|
threading.Thread(target=consume_queue, args=(sonarr_queue,)).start()
|
|
|
|
threading.Thread(target=consume_queue, args=(radarr_queue,)).start()
|
|
|
|
|
|
|
|
# instantiate proper SignalR client
|
2021-12-01 02:44:33 +00:00
|
|
|
sonarr_signalr_client = SonarrSignalrClientLegacy() if get_sonarr_info.version().startswith(('0.', '2.', '3.')) else \
|
|
|
|
SonarrSignalrClient()
|
2021-05-08 14:39:00 +00:00
|
|
|
radarr_signalr_client = RadarrSignalrClient()
|