mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-19 12:24:59 -04:00
Added live update of UI using websocket. Make sure your reverse proxy upgrade the connection!
This commit is contained in:
parent
09a31cf9a4
commit
72b6ab3c6a
214 changed files with 5352 additions and 16886 deletions
156
bazarr/api.py
156
bazarr/api.py
|
@ -20,7 +20,7 @@ from logger import empty_log
|
|||
from init import *
|
||||
import logging
|
||||
from database import database, get_exclusion_clause, get_profiles_list, get_desired_languages, get_profile_id_name, \
|
||||
get_audio_profile_languages, update_profile_id_list
|
||||
get_audio_profile_languages, update_profile_id_list, convert_list_to_clause
|
||||
from helper import path_mappings
|
||||
from get_languages import language_from_alpha2, language_from_alpha3, alpha2_from_alpha3, alpha3_from_alpha2
|
||||
from get_subtitle import download_subtitle, series_download_subtitles, manual_search, manual_download_subtitle, \
|
||||
|
@ -31,7 +31,7 @@ from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_s
|
|||
list_missing_subtitles, list_missing_subtitles_movies
|
||||
from utils import history_log, history_log_movie, blacklist_log, blacklist_delete, blacklist_delete_all, \
|
||||
blacklist_log_movie, blacklist_delete_movie, blacklist_delete_all_movie, get_sonarr_version, get_radarr_version, \
|
||||
delete_subtitles, subtitles_apply_mods, translate_subtitles_file, check_credentials
|
||||
delete_subtitles, subtitles_apply_mods, translate_subtitles_file, check_credentials, get_health_issues
|
||||
from get_providers import get_providers, get_providers_auth, list_throttled_providers, reset_throttled_providers, \
|
||||
get_throttled_providers, set_throttled_providers
|
||||
from event_handler import event_stream
|
||||
|
@ -125,8 +125,6 @@ def postprocessSeries(item):
|
|||
|
||||
if 'path' in item:
|
||||
item['path'] = path_mappings.path_replace(item['path'])
|
||||
# Confirm if path exist
|
||||
item['exist'] = os.path.isdir(item['path'])
|
||||
|
||||
# map poster and fanart to server proxy
|
||||
if 'poster' in item:
|
||||
|
@ -138,9 +136,7 @@ def postprocessSeries(item):
|
|||
item['fanart'] = f"{base_url}/images/series{fanart}"
|
||||
|
||||
|
||||
def postprocessEpisode(item, desired=None):
|
||||
if desired is None:
|
||||
desired = []
|
||||
def postprocessEpisode(item):
|
||||
postprocess(item)
|
||||
if 'audio_language' in item and item['audio_language'] is not None:
|
||||
item['audio_language'] = get_audio_profile_languages(episode_id=item['sonarrEpisodeId'])
|
||||
|
@ -168,10 +164,6 @@ def postprocessEpisode(item, desired=None):
|
|||
|
||||
item.update({"subtitles": subtitles})
|
||||
|
||||
if settings.general.getboolean('embedded_subs_show_desired'):
|
||||
item['subtitles'] = [x for x in item['subtitles'] if
|
||||
x['code2'] in desired or x['path']]
|
||||
|
||||
# Parse missing subtitles
|
||||
if 'missing_subtitles' in item:
|
||||
if item['missing_subtitles'] is None:
|
||||
|
@ -195,11 +187,9 @@ def postprocessEpisode(item, desired=None):
|
|||
item["sceneName"] = item["scene_name"]
|
||||
del item["scene_name"]
|
||||
|
||||
if 'path' in item:
|
||||
if item['path']:
|
||||
# Provide mapped path
|
||||
item['path'] = path_mappings.path_replace(item['path'])
|
||||
item['exist'] = os.path.isfile(item['path'])
|
||||
if 'path' in item and item['path']:
|
||||
# Provide mapped path
|
||||
item['path'] = path_mappings.path_replace(item['path'])
|
||||
|
||||
|
||||
# TODO: Move
|
||||
|
@ -270,8 +260,6 @@ def postprocessMovie(item):
|
|||
if 'path' in item:
|
||||
if item['path']:
|
||||
item['path'] = path_mappings.path_replace_movie(item['path'])
|
||||
# Confirm if path exist
|
||||
item['exist'] = os.path.isfile(item['path'])
|
||||
|
||||
if 'subtitles_path' in item:
|
||||
# Provide mapped subtitles path
|
||||
|
@ -319,7 +307,7 @@ class System(Resource):
|
|||
return '', 204
|
||||
|
||||
|
||||
class BadgesSeries(Resource):
|
||||
class Badges(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
missing_episodes = database.execute("SELECT table_shows.tags, table_episodes.monitored, table_shows.seriesType "
|
||||
|
@ -334,10 +322,13 @@ class BadgesSeries(Resource):
|
|||
|
||||
throttled_providers = len(eval(str(get_throttled_providers())))
|
||||
|
||||
health_issues = len(get_health_issues())
|
||||
|
||||
result = {
|
||||
"episodes": missing_episodes,
|
||||
"movies": missing_movies,
|
||||
"providers": throttled_providers
|
||||
"providers": throttled_providers,
|
||||
"status": health_issues
|
||||
}
|
||||
return jsonify(result)
|
||||
|
||||
|
@ -421,7 +412,8 @@ class SystemSettings(Resource):
|
|||
if len(enabled_languages) != 0:
|
||||
database.execute("UPDATE table_settings_languages SET enabled=0")
|
||||
for code in enabled_languages:
|
||||
database.execute("UPDATE table_settings_languages SET enabled=1 WHERE code2=?", (code,))
|
||||
database.execute("UPDATE table_settings_languages SET enabled=1 WHERE code2=?",(code,))
|
||||
event_stream("languages")
|
||||
|
||||
languages_profiles = request.form.get('languages-profiles')
|
||||
if languages_profiles:
|
||||
|
@ -451,6 +443,7 @@ class SystemSettings(Resource):
|
|||
database.execute('DELETE FROM table_languages_profiles WHERE profileId = ?', (profileId,))
|
||||
|
||||
update_profile_id_list()
|
||||
event_stream("languages")
|
||||
|
||||
if settings.general.getboolean('use_sonarr'):
|
||||
scheduler.add_job(list_missing_subtitles, kwargs={'send_event': False})
|
||||
|
@ -465,6 +458,7 @@ class SystemSettings(Resource):
|
|||
(item['enabled'], item['url'], item['name']))
|
||||
|
||||
save_settings(zip(request.form.keys(), request.form.listvalues()))
|
||||
event_stream("settings")
|
||||
return '', 204
|
||||
|
||||
|
||||
|
@ -533,6 +527,12 @@ class SystemStatus(Resource):
|
|||
return jsonify(data=system_status)
|
||||
|
||||
|
||||
class SystemHealth(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
return jsonify(data=get_health_issues())
|
||||
|
||||
|
||||
class SystemReleases(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
|
@ -577,9 +577,8 @@ class Series(Resource):
|
|||
count = database.execute("SELECT COUNT(*) as count FROM table_shows", only_one=True)['count']
|
||||
|
||||
if len(seriesId) != 0:
|
||||
seriesIdList = ','.join(seriesId)
|
||||
result = database.execute(
|
||||
f"SELECT * FROM table_shows WHERE sonarrSeriesId in ({seriesIdList}) ORDER BY sortTitle ASC")
|
||||
f"SELECT * FROM table_shows WHERE sonarrSeriesId in {convert_list_to_clause(seriesId)} ORDER BY sortTitle ASC")
|
||||
else:
|
||||
result = database.execute("SELECT * FROM table_shows ORDER BY sortTitle ASC LIMIT ? OFFSET ?"
|
||||
, (length, start))
|
||||
|
@ -627,9 +626,10 @@ class Series(Resource):
|
|||
|
||||
database.execute("UPDATE table_shows SET profileId=? WHERE sonarrSeriesId=?", (profileId, seriesId))
|
||||
|
||||
list_missing_subtitles(no=seriesId)
|
||||
list_missing_subtitles(no=seriesId, send_event=False)
|
||||
|
||||
# event_stream(type='series', action='update', series=seriesId)
|
||||
event_stream(type='series', payload=seriesId)
|
||||
event_stream(type='badges')
|
||||
|
||||
return '', 204
|
||||
|
||||
|
@ -653,23 +653,20 @@ class Series(Resource):
|
|||
class Episodes(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
seriesId = request.args.get('seriesid')
|
||||
episodeId = request.args.get('episodeid')
|
||||
if episodeId:
|
||||
result = database.execute("SELECT * FROM table_episodes WHERE sonarrEpisodeId=?", (episodeId,))
|
||||
elif seriesId:
|
||||
result = database.execute("SELECT * FROM table_episodes WHERE sonarrSeriesId=? ORDER BY season DESC, "
|
||||
"episode DESC", (seriesId,))
|
||||
else:
|
||||
return "Series ID not provided", 400
|
||||
seriesId = request.args.getlist('seriesid[]')
|
||||
episodeId = request.args.getlist('episodeid[]')
|
||||
|
||||
profileId = database.execute("SELECT profileId FROM table_shows WHERE sonarrSeriesId = ?", (seriesId,),
|
||||
only_one=True)['profileId']
|
||||
desired_languages = str(get_desired_languages(profileId))
|
||||
desired = ast.literal_eval(desired_languages)
|
||||
if len(episodeId) > 0:
|
||||
result = database.execute(f"SELECT * FROM table_episodes WHERE sonarrEpisodeId in {convert_list_to_clause(episodeId)}")
|
||||
elif len(seriesId) > 0:
|
||||
result = database.execute("SELECT * FROM table_episodes "
|
||||
f"WHERE sonarrSeriesId in {convert_list_to_clause(seriesId)} ORDER BY season DESC, "
|
||||
"episode DESC")
|
||||
else:
|
||||
return "Series or Episode ID not provided", 400
|
||||
|
||||
for item in result:
|
||||
postprocessEpisode(item, desired)
|
||||
postprocessEpisode(item)
|
||||
|
||||
return jsonify(data=result)
|
||||
|
||||
|
@ -727,7 +724,7 @@ class EpisodesSubtitles(Resource):
|
|||
send_notifications(sonarrSeriesId, sonarrEpisodeId, message)
|
||||
store_subtitles(path, episodePath)
|
||||
else:
|
||||
event_stream(type='episode', action='update', series=int(sonarrSeriesId), episode=int(sonarrEpisodeId))
|
||||
event_stream(type='episode', payload=sonarrEpisodeId)
|
||||
|
||||
except OSError:
|
||||
pass
|
||||
|
@ -820,14 +817,12 @@ class Movies(Resource):
|
|||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
id = request.args.getlist('radarrid[]')
|
||||
radarrId = request.args.getlist('radarrid[]')
|
||||
|
||||
count = database.execute("SELECT COUNT(*) as count FROM table_movies", only_one=True)['count']
|
||||
|
||||
if len(id) != 0:
|
||||
movieIdList = ','.join(id)
|
||||
result = database.execute(
|
||||
f"SELECT * FROM table_movies WHERE radarrId in ({movieIdList}) ORDER BY sortTitle ASC")
|
||||
if len(radarrId) != 0:
|
||||
result = database.execute(f"SELECT * FROM table_movies WHERE radarrId in {convert_list_to_clause(radarrId)} ORDER BY sortTitle ASC")
|
||||
else:
|
||||
result = database.execute("SELECT * FROM table_movies ORDER BY sortTitle ASC LIMIT ? OFFSET ?",
|
||||
(length, start))
|
||||
|
@ -857,7 +852,8 @@ class Movies(Resource):
|
|||
|
||||
list_missing_subtitles_movies(no=radarrId)
|
||||
|
||||
# event_stream(type='movies', action='update', movie=radarrId)
|
||||
event_stream(type='movies', payload=radarrId)
|
||||
event_stream(type='badges')
|
||||
|
||||
return '', 204
|
||||
|
||||
|
@ -933,7 +929,7 @@ class MoviesSubtitles(Resource):
|
|||
send_notifications_movie(radarrId, message)
|
||||
store_subtitles_movie(path, moviePath)
|
||||
else:
|
||||
event_stream(type='movie', action='update', movie=int(radarrId))
|
||||
event_stream(type='movie', payload=radarrId)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
@ -1442,17 +1438,30 @@ class HistoryStats(Resource):
|
|||
class EpisodesWanted(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
data = database.execute("SELECT table_shows.title as seriesTitle, table_episodes.monitored, "
|
||||
"table_episodes.season || 'x' || table_episodes.episode as episode_number, "
|
||||
"table_episodes.title as episodeTitle, table_episodes.missing_subtitles, "
|
||||
"table_episodes.sonarrSeriesId, "
|
||||
"table_episodes.sonarrEpisodeId, table_episodes.scene_name as sceneName, table_shows.tags, "
|
||||
"table_episodes.failedAttempts, table_shows.seriesType FROM table_episodes INNER JOIN "
|
||||
"table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE "
|
||||
"table_episodes.missing_subtitles != '[]'" + get_exclusion_clause('series') +
|
||||
" ORDER BY table_episodes._rowid_ DESC LIMIT ? OFFSET ?", (length, start))
|
||||
episodeid = request.args.getlist('episodeid[]')
|
||||
if len(episodeid) > 0:
|
||||
data = database.execute("SELECT table_shows.title as seriesTitle, table_episodes.monitored, "
|
||||
"table_episodes.season || 'x' || table_episodes.episode as episode_number, "
|
||||
"table_episodes.title as episodeTitle, table_episodes.missing_subtitles, "
|
||||
"table_episodes.sonarrSeriesId, "
|
||||
"table_episodes.sonarrEpisodeId, table_episodes.scene_name as sceneName, table_shows.tags, "
|
||||
"table_episodes.failedAttempts, table_shows.seriesType FROM table_episodes INNER JOIN "
|
||||
"table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE "
|
||||
"table_episodes.missing_subtitles != '[]'" + get_exclusion_clause('series') +
|
||||
f" AND sonarrEpisodeId in {convert_list_to_clause(episodeid)}")
|
||||
pass
|
||||
else:
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
data = database.execute("SELECT table_shows.title as seriesTitle, table_episodes.monitored, "
|
||||
"table_episodes.season || 'x' || table_episodes.episode as episode_number, "
|
||||
"table_episodes.title as episodeTitle, table_episodes.missing_subtitles, "
|
||||
"table_episodes.sonarrSeriesId, "
|
||||
"table_episodes.sonarrEpisodeId, table_episodes.scene_name as sceneName, table_shows.tags, "
|
||||
"table_episodes.failedAttempts, table_shows.seriesType FROM table_episodes INNER JOIN "
|
||||
"table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE "
|
||||
"table_episodes.missing_subtitles != '[]'" + get_exclusion_clause('series') +
|
||||
" ORDER BY table_episodes._rowid_ DESC LIMIT ? OFFSET ?", (length, start))
|
||||
|
||||
for item in data:
|
||||
postprocessEpisode(item)
|
||||
|
@ -1469,20 +1478,28 @@ class EpisodesWanted(Resource):
|
|||
class MoviesWanted(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
data = database.execute("SELECT title, missing_subtitles, radarrId, sceneName, "
|
||||
"failedAttempts, tags, monitored FROM table_movies WHERE missing_subtitles != '[]'" +
|
||||
get_exclusion_clause('movie') +
|
||||
" ORDER BY _rowid_ DESC LIMIT ? OFFSET ?", (length, start))
|
||||
radarrid = request.args.getlist("radarrid[]")
|
||||
if len(radarrid) > 0:
|
||||
result = database.execute("SELECT title, missing_subtitles, radarrId, sceneName, "
|
||||
"failedAttempts, tags, monitored FROM table_movies WHERE missing_subtitles != '[]'" +
|
||||
get_exclusion_clause('movie') +
|
||||
f" AND radarrId in {convert_list_to_clause(radarrid)}")
|
||||
pass
|
||||
else:
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
result = database.execute("SELECT title, missing_subtitles, radarrId, sceneName, "
|
||||
"failedAttempts, tags, monitored FROM table_movies WHERE missing_subtitles != '[]'" +
|
||||
get_exclusion_clause('movie') +
|
||||
" ORDER BY _rowid_ DESC LIMIT ? OFFSET ?", (length, start))
|
||||
|
||||
for item in data:
|
||||
for item in result:
|
||||
postprocessMovie(item)
|
||||
|
||||
count = database.execute("SELECT COUNT(*) as count FROM table_movies WHERE missing_subtitles != '[]'" +
|
||||
get_exclusion_clause('movie'), only_one=True)['count']
|
||||
|
||||
return jsonify(data=data, total=count)
|
||||
return jsonify(data=result, total=count)
|
||||
|
||||
|
||||
# GET: get blacklist
|
||||
|
@ -1540,7 +1557,7 @@ class EpisodesBlacklist(Resource):
|
|||
sonarr_series_id=sonarr_series_id,
|
||||
sonarr_episode_id=sonarr_episode_id)
|
||||
episode_download_subtitles(sonarr_episode_id)
|
||||
event_stream(type='episodeHistory')
|
||||
event_stream(type='episode-history')
|
||||
return '', 200
|
||||
|
||||
@authenticate
|
||||
|
@ -1606,7 +1623,7 @@ class MoviesBlacklist(Resource):
|
|||
subtitles_path=subtitles_path,
|
||||
radarr_id=radarr_id)
|
||||
movies_download_subtitles(radarr_id)
|
||||
event_stream(type='movieHistory')
|
||||
event_stream(type='movie-history')
|
||||
return '', 200
|
||||
|
||||
@authenticate
|
||||
|
@ -1746,7 +1763,7 @@ class BrowseRadarrFS(Resource):
|
|||
return jsonify(data)
|
||||
|
||||
|
||||
api.add_resource(BadgesSeries, '/badges')
|
||||
api.add_resource(Badges, '/badges')
|
||||
|
||||
api.add_resource(Providers, '/providers')
|
||||
api.add_resource(ProviderMovies, '/providers/movies')
|
||||
|
@ -1758,6 +1775,7 @@ api.add_resource(SystemAccount, '/system/account')
|
|||
api.add_resource(SystemTasks, '/system/tasks')
|
||||
api.add_resource(SystemLogs, '/system/logs')
|
||||
api.add_resource(SystemStatus, '/system/status')
|
||||
api.add_resource(SystemHealth, '/system/health')
|
||||
api.add_resource(SystemReleases, '/system/releases')
|
||||
api.add_resource(SystemSettings, '/system/settings')
|
||||
api.add_resource(Languages, '/system/languages')
|
||||
|
|
|
@ -38,7 +38,7 @@ def create_app():
|
|||
|
||||
toolbar = DebugToolbarExtension(app)
|
||||
|
||||
socketio.init_app(app, path=base_url.rstrip('/')+'/socket.io', cors_allowed_origins='*', async_mode='threading')
|
||||
socketio.init_app(app, path=base_url.rstrip('/')+'/api/socket.io', cors_allowed_origins='*', async_mode='gevent')
|
||||
return app
|
||||
|
||||
|
||||
|
|
|
@ -397,8 +397,7 @@ def save_settings(settings_items):
|
|||
|
||||
if exclusion_updated:
|
||||
from event_handler import event_stream
|
||||
event_stream(type='badges_series')
|
||||
event_stream(type='badges_movies')
|
||||
event_stream(type='badges')
|
||||
|
||||
|
||||
def url_sonarr():
|
||||
|
|
|
@ -160,6 +160,12 @@ def db_upgrade():
|
|||
database.execute("CREATE TABLE IF NOT EXISTS table_blacklist_movie (radarr_id integer, timestamp integer, "
|
||||
"provider text, subs_id text, language text)")
|
||||
|
||||
# Create rootfolder tables
|
||||
database.execute("CREATE TABLE IF NOT EXISTS table_shows_rootfolder (id integer, path text, accessible integer, "
|
||||
"error text)")
|
||||
database.execute("CREATE TABLE IF NOT EXISTS table_movies_rootfolder (id integer, path text, accessible integer, "
|
||||
"error text)")
|
||||
|
||||
# Create languages profiles table and populate it
|
||||
lang_table_content = database.execute("SELECT * FROM table_languages_profiles")
|
||||
if isinstance(lang_table_content, list):
|
||||
|
@ -483,3 +489,9 @@ def get_audio_profile_languages(series_id=None, episode_id=None, movie_id=None):
|
|||
)
|
||||
|
||||
return audio_languages
|
||||
|
||||
def convert_list_to_clause(arr: list):
|
||||
if isinstance(arr, list):
|
||||
return f"({','.join(str(x) for x in arr)})"
|
||||
else:
|
||||
return ""
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
# coding=utf-8
|
||||
|
||||
import json
|
||||
from app import socketio
|
||||
|
||||
|
||||
def event_stream(type=None, action=None, series=None, episode=None, movie=None, task=None):
|
||||
def event_stream(type, action="update", payload=None):
|
||||
"""
|
||||
:param type: The type of element.
|
||||
:type type: str
|
||||
:param action: The action type of element from insert, update, delete.
|
||||
:param action: The action type of element from update and delete.
|
||||
:type action: str
|
||||
:param series: The series id.
|
||||
:type series: str
|
||||
:param episode: The episode id.
|
||||
:type episode: str
|
||||
:param movie: The movie id.
|
||||
:type movie: str
|
||||
:param task: The task id.
|
||||
:type task: str
|
||||
:param payload: The payload to send, can be anything
|
||||
"""
|
||||
socketio.emit('event', json.dumps({"type": type, "action": action, "series": series, "episode": episode,
|
||||
"movie": movie, "task": task}))
|
||||
|
||||
try:
|
||||
payload = int(payload)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
socketio.emit("data", {"type": type, "action": action, "payload": payload})
|
||||
|
|
|
@ -143,8 +143,7 @@ def sync_episodes():
|
|||
episode_to_delete = database.execute("SELECT sonarrSeriesId, sonarrEpisodeId FROM table_episodes WHERE "
|
||||
"sonarrEpisodeId=?", (removed_episode,), only_one=True)
|
||||
database.execute("DELETE FROM table_episodes WHERE sonarrEpisodeId=?", (removed_episode,))
|
||||
event_stream(type='episode', action='delete', series=episode_to_delete['sonarrSeriesId'],
|
||||
episode=episode_to_delete['sonarrEpisodeId'])
|
||||
event_stream(type='episode', action='delete', payload=episode_to_delete['sonarrEpisodeId'])
|
||||
|
||||
# Update existing episodes in DB
|
||||
episode_in_db_list = []
|
||||
|
@ -175,8 +174,7 @@ def sync_episodes():
|
|||
altered_episodes.append([added_episode['sonarrEpisodeId'],
|
||||
added_episode['path'],
|
||||
added_episode['monitored']])
|
||||
event_stream(type='episode', action='insert', series=added_episode['sonarrSeriesId'],
|
||||
episode=added_episode['sonarrEpisodeId'])
|
||||
event_stream(type='episode', payload=added_episode['sonarrEpisodeId'])
|
||||
else:
|
||||
logging.debug('BAZARR unable to insert this episode into the database:{}'.format(path_mappings.path_replace(added_episode['path'])))
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from config import settings, url_radarr
|
|||
from helper import path_mappings
|
||||
from utils import get_radarr_version
|
||||
from list_subtitles import store_subtitles_movie, movies_full_scan_subtitles
|
||||
from get_rootfolder import check_radarr_rootfolder
|
||||
|
||||
from get_subtitle import movies_download_subtitles
|
||||
from database import database, dict_converter, get_exclusion_clause
|
||||
|
@ -21,6 +22,7 @@ def update_all_movies():
|
|||
|
||||
|
||||
def update_movies():
|
||||
check_radarr_rootfolder()
|
||||
logging.debug('BAZARR Starting movie sync from Radarr.')
|
||||
apikey_radarr = settings.radarr.apikey
|
||||
|
||||
|
|
|
@ -278,7 +278,7 @@ def update_throttled_provider():
|
|||
del tp[provider]
|
||||
set_throttled_providers(str(tp))
|
||||
|
||||
event_stream(type='badges_providers')
|
||||
event_stream(type='badges')
|
||||
|
||||
|
||||
def list_throttled_providers():
|
||||
|
|
116
bazarr/get_rootfolder.py
Normal file
116
bazarr/get_rootfolder.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from config import settings, url_sonarr, url_radarr
|
||||
from helper import path_mappings
|
||||
from database import database
|
||||
|
||||
headers = {"User-Agent": os.environ["SZ_USER_AGENT"]}
|
||||
|
||||
|
||||
def get_sonarr_rootfolder():
|
||||
apikey_sonarr = settings.sonarr.apikey
|
||||
sonarr_rootfolder = []
|
||||
|
||||
# Get root folder data from Sonarr
|
||||
url_sonarr_api_rootfolder = url_sonarr() + "/api/rootfolder?apikey=" + apikey_sonarr
|
||||
|
||||
try:
|
||||
rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=60, verify=False, headers=headers)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Connection Error.")
|
||||
return []
|
||||
except requests.exceptions.Timeout:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Timeout Error.")
|
||||
return []
|
||||
except requests.exceptions.RequestException:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Sonarr.")
|
||||
return []
|
||||
else:
|
||||
for folder in rootfolder.json():
|
||||
sonarr_rootfolder.append({'id': folder['id'], 'path': folder['path']})
|
||||
db_rootfolder = database.execute('SELECT id, path FROM table_shows_rootfolder')
|
||||
rootfolder_to_remove = [x for x in db_rootfolder if not
|
||||
next((item for item in sonarr_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_update = [x for x in sonarr_rootfolder if
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_insert = [x for x in sonarr_rootfolder if not
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
|
||||
for item in rootfolder_to_remove:
|
||||
database.execute('DELETE FROM table_shows_rootfolder WHERE id = ?', (item['id'],))
|
||||
for item in rootfolder_to_update:
|
||||
database.execute('UPDATE table_shows_rootfolder SET path=? WHERE id = ?', (item['path'], item['id']))
|
||||
for item in rootfolder_to_insert:
|
||||
database.execute('INSERT INTO table_shows_rootfolder (id, path) VALUES (?, ?)', (item['id'], item['path']))
|
||||
|
||||
|
||||
def check_sonarr_rootfolder():
|
||||
get_sonarr_rootfolder()
|
||||
rootfolder = database.execute('SELECT id, path FROM table_shows_rootfolder')
|
||||
for item in rootfolder:
|
||||
if not os.path.isdir(path_mappings.path_replace(item['path'])):
|
||||
database.execute("UPDATE table_shows_rootfolder SET accessible = 0, error = 'This Sonarr root directory "
|
||||
"does not seems to be accessible by Bazarr. Please check path mapping.' WHERE id = ?",
|
||||
(item['id'],))
|
||||
elif not os.access(path_mappings.path_replace(item['path']), os.W_OK):
|
||||
database.execute("UPDATE table_shows_rootfolder SET accessible = 0, error = 'Bazarr cannot write to "
|
||||
"this directory' WHERE id = ?", (item['id'],))
|
||||
else:
|
||||
database.execute("UPDATE table_shows_rootfolder SET accessible = 1, error = '' WHERE id = ?", (item['id'],))
|
||||
|
||||
|
||||
def get_radarr_rootfolder():
|
||||
apikey_radarr = settings.radarr.apikey
|
||||
radarr_rootfolder = []
|
||||
|
||||
# Get root folder data from Radarr
|
||||
url_radarr_api_rootfolder = url_radarr() + "/api/rootfolder?apikey=" + apikey_radarr
|
||||
|
||||
try:
|
||||
rootfolder = requests.get(url_radarr_api_rootfolder, timeout=60, verify=False, headers=headers)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Radarr. Connection Error.")
|
||||
return []
|
||||
except requests.exceptions.Timeout:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Radarr. Timeout Error.")
|
||||
return []
|
||||
except requests.exceptions.RequestException:
|
||||
logging.exception("BAZARR Error trying to get rootfolder from Radarr.")
|
||||
return []
|
||||
else:
|
||||
for folder in rootfolder.json():
|
||||
radarr_rootfolder.append({'id': folder['id'], 'path': folder['path']})
|
||||
db_rootfolder = database.execute('SELECT id, path FROM table_movies_rootfolder')
|
||||
rootfolder_to_remove = [x for x in db_rootfolder if not
|
||||
next((item for item in radarr_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_update = [x for x in radarr_rootfolder if
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
rootfolder_to_insert = [x for x in radarr_rootfolder if not
|
||||
next((item for item in db_rootfolder if item['id'] == x['id']), False)]
|
||||
|
||||
for item in rootfolder_to_remove:
|
||||
database.execute('DELETE FROM table_movies_rootfolder WHERE id = ?', (item['id'],))
|
||||
for item in rootfolder_to_update:
|
||||
database.execute('UPDATE table_movies_rootfolder SET path=? WHERE id = ?', (item['path'], item['id']))
|
||||
for item in rootfolder_to_insert:
|
||||
database.execute('INSERT INTO table_movies_rootfolder (id, path) VALUES (?, ?)', (item['id'], item['path']))
|
||||
|
||||
|
||||
def check_radarr_rootfolder():
|
||||
get_radarr_rootfolder()
|
||||
rootfolder = database.execute('SELECT id, path FROM table_movies_rootfolder')
|
||||
for item in rootfolder:
|
||||
if not os.path.isdir(path_mappings.path_replace_movie(item['path'])):
|
||||
database.execute("UPDATE table_movies_rootfolder SET accessible = 0, error = 'This Radarr root directory "
|
||||
"does not seems to be accessible by Bazarr. Please check path mapping.' WHERE id = ?",
|
||||
(item['id'],))
|
||||
elif not os.access(path_mappings.path_replace_movie(item['path']), os.W_OK):
|
||||
database.execute("UPDATE table_movies_rootfolder SET accessible = 0, error = 'Bazarr cannot write to "
|
||||
"this directory' WHERE id = ?", (item['id'],))
|
||||
else:
|
||||
database.execute("UPDATE table_movies_rootfolder SET accessible = 1, error = '' WHERE id = ?",
|
||||
(item['id'],))
|
|
@ -6,6 +6,7 @@ import logging
|
|||
|
||||
from config import settings, url_sonarr
|
||||
from list_subtitles import list_missing_subtitles
|
||||
from get_rootfolder import check_sonarr_rootfolder
|
||||
from database import database, dict_converter
|
||||
from utils import get_sonarr_version
|
||||
from helper import path_mappings
|
||||
|
@ -15,6 +16,7 @@ headers = {"User-Agent": os.environ["SZ_USER_AGENT"]}
|
|||
|
||||
|
||||
def update_series():
|
||||
check_sonarr_rootfolder()
|
||||
apikey_sonarr = settings.sonarr.apikey
|
||||
if apikey_sonarr is None:
|
||||
return
|
||||
|
@ -125,7 +127,7 @@ def update_series():
|
|||
|
||||
for series in removed_series:
|
||||
database.execute("DELETE FROM table_shows WHERE sonarrSeriesId=?",(series,))
|
||||
event_stream(type='series', action='delete', series=series)
|
||||
event_stream(type='series', action='delete', payload=series)
|
||||
|
||||
# Update existing series in DB
|
||||
series_in_db_list = []
|
||||
|
@ -141,7 +143,7 @@ def update_series():
|
|||
query = dict_converter.convert(updated_series)
|
||||
database.execute('''UPDATE table_shows SET ''' + query.keys_update + ''' WHERE sonarrSeriesId = ?''',
|
||||
query.values + (updated_series['sonarrSeriesId'],))
|
||||
event_stream(type='series', action='update', series=updated_series['sonarrSeriesId'])
|
||||
event_stream(type='series', payload=updated_series['sonarrSeriesId'])
|
||||
|
||||
# Insert new series in DB
|
||||
for added_series in series_to_add:
|
||||
|
@ -155,7 +157,7 @@ def update_series():
|
|||
logging.debug('BAZARR unable to insert this series into the database:',
|
||||
path_mappings.path_replace(added_series['path']))
|
||||
|
||||
event_stream(type='series', action='insert', series=added_series['sonarrSeriesId'])
|
||||
event_stream(type='series', series=added_series['sonarrSeriesId'])
|
||||
|
||||
logging.debug('BAZARR All series synced from Sonarr into database.')
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ from subsyncer import subsync
|
|||
from guessit import guessit
|
||||
from database import database, dict_mapper, get_exclusion_clause, get_profiles_list, get_audio_profile_languages, \
|
||||
get_desired_languages
|
||||
from event_handler import event_stream
|
||||
from embedded_subs_reader import parse_video_metadata
|
||||
|
||||
from analytics import track_event
|
||||
|
@ -982,6 +983,7 @@ def wanted_download_subtitles(path, l, count_episodes):
|
|||
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
|
||||
history_log(1, episode['sonarrSeriesId'], episode['sonarrEpisodeId'], message, path,
|
||||
language_code, provider, score, subs_id, subs_path)
|
||||
event_stream(type='episode-wanted', action='delete', payload=episode['sonarrEpisodeId'])
|
||||
send_notifications(episode['sonarrSeriesId'], episode['sonarrEpisodeId'], message)
|
||||
else:
|
||||
logging.debug(
|
||||
|
@ -1050,6 +1052,7 @@ def wanted_download_subtitles_movie(path, l, count_movies):
|
|||
store_subtitles_movie(movie['path'], path_mappings.path_replace_movie(movie['path']))
|
||||
history_log_movie(1, movie['radarrId'], message, path, language_code, provider, score,
|
||||
subs_id, subs_path)
|
||||
event_stream(type='movie-wanted', action='delete', payload=movie['radarrId'])
|
||||
send_notifications_movie(movie['radarrId'], message)
|
||||
else:
|
||||
logging.info(
|
||||
|
|
|
@ -45,7 +45,7 @@ import logging
|
|||
# deploy requirements.txt
|
||||
if not args.no_update:
|
||||
try:
|
||||
import lxml, numpy, webrtcvad
|
||||
import lxml, numpy, webrtcvad, gevent, geventwebsocket
|
||||
except ImportError:
|
||||
try:
|
||||
import pip
|
||||
|
|
|
@ -365,9 +365,8 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
|||
(missing_subtitles_text, episode_subtitles['sonarrEpisodeId']))
|
||||
|
||||
if send_event:
|
||||
event_stream(type='episode', action='update', series=episode_subtitles['sonarrSeriesId'],
|
||||
episode=episode_subtitles['sonarrEpisodeId'])
|
||||
event_stream(type='badges_series')
|
||||
event_stream(type='episode', payload=episode_subtitles['sonarrEpisodeId'])
|
||||
event_stream(type='badges')
|
||||
|
||||
|
||||
def list_missing_subtitles_movies(no=None, epno=None, send_event=True):
|
||||
|
@ -475,8 +474,8 @@ def list_missing_subtitles_movies(no=None, epno=None, send_event=True):
|
|||
(missing_subtitles_text, movie_subtitles['radarrId']))
|
||||
|
||||
if send_event:
|
||||
event_stream(type='movie', action='update', movie=movie_subtitles['radarrId'])
|
||||
event_stream(type='badges_movies')
|
||||
event_stream(type='movie', payload=movie_subtitles['radarrId'])
|
||||
event_stream(type='badges')
|
||||
|
||||
|
||||
def series_full_scan_subtitles():
|
||||
|
|
|
@ -104,7 +104,7 @@ def configure_logging(debug=False):
|
|||
logging.getLogger("ffsubsync.ffsubsync").setLevel(logging.ERROR)
|
||||
logging.getLogger("srt").setLevel(logging.ERROR)
|
||||
|
||||
logging.getLogger("waitress").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
|
||||
logging.getLogger("knowit").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("enzyme").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("guessit").setLevel(logging.WARNING)
|
||||
|
|
|
@ -6,7 +6,7 @@ from get_series import update_series
|
|||
from config import settings
|
||||
from get_subtitle import wanted_search_missing_subtitles_series, wanted_search_missing_subtitles_movies, \
|
||||
upgrade_subtitles
|
||||
from utils import cache_maintenance
|
||||
from utils import cache_maintenance, check_health
|
||||
from get_args import args
|
||||
if not args.no_update:
|
||||
from check_update import check_if_new_update, check_releases
|
||||
|
@ -36,18 +36,19 @@ class Scheduler:
|
|||
def task_listener_add(event):
|
||||
if event.job_id not in self.__running_tasks:
|
||||
self.__running_tasks.append(event.job_id)
|
||||
event_stream(type='task', task=event.job_id)
|
||||
event_stream(type='task')
|
||||
|
||||
def task_listener_remove(event):
|
||||
if event.job_id in self.__running_tasks:
|
||||
self.__running_tasks.remove(event.job_id)
|
||||
event_stream(type='task', task=event.job_id)
|
||||
event_stream(type='task')
|
||||
|
||||
self.aps_scheduler.add_listener(task_listener_add, EVENT_JOB_SUBMITTED)
|
||||
self.aps_scheduler.add_listener(task_listener_remove, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
|
||||
|
||||
# configure all tasks
|
||||
self.__cache_cleanup_task()
|
||||
self.__check_health_task()
|
||||
self.update_configurable_tasks()
|
||||
|
||||
self.aps_scheduler.start()
|
||||
|
@ -161,6 +162,10 @@ class Scheduler:
|
|||
self.aps_scheduler.add_job(cache_maintenance, IntervalTrigger(hours=24), max_instances=1, coalesce=True,
|
||||
misfire_grace_time=15, id='cache_cleanup', name='Cache maintenance')
|
||||
|
||||
def __check_health_task(self):
|
||||
self.aps_scheduler.add_job(check_health, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
|
||||
misfire_grace_time=15, id='check_health', name='Check health')
|
||||
|
||||
def __sonarr_full_update_task(self):
|
||||
if settings.general.getboolean('use_sonarr'):
|
||||
full_update = settings.sonarr.full_update
|
||||
|
|
|
@ -4,7 +4,8 @@ import warnings
|
|||
import logging
|
||||
import os
|
||||
import io
|
||||
from waitress.server import create_server
|
||||
from gevent import pywsgi
|
||||
from geventwebsocket.handler import WebSocketHandler
|
||||
|
||||
from get_args import args
|
||||
from config import settings, base_url
|
||||
|
@ -30,10 +31,10 @@ class Server:
|
|||
self.server = app.run(host=str(settings.general.ip),
|
||||
port=(int(args.port) if args.port else int(settings.general.port)))
|
||||
else:
|
||||
self.server = create_server(app,
|
||||
host=str(settings.general.ip),
|
||||
port=int(args.port) if args.port else int(settings.general.port),
|
||||
threads=24)
|
||||
self.server = pywsgi.WSGIServer((str(settings.general.ip),
|
||||
int(args.port) if args.port else int(settings.general.port)),
|
||||
app,
|
||||
handler_class=WebSocketHandler)
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
|
@ -41,13 +42,13 @@ class Server:
|
|||
'BAZARR is started and waiting for request on http://' + str(settings.general.ip) + ':' + (str(
|
||||
args.port) if args.port else str(settings.general.port)) + str(base_url))
|
||||
if not args.dev:
|
||||
self.server.run()
|
||||
self.server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.server.close()
|
||||
self.server.stop()
|
||||
except Exception as e:
|
||||
logging.error('BAZARR Cannot stop Waitress: ' + repr(e))
|
||||
else:
|
||||
|
@ -64,7 +65,7 @@ class Server:
|
|||
|
||||
def restart(self):
|
||||
try:
|
||||
self.server.close()
|
||||
self.server.stop()
|
||||
except Exception as e:
|
||||
logging.error('BAZARR Cannot stop Waitress: ' + repr(e))
|
||||
else:
|
||||
|
|
|
@ -40,24 +40,24 @@ def history_log(action, sonarr_series_id, sonarr_episode_id, description, video_
|
|||
"video_path, language, provider, score, subs_id, subtitles_path) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(action, sonarr_series_id, sonarr_episode_id, time.time(), description, video_path, language,
|
||||
provider, score, subs_id, subtitles_path))
|
||||
event_stream(type='episodeHistory')
|
||||
event_stream(type='episode-history')
|
||||
|
||||
|
||||
def blacklist_log(sonarr_series_id, sonarr_episode_id, provider, subs_id, language):
|
||||
database.execute("INSERT INTO table_blacklist (sonarr_series_id, sonarr_episode_id, timestamp, provider, "
|
||||
"subs_id, language) VALUES (?,?,?,?,?,?)",
|
||||
(sonarr_series_id, sonarr_episode_id, time.time(), provider, subs_id, language))
|
||||
event_stream(type='episodeBlacklist')
|
||||
event_stream(type='episode-blacklist')
|
||||
|
||||
|
||||
def blacklist_delete(provider, subs_id):
|
||||
database.execute("DELETE FROM table_blacklist WHERE provider=? AND subs_id=?", (provider, subs_id))
|
||||
event_stream(type='episodeBlacklist')
|
||||
event_stream(type='episode-blacklist', action='delete')
|
||||
|
||||
|
||||
def blacklist_delete_all():
|
||||
database.execute("DELETE FROM table_blacklist")
|
||||
event_stream(type='episodeBlacklist')
|
||||
event_stream(type='episode-blacklist', action='delete')
|
||||
|
||||
|
||||
def history_log_movie(action, radarr_id, description, video_path=None, language=None, provider=None, score=None,
|
||||
|
@ -65,23 +65,23 @@ def history_log_movie(action, radarr_id, description, video_path=None, language=
|
|||
database.execute("INSERT INTO table_history_movie (action, radarrId, timestamp, description, video_path, language, "
|
||||
"provider, score, subs_id, subtitles_path) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(action, radarr_id, time.time(), description, video_path, language, provider, score, subs_id, subtitles_path))
|
||||
event_stream(type='movieHistory')
|
||||
event_stream(type='movie-history')
|
||||
|
||||
|
||||
def blacklist_log_movie(radarr_id, provider, subs_id, language):
|
||||
database.execute("INSERT INTO table_blacklist_movie (radarr_id, timestamp, provider, subs_id, language) "
|
||||
"VALUES (?,?,?,?,?)", (radarr_id, time.time(), provider, subs_id, language))
|
||||
event_stream(type='movieBlacklist')
|
||||
event_stream(type='movie-blacklist')
|
||||
|
||||
|
||||
def blacklist_delete_movie(provider, subs_id):
|
||||
database.execute("DELETE FROM table_blacklist_movie WHERE provider=? AND subs_id=?", (provider, subs_id))
|
||||
event_stream(type='movieBlacklist')
|
||||
event_stream(type='movie-blacklist', action='delete')
|
||||
|
||||
|
||||
def blacklist_delete_all_movie():
|
||||
database.execute("DELETE FROM table_blacklist_movie")
|
||||
event_stream(type='movieBlacklist')
|
||||
event_stream(type='movie-blacklist', action='delete')
|
||||
|
||||
|
||||
@region.cache_on_arguments()
|
||||
|
@ -401,7 +401,39 @@ def translate_subtitles_file(video_path, source_srt_file, to_lang, forced, hi):
|
|||
|
||||
return dest_srt_file
|
||||
|
||||
|
||||
def check_credentials(user, pw):
|
||||
username = settings.auth.username
|
||||
password = settings.auth.password
|
||||
return hashlib.md5(pw.encode('utf-8')).hexdigest() == password and user == username
|
||||
return hashlib.md5(pw.encode('utf-8')).hexdigest() == password and user == username
|
||||
|
||||
|
||||
def check_health():
|
||||
from get_rootfolder import check_sonarr_rootfolder, check_radarr_rootfolder
|
||||
if settings.general.getboolean('use_sonarr'):
|
||||
check_sonarr_rootfolder()
|
||||
if settings.general.getboolean('use_radarr'):
|
||||
check_radarr_rootfolder()
|
||||
event_stream(type='badges')
|
||||
|
||||
|
||||
def get_health_issues():
|
||||
# this function must return a list of dictionaries consisting of to keys: object and issue
|
||||
health_issues = []
|
||||
|
||||
# get Sonarr rootfolder issues
|
||||
if settings.general.getboolean('use_sonarr'):
|
||||
rootfolder = database.execute('SELECT path, accessible, error FROM table_shows_rootfolder WHERE accessible = 0')
|
||||
for item in rootfolder:
|
||||
health_issues.append({'object': path_mappings.path_replace(item['path']),
|
||||
'issue': item['error']})
|
||||
|
||||
# get Radarr rootfolder issues
|
||||
if settings.general.getboolean('use_radarr'):
|
||||
rootfolder = database.execute('SELECT path, accessible, error FROM table_movies_rootfolder '
|
||||
'WHERE accessible = 0')
|
||||
for item in rootfolder:
|
||||
health_issues.append({'object': path_mappings.path_replace_movie(item['path']),
|
||||
'issue': item['error']})
|
||||
|
||||
return health_issues
|
||||
|
|
180
frontend/package-lock.json
generated
180
frontend/package-lock.json
generated
|
@ -31,6 +31,7 @@
|
|||
"@types/redux-promise": "^0.5.0",
|
||||
"axios": "^0.21.0",
|
||||
"bootstrap": "^4.0.0",
|
||||
"http-proxy-middleware": "^0.19.1",
|
||||
"lodash": "^4.0.0",
|
||||
"rc-slider": "^9.7.1",
|
||||
"react": "^16.0.0",
|
||||
|
@ -48,6 +49,7 @@
|
|||
"redux-promise": "^0.6.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sass": "^1.0.0",
|
||||
"socket.io-client": "^4.0.0",
|
||||
"typescript": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -2793,6 +2795,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz",
|
||||
"integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw=="
|
||||
},
|
||||
"node_modules/@types/component-emitter": {
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
||||
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
|
||||
|
@ -4544,6 +4551,11 @@
|
|||
"babylon": "bin/babylon.js"
|
||||
}
|
||||
},
|
||||
"node_modules/backo2": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
|
@ -4577,6 +4589,14 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
||||
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
|
@ -7069,6 +7089,33 @@
|
|||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz",
|
||||
"integrity": "sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg==",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "0.1.4",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~4.0.1",
|
||||
"has-cors": "1.1.0",
|
||||
"parseqs": "0.0.6",
|
||||
"parseuri": "0.0.6",
|
||||
"ws": "~7.4.2",
|
||||
"yeast": "0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
|
||||
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "0.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
|
||||
|
@ -9397,6 +9444,11 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-cors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
|
||||
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
@ -13634,6 +13686,16 @@
|
|||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
||||
},
|
||||
"node_modules/parseqs": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
|
||||
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
|
||||
},
|
||||
"node_modules/parseuri": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
|
||||
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
@ -18161,6 +18223,36 @@
|
|||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz",
|
||||
"integrity": "sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw==",
|
||||
"dependencies": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"backo2": "~1.0.2",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-client": "~5.0.0",
|
||||
"parseuri": "0.0.6",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"dependencies": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sockjs": {
|
||||
"version": "0.3.21",
|
||||
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz",
|
||||
|
@ -22025,6 +22117,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yeast": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
@ -24144,6 +24241,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz",
|
||||
"integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw=="
|
||||
},
|
||||
"@types/component-emitter": {
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
||||
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
|
||||
},
|
||||
"@types/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
|
||||
|
@ -25605,6 +25707,11 @@
|
|||
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
|
||||
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
|
||||
},
|
||||
"backo2": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
|
@ -25634,6 +25741,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"base64-arraybuffer": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
||||
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
|
@ -27674,6 +27786,30 @@
|
|||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz",
|
||||
"integrity": "sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg==",
|
||||
"requires": {
|
||||
"base64-arraybuffer": "0.1.4",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~4.0.1",
|
||||
"has-cors": "1.1.0",
|
||||
"parseqs": "0.0.6",
|
||||
"parseuri": "0.0.6",
|
||||
"ws": "~7.4.2",
|
||||
"yeast": "0.1.2"
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
|
||||
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
|
||||
"requires": {
|
||||
"base64-arraybuffer": "0.1.4"
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
|
||||
|
@ -29466,6 +29602,11 @@
|
|||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
|
||||
"integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA=="
|
||||
},
|
||||
"has-cors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
|
||||
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
@ -32737,6 +32878,16 @@
|
|||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
||||
},
|
||||
"parseqs": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
|
||||
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
|
||||
},
|
||||
"parseuri": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
|
||||
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
@ -36328,6 +36479,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"socket.io-client": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz",
|
||||
"integrity": "sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw==",
|
||||
"requires": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"backo2": "~1.0.2",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-client": "~5.0.0",
|
||||
"parseuri": "0.0.6",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
}
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"requires": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1"
|
||||
}
|
||||
},
|
||||
"sockjs": {
|
||||
"version": "0.3.21",
|
||||
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz",
|
||||
|
@ -39451,6 +39626,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"yeast": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
||||
},
|
||||
"yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"url": "https://github.com/morpheus65535/bazarr/issues"
|
||||
},
|
||||
"homepage": "./",
|
||||
"proxy": "http://localhost:6767",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^4.2.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.0",
|
||||
|
@ -36,6 +35,7 @@
|
|||
"@types/redux-promise": "^0.5.0",
|
||||
"axios": "^0.21.0",
|
||||
"bootstrap": "^4.0.0",
|
||||
"http-proxy-middleware": "^0.19.1",
|
||||
"lodash": "^4.0.0",
|
||||
"rc-slider": "^9.7.1",
|
||||
"react": "^16.0.0",
|
||||
|
@ -53,6 +53,7 @@
|
|||
"redux-promise": "^0.6.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sass": "^1.0.0",
|
||||
"socket.io-client": "^4.0.0",
|
||||
"typescript": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { isEqual } from "lodash";
|
||||
import { log } from "../../utilites/logger";
|
||||
import { createAction } from "redux-actions";
|
||||
import {
|
||||
ActionCallback,
|
||||
ActionDispatcher,
|
||||
|
@ -10,42 +9,12 @@ import {
|
|||
PromiseCreator,
|
||||
} from "../types";
|
||||
|
||||
// Limiter the call to API
|
||||
const gLimiter: Map<PromiseCreator, Date> = new Map();
|
||||
const gArgs: Map<PromiseCreator, any[]> = new Map();
|
||||
|
||||
const LIMIT_CALL_MS = 200;
|
||||
|
||||
function asyncActionFactory<T extends PromiseCreator>(
|
||||
type: string,
|
||||
promise: T,
|
||||
args: Parameters<T>
|
||||
): AsyncActionDispatcher<PromiseType<ReturnType<T>>> {
|
||||
return (dispatch) => {
|
||||
const previousArgs = gArgs.get(promise);
|
||||
const date = new Date();
|
||||
|
||||
if (isEqual(previousArgs, args)) {
|
||||
// Get last execute date
|
||||
const previousExec = gLimiter.get(promise);
|
||||
if (previousExec) {
|
||||
const distInMs = date.getTime() - previousExec.getTime();
|
||||
if (distInMs < LIMIT_CALL_MS) {
|
||||
log(
|
||||
"warning",
|
||||
"Multiple calls to API within the range",
|
||||
promise,
|
||||
args
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gArgs.set(promise, args);
|
||||
}
|
||||
|
||||
gLimiter.set(promise, date);
|
||||
|
||||
dispatch({
|
||||
type,
|
||||
payload: {
|
||||
|
@ -153,3 +122,8 @@ export function createCallbackAction<T extends AsyncActionCreator>(
|
|||
return (...args: Parameters<T>) =>
|
||||
callbackActionFactory(fn(args), success, error);
|
||||
}
|
||||
|
||||
// Helper
|
||||
export function createDeleteAction(type: string): SocketIO.ActionFn {
|
||||
return createAction(type, (id?: number[]) => id ?? []);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export * from "./movie";
|
||||
export * from "./providers";
|
||||
export * from "./series";
|
||||
export * from "./site";
|
||||
export * from "./system";
|
||||
|
|
|
@ -1,58 +1,45 @@
|
|||
import { MoviesApi } from "../../apis";
|
||||
import {
|
||||
MOVIES_DELETE_ITEMS,
|
||||
MOVIES_DELETE_WANTED_ITEMS,
|
||||
MOVIES_UPDATE_BLACKLIST,
|
||||
MOVIES_UPDATE_HISTORY_LIST,
|
||||
MOVIES_UPDATE_INFO,
|
||||
MOVIES_UPDATE_LIST,
|
||||
MOVIES_UPDATE_RANGE,
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
MOVIES_UPDATE_WANTED_RANGE,
|
||||
} from "../constants";
|
||||
import {
|
||||
createAsyncAction,
|
||||
createAsyncCombineAction,
|
||||
createCombineAction,
|
||||
} from "./factory";
|
||||
import { badgeUpdateAll } from "./site";
|
||||
import { createAsyncAction, createDeleteAction } from "./factory";
|
||||
|
||||
export const movieUpdateList = createAsyncAction(MOVIES_UPDATE_LIST, () =>
|
||||
MoviesApi.movies()
|
||||
export const movieUpdateList = createAsyncAction(
|
||||
MOVIES_UPDATE_LIST,
|
||||
(id?: number[]) => MoviesApi.movies(id)
|
||||
);
|
||||
|
||||
const movieUpdateWantedList = createAsyncAction(
|
||||
export const movieDeleteItems = createDeleteAction(MOVIES_DELETE_ITEMS);
|
||||
|
||||
export const movieUpdateWantedList = createAsyncAction(
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
(radarrid?: number) => MoviesApi.wantedBy(radarrid)
|
||||
(radarrid: number[]) => MoviesApi.wantedBy(radarrid)
|
||||
);
|
||||
|
||||
export const movieDeleteWantedItems = createDeleteAction(
|
||||
MOVIES_DELETE_WANTED_ITEMS
|
||||
);
|
||||
|
||||
export const movieUpdateWantedByRange = createAsyncAction(
|
||||
MOVIES_UPDATE_WANTED_RANGE,
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
(start: number, length: number) => MoviesApi.wanted(start, length)
|
||||
);
|
||||
|
||||
export const movieUpdateWantedBy = createCombineAction((radarrid?: number) => [
|
||||
movieUpdateWantedList(radarrid),
|
||||
badgeUpdateAll(),
|
||||
]);
|
||||
|
||||
export const movieUpdateHistoryList = createAsyncAction(
|
||||
MOVIES_UPDATE_HISTORY_LIST,
|
||||
() => MoviesApi.history()
|
||||
);
|
||||
|
||||
export const movieUpdateByRange = createAsyncAction(
|
||||
MOVIES_UPDATE_RANGE,
|
||||
MOVIES_UPDATE_LIST,
|
||||
(start: number, length: number) => MoviesApi.moviesBy(start, length)
|
||||
);
|
||||
|
||||
const movieUpdateInfo = createAsyncAction(MOVIES_UPDATE_INFO, (id?: number[]) =>
|
||||
MoviesApi.movies(id)
|
||||
);
|
||||
|
||||
export const movieUpdateInfoAll = createAsyncCombineAction((id?: number[]) => [
|
||||
movieUpdateInfo(id),
|
||||
badgeUpdateAll(),
|
||||
]);
|
||||
|
||||
export const movieUpdateBlacklist = createAsyncAction(
|
||||
MOVIES_UPDATE_BLACKLIST,
|
||||
() => MoviesApi.blacklist()
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { ProvidersApi } from "../../apis";
|
||||
import { PROVIDER_UPDATE_LIST } from "../constants";
|
||||
import { createAsyncAction, createCombineAction } from "./factory";
|
||||
import { badgeUpdateAll } from "./site";
|
||||
|
||||
const providerUpdateList = createAsyncAction(PROVIDER_UPDATE_LIST, () =>
|
||||
ProvidersApi.providers()
|
||||
);
|
||||
|
||||
export const providerUpdateAll = createCombineAction(() => [
|
||||
providerUpdateList(),
|
||||
badgeUpdateAll(),
|
||||
]);
|
|
@ -1,50 +1,52 @@
|
|||
import { EpisodesApi, SeriesApi } from "../../apis";
|
||||
import {
|
||||
SERIES_DELETE_EPISODES,
|
||||
SERIES_DELETE_ITEMS,
|
||||
SERIES_DELETE_WANTED_ITEMS,
|
||||
SERIES_UPDATE_BLACKLIST,
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
SERIES_UPDATE_HISTORY_LIST,
|
||||
SERIES_UPDATE_INFO,
|
||||
SERIES_UPDATE_RANGE,
|
||||
SERIES_UPDATE_LIST,
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
SERIES_UPDATE_WANTED_RANGE,
|
||||
} from "../constants";
|
||||
import {
|
||||
createAsyncAction,
|
||||
createAsyncCombineAction,
|
||||
createCombineAction,
|
||||
} from "./factory";
|
||||
import { badgeUpdateAll } from "./site";
|
||||
import { createAsyncAction, createDeleteAction } from "./factory";
|
||||
|
||||
const seriesUpdateWantedList = createAsyncAction(
|
||||
export const seriesUpdateWantedList = createAsyncAction(
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
(episodeid?: number) => EpisodesApi.wantedBy(episodeid)
|
||||
(episodeid: number[]) => EpisodesApi.wantedBy(episodeid)
|
||||
);
|
||||
|
||||
const seriesUpdateBy = createAsyncAction(SERIES_UPDATE_INFO, (id?: number[]) =>
|
||||
SeriesApi.series(id)
|
||||
);
|
||||
|
||||
const episodeUpdateBy = createAsyncAction(
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
(seriesid: number) => EpisodesApi.bySeriesId(seriesid)
|
||||
);
|
||||
|
||||
export const seriesUpdateByRange = createAsyncAction(
|
||||
SERIES_UPDATE_RANGE,
|
||||
(start: number, length: number) => SeriesApi.seriesBy(start, length)
|
||||
export const seriesDeleteWantedItems = createDeleteAction(
|
||||
SERIES_DELETE_WANTED_ITEMS
|
||||
);
|
||||
|
||||
export const seriesUpdateWantedByRange = createAsyncAction(
|
||||
SERIES_UPDATE_WANTED_RANGE,
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
(start: number, length: number) => EpisodesApi.wanted(start, length)
|
||||
);
|
||||
|
||||
export const seriesUpdateWantedBy = createCombineAction(
|
||||
(episodeid?: number) => [seriesUpdateWantedList(episodeid), badgeUpdateAll()]
|
||||
export const seriesUpdateList = createAsyncAction(
|
||||
SERIES_UPDATE_LIST,
|
||||
(id?: number[]) => SeriesApi.series(id)
|
||||
);
|
||||
|
||||
export const episodeUpdateBySeriesId = createCombineAction(
|
||||
(seriesid: number) => [episodeUpdateBy(seriesid), badgeUpdateAll()]
|
||||
export const seriesDeleteItems = createDeleteAction(SERIES_DELETE_ITEMS);
|
||||
|
||||
export const episodeUpdateBy = createAsyncAction(
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
(seriesid: number[]) => EpisodesApi.bySeriesId(seriesid)
|
||||
);
|
||||
|
||||
export const episodeDeleteItems = createDeleteAction(SERIES_DELETE_EPISODES);
|
||||
|
||||
export const episodeUpdateById = createAsyncAction(
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
(episodeid: number[]) => EpisodesApi.byEpisodeId(episodeid)
|
||||
);
|
||||
|
||||
export const seriesUpdateByRange = createAsyncAction(
|
||||
SERIES_UPDATE_LIST,
|
||||
(start: number, length: number) => SeriesApi.seriesBy(start, length)
|
||||
);
|
||||
|
||||
export const seriesUpdateHistoryList = createAsyncAction(
|
||||
|
@ -52,10 +54,6 @@ export const seriesUpdateHistoryList = createAsyncAction(
|
|||
() => EpisodesApi.history()
|
||||
);
|
||||
|
||||
export const seriesUpdateInfoAll = createAsyncCombineAction(
|
||||
(seriesid?: number[]) => [seriesUpdateBy(seriesid), badgeUpdateAll()]
|
||||
);
|
||||
|
||||
export const seriesUpdateBlacklist = createAsyncAction(
|
||||
SERIES_UPDATE_BLACKLIST,
|
||||
() => EpisodesApi.blacklist()
|
||||
|
|
|
@ -16,7 +16,7 @@ import { createAsyncAction, createCallbackAction } from "./factory";
|
|||
import { systemUpdateLanguagesAll, systemUpdateSettings } from "./system";
|
||||
|
||||
export const bootstrap = createCallbackAction(
|
||||
() => [systemUpdateLanguagesAll(), systemUpdateSettings()],
|
||||
() => [systemUpdateLanguagesAll(), systemUpdateSettings(), badgeUpdateAll()],
|
||||
() => siteInitialized(),
|
||||
() => siteInitializeFailed()
|
||||
);
|
||||
|
@ -36,17 +36,17 @@ export const siteSaveLocalstorage = createAction(
|
|||
(settings: LooseObject) => settings
|
||||
);
|
||||
|
||||
export const siteAddError = createAction(
|
||||
export const siteAddNotification = createAction(
|
||||
SITE_NOTIFICATIONS_ADD,
|
||||
(err: ReduxStore.Notification) => err
|
||||
);
|
||||
|
||||
export const siteRemoveError = createAction(
|
||||
export const siteRemoveNotification = createAction(
|
||||
SITE_NOTIFICATIONS_REMOVE,
|
||||
(id: string) => id
|
||||
);
|
||||
|
||||
export const siteRemoveErrorByTimestamp = createAction(
|
||||
export const siteRemoveNotificationByTime = createAction(
|
||||
SITE_NOTIFICATIONS_REMOVE_BY_TIMESTAMP,
|
||||
(date: Date) => date
|
||||
);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Action } from "redux-actions";
|
||||
import { SystemApi } from "../../apis";
|
||||
import { ProvidersApi, SystemApi } from "../../apis";
|
||||
import {
|
||||
SYSTEM_RUN_TASK,
|
||||
SYSTEM_UPDATE_HEALTH,
|
||||
SYSTEM_UPDATE_LANGUAGES_LIST,
|
||||
SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST,
|
||||
SYSTEM_UPDATE_LOGS,
|
||||
SYSTEM_UPDATE_PROVIDERS,
|
||||
SYSTEM_UPDATE_RELEASES,
|
||||
SYSTEM_UPDATE_SETTINGS,
|
||||
SYSTEM_UPDATE_STATUS,
|
||||
|
@ -31,17 +31,14 @@ export const systemUpdateStatus = createAsyncAction(SYSTEM_UPDATE_STATUS, () =>
|
|||
SystemApi.status()
|
||||
);
|
||||
|
||||
export const systemUpdateHealth = createAsyncAction(SYSTEM_UPDATE_HEALTH, () =>
|
||||
SystemApi.health()
|
||||
);
|
||||
|
||||
export const systemUpdateTasks = createAsyncAction(SYSTEM_UPDATE_TASKS, () =>
|
||||
SystemApi.getTasks()
|
||||
);
|
||||
|
||||
export function systemRunTasks(id: string): Action<string> {
|
||||
return {
|
||||
type: SYSTEM_RUN_TASK,
|
||||
payload: id,
|
||||
};
|
||||
}
|
||||
|
||||
export const systemUpdateLogs = createAsyncAction(SYSTEM_UPDATE_LOGS, () =>
|
||||
SystemApi.logs()
|
||||
);
|
||||
|
@ -56,6 +53,11 @@ export const systemUpdateSettings = createAsyncAction(
|
|||
() => SystemApi.settings()
|
||||
);
|
||||
|
||||
export const providerUpdateList = createAsyncAction(
|
||||
SYSTEM_UPDATE_PROVIDERS,
|
||||
() => ProvidersApi.providers()
|
||||
);
|
||||
|
||||
export const systemUpdateSettingsAll = createAsyncCombineAction(() => [
|
||||
systemUpdateSettings(),
|
||||
systemUpdateLanguagesAll(),
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
// Provider action
|
||||
export const PROVIDER_UPDATE_LIST = "UPDATE_PROVIDER_LIST";
|
||||
|
||||
// System action
|
||||
export const SYSTEM_UPDATE_LANGUAGES_LIST = "UPDATE_ALL_LANGUAGES_LIST";
|
||||
export const SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST =
|
||||
"UPDATE_LANGUAGES_PROFILE_LIST";
|
||||
export const SYSTEM_UPDATE_STATUS = "UPDATE_SYSTEM_STATUS";
|
||||
export const SYSTEM_UPDATE_HEALTH = "UPDATE_SYSTEM_HEALTH";
|
||||
export const SYSTEM_UPDATE_TASKS = "UPDATE_SYSTEM_TASKS";
|
||||
export const SYSTEM_UPDATE_LOGS = "UPDATE_SYSTEM_LOGS";
|
||||
export const SYSTEM_UPDATE_RELEASES = "SYSTEM_UPDATE_RELEASES";
|
||||
export const SYSTEM_UPDATE_SETTINGS = "UPDATE_SYSTEM_SETTINGS";
|
||||
export const SYSTEM_RUN_TASK = "SYSTEM_RUN_TASK";
|
||||
export const SYSTEM_UPDATE_PROVIDERS = "SYSTEM_UPDATE_PROVIDERS";
|
||||
|
||||
// Series action
|
||||
export const SERIES_UPDATE_WANTED_RANGE = "SERIES_UPDATE_WANTED_RANGE";
|
||||
export const SERIES_UPDATE_WANTED_LIST = "UPDATE_SERIES_WANTED_LIST";
|
||||
export const SERIES_DELETE_WANTED_ITEMS = "SERIES_DELETE_WANTED_ITEMS";
|
||||
export const SERIES_UPDATE_EPISODE_LIST = "UPDATE_SERIES_EPISODE_LIST";
|
||||
export const SERIES_DELETE_EPISODES = "SERIES_DELETE_EPISODES";
|
||||
export const SERIES_UPDATE_HISTORY_LIST = "UPDATE_SERIES_HISTORY_LIST";
|
||||
export const SERIES_UPDATE_INFO = "UPDATE_SEIRES_INFO";
|
||||
export const SERIES_UPDATE_RANGE = "SERIES_UPDATE_RANGE";
|
||||
export const SERIES_UPDATE_LIST = "UPDATE_SEIRES_LIST";
|
||||
export const SERIES_DELETE_ITEMS = "SERIES_DELETE_ITEMS";
|
||||
export const SERIES_UPDATE_BLACKLIST = "UPDATE_SERIES_BLACKLIST";
|
||||
|
||||
// Movie action
|
||||
export const MOVIES_UPDATE_LIST = "UPDATE_MOVIE_LIST";
|
||||
export const MOVIES_UPDATE_WANTED_RANGE = "MOVIES_UPDATE_WANTED_RANGE";
|
||||
export const MOVIES_DELETE_ITEMS = "MOVIES_DELETE_ITEMS";
|
||||
export const MOVIES_UPDATE_WANTED_LIST = "UPDATE_MOVIE_WANTED_LIST";
|
||||
export const MOVIES_DELETE_WANTED_ITEMS = "MOVIES_DELETE_WANTED_ITEMS";
|
||||
export const MOVIES_UPDATE_HISTORY_LIST = "UPDATE_MOVIE_HISTORY_LIST";
|
||||
export const MOVIES_UPDATE_INFO = "UPDATE_MOVIE_INFO";
|
||||
export const MOVIES_UPDATE_RANGE = "MOVIES_UPDATE_RANGE";
|
||||
export const MOVIES_UPDATE_BLACKLIST = "UPDATE_MOVIES_BLACKLIST";
|
||||
|
||||
// Site Action
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useSocketIOReducer, useWrapToOptionalId } from "../../@socketio/hooks";
|
||||
import { buildOrderList } from "../../utilites";
|
||||
import {
|
||||
episodeUpdateBySeriesId,
|
||||
episodeDeleteItems,
|
||||
episodeUpdateBy,
|
||||
episodeUpdateById,
|
||||
movieDeleteWantedItems,
|
||||
movieUpdateBlacklist,
|
||||
movieUpdateHistoryList,
|
||||
movieUpdateInfoAll,
|
||||
movieUpdateWantedBy,
|
||||
providerUpdateAll,
|
||||
movieUpdateList,
|
||||
movieUpdateWantedList,
|
||||
providerUpdateList,
|
||||
seriesDeleteWantedItems,
|
||||
seriesUpdateBlacklist,
|
||||
seriesUpdateHistoryList,
|
||||
seriesUpdateInfoAll,
|
||||
seriesUpdateWantedBy,
|
||||
seriesUpdateList,
|
||||
seriesUpdateWantedList,
|
||||
systemUpdateHealth,
|
||||
systemUpdateLanguages,
|
||||
systemUpdateLanguagesProfiles,
|
||||
systemUpdateLogs,
|
||||
systemUpdateReleases,
|
||||
systemUpdateSettingsAll,
|
||||
systemUpdateStatus,
|
||||
systemUpdateTasks,
|
||||
} from "../actions";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
|
@ -25,9 +35,71 @@ function stateBuilder<T, D extends (...args: any[]) => any>(
|
|||
}
|
||||
|
||||
export function useSystemSettings() {
|
||||
const action = useReduxAction(systemUpdateSettingsAll);
|
||||
const update = useReduxAction(systemUpdateSettingsAll);
|
||||
const items = useReduxStore((s) => s.system.settings);
|
||||
return stateBuilder(items, action);
|
||||
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemLogs() {
|
||||
const items = useReduxStore(({ system }) => system.logs);
|
||||
const update = useReduxAction(systemUpdateLogs);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemTasks() {
|
||||
const items = useReduxStore((s) => s.system.tasks);
|
||||
const update = useReduxAction(systemUpdateTasks);
|
||||
useSocketIOReducer("task", update);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemStatus() {
|
||||
const items = useReduxStore((s) => s.system.status.data);
|
||||
const update = useReduxAction(systemUpdateStatus);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemHealth() {
|
||||
const update = useReduxAction(systemUpdateHealth);
|
||||
const items = useReduxStore((s) => s.system.health);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemProviders() {
|
||||
const update = useReduxAction(providerUpdateList);
|
||||
const items = useReduxStore((d) => d.system.providers);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemReleases() {
|
||||
const items = useReduxStore(({ system }) => system.releases);
|
||||
const update = useReduxAction(systemUpdateReleases);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useLanguageProfiles() {
|
||||
|
@ -92,9 +164,9 @@ export function useProfileItems(profile?: Profile.Languages) {
|
|||
}
|
||||
|
||||
export function useRawSeries() {
|
||||
const action = useReduxAction(seriesUpdateInfoAll);
|
||||
const update = useReduxAction(seriesUpdateList);
|
||||
const items = useReduxStore((d) => d.series.seriesList);
|
||||
return stateBuilder(items, action);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSeries(order = true) {
|
||||
|
@ -118,7 +190,6 @@ export function useSeries(order = true) {
|
|||
|
||||
export function useSerieBy(id?: number) {
|
||||
const [series, updateSerie] = useRawSeries();
|
||||
const updateEpisodes = useReduxAction(episodeUpdateBySeriesId);
|
||||
const serie = useMemo<AsyncState<Item.Series | null>>(() => {
|
||||
const items = series.data.items;
|
||||
let item: Item.Series | null = null;
|
||||
|
@ -134,18 +205,22 @@ export function useSerieBy(id?: number) {
|
|||
const update = useCallback(() => {
|
||||
if (id && !isNaN(id)) {
|
||||
updateSerie([id]);
|
||||
updateEpisodes(id);
|
||||
}
|
||||
}, [id, updateSerie, updateEpisodes]);
|
||||
}, [id, updateSerie]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serie.data === null) {
|
||||
update();
|
||||
}
|
||||
}, [serie.data, update]);
|
||||
return stateBuilder(serie, update);
|
||||
}
|
||||
|
||||
export function useEpisodesBy(seriesId?: number) {
|
||||
const action = useReduxAction(episodeUpdateBySeriesId);
|
||||
const callback = useCallback(() => {
|
||||
const action = useReduxAction(episodeUpdateBy);
|
||||
const update = useCallback(() => {
|
||||
if (seriesId !== undefined && !isNaN(seriesId)) {
|
||||
action(seriesId);
|
||||
action([seriesId]);
|
||||
}
|
||||
}, [action, seriesId]);
|
||||
|
||||
|
@ -153,24 +228,38 @@ export function useEpisodesBy(seriesId?: number) {
|
|||
|
||||
const items = useMemo(() => {
|
||||
if (seriesId !== undefined && !isNaN(seriesId)) {
|
||||
return list.data[seriesId] ?? [];
|
||||
return list.data.filter((v) => v.sonarrSeriesId === seriesId);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [seriesId, list.data]);
|
||||
|
||||
const state: AsyncState<Item.Episode[]> = {
|
||||
...list,
|
||||
data: items,
|
||||
};
|
||||
const state: AsyncState<Item.Episode[]> = useMemo(
|
||||
() => ({
|
||||
...list,
|
||||
data: items,
|
||||
}),
|
||||
[list, items]
|
||||
);
|
||||
|
||||
return stateBuilder(state, callback);
|
||||
const actionById = useReduxAction(episodeUpdateById);
|
||||
const wrapActionById = useWrapToOptionalId(actionById);
|
||||
const deleteAction = useReduxAction(episodeDeleteItems);
|
||||
useSocketIOReducer("episode", undefined, wrapActionById, deleteAction);
|
||||
|
||||
const wrapAction = useWrapToOptionalId(action);
|
||||
useSocketIOReducer("series", undefined, wrapAction);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(state, update);
|
||||
}
|
||||
|
||||
export function useRawMovies() {
|
||||
const action = useReduxAction(movieUpdateInfoAll);
|
||||
const update = useReduxAction(movieUpdateList);
|
||||
const items = useReduxStore((d) => d.movie.movieList);
|
||||
return stateBuilder(items, action);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useMovies(order = true) {
|
||||
|
@ -212,54 +301,80 @@ export function useMovieBy(id?: number) {
|
|||
}
|
||||
}, [id, updateMovies]);
|
||||
|
||||
useEffect(() => {
|
||||
if (movie.data === null) {
|
||||
update();
|
||||
}
|
||||
}, [movie.data, update]);
|
||||
return stateBuilder(movie, update);
|
||||
}
|
||||
|
||||
export function useWantedSeries() {
|
||||
const action = useReduxAction(seriesUpdateWantedBy);
|
||||
const update = useReduxAction(seriesUpdateWantedList);
|
||||
const items = useReduxStore((d) => d.series.wantedEpisodesList);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
const updateAction = useWrapToOptionalId(update);
|
||||
const deleteAction = useReduxAction(seriesDeleteWantedItems);
|
||||
useSocketIOReducer("episode-wanted", undefined, updateAction, deleteAction);
|
||||
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useWantedMovies() {
|
||||
const action = useReduxAction(movieUpdateWantedBy);
|
||||
const update = useReduxAction(movieUpdateWantedList);
|
||||
const items = useReduxStore((d) => d.movie.wantedMovieList);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
}
|
||||
const updateAction = useWrapToOptionalId(update);
|
||||
const deleteAction = useReduxAction(movieDeleteWantedItems);
|
||||
useSocketIOReducer("movie-wanted", undefined, updateAction, deleteAction);
|
||||
|
||||
export function useProviders() {
|
||||
const action = useReduxAction(providerUpdateAll);
|
||||
const items = useReduxStore((d) => d.system.providers);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useBlacklistMovies() {
|
||||
const action = useReduxAction(movieUpdateBlacklist);
|
||||
const update = useReduxAction(movieUpdateBlacklist);
|
||||
const items = useReduxStore((d) => d.movie.blacklist);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
useSocketIOReducer("movie-blacklist", update);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useBlacklistSeries() {
|
||||
const action = useReduxAction(seriesUpdateBlacklist);
|
||||
const update = useReduxAction(seriesUpdateBlacklist);
|
||||
const items = useReduxStore((d) => d.series.blacklist);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
useSocketIOReducer("episode-blacklist", update);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useMoviesHistory() {
|
||||
const action = useReduxAction(movieUpdateHistoryList);
|
||||
const update = useReduxAction(movieUpdateHistoryList);
|
||||
const items = useReduxStore((s) => s.movie.historyList);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
useSocketIOReducer("movie-history", update);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSeriesHistory() {
|
||||
const action = useReduxAction(seriesUpdateHistoryList);
|
||||
const update = useReduxAction(seriesUpdateHistoryList);
|
||||
const items = useReduxStore((s) => s.series.historyList);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
useSocketIOReducer("episode-history", update);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { useCallback } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSystemSettings } from ".";
|
||||
import { siteAddError, siteRemoveErrorByTimestamp } from "../actions";
|
||||
import {
|
||||
siteAddNotification,
|
||||
siteChangeSidebar,
|
||||
siteRemoveNotificationByTime,
|
||||
} from "../actions";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
export function useNotification(id: string, sec: number = 5) {
|
||||
const add = useReduxAction(siteAddError);
|
||||
const remove = useReduxAction(siteRemoveErrorByTimestamp);
|
||||
const add = useReduxAction(siteAddNotification);
|
||||
const remove = useReduxAction(siteRemoveNotificationByTime);
|
||||
|
||||
return useCallback(
|
||||
(msg: Omit<ReduxStore.Notification, "id" | "timestamp">) => {
|
||||
|
@ -34,3 +38,15 @@ export function useIsRadarrEnabled() {
|
|||
const [settings] = useSystemSettings();
|
||||
return settings.data?.general.use_radarr ?? true;
|
||||
}
|
||||
|
||||
export function useShowOnlyDesired() {
|
||||
const [settings] = useSystemSettings();
|
||||
return settings.data?.general.embedded_subs_show_desired ?? false;
|
||||
}
|
||||
|
||||
export function useSetSidebar(key: string) {
|
||||
const update = useReduxAction(siteChangeSidebar);
|
||||
useEffect(() => {
|
||||
update(key);
|
||||
}, [update, key]);
|
||||
}
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
import { mergeArray } from "../../utilites";
|
||||
import { AsyncAction } from "../types";
|
||||
|
||||
export function updateAsyncState<Payload>(
|
||||
action: AsyncAction<Payload>,
|
||||
defVal: Readonly<Payload>
|
||||
): AsyncState<Payload> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
updating: true,
|
||||
data: defVal,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
data: defVal,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
updating: false,
|
||||
error: undefined,
|
||||
data: action.payload.item as Payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateOrderIdState<T extends LooseObject>(
|
||||
action: AsyncAction<AsyncDataWrapper<T>>,
|
||||
state: AsyncState<OrderIdState<T>>,
|
||||
id: ItemIdType<T>
|
||||
): AsyncState<OrderIdState<T>> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
...state,
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
...state,
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const { data, total } = action.payload.item as AsyncDataWrapper<T>;
|
||||
const [start, length] = action.payload.parameters;
|
||||
|
||||
// Convert item list to object
|
||||
const idState: IdState<T> = data.reduce<IdState<T>>((prev, curr) => {
|
||||
const tid = curr[id];
|
||||
prev[tid] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
const dataOrder: number[] = data.map((v) => v[id]);
|
||||
|
||||
let newItems = { ...state.data.items, ...idState };
|
||||
let newOrder = state.data.order;
|
||||
|
||||
const countDist = total - newOrder.length;
|
||||
if (countDist > 0) {
|
||||
newOrder.push(...Array(countDist).fill(null));
|
||||
} else if (countDist < 0) {
|
||||
// Completely drop old data if list has shrinked
|
||||
newOrder = Array(total).fill(null);
|
||||
newItems = { ...idState };
|
||||
}
|
||||
|
||||
if (typeof start === "number" && typeof length === "number") {
|
||||
newOrder.splice(start, length, ...dataOrder);
|
||||
} else if (start === undefined) {
|
||||
// Full Update
|
||||
newOrder = dataOrder;
|
||||
}
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: {
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAsyncList<T, ID extends keyof T>(
|
||||
action: AsyncAction<T[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ID
|
||||
): AsyncState<T[]> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
...state,
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
...state,
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const list = state.data as T[];
|
||||
const payload = action.payload.item as T[];
|
||||
const result = mergeArray(list, payload, (l, r) => l[match] === r[match]);
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,14 +1,19 @@
|
|||
import { handleActions } from "redux-actions";
|
||||
import { Action, handleActions } from "redux-actions";
|
||||
import {
|
||||
MOVIES_DELETE_ITEMS,
|
||||
MOVIES_DELETE_WANTED_ITEMS,
|
||||
MOVIES_UPDATE_BLACKLIST,
|
||||
MOVIES_UPDATE_HISTORY_LIST,
|
||||
MOVIES_UPDATE_INFO,
|
||||
MOVIES_UPDATE_RANGE,
|
||||
MOVIES_UPDATE_LIST,
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
MOVIES_UPDATE_WANTED_RANGE,
|
||||
} from "../constants";
|
||||
import { AsyncAction } from "../types";
|
||||
import { updateAsyncState, updateOrderIdState } from "./mapper";
|
||||
import { defaultAOS } from "../utils";
|
||||
import {
|
||||
deleteOrderListItemBy,
|
||||
updateAsyncState,
|
||||
updateOrderIdState,
|
||||
} from "../utils/mapper";
|
||||
|
||||
const reducer = handleActions<ReduxStore.Movie, any>(
|
||||
{
|
||||
|
@ -25,17 +30,10 @@ const reducer = handleActions<ReduxStore.Movie, any>(
|
|||
),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_WANTED_RANGE]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Wanted.Movie>>
|
||||
) => {
|
||||
[MOVIES_DELETE_WANTED_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
wantedMovieList: updateOrderIdState(
|
||||
action,
|
||||
state.wantedMovieList,
|
||||
"radarrId"
|
||||
),
|
||||
wantedMovieList: deleteOrderListItemBy(action, state.wantedMovieList),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_HISTORY_LIST]: (
|
||||
|
@ -47,7 +45,7 @@ const reducer = handleActions<ReduxStore.Movie, any>(
|
|||
historyList: updateAsyncState(action, state.historyList.data),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_INFO]: (
|
||||
[MOVIES_UPDATE_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Item.Movie>>
|
||||
) => {
|
||||
|
@ -56,13 +54,10 @@ const reducer = handleActions<ReduxStore.Movie, any>(
|
|||
movieList: updateOrderIdState(action, state.movieList, "radarrId"),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_RANGE]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Item.Movie>>
|
||||
) => {
|
||||
[MOVIES_DELETE_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
movieList: updateOrderIdState(action, state.movieList, "radarrId"),
|
||||
movieList: deleteOrderListItemBy(action, state.movieList),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_BLACKLIST]: (
|
||||
|
@ -76,8 +71,8 @@ const reducer = handleActions<ReduxStore.Movie, any>(
|
|||
},
|
||||
},
|
||||
{
|
||||
movieList: { updating: true, data: { items: {}, order: [] } },
|
||||
wantedMovieList: { updating: true, data: { items: {}, order: [] } },
|
||||
movieList: defaultAOS(),
|
||||
wantedMovieList: defaultAOS(),
|
||||
historyList: { updating: true, data: [] },
|
||||
blacklist: { updating: true, data: [] },
|
||||
}
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
import { handleActions } from "redux-actions";
|
||||
import { Action, handleActions } from "redux-actions";
|
||||
import {
|
||||
SERIES_DELETE_EPISODES,
|
||||
SERIES_DELETE_ITEMS,
|
||||
SERIES_DELETE_WANTED_ITEMS,
|
||||
SERIES_UPDATE_BLACKLIST,
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
SERIES_UPDATE_HISTORY_LIST,
|
||||
SERIES_UPDATE_INFO,
|
||||
SERIES_UPDATE_RANGE,
|
||||
SERIES_UPDATE_LIST,
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
SERIES_UPDATE_WANTED_RANGE,
|
||||
} from "../constants";
|
||||
import { AsyncAction } from "../types";
|
||||
import { updateAsyncState, updateOrderIdState } from "./mapper";
|
||||
import { defaultAOS } from "../utils";
|
||||
import {
|
||||
deleteAsyncListItemBy,
|
||||
deleteOrderListItemBy,
|
||||
updateAsyncList,
|
||||
updateAsyncState,
|
||||
updateOrderIdState,
|
||||
} from "../utils/mapper";
|
||||
|
||||
const reducer = handleActions<ReduxStore.Series, any>(
|
||||
{
|
||||
|
@ -26,16 +34,12 @@ const reducer = handleActions<ReduxStore.Series, any>(
|
|||
),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_WANTED_RANGE]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Wanted.Episode>>
|
||||
) => {
|
||||
[SERIES_DELETE_WANTED_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
wantedEpisodesList: updateOrderIdState(
|
||||
wantedEpisodesList: deleteOrderListItemBy(
|
||||
action,
|
||||
state.wantedEpisodesList,
|
||||
"sonarrEpisodeId"
|
||||
state.wantedEpisodesList
|
||||
),
|
||||
};
|
||||
},
|
||||
|
@ -43,22 +47,23 @@ const reducer = handleActions<ReduxStore.Series, any>(
|
|||
state,
|
||||
action: AsyncAction<Item.Episode[]>
|
||||
) => {
|
||||
const { updating, error, data: items } = updateAsyncState(action, []);
|
||||
|
||||
const stateItems = { ...state.episodeList.data };
|
||||
|
||||
if (items.length > 0) {
|
||||
const id = items[0].sonarrSeriesId;
|
||||
stateItems[id] = items;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
episodeList: {
|
||||
updating,
|
||||
error,
|
||||
data: stateItems,
|
||||
},
|
||||
episodeList: updateAsyncList(
|
||||
action,
|
||||
state.episodeList,
|
||||
"sonarrEpisodeId"
|
||||
),
|
||||
};
|
||||
},
|
||||
[SERIES_DELETE_EPISODES]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
episodeList: deleteAsyncListItemBy(
|
||||
action,
|
||||
state.episodeList,
|
||||
"sonarrEpisodeId"
|
||||
),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_HISTORY_LIST]: (
|
||||
|
@ -70,7 +75,7 @@ const reducer = handleActions<ReduxStore.Series, any>(
|
|||
historyList: updateAsyncState(action, state.historyList.data),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_INFO]: (
|
||||
[SERIES_UPDATE_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Item.Series>>
|
||||
) => {
|
||||
|
@ -83,17 +88,10 @@ const reducer = handleActions<ReduxStore.Series, any>(
|
|||
),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_RANGE]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Item.Series>>
|
||||
) => {
|
||||
[SERIES_DELETE_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
seriesList: updateOrderIdState(
|
||||
action,
|
||||
state.seriesList,
|
||||
"sonarrSeriesId"
|
||||
),
|
||||
seriesList: deleteOrderListItemBy(action, state.seriesList),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_BLACKLIST]: (
|
||||
|
@ -107,9 +105,9 @@ const reducer = handleActions<ReduxStore.Series, any>(
|
|||
},
|
||||
},
|
||||
{
|
||||
seriesList: { updating: true, data: { items: {}, order: [] } },
|
||||
wantedEpisodesList: { updating: true, data: { items: {}, order: [] } },
|
||||
episodeList: { updating: true, data: {} },
|
||||
seriesList: defaultAOS(),
|
||||
wantedEpisodesList: defaultAOS(),
|
||||
episodeList: { updating: true, data: [] },
|
||||
historyList: { updating: true, data: [] },
|
||||
blacklist: { updating: true, data: [] },
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@ const reducer = handleActions<ReduxStore.Site, any>(
|
|||
movies: 0,
|
||||
episodes: 0,
|
||||
providers: 0,
|
||||
status: 0,
|
||||
},
|
||||
offline: false,
|
||||
...updateLocalStorage(),
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { Action, handleActions } from "redux-actions";
|
||||
import { handleActions } from "redux-actions";
|
||||
import {
|
||||
PROVIDER_UPDATE_LIST,
|
||||
SYSTEM_RUN_TASK,
|
||||
SYSTEM_UPDATE_HEALTH,
|
||||
SYSTEM_UPDATE_LANGUAGES_LIST,
|
||||
SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST,
|
||||
SYSTEM_UPDATE_LOGS,
|
||||
SYSTEM_UPDATE_PROVIDERS,
|
||||
SYSTEM_UPDATE_RELEASES,
|
||||
SYSTEM_UPDATE_SETTINGS,
|
||||
SYSTEM_UPDATE_STATUS,
|
||||
SYSTEM_UPDATE_TASKS,
|
||||
} from "../constants";
|
||||
import { updateAsyncState } from "./mapper";
|
||||
import { updateAsyncState } from "../utils/mapper";
|
||||
|
||||
const reducer = handleActions<ReduxStore.System, any>(
|
||||
{
|
||||
|
@ -46,32 +46,19 @@ const reducer = handleActions<ReduxStore.System, any>(
|
|||
),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_HEALTH]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
health: updateAsyncState(action, state.health.data),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_TASKS]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
tasks: updateAsyncState<Array<System.Task>>(action, state.tasks.data),
|
||||
};
|
||||
},
|
||||
[SYSTEM_RUN_TASK]: (state, action: Action<string>) => {
|
||||
const id = action.payload;
|
||||
const tasks = state.tasks;
|
||||
const newItems = [...tasks.data];
|
||||
|
||||
const idx = newItems.findIndex((v) => v.job_id === id);
|
||||
|
||||
if (idx !== -1) {
|
||||
newItems[idx].job_running = true;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
tasks: {
|
||||
...tasks,
|
||||
data: newItems,
|
||||
},
|
||||
};
|
||||
},
|
||||
[PROVIDER_UPDATE_LIST]: (state, action) => {
|
||||
[SYSTEM_UPDATE_PROVIDERS]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
providers: updateAsyncState(action, state.providers.data),
|
||||
|
@ -104,6 +91,10 @@ const reducer = handleActions<ReduxStore.System, any>(
|
|||
updating: true,
|
||||
data: undefined,
|
||||
},
|
||||
health: {
|
||||
updating: true,
|
||||
data: [],
|
||||
},
|
||||
tasks: {
|
||||
updating: true,
|
||||
data: [],
|
||||
|
|
20
frontend/src/@redux/redux.d.ts
vendored
20
frontend/src/@redux/redux.d.ts
vendored
|
@ -1,12 +1,3 @@
|
|||
interface IdState<T> {
|
||||
[key: number]: Readonly<T>;
|
||||
}
|
||||
|
||||
interface OrderIdState<T> {
|
||||
items: IdState<T>;
|
||||
order: (number | null)[];
|
||||
}
|
||||
|
||||
interface ReduxStore {
|
||||
system: ReduxStore.System;
|
||||
series: ReduxStore.Series;
|
||||
|
@ -38,6 +29,7 @@ namespace ReduxStore {
|
|||
enabledLanguage: AsyncState<Array<Language>>;
|
||||
languagesProfiles: AsyncState<Array<Profile.Languages>>;
|
||||
status: AsyncState<System.Status | undefined>;
|
||||
health: AsyncState<Array<System.Health>>;
|
||||
tasks: AsyncState<Array<System.Task>>;
|
||||
providers: AsyncState<Array<System.Provider>>;
|
||||
logs: AsyncState<Array<System.Log>>;
|
||||
|
@ -46,16 +38,16 @@ namespace ReduxStore {
|
|||
}
|
||||
|
||||
interface Series {
|
||||
seriesList: AsyncState<OrderIdState<Item.Series>>;
|
||||
wantedEpisodesList: AsyncState<OrderIdState<Wanted.Episode>>;
|
||||
episodeList: AsyncState<IdState<Item.Episode[]>>;
|
||||
seriesList: AsyncOrderState<Item.Series>;
|
||||
wantedEpisodesList: AsyncOrderState<Wanted.Episode>;
|
||||
episodeList: AsyncState<Item.Episode[]>;
|
||||
historyList: AsyncState<Array<History.Episode>>;
|
||||
blacklist: AsyncState<Array<Blacklist.Episode>>;
|
||||
}
|
||||
|
||||
interface Movie {
|
||||
movieList: AsyncState<OrderIdState<Item.Movie>>;
|
||||
wantedMovieList: AsyncState<OrderIdState<Wanted.Movie>>;
|
||||
movieList: AsyncOrderState<Item.Movie>;
|
||||
wantedMovieList: AsyncOrderState<Wanted.Movie>;
|
||||
historyList: AsyncState<Array<History.Movie>>;
|
||||
blacklist: AsyncState<Array<Blacklist.Movie>>;
|
||||
}
|
||||
|
|
10
frontend/src/@redux/utils/index.ts
Normal file
10
frontend/src/@redux/utils/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export function defaultAOS(): AsyncOrderState<any> {
|
||||
return {
|
||||
updating: true,
|
||||
data: {
|
||||
items: [],
|
||||
order: [],
|
||||
fetched: false,
|
||||
},
|
||||
};
|
||||
}
|
181
frontend/src/@redux/utils/mapper.ts
Normal file
181
frontend/src/@redux/utils/mapper.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
import { difference, has, isArray, isNull, isNumber, uniqBy } from "lodash";
|
||||
import { Action } from "redux-actions";
|
||||
import { conditionalLog } from "../../utilites/logger";
|
||||
import { AsyncAction } from "../types";
|
||||
|
||||
export function updateAsyncState<Payload>(
|
||||
action: AsyncAction<Payload>,
|
||||
defVal: Readonly<Payload>
|
||||
): AsyncState<Payload> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
updating: true,
|
||||
data: defVal,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
data: defVal,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
updating: false,
|
||||
error: undefined,
|
||||
data: action.payload.item as Payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateOrderIdState<T extends LooseObject>(
|
||||
action: AsyncAction<AsyncDataWrapper<T>>,
|
||||
state: AsyncOrderState<T>,
|
||||
id: ItemIdType<T>
|
||||
): AsyncOrderState<T> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
data: {
|
||||
...state.data,
|
||||
fetched: true,
|
||||
},
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
data: {
|
||||
...state.data,
|
||||
fetched: true,
|
||||
},
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const { data, total } = action.payload.item as AsyncDataWrapper<T>;
|
||||
const { parameters } = action.payload;
|
||||
const [start, length] = parameters;
|
||||
|
||||
// Convert item list to object
|
||||
const newItems = data.reduce<IdState<T>>(
|
||||
(prev, curr) => {
|
||||
const tid = curr[id];
|
||||
prev[tid] = curr;
|
||||
return prev;
|
||||
},
|
||||
{ ...state.data.items }
|
||||
);
|
||||
|
||||
let newOrder = [...state.data.order];
|
||||
|
||||
const countDist = total - newOrder.length;
|
||||
if (countDist > 0) {
|
||||
newOrder = Array(countDist).fill(null).concat(newOrder);
|
||||
} else if (countDist < 0) {
|
||||
// Completely drop old data if list has shrinked
|
||||
newOrder = Array(total).fill(null);
|
||||
}
|
||||
|
||||
const idList = newOrder.filter(isNumber);
|
||||
|
||||
const dataOrder: number[] = data.map((v) => v[id]);
|
||||
|
||||
if (typeof start === "number" && typeof length === "number") {
|
||||
newOrder.splice(start, length, ...dataOrder);
|
||||
} else if (isArray(start)) {
|
||||
// Find the null values and delete them, insert new values to the front of array
|
||||
const addition = difference(dataOrder, idList);
|
||||
let addCount = addition.length;
|
||||
newOrder.unshift(...addition);
|
||||
|
||||
newOrder = newOrder.flatMap((v) => {
|
||||
if (isNull(v) && addCount > 0) {
|
||||
--addCount;
|
||||
return [];
|
||||
} else {
|
||||
return [v];
|
||||
}
|
||||
}, []);
|
||||
|
||||
conditionalLog(
|
||||
addCount !== 0,
|
||||
"Error when replacing item in OrderIdState"
|
||||
);
|
||||
} else if (parameters.length === 0) {
|
||||
// TODO: Delete me -> Full Update
|
||||
newOrder = dataOrder;
|
||||
}
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: {
|
||||
fetched: true,
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteOrderListItemBy<T extends LooseObject>(
|
||||
action: Action<number[]>,
|
||||
state: AsyncOrderState<T>
|
||||
): AsyncOrderState<T> {
|
||||
const ids = action.payload;
|
||||
const { items, order } = state.data;
|
||||
const newItems = { ...items };
|
||||
ids.forEach((v) => {
|
||||
if (has(newItems, v)) {
|
||||
delete newItems[v];
|
||||
}
|
||||
});
|
||||
const newOrder = difference(order, ids);
|
||||
return {
|
||||
...state,
|
||||
data: {
|
||||
fetched: true,
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteAsyncListItemBy<T extends LooseObject>(
|
||||
action: Action<number[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ItemIdType<T>
|
||||
): AsyncState<T[]> {
|
||||
const ids = new Set(action.payload);
|
||||
const data = [...state.data].filter((v) => !ids.has(v[match]));
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAsyncList<T, ID extends keyof T>(
|
||||
action: AsyncAction<T[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ID
|
||||
): AsyncState<T[]> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
...state,
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
...state,
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const olds = state.data as T[];
|
||||
const news = action.payload.item as T[];
|
||||
|
||||
const result = uniqBy([...news, ...olds], match);
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
}
|
35
frontend/src/@socketio/hooks.ts
Normal file
35
frontend/src/@socketio/hooks.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import Socketio from ".";
|
||||
import { log } from "../utilites/logger";
|
||||
|
||||
export function useSocketIOReducer(
|
||||
key: SocketIO.EventType,
|
||||
any?: () => void,
|
||||
update?: SocketIO.ActionFn,
|
||||
remove?: SocketIO.ActionFn
|
||||
) {
|
||||
const reducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key, any, update, delete: remove }),
|
||||
[key, any, update, remove]
|
||||
);
|
||||
useEffect(() => {
|
||||
Socketio.addReducer(reducer);
|
||||
log("info", "listening to SocketIO event", key);
|
||||
return () => {
|
||||
Socketio.removeReducer(reducer);
|
||||
};
|
||||
}, [reducer, key]);
|
||||
}
|
||||
|
||||
export function useWrapToOptionalId(
|
||||
fn: (id: number[]) => void
|
||||
): SocketIO.ActionFn {
|
||||
return useCallback(
|
||||
(id?: number[]) => {
|
||||
if (id) {
|
||||
fn(id);
|
||||
}
|
||||
},
|
||||
[fn]
|
||||
);
|
||||
}
|
123
frontend/src/@socketio/index.ts
Normal file
123
frontend/src/@socketio/index.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { debounce, forIn, remove, uniq } from "lodash";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { getBaseUrl } from "../utilites";
|
||||
import { conditionalLog, log } from "../utilites/logger";
|
||||
import { createDefaultReducer } from "./reducer";
|
||||
|
||||
class SocketIOClient {
|
||||
private socket: Socket;
|
||||
private events: SocketIO.Event[];
|
||||
private debounceReduce: () => void;
|
||||
|
||||
private reducers: SocketIO.Reducer[];
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getBaseUrl();
|
||||
this.socket = io({
|
||||
path: `${baseUrl}/api/socket.io`,
|
||||
transports: ["polling", "websocket"],
|
||||
upgrade: true,
|
||||
rememberUpgrade: true,
|
||||
autoConnect: false,
|
||||
});
|
||||
|
||||
this.socket.on("connect", this.onConnect.bind(this));
|
||||
this.socket.on("disconnect", this.onDisconnect.bind(this));
|
||||
this.socket.on("connect_error", this.onDisconnect.bind(this));
|
||||
this.socket.on("data", this.onEvent.bind(this));
|
||||
|
||||
this.events = [];
|
||||
this.debounceReduce = debounce(this.reduce, 200);
|
||||
this.reducers = [];
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.reducers.push(...createDefaultReducer());
|
||||
this.socket.connect();
|
||||
|
||||
// Debug Command
|
||||
window._socketio = {
|
||||
dump: this.dump.bind(this),
|
||||
emit: this.onEvent.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private dump() {
|
||||
console.log("SocketIO reducers", this.reducers);
|
||||
}
|
||||
|
||||
addReducer(reducer: SocketIO.Reducer) {
|
||||
this.reducers.push(reducer);
|
||||
}
|
||||
|
||||
removeReducer(reducer: SocketIO.Reducer) {
|
||||
const removed = remove(this.reducers, (r) => r === reducer);
|
||||
conditionalLog(removed.length === 0, "Fail to remove reducer", reducer);
|
||||
}
|
||||
|
||||
private reduce() {
|
||||
const events = [...this.events];
|
||||
this.events = [];
|
||||
|
||||
const records: SocketIO.ActionRecord = {};
|
||||
|
||||
events.forEach((e) => {
|
||||
if (!(e.type in records)) {
|
||||
records[e.type] = {};
|
||||
}
|
||||
const record = records[e.type]!;
|
||||
if (!(e.action in record)) {
|
||||
record[e.action] = [];
|
||||
}
|
||||
if (e.payload) {
|
||||
record[e.action]?.push(e.payload);
|
||||
}
|
||||
});
|
||||
|
||||
forIn(records, (element, type) => {
|
||||
if (element) {
|
||||
const handlers = this.reducers.filter((v) => v.key === type);
|
||||
if (handlers.length === 0) {
|
||||
log("warning", "Unhandle SocketIO event", type);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-loop-func
|
||||
handlers.forEach((handler) => {
|
||||
const anyAction = handler.any;
|
||||
if (anyAction) {
|
||||
anyAction();
|
||||
}
|
||||
|
||||
forIn(element, (ids, key) => {
|
||||
ids = uniq(ids);
|
||||
const action = handler[key as SocketIO.ActionType];
|
||||
if (action) {
|
||||
action(ids);
|
||||
} else if (anyAction === undefined) {
|
||||
log("warning", "Unhandle action of SocketIO event", key, type);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onConnect() {
|
||||
log("info", "Socket.IO has connected");
|
||||
this.onEvent({ type: "connect", action: "update", payload: null });
|
||||
}
|
||||
|
||||
private onDisconnect() {
|
||||
log("warning", "Socket.IO has disconnected");
|
||||
this.onEvent({ type: "disconnect", action: "update", payload: null });
|
||||
}
|
||||
|
||||
private onEvent(event: SocketIO.Event) {
|
||||
log("info", "Socket.IO receives", event);
|
||||
this.events.push(event);
|
||||
this.debounceReduce();
|
||||
}
|
||||
}
|
||||
|
||||
export default new SocketIOClient();
|
55
frontend/src/@socketio/reducer.ts
Normal file
55
frontend/src/@socketio/reducer.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
badgeUpdateAll,
|
||||
bootstrap,
|
||||
movieDeleteItems,
|
||||
movieUpdateList,
|
||||
seriesDeleteItems,
|
||||
seriesUpdateList,
|
||||
siteUpdateOffline,
|
||||
systemUpdateLanguagesAll,
|
||||
systemUpdateSettings,
|
||||
} from "../@redux/actions";
|
||||
import reduxStore from "../@redux/store";
|
||||
|
||||
function bindToReduxStore(fn: (ids?: number[]) => any): SocketIO.ActionFn {
|
||||
return (ids?: number[]) => reduxStore.dispatch(fn(ids));
|
||||
}
|
||||
|
||||
export function createDefaultReducer(): SocketIO.Reducer[] {
|
||||
return [
|
||||
{
|
||||
key: "connect",
|
||||
any: () => reduxStore.dispatch(siteUpdateOffline(false)),
|
||||
},
|
||||
{
|
||||
key: "connect",
|
||||
any: () => reduxStore.dispatch<any>(bootstrap()),
|
||||
},
|
||||
{
|
||||
key: "disconnect",
|
||||
any: () => reduxStore.dispatch(siteUpdateOffline(true)),
|
||||
},
|
||||
{
|
||||
key: "series",
|
||||
update: bindToReduxStore(seriesUpdateList),
|
||||
delete: bindToReduxStore(seriesDeleteItems),
|
||||
},
|
||||
{
|
||||
key: "movie",
|
||||
update: bindToReduxStore(movieUpdateList),
|
||||
delete: bindToReduxStore(movieDeleteItems),
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
any: bindToReduxStore(systemUpdateSettings),
|
||||
},
|
||||
{
|
||||
key: "languages",
|
||||
any: bindToReduxStore(systemUpdateLanguagesAll),
|
||||
},
|
||||
{
|
||||
key: "badges",
|
||||
any: bindToReduxStore(badgeUpdateAll),
|
||||
},
|
||||
];
|
||||
}
|
2
frontend/src/@types/api.d.ts
vendored
2
frontend/src/@types/api.d.ts
vendored
|
@ -4,6 +4,7 @@ interface Badge {
|
|||
episodes: number;
|
||||
movies: number;
|
||||
providers: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface ApiLanguage {
|
||||
|
@ -40,7 +41,6 @@ interface Subtitle extends Language {
|
|||
|
||||
interface PathType {
|
||||
path: string;
|
||||
exist: boolean;
|
||||
}
|
||||
|
||||
interface SubtitlePathType {
|
||||
|
|
10
frontend/src/@types/basic.d.ts
vendored
10
frontend/src/@types/basic.d.ts
vendored
|
@ -11,12 +11,20 @@ type FileTree = {
|
|||
|
||||
type StorageType = string | null;
|
||||
|
||||
interface OrderIdState<T> {
|
||||
items: IdState<T>;
|
||||
order: (number | null)[];
|
||||
fetched: boolean;
|
||||
}
|
||||
|
||||
interface AsyncState<T> {
|
||||
updating: boolean;
|
||||
error?: Error;
|
||||
data: Readonly<T>;
|
||||
}
|
||||
|
||||
type AsyncOrderState<T> = AsyncState<OrderIdState<T>>;
|
||||
|
||||
type AsyncPayload<T> = T extends AsyncState<infer D> ? D : never;
|
||||
|
||||
type SelectorOption<PAYLOAD> = {
|
||||
|
@ -32,3 +40,5 @@ type SimpleStateType<T> = [
|
|||
T,
|
||||
((item: T) => void) | ((fn: (item: T) => T) => void)
|
||||
];
|
||||
|
||||
type Factory<T> = () => T;
|
||||
|
|
18
frontend/src/@types/react-table.d.ts
vendored
18
frontend/src/@types/react-table.d.ts
vendored
|
@ -32,7 +32,6 @@ import {
|
|||
UseSortByState,
|
||||
} from "react-table";
|
||||
import {} from "../components/tables/plugins";
|
||||
import { PageControlAction } from "../components/tables/types";
|
||||
|
||||
declare module "react-table" {
|
||||
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
|
||||
|
@ -40,17 +39,6 @@ declare module "react-table" {
|
|||
// Customize of React Table
|
||||
type TableUpdater<D extends object> = (row: Row<D>, ...others: any[]) => void;
|
||||
|
||||
interface useAsyncPaginationProps<D extends Record<string, unknown>> {
|
||||
asyncLoader?: (start: number, length: number) => void;
|
||||
asyncState?: AsyncState<OrderIdState<D>>;
|
||||
asyncId?: (item: D) => number;
|
||||
}
|
||||
|
||||
interface useAsyncPaginationState<D extends Record<string, unknown>> {
|
||||
pageToLoad?: PageControlAction;
|
||||
needLoadingScreen?: boolean;
|
||||
}
|
||||
|
||||
interface useSelectionProps<D extends Record<string, unknown>> {
|
||||
isSelecting?: boolean;
|
||||
onSelect?: (items: D[]) => void;
|
||||
|
@ -59,15 +47,13 @@ declare module "react-table" {
|
|||
interface useSelectionState<D extends Record<string, unknown>> {}
|
||||
|
||||
interface CustomTableProps<D extends Record<string, unknown>>
|
||||
extends useSelectionProps<D>,
|
||||
useAsyncPaginationProps<D> {
|
||||
extends useSelectionProps<D> {
|
||||
externalUpdate?: TableUpdater<D>;
|
||||
loose?: any[];
|
||||
}
|
||||
|
||||
interface CustomTableState<D extends Record<string, unknown>>
|
||||
extends useSelectionState<D>,
|
||||
useAsyncPaginationState<D> {}
|
||||
extends useSelectionState<D> {}
|
||||
|
||||
export interface TableOptions<
|
||||
D extends Record<string, unknown>
|
||||
|
|
39
frontend/src/@types/socket.d.ts
vendored
Normal file
39
frontend/src/@types/socket.d.ts
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
namespace SocketIO {
|
||||
type EventType =
|
||||
| "connect"
|
||||
| "disconnect"
|
||||
| "movie"
|
||||
| "series"
|
||||
| "episode"
|
||||
| "episode-history"
|
||||
| "episode-blacklist"
|
||||
| "episode-wanted"
|
||||
| "movie-history"
|
||||
| "movie-blacklist"
|
||||
| "movie-wanted"
|
||||
| "badges"
|
||||
| "task"
|
||||
| "settings"
|
||||
| "languages"
|
||||
| "message";
|
||||
|
||||
type ActionType = "update" | "delete";
|
||||
|
||||
interface Event {
|
||||
type: EventType;
|
||||
action: ActionType;
|
||||
payload: any; // TODO: Use specific types
|
||||
}
|
||||
|
||||
type ActionFn = (payload?: any[]) => void;
|
||||
|
||||
type Reducer = {
|
||||
key: EventType;
|
||||
any?: () => any;
|
||||
} & Partial<Record<ActionType, ActionFn>>;
|
||||
|
||||
type ActionRecord = OptionalRecord<
|
||||
EventType,
|
||||
OptionalRecord<ActionType, any[]>
|
||||
>;
|
||||
}
|
5
frontend/src/@types/system.d.ts
vendored
5
frontend/src/@types/system.d.ts
vendored
|
@ -18,6 +18,11 @@ namespace System {
|
|||
sonarr_version: string;
|
||||
}
|
||||
|
||||
interface Health {
|
||||
object: string;
|
||||
issue: string;
|
||||
}
|
||||
|
||||
interface Provider {
|
||||
name: string;
|
||||
status: string;
|
||||
|
|
6
frontend/src/@types/utilities.d.ts
vendored
6
frontend/src/@types/utilities.d.ts
vendored
|
@ -37,3 +37,9 @@ type KeysOfType<D, T> = NonNullable<
|
|||
>;
|
||||
|
||||
type ItemIdType<T> = KeysOfType<T, number>;
|
||||
|
||||
type OptionalRecord<T, D> = { [P in T]?: D };
|
||||
|
||||
interface IdState<T> {
|
||||
[key: number]: Readonly<T>;
|
||||
}
|
||||
|
|
6
frontend/src/@types/window.d.ts
vendored
6
frontend/src/@types/window.d.ts
vendored
|
@ -1,6 +1,12 @@
|
|||
interface SocketIODebugger {
|
||||
dump: () => void;
|
||||
emit: (event: SocketIO.Event) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Bazarr: BazarrServer;
|
||||
_socketio: SocketIODebugger;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,7 @@ import {
|
|||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { FunctionComponent, useContext, useMemo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
|
@ -100,12 +94,6 @@ const Header: FunctionComponent<Props> = () => {
|
|||
[canLogout, setNeedAuth]
|
||||
);
|
||||
|
||||
const [reconnecting, setReconnect] = useState(false);
|
||||
const reconnect = useCallback(() => {
|
||||
setReconnect(true);
|
||||
SystemApi.status().finally(() => setReconnect(false));
|
||||
}, []);
|
||||
|
||||
const goHome = useGotoHomepage();
|
||||
|
||||
return (
|
||||
|
@ -137,13 +125,13 @@ const Header: FunctionComponent<Props> = () => {
|
|||
</Button>
|
||||
{offline ? (
|
||||
<ActionButton
|
||||
loading={reconnecting}
|
||||
loading
|
||||
alwaysShowText
|
||||
className="ml-2"
|
||||
variant="warning"
|
||||
icon={faNetworkWired}
|
||||
onClick={reconnect}
|
||||
>
|
||||
Reconnect
|
||||
Connecting...
|
||||
</ActionButton>
|
||||
) : (
|
||||
dropdown
|
||||
|
|
|
@ -6,8 +6,7 @@ import React, {
|
|||
} from "react";
|
||||
import { Row } from "react-bootstrap";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { bootstrap as ReduxBootstrap } from "../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../@redux/hooks/base";
|
||||
import { useReduxStore } from "../@redux/hooks/base";
|
||||
import { useNotification } from "../@redux/hooks/site";
|
||||
import { LoadingIndicator, ModalProvider } from "../components";
|
||||
import Sidebar from "../Sidebar";
|
||||
|
@ -24,8 +23,6 @@ export const SidebarToggleContext = React.createContext<() => void>(() => {});
|
|||
interface Props {}
|
||||
|
||||
const App: FunctionComponent<Props> = () => {
|
||||
const bootstrap = useReduxAction(ReduxBootstrap);
|
||||
|
||||
const { initialized, auth } = useReduxStore((s) => s.site);
|
||||
|
||||
const notify = useNotification("has-update", 10);
|
||||
|
@ -44,10 +41,6 @@ const App: FunctionComponent<Props> = () => {
|
|||
}
|
||||
}, [initialized, hasUpdate, notify]);
|
||||
|
||||
useEffect(() => {
|
||||
bootstrap();
|
||||
}, [bootstrap]);
|
||||
|
||||
const [sidebar, setSidebar] = useState(false);
|
||||
const toggleSidebar = useCallback(() => setSidebar(!sidebar), [sidebar]);
|
||||
|
||||
|
|
|
@ -3,22 +3,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||
import { capitalize } from "lodash";
|
||||
import React, { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { Toast } from "react-bootstrap";
|
||||
import { siteRemoveError } from "../../@redux/actions";
|
||||
import { siteRemoveNotification } from "../../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../../@redux/hooks/base";
|
||||
import "./style.scss";
|
||||
|
||||
function useNotificationList() {
|
||||
return useReduxStore((s) => s.site.notifications);
|
||||
}
|
||||
|
||||
function useRemoveNotification() {
|
||||
return useReduxAction(siteRemoveError);
|
||||
}
|
||||
|
||||
export interface NotificationContainerProps {}
|
||||
|
||||
const NotificationContainer: FunctionComponent<NotificationContainerProps> = () => {
|
||||
const list = useNotificationList();
|
||||
const list = useReduxStore((s) => s.site.notifications);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
|
@ -38,7 +30,7 @@ type MessageHolderProps = ReduxStore.Notification & {};
|
|||
|
||||
const NotificationToast: FunctionComponent<MessageHolderProps> = (props) => {
|
||||
const { message, id, type } = props;
|
||||
const removeNotification = useRemoveNotification();
|
||||
const removeNotification = useReduxAction(siteRemoveNotification);
|
||||
|
||||
const remove = useCallback(() => removeNotification(id), [
|
||||
removeNotification,
|
||||
|
|
|
@ -5,17 +5,15 @@ import { Helmet } from "react-helmet";
|
|||
import { useBlacklistMovies } from "../../@redux/hooks";
|
||||
import { MoviesApi } from "../../apis";
|
||||
import { AsyncStateOverlay, ContentHeader } from "../../components";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const BlacklistMoviesView: FunctionComponent<Props> = () => {
|
||||
const [blacklist, update] = useBlacklistMovies();
|
||||
useAutoUpdate(update);
|
||||
const [blacklist] = useBlacklistMovies();
|
||||
return (
|
||||
<AsyncStateOverlay state={blacklist}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Movies Blacklist - Bazarr</title>
|
||||
|
@ -25,13 +23,12 @@ const BlacklistMoviesView: FunctionComponent<Props> = () => {
|
|||
icon={faTrash}
|
||||
disabled={data.length === 0}
|
||||
promise={() => MoviesApi.deleteBlacklist(true)}
|
||||
onSuccess={update}
|
||||
>
|
||||
Remove All
|
||||
</ContentHeader.AsyncButton>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table blacklist={data} update={update}></Table>
|
||||
<Table blacklist={data}></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
|
|
|
@ -13,10 +13,9 @@ import {
|
|||
|
||||
interface Props {
|
||||
blacklist: readonly Blacklist.Movie[];
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
const Table: FunctionComponent<Props> = ({ blacklist, update }) => {
|
||||
const Table: FunctionComponent<Props> = ({ blacklist }) => {
|
||||
const columns = useMemo<Column<Blacklist.Movie>[]>(
|
||||
() => [
|
||||
{
|
||||
|
@ -78,7 +77,6 @@ const Table: FunctionComponent<Props> = ({ blacklist, update }) => {
|
|||
subs_id,
|
||||
})
|
||||
}
|
||||
onSuccess={update}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
|
||||
</AsyncButton>
|
||||
|
@ -86,7 +84,7 @@ const Table: FunctionComponent<Props> = ({ blacklist, update }) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
[update]
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<PageTable
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site";
|
||||
import {
|
||||
useIsRadarrEnabled,
|
||||
useIsSonarrEnabled,
|
||||
useSetSidebar,
|
||||
} from "../@redux/hooks/site";
|
||||
import { RouterEmptyPath } from "../special-pages/404";
|
||||
import BlacklistMovies from "./Movies";
|
||||
import BlacklistSeries from "./Series";
|
||||
|
@ -8,6 +12,8 @@ import BlacklistSeries from "./Series";
|
|||
const Router: FunctionComponent = () => {
|
||||
const sonarr = useIsSonarrEnabled();
|
||||
const radarr = useIsRadarrEnabled();
|
||||
|
||||
useSetSidebar("Blacklist");
|
||||
return (
|
||||
<Switch>
|
||||
{sonarr && (
|
||||
|
|
|
@ -5,17 +5,15 @@ import { Helmet } from "react-helmet";
|
|||
import { useBlacklistSeries } from "../../@redux/hooks";
|
||||
import { EpisodesApi } from "../../apis";
|
||||
import { AsyncStateOverlay, ContentHeader } from "../../components";
|
||||
import { useAutoUpdate } from "../../utilites";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const BlacklistSeriesView: FunctionComponent<Props> = () => {
|
||||
const [blacklist, update] = useBlacklistSeries();
|
||||
useAutoUpdate(update);
|
||||
const [blacklist] = useBlacklistSeries();
|
||||
return (
|
||||
<AsyncStateOverlay state={blacklist}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Series Blacklist - Bazarr</title>
|
||||
|
@ -25,13 +23,12 @@ const BlacklistSeriesView: FunctionComponent<Props> = () => {
|
|||
icon={faTrash}
|
||||
disabled={data.length === 0}
|
||||
promise={() => EpisodesApi.deleteBlacklist(true)}
|
||||
onSuccess={update}
|
||||
>
|
||||
Remove All
|
||||
</ContentHeader.AsyncButton>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table blacklist={data} update={update}></Table>
|
||||
<Table blacklist={data}></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
|
|
|
@ -13,10 +13,9 @@ import {
|
|||
|
||||
interface Props {
|
||||
blacklist: readonly Blacklist.Episode[];
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
const Table: FunctionComponent<Props> = ({ blacklist, update }) => {
|
||||
const Table: FunctionComponent<Props> = ({ blacklist }) => {
|
||||
const columns = useMemo<Column<Blacklist.Episode>[]>(
|
||||
() => [
|
||||
{
|
||||
|
@ -84,7 +83,6 @@ const Table: FunctionComponent<Props> = ({ blacklist, update }) => {
|
|||
subs_id,
|
||||
})
|
||||
}
|
||||
onSuccess={update}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
|
||||
</AsyncButton>
|
||||
|
@ -92,7 +90,7 @@ const Table: FunctionComponent<Props> = ({ blacklist, update }) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
[update]
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<PageTable
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
import { faInfoCircle, faRecycle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Badge, OverlayTrigger, Popover } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Column, Row } from "react-table";
|
||||
import { Column } from "react-table";
|
||||
import { useMoviesHistory } from "../../@redux/hooks";
|
||||
import { MoviesApi } from "../../apis";
|
||||
import { HistoryIcon, LanguageText, TextPopover } from "../../components";
|
||||
import { BlacklistButton } from "../../generic/blacklist";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
import HistoryGenericView from "../generic";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const MoviesHistoryView: FunctionComponent<Props> = () => {
|
||||
const [movies, update] = useMoviesHistory();
|
||||
useAutoUpdate(update);
|
||||
|
||||
const tableUpdate = useCallback((row: Row<History.Base>) => update(), [
|
||||
update,
|
||||
]);
|
||||
const [movies] = useMoviesHistory();
|
||||
|
||||
const columns: Column<History.Movie>[] = useMemo<Column<History.Movie>[]>(
|
||||
() => [
|
||||
|
@ -114,12 +108,11 @@ const MoviesHistoryView: FunctionComponent<Props> = () => {
|
|||
},
|
||||
{
|
||||
accessor: "blacklisted",
|
||||
Cell: ({ row, externalUpdate }) => {
|
||||
Cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return (
|
||||
<BlacklistButton
|
||||
history={original}
|
||||
update={() => externalUpdate && externalUpdate(row)}
|
||||
promise={(form) =>
|
||||
MoviesApi.addBlacklist(original.radarrId, form)
|
||||
}
|
||||
|
@ -136,7 +129,6 @@ const MoviesHistoryView: FunctionComponent<Props> = () => {
|
|||
type="movies"
|
||||
state={movies}
|
||||
columns={columns as Column<History.Base>[]}
|
||||
tableUpdater={tableUpdate}
|
||||
></HistoryGenericView>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site";
|
||||
import {
|
||||
useIsRadarrEnabled,
|
||||
useIsSonarrEnabled,
|
||||
useSetSidebar,
|
||||
} from "../@redux/hooks/site";
|
||||
import { RouterEmptyPath } from "../special-pages/404";
|
||||
import MoviesHistory from "./Movies";
|
||||
import SeriesHistory from "./Series";
|
||||
|
@ -9,6 +13,8 @@ import HistoryStats from "./Statistics";
|
|||
const Router: FunctionComponent = () => {
|
||||
const sonarr = useIsSonarrEnabled();
|
||||
const radarr = useIsRadarrEnabled();
|
||||
|
||||
useSetSidebar("History");
|
||||
return (
|
||||
<Switch>
|
||||
{sonarr && (
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
import { faInfoCircle, faRecycle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Badge, OverlayTrigger, Popover } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Column, Row } from "react-table";
|
||||
import { Column } from "react-table";
|
||||
import { useSeriesHistory } from "../../@redux/hooks";
|
||||
import { EpisodesApi } from "../../apis";
|
||||
import { HistoryIcon, LanguageText, TextPopover } from "../../components";
|
||||
import { BlacklistButton } from "../../generic/blacklist";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
import HistoryGenericView from "../generic";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SeriesHistoryView: FunctionComponent<Props> = () => {
|
||||
const [series, update] = useSeriesHistory();
|
||||
useAutoUpdate(update);
|
||||
|
||||
const tableUpdate = useCallback((row: Row<History.Base>) => update(), [
|
||||
update,
|
||||
]);
|
||||
const [series] = useSeriesHistory();
|
||||
|
||||
const columns: Column<History.Episode>[] = useMemo<Column<History.Episode>[]>(
|
||||
() => [
|
||||
|
@ -121,14 +115,13 @@ const SeriesHistoryView: FunctionComponent<Props> = () => {
|
|||
},
|
||||
{
|
||||
accessor: "blacklisted",
|
||||
Cell: ({ row, externalUpdate }) => {
|
||||
Cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
|
||||
const { sonarrEpisodeId, sonarrSeriesId } = original;
|
||||
return (
|
||||
<BlacklistButton
|
||||
history={original}
|
||||
update={() => externalUpdate && externalUpdate(row)}
|
||||
promise={(form) =>
|
||||
EpisodesApi.addBlacklist(sonarrSeriesId, sonarrEpisodeId, form)
|
||||
}
|
||||
|
@ -145,7 +138,6 @@ const SeriesHistoryView: FunctionComponent<Props> = () => {
|
|||
type="series"
|
||||
state={series}
|
||||
columns={columns as Column<History.Base>[]}
|
||||
tableUpdater={tableUpdate}
|
||||
></HistoryGenericView>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { useLanguages, useProviders } from "../../@redux/hooks";
|
||||
import { useLanguages, useSystemProviders } from "../../@redux/hooks";
|
||||
import { HistoryApi } from "../../apis";
|
||||
import {
|
||||
AsyncSelector,
|
||||
|
@ -21,7 +21,6 @@ import {
|
|||
PromiseOverlay,
|
||||
Selector,
|
||||
} from "../../components";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
import { actionOptions, timeframeOptions } from "./options";
|
||||
|
||||
function converter(item: History.Stat) {
|
||||
|
@ -48,8 +47,7 @@ const SelectorContainer: FunctionComponent = ({ children }) => (
|
|||
const HistoryStats: FunctionComponent = () => {
|
||||
const [languages] = useLanguages(true);
|
||||
|
||||
const [providerList, update] = useProviders();
|
||||
useAutoUpdate(update);
|
||||
const [providerList] = useSystemProviders();
|
||||
|
||||
const [timeframe, setTimeframe] = useState<History.TimeframeOptions>("month");
|
||||
const [action, setAction] = useState<Nullable<History.ActionOptions>>(null);
|
||||
|
|
|
@ -2,21 +2,19 @@ import { capitalize } from "lodash";
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Column, TableUpdater } from "react-table";
|
||||
import { Column } from "react-table";
|
||||
import { AsyncStateOverlay, PageTable } from "../../components";
|
||||
|
||||
interface Props {
|
||||
type: "movies" | "series";
|
||||
state: Readonly<AsyncState<History.Base[]>>;
|
||||
columns: Column<History.Base>[];
|
||||
tableUpdater?: TableUpdater<History.Base>;
|
||||
}
|
||||
|
||||
const HistoryGenericView: FunctionComponent<Props> = ({
|
||||
state,
|
||||
columns,
|
||||
type,
|
||||
tableUpdater,
|
||||
}) => {
|
||||
const typeName = capitalize(type);
|
||||
return (
|
||||
|
@ -26,12 +24,11 @@ const HistoryGenericView: FunctionComponent<Props> = ({
|
|||
</Helmet>
|
||||
<Row>
|
||||
<AsyncStateOverlay state={state}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<PageTable
|
||||
emptyText={`Nothing Found in ${typeName} History`}
|
||||
columns={columns}
|
||||
data={data}
|
||||
externalUpdate={tableUpdater}
|
||||
></PageTable>
|
||||
)}
|
||||
</AsyncStateOverlay>
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
import { ManualSearchModal } from "../../components/modals/ManualSearchModal";
|
||||
import ItemOverview from "../../generic/ItemOverview";
|
||||
import { RouterEmptyPath } from "../../special-pages/404";
|
||||
import { useAutoUpdate, useWhenLoadingFinish } from "../../utilites";
|
||||
import { useWhenLoadingFinish } from "../../utilites";
|
||||
import Table from "./table";
|
||||
|
||||
const download = (item: any, result: SearchResultType) => {
|
||||
|
@ -48,8 +48,7 @@ interface Props extends RouteComponentProps<Params> {}
|
|||
|
||||
const MovieDetailView: FunctionComponent<Props> = ({ match }) => {
|
||||
const id = Number.parseInt(match.params.id);
|
||||
const [movie, update] = useMovieBy(id);
|
||||
useAutoUpdate(update);
|
||||
const [movie] = useMovieBy(id);
|
||||
const item = movie.data;
|
||||
|
||||
const showModal = useShowModal();
|
||||
|
@ -86,7 +85,6 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => {
|
|||
promise={() =>
|
||||
MoviesApi.action({ action: "scan-disk", radarrid: item.radarrId })
|
||||
}
|
||||
onSuccess={update}
|
||||
>
|
||||
Scan Disk
|
||||
</ContentHeader.AsyncButton>
|
||||
|
@ -99,7 +97,6 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => {
|
|||
radarrid: item.radarrId,
|
||||
})
|
||||
}
|
||||
onSuccess={update}
|
||||
>
|
||||
Search
|
||||
</ContentHeader.AsyncButton>
|
||||
|
@ -144,23 +141,17 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => {
|
|||
<ItemOverview item={item} details={[]}></ItemOverview>
|
||||
</Row>
|
||||
<Row>
|
||||
<Table movie={item} update={update}></Table>
|
||||
<Table movie={item}></Table>
|
||||
</Row>
|
||||
<ItemEditorModal
|
||||
modalKey="edit"
|
||||
submit={(form) => MoviesApi.modify(form)}
|
||||
onSuccess={update}
|
||||
></ItemEditorModal>
|
||||
<SubtitleToolModal
|
||||
modalKey="tools"
|
||||
size="lg"
|
||||
update={update}
|
||||
></SubtitleToolModal>
|
||||
<SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal>
|
||||
<MovieHistoryModal modalKey="history" size="lg"></MovieHistoryModal>
|
||||
<MovieUploadModal modalKey="upload" size="lg"></MovieUploadModal>
|
||||
<ManualSearchModal
|
||||
modalKey="manual-search"
|
||||
onDownload={update}
|
||||
onSelect={download}
|
||||
></ManualSearchModal>
|
||||
</Container>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { faSearch, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { intersectionWith } from "lodash";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Badge } from "react-bootstrap";
|
||||
import { Column } from "react-table";
|
||||
import { useProfileItems } from "../../@redux/hooks";
|
||||
import { useShowOnlyDesired } from "../../@redux/hooks/site";
|
||||
import { MoviesApi } from "../../apis";
|
||||
import { AsyncButton, LanguageText, SimpleTable } from "../../components";
|
||||
|
||||
|
@ -10,11 +13,13 @@ const missingText = "Missing Subtitles";
|
|||
|
||||
interface Props {
|
||||
movie: Item.Movie;
|
||||
update: (id: number) => void;
|
||||
profile?: Profile.Languages;
|
||||
}
|
||||
|
||||
const Table: FunctionComponent<Props> = (props) => {
|
||||
const { movie, update } = props;
|
||||
const Table: FunctionComponent<Props> = ({ movie, profile }) => {
|
||||
const onlyDesired = useShowOnlyDesired();
|
||||
|
||||
const profileItems = useProfileItems(profile);
|
||||
|
||||
const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>(
|
||||
() => [
|
||||
|
@ -66,7 +71,6 @@ const Table: FunctionComponent<Props> = (props) => {
|
|||
forced: original.forced,
|
||||
})
|
||||
}
|
||||
onSuccess={() => update(movie.radarrId)}
|
||||
variant="light"
|
||||
size="sm"
|
||||
>
|
||||
|
@ -86,7 +90,6 @@ const Table: FunctionComponent<Props> = (props) => {
|
|||
path: original.path ?? "",
|
||||
})
|
||||
}
|
||||
onSuccess={() => update(movie.radarrId)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
|
||||
</AsyncButton>
|
||||
|
@ -95,7 +98,7 @@ const Table: FunctionComponent<Props> = (props) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
[movie, update]
|
||||
[movie]
|
||||
);
|
||||
|
||||
const data: Subtitle[] = useMemo(() => {
|
||||
|
@ -104,8 +107,17 @@ const Table: FunctionComponent<Props> = (props) => {
|
|||
return item;
|
||||
});
|
||||
|
||||
return movie.subtitles.concat(missing);
|
||||
}, [movie.missing_subtitles, movie.subtitles]);
|
||||
let raw_subtitles = movie.subtitles;
|
||||
if (onlyDesired) {
|
||||
raw_subtitles = intersectionWith(
|
||||
raw_subtitles,
|
||||
profileItems,
|
||||
(l, r) => l.code2 === r.code2
|
||||
);
|
||||
}
|
||||
|
||||
return [...raw_subtitles, ...missing];
|
||||
}, [movie.missing_subtitles, movie.subtitles, onlyDesired, profileItems]);
|
||||
|
||||
return (
|
||||
<SimpleTable
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faBookmark,
|
||||
faCheck,
|
||||
faExclamationTriangle,
|
||||
faWrench,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Badge } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Column } from "react-table";
|
||||
import { movieUpdateByRange, movieUpdateInfoAll } from "../@redux/actions";
|
||||
import { movieUpdateByRange, movieUpdateList } from "../@redux/actions";
|
||||
import { useRawMovies } from "../@redux/hooks";
|
||||
import { useReduxAction } from "../@redux/hooks/base";
|
||||
import { MoviesApi } from "../apis";
|
||||
|
@ -54,21 +49,6 @@ const MovieView: FunctionComponent<Props> = () => {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Exist",
|
||||
accessor: "exist",
|
||||
selectHide: true,
|
||||
Cell: ({ row, value }) => {
|
||||
const exist = value;
|
||||
const { path } = row.original;
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
title={path}
|
||||
icon={exist ? faCheck : faExclamationTriangle}
|
||||
></FontAwesomeIcon>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Audio",
|
||||
accessor: "audio_language",
|
||||
|
@ -133,8 +113,8 @@ const MovieView: FunctionComponent<Props> = () => {
|
|||
state={movies}
|
||||
name="Movies"
|
||||
loader={load}
|
||||
updateAction={movieUpdateInfoAll}
|
||||
columns={columns as Column<Item.Base>[]}
|
||||
updateAction={movieUpdateList}
|
||||
columns={columns}
|
||||
modify={(form) => MoviesApi.modify(form)}
|
||||
></BaseItemView>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,6 @@ import { faSearch, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Badge } from "react-bootstrap";
|
||||
import { useSerieBy } from "../../@redux/hooks";
|
||||
import { EpisodesApi } from "../../apis";
|
||||
import { AsyncButton, LanguageText } from "../../components";
|
||||
|
||||
|
@ -21,8 +20,6 @@ export const SubtitleAction: FunctionComponent<Props> = ({
|
|||
}) => {
|
||||
const { hi, forced } = subtitle;
|
||||
|
||||
const [, update] = useSerieBy(seriesid);
|
||||
|
||||
const path = subtitle.path;
|
||||
|
||||
if (missing || path) {
|
||||
|
@ -46,7 +43,6 @@ export const SubtitleAction: FunctionComponent<Props> = ({
|
|||
return null;
|
||||
}
|
||||
}}
|
||||
onSuccess={update}
|
||||
as={Badge}
|
||||
className="mr-1"
|
||||
variant={missing ? "primary" : "secondary"}
|
||||
|
|
|
@ -16,7 +16,7 @@ import React, {
|
|||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Redirect, RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { useEpisodesBy, useSerieBy } from "../../@redux/hooks";
|
||||
import { useEpisodesBy, useProfileBy, useSerieBy } from "../../@redux/hooks";
|
||||
import { SeriesApi } from "../../apis";
|
||||
import {
|
||||
ContentHeader,
|
||||
|
@ -27,7 +27,7 @@ import {
|
|||
} from "../../components";
|
||||
import ItemOverview from "../../generic/ItemOverview";
|
||||
import { RouterEmptyPath } from "../../special-pages/404";
|
||||
import { useAutoUpdate, useWhenLoadingFinish } from "../../utilites";
|
||||
import { useWhenLoadingFinish } from "../../utilites";
|
||||
import Table from "./table";
|
||||
|
||||
interface Params {
|
||||
|
@ -39,13 +39,11 @@ interface Props extends RouteComponentProps<Params> {}
|
|||
const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
|
||||
const { match } = props;
|
||||
const id = Number.parseInt(match.params.id);
|
||||
const [serie, update] = useSerieBy(id);
|
||||
const [serie] = useSerieBy(id);
|
||||
const item = serie.data;
|
||||
|
||||
const [episodes] = useEpisodesBy(serie.data?.sonarrSeriesId);
|
||||
|
||||
useAutoUpdate(update);
|
||||
|
||||
const available = episodes.data.length !== 0;
|
||||
|
||||
const details = useMemo(
|
||||
|
@ -74,6 +72,8 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
|
|||
|
||||
useWhenLoadingFinish(serie, validator);
|
||||
|
||||
const profile = useProfileBy(serie.data?.profileId);
|
||||
|
||||
if (isNaN(id) || !valid) {
|
||||
return <Redirect to={RouterEmptyPath}></Redirect>;
|
||||
}
|
||||
|
@ -95,7 +95,6 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
|
|||
promise={() =>
|
||||
SeriesApi.action({ action: "scan-disk", seriesid: id })
|
||||
}
|
||||
onSuccess={update}
|
||||
>
|
||||
Scan Disk
|
||||
</ContentHeader.AsyncButton>
|
||||
|
@ -104,7 +103,6 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
|
|||
promise={() =>
|
||||
SeriesApi.action({ action: "search-missing", seriesid: id })
|
||||
}
|
||||
onSuccess={update}
|
||||
disabled={
|
||||
item.episodeFileCount === 0 ||
|
||||
item.profileId === null ||
|
||||
|
@ -145,14 +143,16 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
|
|||
<ItemOverview item={item} details={details}></ItemOverview>
|
||||
</Row>
|
||||
<Row>
|
||||
<Table episodes={episodes} update={update}></Table>
|
||||
<Table episodes={episodes} profile={profile}></Table>
|
||||
</Row>
|
||||
<ItemEditorModal
|
||||
modalKey="edit"
|
||||
submit={(form) => SeriesApi.modify(form)}
|
||||
onSuccess={update}
|
||||
></ItemEditorModal>
|
||||
<SeriesUploadModal modalKey="upload"></SeriesUploadModal>
|
||||
<SeriesUploadModal
|
||||
modalKey="upload"
|
||||
episodes={episodes.data}
|
||||
></SeriesUploadModal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,10 +6,12 @@ import {
|
|||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { intersectionWith } from "lodash";
|
||||
import React, { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { Badge, ButtonGroup } from "react-bootstrap";
|
||||
import { Column, TableOptions, TableUpdater } from "react-table";
|
||||
import { useSerieBy } from "../../@redux/hooks";
|
||||
import { Column, TableUpdater } from "react-table";
|
||||
import { useProfileItems, useSerieBy } from "../../@redux/hooks";
|
||||
import { useShowOnlyDesired } from "../../@redux/hooks/site";
|
||||
import { ProvidersApi } from "../../apis";
|
||||
import {
|
||||
ActionButton,
|
||||
|
@ -26,7 +28,7 @@ import { SubtitleAction } from "./components";
|
|||
|
||||
interface Props {
|
||||
episodes: AsyncState<Item.Episode[]>;
|
||||
update: () => void;
|
||||
profile?: Profile.Languages;
|
||||
}
|
||||
|
||||
const download = (item: any, result: SearchResultType) => {
|
||||
|
@ -45,9 +47,13 @@ const download = (item: any, result: SearchResultType) => {
|
|||
);
|
||||
};
|
||||
|
||||
const Table: FunctionComponent<Props> = ({ episodes, update }) => {
|
||||
const Table: FunctionComponent<Props> = ({ episodes, profile }) => {
|
||||
const showModal = useShowModal();
|
||||
|
||||
const onlyDesired = useShowOnlyDesired();
|
||||
|
||||
const profileItems = useProfileItems(profile);
|
||||
|
||||
const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>(
|
||||
() => [
|
||||
{
|
||||
|
@ -113,7 +119,16 @@ const Table: FunctionComponent<Props> = ({ episodes, update }) => {
|
|||
></SubtitleAction>
|
||||
));
|
||||
|
||||
const subtitles = episode.subtitles.map((val, idx) => (
|
||||
let raw_subtitles = episode.subtitles;
|
||||
if (onlyDesired) {
|
||||
raw_subtitles = intersectionWith(
|
||||
raw_subtitles,
|
||||
profileItems,
|
||||
(l, r) => l.code2 === r.code2
|
||||
);
|
||||
}
|
||||
|
||||
const subtitles = raw_subtitles.map((val, idx) => (
|
||||
<SubtitleAction
|
||||
key={BuildKey(idx, val.code2, "valid")}
|
||||
seriesid={seriesid}
|
||||
|
@ -160,7 +175,7 @@ const Table: FunctionComponent<Props> = ({ episodes, update }) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
[onlyDesired, profileItems]
|
||||
);
|
||||
|
||||
const updateRow = useCallback<TableUpdater<Item.Episode>>(
|
||||
|
@ -183,43 +198,32 @@ const Table: FunctionComponent<Props> = ({ episodes, update }) => {
|
|||
[episodes]
|
||||
);
|
||||
|
||||
const options: TableOptions<Item.Episode> = useMemo(() => {
|
||||
return {
|
||||
columns,
|
||||
data: episodes.data,
|
||||
externalUpdate: updateRow,
|
||||
initialState: {
|
||||
sortBy: [
|
||||
{ id: "season", desc: true },
|
||||
{ id: "episode", desc: true },
|
||||
],
|
||||
groupBy: ["season"],
|
||||
expanded: {
|
||||
[`season:${maxSeason}`]: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [episodes, columns, maxSeason, updateRow]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AsyncStateOverlay state={episodes}>
|
||||
{() => (
|
||||
{({ data }) => (
|
||||
<GroupTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
externalUpdate={updateRow}
|
||||
initialState={{
|
||||
sortBy: [
|
||||
{ id: "season", desc: true },
|
||||
{ id: "episode", desc: true },
|
||||
],
|
||||
groupBy: ["season"],
|
||||
expanded: {
|
||||
[`season:${maxSeason}`]: true,
|
||||
},
|
||||
}}
|
||||
emptyText="No Episode Found For This Series"
|
||||
{...options}
|
||||
></GroupTable>
|
||||
)}
|
||||
</AsyncStateOverlay>
|
||||
<SubtitleToolModal
|
||||
modalKey="tools"
|
||||
size="lg"
|
||||
update={update}
|
||||
></SubtitleToolModal>
|
||||
<SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal>
|
||||
<EpisodeHistoryModal modalKey="history" size="lg"></EpisodeHistoryModal>
|
||||
<ManualSearchModal
|
||||
modalKey="manual-search"
|
||||
onDownload={update}
|
||||
onSelect={download}
|
||||
></ManualSearchModal>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import {
|
||||
faCheck,
|
||||
faExclamationTriangle,
|
||||
faWrench,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Badge, ProgressBar } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Column } from "react-table";
|
||||
import { seriesUpdateByRange, seriesUpdateInfoAll } from "../@redux/actions";
|
||||
import { seriesUpdateByRange, seriesUpdateList } from "../@redux/actions";
|
||||
import { useRawSeries } from "../@redux/hooks";
|
||||
import { useReduxAction } from "../@redux/hooks/base";
|
||||
import { SeriesApi } from "../apis";
|
||||
|
@ -40,21 +35,6 @@ const SeriesView: FunctionComponent<Props> = () => {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Exist",
|
||||
accessor: "exist",
|
||||
selectHide: true,
|
||||
Cell: (row) => {
|
||||
const exist = row.value;
|
||||
const { path } = row.row.original;
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
title={path}
|
||||
icon={exist ? faCheck : faExclamationTriangle}
|
||||
></FontAwesomeIcon>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Audio",
|
||||
accessor: "audio_language",
|
||||
|
@ -138,9 +118,9 @@ const SeriesView: FunctionComponent<Props> = () => {
|
|||
<BaseItemView
|
||||
state={series}
|
||||
name="Series"
|
||||
updateAction={seriesUpdateInfoAll}
|
||||
updateAction={seriesUpdateList}
|
||||
loader={load}
|
||||
columns={columns as Column<Item.Base>[]}
|
||||
columns={columns}
|
||||
modify={(form) => SeriesApi.modify(form)}
|
||||
></BaseItemView>
|
||||
);
|
||||
|
|
|
@ -17,18 +17,13 @@ import {
|
|||
useShowModal,
|
||||
} from "../../components";
|
||||
import { BuildKey } from "../../utilites";
|
||||
import { ColCard, useLatestMergeArray, useUpdateArray } from "../components";
|
||||
import { ColCard, useLatestArray, useUpdateArray } from "../components";
|
||||
import { notificationsKey } from "../keys";
|
||||
|
||||
interface ModalProps {
|
||||
selections: readonly Settings.NotificationInfo[];
|
||||
}
|
||||
|
||||
const notificationComparer = (
|
||||
one: Settings.NotificationInfo,
|
||||
another: Settings.NotificationInfo
|
||||
) => one.name === another.name;
|
||||
|
||||
const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
|
||||
selections,
|
||||
...modal
|
||||
|
@ -46,7 +41,7 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
|
|||
|
||||
const update = useUpdateArray<Settings.NotificationInfo>(
|
||||
notificationsKey,
|
||||
notificationComparer
|
||||
"name"
|
||||
);
|
||||
|
||||
const payload = usePayload<Settings.NotificationInfo>(modal.modalKey);
|
||||
|
@ -158,10 +153,10 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
|
|||
};
|
||||
|
||||
export const NotificationView: FunctionComponent = () => {
|
||||
const notifications = useLatestMergeArray<Settings.NotificationInfo>(
|
||||
const notifications = useLatestArray<Settings.NotificationInfo>(
|
||||
notificationsKey,
|
||||
notificationComparer,
|
||||
(settings) => settings.notifications.providers
|
||||
"name",
|
||||
(s) => s.notifications.providers
|
||||
);
|
||||
|
||||
const showModal = useShowModal();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useEffect } from "react";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { systemUpdateSettings } from "../@redux/actions";
|
||||
import { useReduxAction } from "../@redux/hooks/base";
|
||||
import { useSetSidebar } from "../@redux/hooks/site";
|
||||
import { RouterEmptyPath } from "../special-pages/404";
|
||||
import { useAutoUpdate } from "../utilites/hooks";
|
||||
import General from "./General";
|
||||
import Languages from "./Languages";
|
||||
import Notifications from "./Notifications";
|
||||
|
@ -18,8 +18,9 @@ interface Props {}
|
|||
|
||||
const Router: FunctionComponent<Props> = () => {
|
||||
const update = useReduxAction(systemUpdateSettings);
|
||||
useAutoUpdate(update);
|
||||
useEffect(() => update, [update]);
|
||||
|
||||
useSetSidebar("Settings");
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/settings">
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { isArray, isEqual } from "lodash";
|
||||
import { isArray, uniqBy } from "lodash";
|
||||
import { useCallback, useContext, useMemo } from "react";
|
||||
import { useStore } from "react-redux";
|
||||
import { useSystemSettings } from "../../@redux/hooks";
|
||||
import { mergeArray } from "../../utilites";
|
||||
import { log } from "../../utilites/logger";
|
||||
import { StagedChangesContext } from "./provider";
|
||||
|
||||
|
@ -96,40 +95,6 @@ export function useExtract<T>(
|
|||
}
|
||||
}
|
||||
|
||||
export function useUpdateArray<T>(
|
||||
key: string,
|
||||
compare?: (one: T, another: T) => boolean
|
||||
) {
|
||||
const update = useSingleUpdate();
|
||||
const stagedValue = useStagedValues();
|
||||
|
||||
if (compare === undefined) {
|
||||
compare = isEqual;
|
||||
}
|
||||
|
||||
const staged: T[] = useMemo(() => {
|
||||
if (key in stagedValue) {
|
||||
return stagedValue[key];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [key, stagedValue]);
|
||||
|
||||
return useCallback(
|
||||
(v: T) => {
|
||||
const newArray = [...staged];
|
||||
const idx = newArray.findIndex((inn) => compare!(inn, v));
|
||||
if (idx !== -1) {
|
||||
newArray[idx] = v;
|
||||
} else {
|
||||
newArray.push(v);
|
||||
}
|
||||
update(newArray, key);
|
||||
},
|
||||
[compare, staged, key, update]
|
||||
);
|
||||
}
|
||||
|
||||
export function useLatest<T>(
|
||||
key: string,
|
||||
validate: ValidateFuncType<T>,
|
||||
|
@ -144,19 +109,14 @@ export function useLatest<T>(
|
|||
}
|
||||
}
|
||||
|
||||
// Merge Two Array
|
||||
export function useLatestMergeArray<T>(
|
||||
export function useLatestArray<T>(
|
||||
key: string,
|
||||
compare: Comparer<T>,
|
||||
compare: keyof T,
|
||||
override?: OverrideFuncType<T[]>
|
||||
): Readonly<Nullable<T[]>> {
|
||||
const extractValue = useExtract<T[]>(key, isArray, override);
|
||||
const stagedValue = useStagedValues();
|
||||
|
||||
if (compare === undefined) {
|
||||
compare = isEqual;
|
||||
}
|
||||
|
||||
let staged: T[] | undefined = undefined;
|
||||
if (key in stagedValue) {
|
||||
staged = stagedValue[key];
|
||||
|
@ -164,9 +124,30 @@ export function useLatestMergeArray<T>(
|
|||
|
||||
return useMemo(() => {
|
||||
if (staged !== undefined && extractValue) {
|
||||
return mergeArray(extractValue, staged, compare);
|
||||
return uniqBy([...staged, ...extractValue], compare);
|
||||
} else {
|
||||
return extractValue;
|
||||
}
|
||||
}, [extractValue, staged, compare]);
|
||||
}
|
||||
|
||||
export function useUpdateArray<T>(key: string, compare: keyof T) {
|
||||
const update = useSingleUpdate();
|
||||
const stagedValue = useStagedValues();
|
||||
|
||||
const staged: T[] = useMemo(() => {
|
||||
if (key in stagedValue) {
|
||||
return stagedValue[key];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [key, stagedValue]);
|
||||
|
||||
return useCallback(
|
||||
(v: T) => {
|
||||
const newArray = uniqBy([v, ...staged], compare);
|
||||
update(newArray, key);
|
||||
},
|
||||
[compare, staged, key, update]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,13 +10,12 @@ import React, {
|
|||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Prompt } from "react-router";
|
||||
import {
|
||||
siteSaveLocalstorage,
|
||||
systemUpdateSettingsAll,
|
||||
} from "../../@redux/actions";
|
||||
import { useReduxAction, useReduxActionWith } from "../../@redux/hooks/base";
|
||||
import { siteSaveLocalstorage } from "../../@redux/actions";
|
||||
import { useSystemSettings } from "../../@redux/hooks";
|
||||
import { useReduxAction } from "../../@redux/hooks/base";
|
||||
import { SystemApi } from "../../apis";
|
||||
import { ContentHeader } from "../../components";
|
||||
import { useWhenLoadingFinish } from "../../utilites";
|
||||
import { log } from "../../utilites/logger";
|
||||
import {
|
||||
enabledLanguageKey,
|
||||
|
@ -66,17 +65,15 @@ const SettingsProvider: FunctionComponent<Props> = (props) => {
|
|||
setUpdating(false);
|
||||
}, []);
|
||||
|
||||
const update = useReduxActionWith(systemUpdateSettingsAll, cleanup);
|
||||
const [settings] = useSystemSettings();
|
||||
useWhenLoadingFinish(settings, cleanup);
|
||||
|
||||
const saveSettings = useCallback(
|
||||
(settings: LooseObject) => {
|
||||
submitHooks(settings);
|
||||
setUpdating(true);
|
||||
log("info", "submitting settings", settings);
|
||||
SystemApi.setSettings(settings).finally(update);
|
||||
},
|
||||
[update]
|
||||
);
|
||||
const saveSettings = useCallback((settings: LooseObject) => {
|
||||
submitHooks(settings);
|
||||
setUpdating(true);
|
||||
log("info", "submitting settings", settings);
|
||||
SystemApi.setSettings(settings);
|
||||
}, []);
|
||||
|
||||
const saveLocalStorage = useCallback(
|
||||
(settings: LooseObject) => {
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
import React, {
|
||||
FunctionComponent,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import React, { FunctionComponent, useContext, useMemo } from "react";
|
||||
import { Container, Image, ListGroup } from "react-bootstrap";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { badgeUpdateAll, siteChangeSidebar } from "../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../@redux/hooks/base";
|
||||
import { useReduxStore } from "../@redux/hooks/base";
|
||||
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site";
|
||||
import logo from "../@static/logo64.png";
|
||||
import { SidebarToggleContext } from "../App";
|
||||
import { useAutoUpdate, useGotoHomepage } from "../utilites/hooks";
|
||||
import { useGotoHomepage } from "../utilites/hooks";
|
||||
import {
|
||||
BadgesContext,
|
||||
CollapseItem,
|
||||
|
@ -22,25 +15,16 @@ import { RadarrDisabledKey, SidebarList, SonarrDisabledKey } from "./list";
|
|||
import "./style.scss";
|
||||
import { BadgeProvider } from "./types";
|
||||
|
||||
export function useSidebarKey() {
|
||||
return useReduxStore((s) => s.site.sidebar);
|
||||
}
|
||||
|
||||
export function useUpdateSidebar() {
|
||||
return useReduxAction(siteChangeSidebar);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const Sidebar: FunctionComponent<Props> = ({ open }) => {
|
||||
const updateBadges = useReduxAction(badgeUpdateAll);
|
||||
useAutoUpdate(updateBadges);
|
||||
|
||||
const toggle = useContext(SidebarToggleContext);
|
||||
|
||||
const { movies, episodes, providers } = useReduxStore((s) => s.site.badges);
|
||||
const { movies, episodes, providers, status } = useReduxStore(
|
||||
(s) => s.site.badges
|
||||
);
|
||||
|
||||
const sonarrEnabled = useIsSonarrEnabled();
|
||||
const radarrEnabled = useIsRadarrEnabled();
|
||||
|
@ -53,9 +37,10 @@ const Sidebar: FunctionComponent<Props> = ({ open }) => {
|
|||
},
|
||||
System: {
|
||||
Providers: providers,
|
||||
Status: status,
|
||||
},
|
||||
}),
|
||||
[movies, episodes, providers, sonarrEnabled, radarrEnabled]
|
||||
[movies, episodes, providers, sonarrEnabled, radarrEnabled, status]
|
||||
);
|
||||
|
||||
const hiddenKeys = useMemo<string[]>(() => {
|
||||
|
@ -69,20 +54,6 @@ const Sidebar: FunctionComponent<Props> = ({ open }) => {
|
|||
return list;
|
||||
}, [sonarrEnabled, radarrEnabled]);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const updateSidebar = useUpdateSidebar();
|
||||
|
||||
useEffect(() => {
|
||||
const path = history.location.pathname.split("/");
|
||||
const len = path.length;
|
||||
if (len >= 3) {
|
||||
updateSidebar(path[len - 2]);
|
||||
} else {
|
||||
updateSidebar(path[len - 1]);
|
||||
}
|
||||
}, [history.location.pathname, updateSidebar]);
|
||||
|
||||
const cls = ["sidebar-container"];
|
||||
const overlay = ["sidebar-overlay"];
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||
import React, { FunctionComponent, useContext, useMemo } from "react";
|
||||
import { Badge, Collapse, ListGroupItem } from "react-bootstrap";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { useSidebarKey, useUpdateSidebar } from ".";
|
||||
import { siteChangeSidebar } from "../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../@redux/hooks/base";
|
||||
import { SidebarToggleContext } from "../App";
|
||||
import {
|
||||
BadgeProvider,
|
||||
|
@ -16,6 +17,14 @@ export const HiddenKeysContext = React.createContext<string[]>([]);
|
|||
|
||||
export const BadgesContext = React.createContext<BadgeProvider>({});
|
||||
|
||||
function useToggleSidebar() {
|
||||
return useReduxAction(siteChangeSidebar);
|
||||
}
|
||||
|
||||
function useSidebarKey() {
|
||||
return useReduxStore((s) => s.site.sidebar);
|
||||
}
|
||||
|
||||
export const LinkItem: FunctionComponent<LinkItemType> = ({
|
||||
link,
|
||||
name,
|
||||
|
@ -60,10 +69,8 @@ export const CollapseItem: FunctionComponent<CollapseItemType> = ({
|
|||
const hiddenKeys = useContext(HiddenKeysContext);
|
||||
const toggleSidebar = useContext(SidebarToggleContext);
|
||||
|
||||
const itemKey = name.toLowerCase();
|
||||
|
||||
const sidebarKey = useSidebarKey();
|
||||
const updateSidebar = useUpdateSidebar();
|
||||
const updateSidebar = useToggleSidebar();
|
||||
|
||||
const [badgeValue, childValue] = useMemo<
|
||||
[Nullable<number>, Nullable<ChildBadgeProvider>]
|
||||
|
@ -86,7 +93,7 @@ export const CollapseItem: FunctionComponent<CollapseItemType> = ({
|
|||
return [badge, child];
|
||||
}, [badges, name]);
|
||||
|
||||
const active = useMemo(() => sidebarKey === itemKey, [sidebarKey, itemKey]);
|
||||
const active = useMemo(() => sidebarKey === name, [sidebarKey, name]);
|
||||
|
||||
const collapseBoxClass = useMemo(
|
||||
() => `sidebar-collapse-box ${active ? "active" : ""}`,
|
||||
|
@ -133,7 +140,7 @@ export const CollapseItem: FunctionComponent<CollapseItemType> = ({
|
|||
if (active) {
|
||||
updateSidebar("");
|
||||
} else {
|
||||
updateSidebar(itemKey);
|
||||
updateSidebar(name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -2,20 +2,16 @@ import { faDownload, faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { systemUpdateLogs } from "../../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../../@redux/hooks/base";
|
||||
import { useSystemLogs } from "../../@redux/hooks";
|
||||
import { SystemApi } from "../../apis";
|
||||
import { AsyncStateOverlay, ContentHeader } from "../../components";
|
||||
import { useBaseUrl } from "../../utilites";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SystemLogsView: FunctionComponent<Props> = () => {
|
||||
const logs = useReduxStore(({ system }) => system.logs);
|
||||
const update = useReduxAction(systemUpdateLogs);
|
||||
useAutoUpdate(update);
|
||||
const [logs, update] = useSystemLogs();
|
||||
|
||||
const [resetting, setReset] = useState(false);
|
||||
|
||||
|
@ -27,7 +23,7 @@ const SystemLogsView: FunctionComponent<Props> = () => {
|
|||
|
||||
return (
|
||||
<AsyncStateOverlay state={logs}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Logs - Bazarr (System)</title>
|
||||
|
|
|
@ -2,21 +2,19 @@ import { faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useProviders } from "../../@redux/hooks";
|
||||
import { useSystemProviders } from "../../@redux/hooks";
|
||||
import { ProvidersApi } from "../../apis";
|
||||
import { AsyncStateOverlay, ContentHeader } from "../../components";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SystemProvidersView: FunctionComponent<Props> = () => {
|
||||
const [providers, update] = useProviders();
|
||||
useAutoUpdate(update);
|
||||
const [providers, update] = useSystemProviders();
|
||||
|
||||
return (
|
||||
<AsyncStateOverlay state={providers}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Providers - Bazarr (System)</title>
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Badge, Card, Col, Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { systemUpdateReleases } from "../../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../../@redux/hooks/base";
|
||||
import { useSystemReleases } from "../../@redux/hooks";
|
||||
import { AsyncStateOverlay } from "../../components";
|
||||
import { BuildKey } from "../../utilites";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const ReleasesView: FunctionComponent<Props> = () => {
|
||||
const releases = useReduxStore(({ system }) => system.releases);
|
||||
const update = useReduxAction(systemUpdateReleases);
|
||||
useAutoUpdate(update);
|
||||
const [releases] = useSystemReleases();
|
||||
|
||||
return (
|
||||
<AsyncStateOverlay state={releases}>
|
||||
{(item) => (
|
||||
{({ data }) => (
|
||||
<Container fluid className="px-5 py-4 bg-light">
|
||||
<Helmet>
|
||||
<title>Releases - Bazarr (System)</title>
|
||||
</Helmet>
|
||||
<Row>
|
||||
{item.map((v, idx) => (
|
||||
{data.map((v, idx) => (
|
||||
<Col xs={12} key={BuildKey(idx, v.date)}>
|
||||
<InfoElement {...v}></InfoElement>
|
||||
</Col>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { useSetSidebar } from "../@redux/hooks/site";
|
||||
import { RouterEmptyPath } from "../special-pages/404";
|
||||
import Logs from "./Logs";
|
||||
import Providers from "./Providers";
|
||||
|
@ -8,6 +9,7 @@ import Status from "./Status";
|
|||
import Tasks from "./Tasks";
|
||||
|
||||
const Router: FunctionComponent = () => {
|
||||
useSetSidebar("System");
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/system/tasks">
|
||||
|
|
|
@ -9,10 +9,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Col, Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { systemUpdateStatus } from "../../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../../@redux/hooks/base";
|
||||
import { useSystemHealth, useSystemStatus } from "../../@redux/hooks";
|
||||
import { AsyncStateOverlay } from "../../components";
|
||||
import { GithubRepoRoot } from "../../constants";
|
||||
import { useAutoUpdate } from "../../utilites/hooks";
|
||||
import Table from "./table";
|
||||
|
||||
interface InfoProps {
|
||||
title: string;
|
||||
|
@ -65,15 +65,28 @@ const InfoContainer: FunctionComponent<{ title: string }> = ({
|
|||
interface Props {}
|
||||
|
||||
const SystemStatusView: FunctionComponent<Props> = () => {
|
||||
const status = useReduxStore((s) => s.system.status.data);
|
||||
const update = useReduxAction(systemUpdateStatus);
|
||||
useAutoUpdate(update);
|
||||
const [health] = useSystemHealth();
|
||||
const [status] = useSystemStatus();
|
||||
|
||||
let health_table;
|
||||
if (health.data.length) {
|
||||
health_table = (
|
||||
<AsyncStateOverlay state={health}>
|
||||
{({ data }) => <Table health={data}></Table>}
|
||||
</AsyncStateOverlay>
|
||||
);
|
||||
} else {
|
||||
health_table = "No issues with your configuration";
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-5">
|
||||
<Helmet>
|
||||
<title>Status - Bazarr (System)</title>
|
||||
</Helmet>
|
||||
<Row>
|
||||
<InfoContainer title="Health">{health_table}</InfoContainer>
|
||||
</Row>
|
||||
<Row>
|
||||
<InfoContainer title="About">
|
||||
<CRow title="Bazarr Version">
|
||||
|
|
27
frontend/src/System/Status/table.tsx
Normal file
27
frontend/src/System/Status/table.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
import { SimpleTable } from "../../components";
|
||||
|
||||
interface Props {
|
||||
health: readonly System.Health[];
|
||||
}
|
||||
|
||||
const Table: FunctionComponent<Props> = (props) => {
|
||||
const columns: Column<System.Health>[] = useMemo<Column<System.Health>[]>(
|
||||
() => [
|
||||
{
|
||||
Header: "Object",
|
||||
accessor: "object",
|
||||
},
|
||||
{
|
||||
Header: "Issue",
|
||||
accessor: "issue",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return <SimpleTable columns={columns} data={props.health}></SimpleTable>;
|
||||
};
|
||||
|
||||
export default Table;
|
|
@ -2,24 +2,18 @@ import { faSync } from "@fortawesome/free-solid-svg-icons";
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { systemUpdateTasks } from "../../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../../@redux/hooks/base";
|
||||
import { useSystemTasks } from "../../@redux/hooks";
|
||||
import { AsyncStateOverlay, ContentHeader } from "../../components";
|
||||
import { useAutoUpdate } from "../../utilites";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SystemTasksView: FunctionComponent<Props> = () => {
|
||||
const tasks = useReduxStore((s) => s.system.tasks);
|
||||
const update = useReduxAction(systemUpdateTasks);
|
||||
|
||||
// TODO: Use Websocket
|
||||
useAutoUpdate(update, 10 * 1000);
|
||||
const [tasks, update] = useSystemTasks();
|
||||
|
||||
return (
|
||||
<AsyncStateOverlay state={tasks}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Tasks - Bazarr (System)</title>
|
||||
|
|
|
@ -2,8 +2,6 @@ import { faSync } from "@fortawesome/free-solid-svg-icons";
|
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
import { systemRunTasks } from "../../@redux/actions";
|
||||
import { useReduxAction } from "../../@redux/hooks/base";
|
||||
import { SystemApi } from "../../apis";
|
||||
import { AsyncButton, SimpleTable } from "../../components";
|
||||
|
||||
|
@ -12,7 +10,6 @@ interface Props {
|
|||
}
|
||||
|
||||
const Table: FunctionComponent<Props> = ({ tasks }) => {
|
||||
const run = useReduxAction(systemRunTasks);
|
||||
const columns: Column<System.Task>[] = useMemo<Column<System.Task>[]>(
|
||||
() => [
|
||||
{
|
||||
|
@ -37,10 +34,10 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
|
|||
return (
|
||||
<AsyncButton
|
||||
promise={() => SystemApi.runTask(job_id)}
|
||||
onSuccess={() => run(job_id)}
|
||||
variant="light"
|
||||
size="sm"
|
||||
disabled={row.value}
|
||||
animation={false}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSync} spin={row.value}></FontAwesomeIcon>
|
||||
</AsyncButton>
|
||||
|
@ -48,7 +45,7 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
[run]
|
||||
[]
|
||||
);
|
||||
|
||||
return <SimpleTable columns={columns} data={tasks}></SimpleTable>;
|
||||
|
|
|
@ -15,7 +15,7 @@ import GenericWantedView from "../generic";
|
|||
interface Props {}
|
||||
|
||||
const WantedMoviesView: FunctionComponent<Props> = () => {
|
||||
const [movies, update] = useWantedMovies();
|
||||
const [movies] = useWantedMovies();
|
||||
|
||||
const loader = useReduxAction(movieUpdateWantedByRange);
|
||||
|
||||
|
@ -74,9 +74,8 @@ const WantedMoviesView: FunctionComponent<Props> = () => {
|
|||
return (
|
||||
<GenericWantedView
|
||||
type="movies"
|
||||
columns={columns as Column<Wanted.Base>[]}
|
||||
columns={columns}
|
||||
state={movies}
|
||||
update={update}
|
||||
loader={loader}
|
||||
searchAll={searchAll}
|
||||
></GenericWantedView>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import React, { FunctionComponent } from "react";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site";
|
||||
import {
|
||||
useIsRadarrEnabled,
|
||||
useIsSonarrEnabled,
|
||||
useSetSidebar,
|
||||
} from "../@redux/hooks/site";
|
||||
import { RouterEmptyPath } from "../special-pages/404";
|
||||
import Movies from "./Movies";
|
||||
import Series from "./Series";
|
||||
|
@ -8,6 +12,8 @@ import Series from "./Series";
|
|||
const Router: FunctionComponent = () => {
|
||||
const sonarr = useIsSonarrEnabled();
|
||||
const radarr = useIsRadarrEnabled();
|
||||
|
||||
useSetSidebar("Wanted");
|
||||
return (
|
||||
<Switch>
|
||||
{sonarr && (
|
||||
|
|
|
@ -15,7 +15,7 @@ import GenericWantedView from "../generic";
|
|||
interface Props {}
|
||||
|
||||
const WantedSeriesView: FunctionComponent<Props> = () => {
|
||||
const [series, update] = useWantedSeries();
|
||||
const [series] = useWantedSeries();
|
||||
|
||||
const loader = useReduxAction(seriesUpdateWantedByRange);
|
||||
|
||||
|
@ -82,9 +82,8 @@ const WantedSeriesView: FunctionComponent<Props> = () => {
|
|||
return (
|
||||
<GenericWantedView
|
||||
type="series"
|
||||
columns={columns as Column<Wanted.Base>[]}
|
||||
columns={columns}
|
||||
state={series}
|
||||
update={update}
|
||||
loader={loader}
|
||||
searchAll={searchAll}
|
||||
></GenericWantedView>
|
||||
|
|
|
@ -1,39 +1,29 @@
|
|||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { capitalize } from "lodash";
|
||||
import React, { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import React from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Column, TableUpdater } from "react-table";
|
||||
import { ContentHeader, PageTable } from "../../components";
|
||||
import { buildOrderList, GetItemId } from "../../utilites";
|
||||
import { Column } from "react-table";
|
||||
import { AsyncPageTable, ContentHeader } from "../../components";
|
||||
|
||||
interface Props {
|
||||
interface Props<T extends Wanted.Base> {
|
||||
type: "movies" | "series";
|
||||
columns: Column<Wanted.Base>[];
|
||||
state: Readonly<AsyncState<OrderIdState<Wanted.Base>>>;
|
||||
columns: Column<T>[];
|
||||
state: Readonly<AsyncOrderState<T>>;
|
||||
loader: (start: number, length: number) => void;
|
||||
update: (id?: number) => void;
|
||||
searchAll: () => Promise<void>;
|
||||
}
|
||||
|
||||
const GenericWantedView: FunctionComponent<Props> = ({
|
||||
function GenericWantedView<T extends Wanted.Base>({
|
||||
type,
|
||||
columns,
|
||||
state,
|
||||
update,
|
||||
loader,
|
||||
searchAll,
|
||||
}) => {
|
||||
}: Props<T>) {
|
||||
const typeName = capitalize(type);
|
||||
|
||||
const data = useMemo(() => buildOrderList(state.data), [state.data]);
|
||||
|
||||
const updater = useCallback<TableUpdater<Wanted.Base>>(
|
||||
(row, id: number) => {
|
||||
update(id);
|
||||
},
|
||||
[update]
|
||||
);
|
||||
const dataCount = Object.keys(state.data.items).length;
|
||||
|
||||
return (
|
||||
<Container fluid>
|
||||
|
@ -42,28 +32,24 @@ const GenericWantedView: FunctionComponent<Props> = ({
|
|||
</Helmet>
|
||||
<ContentHeader>
|
||||
<ContentHeader.AsyncButton
|
||||
disabled={data.length === 0}
|
||||
disabled={dataCount === 0}
|
||||
promise={searchAll}
|
||||
onSuccess={update as () => void}
|
||||
icon={faSearch}
|
||||
>
|
||||
Search All
|
||||
</ContentHeader.AsyncButton>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<PageTable
|
||||
async
|
||||
asyncState={state}
|
||||
asyncId={GetItemId}
|
||||
asyncLoader={loader}
|
||||
<AsyncPageTable
|
||||
aos={state}
|
||||
loader={loader}
|
||||
emptyText={`No Missing ${typeName} Subtitles`}
|
||||
columns={columns}
|
||||
externalUpdate={updater}
|
||||
data={data}
|
||||
></PageTable>
|
||||
data={[]}
|
||||
></AsyncPageTable>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default GenericWantedView;
|
||||
|
|
|
@ -5,7 +5,7 @@ class EpisodeApi extends BaseApi {
|
|||
super("/episodes");
|
||||
}
|
||||
|
||||
async bySeriesId(seriesid: number): Promise<Array<Item.Episode>> {
|
||||
async bySeriesId(seriesid: number[]): Promise<Array<Item.Episode>> {
|
||||
return new Promise<Array<Item.Episode>>((resolve, reject) => {
|
||||
this.get<DataWrapper<Array<Item.Episode>>>("", { seriesid })
|
||||
.then((result) => {
|
||||
|
@ -17,6 +17,18 @@ class EpisodeApi extends BaseApi {
|
|||
});
|
||||
}
|
||||
|
||||
async byEpisodeId(episodeid: number[]): Promise<Array<Item.Episode>> {
|
||||
return new Promise<Array<Item.Episode>>((resolve, reject) => {
|
||||
this.get<DataWrapper<Array<Item.Episode>>>("", { episodeid })
|
||||
.then((result) => {
|
||||
resolve(result.data.data);
|
||||
})
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async wanted(start: number, length: number) {
|
||||
return new Promise<AsyncDataWrapper<Wanted.Episode>>((resolve, reject) => {
|
||||
this.get<AsyncDataWrapper<Wanted.Episode>>("/wanted", { start, length })
|
||||
|
@ -29,8 +41,7 @@ class EpisodeApi extends BaseApi {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO: Implement this on backend
|
||||
async wantedBy(episodeid?: number) {
|
||||
async wantedBy(episodeid: number[]) {
|
||||
return new Promise<AsyncDataWrapper<Wanted.Episode>>((resolve, reject) => {
|
||||
this.get<AsyncDataWrapper<Wanted.Episode>>("/wanted", { episodeid })
|
||||
.then((result) => {
|
||||
|
@ -42,18 +53,6 @@ class EpisodeApi extends BaseApi {
|
|||
});
|
||||
}
|
||||
|
||||
async byEpisodeId(episodeid: number): Promise<Array<Item.Episode>> {
|
||||
return new Promise<Array<Item.Episode>>((resolve, reject) => {
|
||||
this.get<DataWrapper<Array<Item.Episode>>>("", { episodeid })
|
||||
.then((result) => {
|
||||
resolve(result.data.data);
|
||||
})
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async history(episodeid?: number): Promise<Array<History.Episode>> {
|
||||
return new Promise<Array<History.Episode>>((resolve, reject) => {
|
||||
this.get<DataWrapper<Array<History.Episode>>>("/history", { episodeid })
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
import Axios, { AxiosError, AxiosInstance, CancelTokenSource } from "axios";
|
||||
import { siteRedirectToAuth, siteUpdateOffline } from "../@redux/actions";
|
||||
import reduxStore from "../@redux/store";
|
||||
import { getBaseUrl } from "../utilites";
|
||||
class Api {
|
||||
axios!: AxiosInstance;
|
||||
source!: CancelTokenSource;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = `${getBaseUrl()}/api/`;
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
this.initialize("/api/", process.env["REACT_APP_APIKEY"]!);
|
||||
this.initialize(baseUrl, process.env["REACT_APP_APIKEY"]!);
|
||||
} else {
|
||||
const baseUrl =
|
||||
window.Bazarr.baseUrl === "/"
|
||||
? "/api/"
|
||||
: `${window.Bazarr.baseUrl}/api/`;
|
||||
this.initialize(baseUrl, window.Bazarr.apiKey);
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +32,6 @@ class Api {
|
|||
|
||||
this.axios.interceptors.response.use(
|
||||
(resp) => {
|
||||
this.onOnline();
|
||||
if (resp.status >= 200 && resp.status < 300) {
|
||||
return Promise.resolve(resp);
|
||||
} else {
|
||||
|
@ -46,9 +43,7 @@ class Api {
|
|||
if (error.response) {
|
||||
const response = error.response;
|
||||
this.handleError(response.status);
|
||||
this.onOnline();
|
||||
} else {
|
||||
this.onOffline();
|
||||
error.message = "You have disconnected to Bazarr backend";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
|
|
|
@ -37,9 +37,9 @@ class MovieApi extends BaseApi {
|
|||
});
|
||||
}
|
||||
|
||||
async movies(id?: number[]) {
|
||||
async movies(radarrid?: number[]) {
|
||||
return new Promise<AsyncDataWrapper<Item.Movie>>((resolve, reject) => {
|
||||
this.get<AsyncDataWrapper<Item.Movie>>("", { radarrid: id })
|
||||
this.get<AsyncDataWrapper<Item.Movie>>("", { radarrid })
|
||||
.then((result) => {
|
||||
resolve(result.data);
|
||||
})
|
||||
|
@ -81,8 +81,7 @@ class MovieApi extends BaseApi {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO: Implement this on backend
|
||||
async wantedBy(radarrid?: number) {
|
||||
async wantedBy(radarrid: number[]) {
|
||||
return new Promise<AsyncDataWrapper<Wanted.Movie>>((resolve, reject) => {
|
||||
this.get<AsyncDataWrapper<Wanted.Movie>>("/wanted", { radarrid })
|
||||
.then((result) => {
|
||||
|
|
|
@ -5,9 +5,9 @@ class SeriesApi extends BaseApi {
|
|||
super("/series");
|
||||
}
|
||||
|
||||
async series(id?: number[]) {
|
||||
async series(seriesid?: number[]) {
|
||||
return new Promise<AsyncDataWrapper<Item.Series>>((resolve, reject) => {
|
||||
this.get<AsyncDataWrapper<Item.Series>>("", { seriesid: id })
|
||||
this.get<AsyncDataWrapper<Item.Series>>("", { seriesid })
|
||||
.then((result) => {
|
||||
resolve(result.data);
|
||||
})
|
||||
|
|
|
@ -89,6 +89,18 @@ class SystemApi extends BaseApi {
|
|||
});
|
||||
}
|
||||
|
||||
async health() {
|
||||
return new Promise<System.Health>((resolve, reject) => {
|
||||
this.get<DataWrapper<System.Health>>("/health")
|
||||
.then((result) => {
|
||||
resolve(result.data.data);
|
||||
})
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async logs() {
|
||||
return new Promise<Array<System.Log>>((resolve, reject) => {
|
||||
this.get<DataWrapper<Array<System.Log>>>("/logs")
|
||||
|
|
|
@ -25,10 +25,15 @@ enum RequestState {
|
|||
Invalid,
|
||||
}
|
||||
|
||||
interface ChildProps<T> {
|
||||
data: NonNullable<Readonly<T>>;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface AsyncStateOverlayProps<T> {
|
||||
state: AsyncState<T>;
|
||||
exist?: (item: T) => boolean;
|
||||
children?: (item: NonNullable<Readonly<T>>, error?: Error) => JSX.Element;
|
||||
children?: FunctionComponent<ChildProps<T>>;
|
||||
}
|
||||
|
||||
function defaultExist(item: any) {
|
||||
|
@ -83,7 +88,7 @@ export function AsyncStateOverlay<T>(props: AsyncStateOverlayProps<T>) {
|
|||
}
|
||||
}
|
||||
|
||||
return children ? children(state.data!, state.error) : null;
|
||||
return children ? children({ data: state.data!, error: state.error }) : null;
|
||||
}
|
||||
|
||||
interface PromiseProps<T> {
|
||||
|
@ -156,6 +161,7 @@ interface AsyncButtonProps<T> {
|
|||
onChange?: (v: boolean) => void;
|
||||
|
||||
noReset?: boolean;
|
||||
animation?: boolean;
|
||||
|
||||
promise: () => Promise<T> | null;
|
||||
onSuccess?: (result: T) => void;
|
||||
|
@ -171,6 +177,7 @@ export function AsyncButton<T>(
|
|||
promise,
|
||||
onSuccess,
|
||||
noReset,
|
||||
animation,
|
||||
error,
|
||||
onChange,
|
||||
disabled,
|
||||
|
@ -230,15 +237,19 @@ export function AsyncButton<T>(
|
|||
}
|
||||
}, [error, onChange, promise, onSuccess, state]);
|
||||
|
||||
let children = propChildren;
|
||||
if (loading) {
|
||||
children = <FontAwesomeIcon icon={faCircleNotch} spin></FontAwesomeIcon>;
|
||||
}
|
||||
const showAnimation = animation ?? true;
|
||||
|
||||
if (state === RequestState.Success) {
|
||||
children = <FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>;
|
||||
} else if (state === RequestState.Error) {
|
||||
children = <FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>;
|
||||
let children = propChildren;
|
||||
if (showAnimation) {
|
||||
if (loading) {
|
||||
children = <FontAwesomeIcon icon={faCircleNotch} spin></FontAwesomeIcon>;
|
||||
}
|
||||
|
||||
if (state === RequestState.Success) {
|
||||
children = <FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>;
|
||||
} else if (state === RequestState.Error) {
|
||||
children = <FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -53,6 +53,7 @@ export const ActionButton: FunctionComponent<ActionButtonProps> = ({
|
|||
|
||||
interface ActionButtonItemProps {
|
||||
loading?: boolean;
|
||||
alwaysShowText?: boolean;
|
||||
icon: IconDefinition;
|
||||
children?: string;
|
||||
}
|
||||
|
@ -61,7 +62,9 @@ export const ActionButtonItem: FunctionComponent<ActionButtonItemProps> = ({
|
|||
icon,
|
||||
children,
|
||||
loading,
|
||||
alwaysShowText,
|
||||
}) => {
|
||||
const showText = alwaysShowText === true || loading !== true;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon
|
||||
|
@ -69,7 +72,7 @@ export const ActionButtonItem: FunctionComponent<ActionButtonItemProps> = ({
|
|||
icon={loading ? faCircleNotch : icon}
|
||||
spin={loading}
|
||||
></FontAwesomeIcon>
|
||||
{children && !loading ? (
|
||||
{children && showText ? (
|
||||
<span className="ml-2 font-weight-bold">{children}</span>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -105,7 +105,7 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
|
|||
return (
|
||||
<BaseModal title={`History - ${movie?.title ?? ""}`} {...modal}>
|
||||
<AsyncStateOverlay state={history}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<PageTable
|
||||
emptyText="No History Found"
|
||||
columns={columns}
|
||||
|
@ -208,7 +208,7 @@ export const EpisodeHistoryModal: FunctionComponent<
|
|||
return (
|
||||
<BaseModal title={`History - ${episode?.title ?? ""}`} {...props}>
|
||||
<AsyncStateOverlay state={history}>
|
||||
{(data) => (
|
||||
{({ data }) => (
|
||||
<PageTable
|
||||
emptyText="No History Found"
|
||||
columns={columns}
|
||||
|
|
|
@ -7,12 +7,7 @@ import {
|
|||
useCloseModal,
|
||||
usePayload,
|
||||
} from "..";
|
||||
import {
|
||||
useLanguageBy,
|
||||
useLanguages,
|
||||
useMovieBy,
|
||||
useProfileBy,
|
||||
} from "../../@redux/hooks";
|
||||
import { useLanguageBy, useLanguages, useProfileBy } from "../../@redux/hooks";
|
||||
import { MoviesApi } from "../../apis";
|
||||
import BaseModal, { BaseModalProps } from "./BaseModal";
|
||||
interface MovieProps {}
|
||||
|
@ -25,7 +20,6 @@ const MovieUploadModal: FunctionComponent<MovieProps & BaseModalProps> = (
|
|||
const [availableLanguages] = useLanguages(true);
|
||||
|
||||
const movie = usePayload<Item.Movie>(modal.modalKey);
|
||||
const [, update] = useMovieBy(movie?.radarrId);
|
||||
|
||||
const closeModal = useCloseModal();
|
||||
|
||||
|
@ -63,10 +57,7 @@ const MovieUploadModal: FunctionComponent<MovieProps & BaseModalProps> = (
|
|||
return null;
|
||||
}
|
||||
}}
|
||||
onSuccess={() => {
|
||||
closeModal();
|
||||
update();
|
||||
}}
|
||||
onSuccess={closeModal}
|
||||
>
|
||||
Upload
|
||||
</AsyncButton>
|
||||
|
|
|
@ -24,11 +24,7 @@ import {
|
|||
useCloseModal,
|
||||
usePayload,
|
||||
} from "..";
|
||||
import {
|
||||
useEpisodesBy,
|
||||
useProfileBy,
|
||||
useProfileItems,
|
||||
} from "../../@redux/hooks";
|
||||
import { useProfileBy, useProfileItems } from "../../@redux/hooks";
|
||||
import { EpisodesApi, SubtitlesApi } from "../../apis";
|
||||
import { Selector } from "../inputs";
|
||||
import BaseModal, { BaseModalProps } from "./BaseModal";
|
||||
|
@ -59,15 +55,16 @@ type EpisodeMap = {
|
|||
[name: string]: Item.Episode;
|
||||
};
|
||||
|
||||
interface MovieProps {}
|
||||
interface SerieProps {
|
||||
episodes: readonly Item.Episode[];
|
||||
}
|
||||
|
||||
const SeriesUploadModal: FunctionComponent<MovieProps & BaseModalProps> = (
|
||||
modal
|
||||
) => {
|
||||
const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
|
||||
episodes,
|
||||
...modal
|
||||
}) => {
|
||||
const series = usePayload<Item.Series>(modal.modalKey);
|
||||
|
||||
const [episodes, updateEpisodes] = useEpisodesBy(series?.sonarrSeriesId);
|
||||
|
||||
const [uploading, setUpload] = useState(false);
|
||||
|
||||
const closeModal = useCloseModal();
|
||||
|
@ -122,7 +119,7 @@ const SeriesUploadModal: FunctionComponent<MovieProps & BaseModalProps> = (
|
|||
const results = await SubtitlesApi.info(names);
|
||||
|
||||
const episodeMap = results.reduce<EpisodeMap>((prev, curr) => {
|
||||
const ep = episodes.data.find(
|
||||
const ep = episodes.find(
|
||||
(v) => v.season === curr.season && v.episode === curr.episode
|
||||
);
|
||||
if (ep) {
|
||||
|
@ -140,7 +137,7 @@ const SeriesUploadModal: FunctionComponent<MovieProps & BaseModalProps> = (
|
|||
);
|
||||
}
|
||||
},
|
||||
[episodes.data]
|
||||
[episodes]
|
||||
);
|
||||
|
||||
const updateLanguage = useCallback(
|
||||
|
@ -386,7 +383,6 @@ const SeriesUploadModal: FunctionComponent<MovieProps & BaseModalProps> = (
|
|||
onSuccess={() => {
|
||||
closeModal();
|
||||
setFiles([]);
|
||||
updateEpisodes();
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
|
@ -419,7 +415,7 @@ const SeriesUploadModal: FunctionComponent<MovieProps & BaseModalProps> = (
|
|||
<SimpleTable
|
||||
columns={columns}
|
||||
data={pending}
|
||||
loose={[uploading, processState, episodes.data]}
|
||||
loose={[uploading, processState, episodes]}
|
||||
responsive={false}
|
||||
externalUpdate={updateItem}
|
||||
></SimpleTable>
|
||||
|
|
|
@ -330,14 +330,9 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
interface STMProps {
|
||||
update: () => void;
|
||||
}
|
||||
interface STMProps {}
|
||||
|
||||
const STM: FunctionComponent<BaseModalProps & STMProps> = ({
|
||||
update,
|
||||
...props
|
||||
}) => {
|
||||
const STM: FunctionComponent<BaseModalProps & STMProps> = ({ ...props }) => {
|
||||
const items = usePayload<SupportType[]>(props.modalKey);
|
||||
|
||||
const [updating, setUpdate] = useState<boolean>(false);
|
||||
|
@ -380,10 +375,8 @@ const STM: FunctionComponent<BaseModalProps & STMProps> = ({
|
|||
setProcessState(states);
|
||||
}
|
||||
setUpdate(false);
|
||||
|
||||
update();
|
||||
},
|
||||
[closeUntil, selections, update]
|
||||
[closeUntil, selections]
|
||||
);
|
||||
|
||||
const showModal = useShowModal();
|
||||
|
|
128
frontend/src/components/tables/AsyncPageTable.tsx
Normal file
128
frontend/src/components/tables/AsyncPageTable.tsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
import { isNull } from "lodash";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { PluginHook, TableOptions, useTable } from "react-table";
|
||||
import { LoadingIndicator } from "..";
|
||||
import { useReduxStore } from "../../@redux/hooks/base";
|
||||
import { buildOrderListFrom, isNonNullable, ScrollToTop } from "../../utilites";
|
||||
import BaseTable, { TableStyleProps, useStyleAndOptions } from "./BaseTable";
|
||||
import PageControl from "./PageControl";
|
||||
import { useDefaultSettings } from "./plugins";
|
||||
|
||||
type Props<T extends object> = TableOptions<T> &
|
||||
TableStyleProps<T> & {
|
||||
plugins?: PluginHook<T>[];
|
||||
aos: AsyncOrderState<T>;
|
||||
loader: (start: number, length: number) => void;
|
||||
};
|
||||
|
||||
export default function AsyncPageTable<T extends object>(props: Props<T>) {
|
||||
const { aos, plugins, loader, ...remain } = props;
|
||||
const { style, options } = useStyleAndOptions(remain);
|
||||
|
||||
const {
|
||||
updating,
|
||||
data: { order, items, fetched },
|
||||
} = aos;
|
||||
|
||||
const allPlugins: PluginHook<T>[] = [useDefaultSettings];
|
||||
|
||||
if (plugins) {
|
||||
allPlugins.push(...plugins);
|
||||
}
|
||||
|
||||
// Impl a new pagination system instead of hooking into the existing one
|
||||
const [pageIndex, setIndex] = useState(0);
|
||||
const pageSize = useReduxStore((s) => s.site.pageSize);
|
||||
const totalRows = order.length;
|
||||
const pageCount = Math.ceil(totalRows / pageSize);
|
||||
|
||||
const previous = useCallback(() => {
|
||||
setIndex((idx) => idx - 1);
|
||||
}, []);
|
||||
|
||||
const next = useCallback(() => {
|
||||
setIndex((idx) => idx + 1);
|
||||
}, []);
|
||||
|
||||
const goto = useCallback((idx: number) => {
|
||||
setIndex(idx);
|
||||
}, []);
|
||||
|
||||
const pageStart = pageIndex * pageSize;
|
||||
const pageEnd = pageStart + pageSize;
|
||||
|
||||
const visibleItemIds = useMemo(() => order.slice(pageStart, pageEnd), [
|
||||
pageStart,
|
||||
pageEnd,
|
||||
order,
|
||||
]);
|
||||
|
||||
const newData = useMemo(() => buildOrderListFrom(items, visibleItemIds), [
|
||||
items,
|
||||
visibleItemIds,
|
||||
]);
|
||||
|
||||
const newOptions = useMemo<TableOptions<T>>(
|
||||
() => ({
|
||||
...options,
|
||||
data: newData,
|
||||
}),
|
||||
[options, newData]
|
||||
);
|
||||
|
||||
const instance = useTable(newOptions, ...allPlugins);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
} = instance;
|
||||
|
||||
useEffect(() => {
|
||||
ScrollToTop();
|
||||
}, [pageIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const needInit = visibleItemIds.length === 0 && fetched === false;
|
||||
const needRefresh = !visibleItemIds.every(isNonNullable);
|
||||
if (needInit || needRefresh) {
|
||||
loader(pageStart, pageSize);
|
||||
}
|
||||
}, [visibleItemIds, pageStart, pageSize, loader, fetched]);
|
||||
|
||||
const showLoading = useMemo(
|
||||
() =>
|
||||
updating && (visibleItemIds.every(isNull) || visibleItemIds.length === 0),
|
||||
[visibleItemIds, updating]
|
||||
);
|
||||
|
||||
if (showLoading) {
|
||||
return <LoadingIndicator></LoadingIndicator>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BaseTable
|
||||
{...style}
|
||||
headers={headerGroups}
|
||||
rows={rows}
|
||||
prepareRow={prepareRow}
|
||||
tableProps={getTableProps()}
|
||||
tableBodyProps={getTableBodyProps()}
|
||||
></BaseTable>
|
||||
<PageControl
|
||||
count={pageCount}
|
||||
index={pageIndex}
|
||||
size={pageSize}
|
||||
total={totalRows}
|
||||
canPrevious={pageIndex > 0}
|
||||
canNext={pageIndex < pageCount - 1}
|
||||
previous={previous}
|
||||
next={next}
|
||||
goto={goto}
|
||||
></PageControl>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -79,12 +79,12 @@ const PageControl: FunctionComponent<Props> = ({
|
|||
<Pagination className="m-0" hidden={count <= 1}>
|
||||
<Pagination.Prev
|
||||
onClick={previous}
|
||||
disabled={!canPrevious && loading}
|
||||
disabled={!canPrevious || loading}
|
||||
></Pagination.Prev>
|
||||
{pageButtons}
|
||||
<Pagination.Next
|
||||
onClick={next}
|
||||
disabled={!canNext && loading}
|
||||
disabled={!canNext || loading}
|
||||
></Pagination.Next>
|
||||
</Pagination>
|
||||
</Col>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { isNull, isUndefined } from "lodash";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { isUndefined } from "lodash";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
PluginHook,
|
||||
TableOptions,
|
||||
|
@ -9,33 +9,23 @@ import {
|
|||
} from "react-table";
|
||||
import { useReduxStore } from "../../@redux/hooks/base";
|
||||
import { ScrollToTop } from "../../utilites";
|
||||
import { AsyncStateOverlay } from "../async";
|
||||
import BaseTable, { TableStyleProps, useStyleAndOptions } from "./BaseTable";
|
||||
import PageControl from "./PageControl";
|
||||
import {
|
||||
useAsyncPagination,
|
||||
useCustomSelection,
|
||||
useDefaultSettings,
|
||||
} from "./plugins";
|
||||
import { useCustomSelection, useDefaultSettings } from "./plugins";
|
||||
|
||||
type Props<T extends object> = TableOptions<T> &
|
||||
TableStyleProps<T> & {
|
||||
async?: boolean;
|
||||
canSelect?: boolean;
|
||||
autoScroll?: boolean;
|
||||
plugins?: PluginHook<T>[];
|
||||
};
|
||||
|
||||
export default function PageTable<T extends object>(props: Props<T>) {
|
||||
const { async, autoScroll, canSelect, plugins, ...remain } = props;
|
||||
const { autoScroll, canSelect, plugins, ...remain } = props;
|
||||
const { style, options } = useStyleAndOptions(remain);
|
||||
|
||||
const allPlugins: PluginHook<T>[] = [useDefaultSettings, usePagination];
|
||||
|
||||
if (async) {
|
||||
allPlugins.push(useAsyncPagination);
|
||||
}
|
||||
|
||||
if (canSelect) {
|
||||
allPlugins.push(useRowSelect, useCustomSelection);
|
||||
}
|
||||
|
@ -62,7 +52,7 @@ export default function PageTable<T extends object>(props: Props<T>) {
|
|||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize, pageToLoad, needLoadingScreen },
|
||||
state: { pageIndex, pageSize },
|
||||
} = instance;
|
||||
|
||||
const globalPageSize = useReduxStore((s) => s.site.pageSize);
|
||||
|
@ -91,28 +81,6 @@ export default function PageTable<T extends object>(props: Props<T>) {
|
|||
setPageSize,
|
||||
]);
|
||||
|
||||
const total = options.asyncState
|
||||
? options.asyncState.data.order.length
|
||||
: rows.length;
|
||||
|
||||
const orderIdStateValidater = useCallback(
|
||||
(state: OrderIdState<any>) => {
|
||||
const start = pageIndex * pageSize;
|
||||
const end = start + pageSize;
|
||||
return state.order.slice(start, end).every(isNull) === false;
|
||||
},
|
||||
[pageIndex, pageSize]
|
||||
);
|
||||
|
||||
if (needLoadingScreen && options.asyncState) {
|
||||
return (
|
||||
<AsyncStateOverlay
|
||||
state={options.asyncState}
|
||||
exist={orderIdStateValidater}
|
||||
></AsyncStateOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BaseTable
|
||||
|
@ -124,11 +92,10 @@ export default function PageTable<T extends object>(props: Props<T>) {
|
|||
tableBodyProps={getTableBodyProps()}
|
||||
></BaseTable>
|
||||
<PageControl
|
||||
loadState={pageToLoad}
|
||||
count={pageCount}
|
||||
index={pageIndex}
|
||||
size={pageSize}
|
||||
total={total}
|
||||
total={rows.length}
|
||||
canPrevious={canPreviousPage}
|
||||
canNext={canNextPage}
|
||||
previous={previousPage}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default as AsyncPageTable } from "./AsyncPageTable";
|
||||
export { default as GroupTable } from "./GroupTable";
|
||||
export { default as PageTable } from "./PageTable";
|
||||
export { default as SimpleTable } from "./SimpleTable";
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue