|
|
@ -10,6 +10,7 @@ from media.trakt import Trakt
|
|
|
|
from misc import helpers
|
|
|
|
from misc import helpers
|
|
|
|
from misc.config import cfg
|
|
|
|
from misc.config import cfg
|
|
|
|
from misc.log import logger
|
|
|
|
from misc.log import logger
|
|
|
|
|
|
|
|
from notifications import Notifications
|
|
|
|
|
|
|
|
|
|
|
|
############################################################
|
|
|
|
############################################################
|
|
|
|
# INIT
|
|
|
|
# INIT
|
|
|
@ -18,6 +19,9 @@ from misc.log import logger
|
|
|
|
# Logging
|
|
|
|
# Logging
|
|
|
|
log = logger.get_logger('traktarr')
|
|
|
|
log = logger.get_logger('traktarr')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Notifications
|
|
|
|
|
|
|
|
notify = Notifications()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Click
|
|
|
|
# Click
|
|
|
|
@click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.')
|
|
|
|
@click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt lists.')
|
|
|
@ -45,7 +49,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
if not trakt.validate_api_key():
|
|
|
|
if not trakt.validate_api_key():
|
|
|
|
log.error("Aborting due to failure to validate Trakt API Key")
|
|
|
|
log.error("Aborting due to failure to validate Trakt API Key")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to validate Trakt API Key'})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to validate Trakt API Key'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Validated Trakt API Key")
|
|
|
|
log.info("Validated Trakt API Key")
|
|
|
@ -55,7 +60,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
if not sonarr.validate_api_key():
|
|
|
|
if not sonarr.validate_api_key():
|
|
|
|
log.error("Aborting due to failure to validate Sonarr URL / API Key")
|
|
|
|
log.error("Aborting due to failure to validate Sonarr URL / API Key")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to validate Sonarr URL / API Key'})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to validate Sonarr URL / API Key'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Validated Sonarr URL & API Key")
|
|
|
|
log.info("Validated Sonarr URL & API Key")
|
|
|
@ -65,7 +71,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
if not profile_id or not profile_id > 0:
|
|
|
|
if not profile_id or not profile_id > 0:
|
|
|
|
log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile)
|
|
|
|
log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.sonarr.profile)
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows',
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
'reason': 'Failure to retrieve Sonarr Profile ID of %s' % cfg.sonarr.profile})
|
|
|
|
'reason': 'Failure to retrieve Sonarr Profile ID of %s' % cfg.sonarr.profile})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
@ -76,7 +82,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
if profile_tags is None:
|
|
|
|
if profile_tags is None:
|
|
|
|
log.error("Aborting due to failure to retrieve Tag ID's")
|
|
|
|
log.error("Aborting due to failure to retrieve Tag ID's")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'reason': "Failure to retrieve Sonarr Tag ID's"})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': "Failure to retrieve Sonarr Tag ID's"})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Retrieved %d Tag ID's", len(profile_tags))
|
|
|
|
log.info("Retrieved %d Tag ID's", len(profile_tags))
|
|
|
@ -86,7 +93,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
if not sonarr_series_list:
|
|
|
|
if not sonarr_series_list:
|
|
|
|
log.error("Aborting due to failure to retrieve Sonarr shows list")
|
|
|
|
log.error("Aborting due to failure to retrieve Sonarr shows list")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to retrieve Sonarr shows list'})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to retrieve Sonarr shows list'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list))
|
|
|
|
log.info("Retrieved Sonarr shows list, shows found: %d", len(sonarr_series_list))
|
|
|
@ -102,13 +110,15 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.error("Aborting due to unknown Trakt list type")
|
|
|
|
log.error("Aborting due to unknown Trakt list type")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'reason': 'Failure to determine Trakt list type'})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to determine Trakt list type'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
if not trakt_series_list:
|
|
|
|
if not trakt_series_list:
|
|
|
|
log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type)
|
|
|
|
log.error("Aborting due to failure to retrieve Trakt %s shows list", list_type)
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify(
|
|
|
|
callback_notify(
|
|
|
|
{'event': 'abort', 'type': 'shows', 'reason': 'Failure to retrieve Trakt %s shows list' % list_type})
|
|
|
|
{'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to retrieve Trakt %s shows list' % list_type})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Retrieved Trakt %s shows list, shows found: %d", list_type, len(trakt_series_list))
|
|
|
|
log.info("Retrieved Trakt %s shows list, shows found: %d", list_type, len(trakt_series_list))
|
|
|
@ -118,7 +128,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
if processed_series_list is None:
|
|
|
|
if processed_series_list is None:
|
|
|
|
log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list")
|
|
|
|
log.error("Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows',
|
|
|
|
callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
|
|
|
|
'reason': 'Failure to remove existing Sonarr shows from retrieved Trakt %s shows list' % list_type
|
|
|
|
'reason': 'Failure to remove existing Sonarr shows from retrieved Trakt %s shows list' % list_type
|
|
|
|
})
|
|
|
|
})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
@ -149,7 +159,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications=
|
|
|
|
not no_search):
|
|
|
|
not no_search):
|
|
|
|
log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], use_tags)
|
|
|
|
log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'], use_tags)
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'add_show', 'show': series['show']})
|
|
|
|
callback_notify({'event': 'add_show', 'list_type': list_type, 'show': series['show']})
|
|
|
|
added_shows += 1
|
|
|
|
added_shows += 1
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'],
|
|
|
|
log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'],
|
|
|
@ -188,7 +198,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications
|
|
|
|
if not trakt.validate_api_key():
|
|
|
|
if not trakt.validate_api_key():
|
|
|
|
log.error("Aborting due to failure to validate Trakt API Key")
|
|
|
|
log.error("Aborting due to failure to validate Trakt API Key")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to validate Trakt API Key'})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to validate Trakt API Key'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Validated Trakt API Key")
|
|
|
|
log.info("Validated Trakt API Key")
|
|
|
@ -199,7 +210,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications
|
|
|
|
log.error("Aborting due to failure to validate Radarr URL / API Key")
|
|
|
|
log.error("Aborting due to failure to validate Radarr URL / API Key")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify(
|
|
|
|
callback_notify(
|
|
|
|
{'event': 'abort', 'type': 'movies', 'reason': 'Failure to validate Radarr URL / API Key'})
|
|
|
|
{'event': 'abort', 'type': 'movies', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to validate Radarr URL / API Key'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Validated Radarr URL & API Key")
|
|
|
|
log.info("Validated Radarr URL & API Key")
|
|
|
@ -209,7 +221,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications
|
|
|
|
if not profile_id or not profile_id > 0:
|
|
|
|
if not profile_id or not profile_id > 0:
|
|
|
|
log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile)
|
|
|
|
log.error("Aborting due to failure to retrieve Profile ID for: %s", cfg.radarr.profile)
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies',
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
|
|
|
|
'reason': 'Failure to retrieve Radarr Profile ID of %s' % cfg.radarr.profile})
|
|
|
|
'reason': 'Failure to retrieve Radarr Profile ID of %s' % cfg.radarr.profile})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
@ -220,7 +232,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications
|
|
|
|
if not radarr_movie_list:
|
|
|
|
if not radarr_movie_list:
|
|
|
|
log.error("Aborting due to failure to retrieve Radarr movies list")
|
|
|
|
log.error("Aborting due to failure to retrieve Radarr movies list")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to retrieve Radarr movies list'})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to retrieve Radarr movies list'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list))
|
|
|
|
log.info("Retrieved Radarr movies list, movies found: %d", len(radarr_movie_list))
|
|
|
@ -238,13 +251,15 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.error("Aborting due to unknown Trakt list type")
|
|
|
|
log.error("Aborting due to unknown Trakt list type")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'reason': 'Failure to determine Trakt list type'})
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to determine Trakt list type'})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
if not trakt_movies_list:
|
|
|
|
if not trakt_movies_list:
|
|
|
|
log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type)
|
|
|
|
log.error("Aborting due to failure to retrieve Trakt %s movies list", list_type)
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify(
|
|
|
|
callback_notify(
|
|
|
|
{'event': 'abort', 'type': 'movies', 'reason': 'Failure to retrieve Trakt %s movies list' % list_type})
|
|
|
|
{'event': 'abort', 'type': 'movies', 'list_type': list_type,
|
|
|
|
|
|
|
|
'reason': 'Failure to retrieve Trakt %s movies list' % list_type})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_movies_list))
|
|
|
|
log.info("Retrieved Trakt %s movies list, movies found: %d", list_type, len(trakt_movies_list))
|
|
|
@ -254,7 +269,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications
|
|
|
|
if processed_movies_list is None:
|
|
|
|
if processed_movies_list is None:
|
|
|
|
log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list")
|
|
|
|
log.error("Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list")
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies',
|
|
|
|
callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
|
|
|
|
'reason': 'Failure to remove existing Radarr movies from retrieved '
|
|
|
|
'reason': 'Failure to remove existing Radarr movies from retrieved '
|
|
|
|
'Trakt %s movies list' % list_type})
|
|
|
|
'Trakt %s movies list' % list_type})
|
|
|
|
return None
|
|
|
|
return None
|
|
|
@ -279,7 +294,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, no_search=False, notifications
|
|
|
|
movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search):
|
|
|
|
movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search):
|
|
|
|
log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year'])
|
|
|
|
log.info("ADDED %s (%d)", movie['movie']['title'], movie['movie']['year'])
|
|
|
|
if notifications:
|
|
|
|
if notifications:
|
|
|
|
callback_notify({'event': 'add_movie', 'movie': movie['movie']})
|
|
|
|
callback_notify({'event': 'add_movie', 'list_type': list_type, 'movie': movie['movie']})
|
|
|
|
added_movies += 1
|
|
|
|
added_movies += 1
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.error("FAILED adding %s (%d)", movie['movie']['title'], movie['movie']['year'])
|
|
|
|
log.error("FAILED adding %s (%d)", movie['movie']['title'], movie['movie']['year'])
|
|
|
@ -307,13 +322,17 @@ def callback_notify(data):
|
|
|
|
|
|
|
|
|
|
|
|
# handle event
|
|
|
|
# handle event
|
|
|
|
if data['event'] == 'add_movie':
|
|
|
|
if data['event'] == 'add_movie':
|
|
|
|
log.info("Added movie: %s (%d)", data['movie']['title'], data['movie']['year'])
|
|
|
|
if cfg.notifications.verbose:
|
|
|
|
|
|
|
|
notify.send(
|
|
|
|
|
|
|
|
message="Added %s movie: %s (%d)" % (data['list_type'], data['movie']['title'], data['movie']['year']))
|
|
|
|
return
|
|
|
|
return
|
|
|
|
elif data['event'] == 'add_show':
|
|
|
|
elif data['event'] == 'add_show':
|
|
|
|
log.info("Added show: %s (%d)", data['show']['title'], data['show']['year'])
|
|
|
|
if cfg.notifications.verbose:
|
|
|
|
|
|
|
|
notify.send(
|
|
|
|
|
|
|
|
message="Added %s show: %s (%d)" % (data['list_type'], data['show']['title'], data['show']['year']))
|
|
|
|
return
|
|
|
|
return
|
|
|
|
elif data['event'] == 'abort':
|
|
|
|
elif data['event'] == 'abort':
|
|
|
|
log.error("Error while adding %s due to: %s", data['type'], data['reason'])
|
|
|
|
notify.send(message="Aborted adding Trakt %s %s due to: %s" % (data['list_type'], data['type'], data['reason']))
|
|
|
|
return
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
log.error("Unexpected callback: %s", data)
|
|
|
|
log.error("Unexpected callback: %s", data)
|
|
|
@ -345,12 +364,16 @@ def automatic_shows(add_delay=2.5, no_search=False, notifications=False):
|
|
|
|
total_shows_added += added_shows
|
|
|
|
total_shows_added += added_shows
|
|
|
|
|
|
|
|
|
|
|
|
# send notification
|
|
|
|
# send notification
|
|
|
|
log.info("Added %d shows from Trakt's %s list", added_shows, list_type)
|
|
|
|
if notifications and not cfg.notifications.verbose:
|
|
|
|
|
|
|
|
notify.send(message="Added %d shows from Trakt's %s list" % (added_shows, list_type))
|
|
|
|
|
|
|
|
|
|
|
|
# sleep
|
|
|
|
# sleep
|
|
|
|
time.sleep(10)
|
|
|
|
time.sleep(10)
|
|
|
|
|
|
|
|
|
|
|
|
log.info("Finished, added %d shows in total to Sonarr", total_shows_added)
|
|
|
|
log.info("Added %d shows total to Sonarr", total_shows_added)
|
|
|
|
|
|
|
|
# send notification
|
|
|
|
|
|
|
|
if notifications:
|
|
|
|
|
|
|
|
notify.send(message="Added %d shows total to Sonarr" % total_shows_added)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
log.exception("Exception while automatically adding shows: ")
|
|
|
|
log.exception("Exception while automatically adding shows: ")
|
|
|
@ -382,12 +405,16 @@ def automatic_movies(add_delay=2.5, no_search=False, notifications=False):
|
|
|
|
total_movies_added += added_movies
|
|
|
|
total_movies_added += added_movies
|
|
|
|
|
|
|
|
|
|
|
|
# send notification
|
|
|
|
# send notification
|
|
|
|
log.info("Added %d movies from Trakt's %s list", added_movies, list_type)
|
|
|
|
if notifications and not cfg.notifications.verbose:
|
|
|
|
|
|
|
|
notify.send(message="Added %d movies from Trakt's %s list" % (added_movies, list_type))
|
|
|
|
|
|
|
|
|
|
|
|
# sleep
|
|
|
|
# sleep
|
|
|
|
time.sleep(10)
|
|
|
|
time.sleep(10)
|
|
|
|
|
|
|
|
|
|
|
|
log.info("Finished, added %d movies in total to Radarr", total_movies_added)
|
|
|
|
log.info("Added %d movies total to Radarr", total_movies_added)
|
|
|
|
|
|
|
|
# send notification
|
|
|
|
|
|
|
|
if notifications:
|
|
|
|
|
|
|
|
notify.send(message="Added %d movies total to Radarr" % total_movies_added)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
log.exception("Exception while automatically adding movies: ")
|
|
|
|
log.exception("Exception while automatically adding movies: ")
|
|
|
@ -416,9 +443,26 @@ def run(add_delay=2.5, no_search=False, no_notifications=False):
|
|
|
|
time.sleep(1)
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
############################################################
|
|
|
|
|
|
|
|
# MISC
|
|
|
|
|
|
|
|
############################################################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_notifications():
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
for notification_name, notification_config in cfg.notifications.items():
|
|
|
|
|
|
|
|
if notification_name.lower() == 'verbose':
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
notify.load(**notification_config)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
log.exception("Exception initializing notification agents: ")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
############################################################
|
|
|
|
############################################################
|
|
|
|
# MAIN
|
|
|
|
# MAIN
|
|
|
|
############################################################
|
|
|
|
############################################################
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
init_notifications()
|
|
|
|
app()
|
|
|
|
app()
|
|
|
|