summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--bazarr/api/system/settings.py2
-rw-r--r--bazarr/api/system/status.py14
-rw-r--r--bazarr/app/config.py28
-rw-r--r--bazarr/app/database.py38
-rw-r--r--bazarr/app/get_providers.py10
-rw-r--r--bazarr/app/logger.py14
-rw-r--r--bazarr/app/server.py5
-rw-r--r--bazarr/languages/custom_lang.py27
-rw-r--r--bazarr/languages/get_languages.py19
-rw-r--r--bazarr/main.py4
-rw-r--r--bazarr/radarr/sync/movies.py15
-rw-r--r--bazarr/radarr/sync/parser.py27
-rw-r--r--bazarr/sonarr/sync/parser.py87
-rw-r--r--bazarr/sonarr/sync/series.py12
-rw-r--r--bazarr/subtitles/indexer/movies.py9
-rw-r--r--bazarr/subtitles/indexer/series.py9
-rw-r--r--bazarr/subtitles/indexer/utils.py4
-rw-r--r--bazarr/subtitles/refiners/__init__.py2
-rw-r--r--bazarr/subtitles/refiners/anidb.py136
-rw-r--r--bazarr/subtitles/refiners/anilist.py79
-rw-r--r--bazarr/subtitles/tools/delete.py23
-rw-r--r--bazarr/subtitles/tools/subsyncer.py12
-rw-r--r--bazarr/subtitles/tools/translate.py81
-rw-r--r--bazarr/utilities/health.py21
-rw-r--r--custom_libs/subliminal/video.py12
-rw-r--r--custom_libs/subliminal_patch/core.py17
-rw-r--r--custom_libs/subliminal_patch/providers/animetosho.py3
-rw-r--r--custom_libs/subliminal_patch/providers/avistaz_network.py9
-rw-r--r--custom_libs/subliminal_patch/providers/embeddedsubtitles.py9
-rw-r--r--custom_libs/subliminal_patch/providers/hdbits.py7
-rw-r--r--custom_libs/subliminal_patch/providers/jimaku.py419
-rw-r--r--custom_libs/subliminal_patch/providers/legendasnet.py264
-rw-r--r--custom_libs/subliminal_patch/providers/podnapisi.py3
-rw-r--r--custom_libs/subliminal_patch/providers/soustitreseu.py6
-rw-r--r--custom_libs/subliminal_patch/providers/subdivx.py2
-rw-r--r--custom_libs/subliminal_patch/providers/subdl.py25
-rw-r--r--custom_libs/subliminal_patch/providers/subf2m.py8
-rw-r--r--custom_libs/subliminal_patch/providers/supersubtitles.py8
-rw-r--r--custom_libs/subliminal_patch/providers/utils.py10
-rw-r--r--custom_libs/subliminal_patch/providers/whisperai.py2
-rw-r--r--custom_libs/subliminal_patch/providers/zimuku.py2
-rw-r--r--custom_libs/subliminal_patch/subtitle.py8
-rw-r--r--custom_libs/subliminal_patch/video.py4
-rw-r--r--custom_libs/subzero/language.py12
-rw-r--r--frontend/.eslintrc.json6
-rw-r--r--frontend/package-lock.json1931
-rw-r--r--frontend/package.json28
-rw-r--r--frontend/src/Router/index.tsx2
-rw-r--r--frontend/src/apis/hooks/episodes.ts26
-rw-r--r--frontend/src/apis/hooks/movies.ts33
-rw-r--r--frontend/src/apis/hooks/system.ts37
-rw-r--r--frontend/src/assets/badge.module.scss4
-rw-r--r--frontend/src/components/Search.tsx41
-rw-r--r--frontend/src/components/TextPopover.tsx2
-rw-r--r--frontend/src/components/async/MutateAction.tsx1
-rw-r--r--frontend/src/components/async/MutateButton.tsx1
-rw-r--r--frontend/src/components/async/QueryOverlay.tsx2
-rw-r--r--frontend/src/components/bazarr/AudioList.tsx3
-rw-r--r--frontend/src/components/forms/MovieUploadForm.tsx92
-rw-r--r--frontend/src/components/forms/ProfileEditForm.module.scss8
-rw-r--r--frontend/src/components/forms/ProfileEditForm.tsx31
-rw-r--r--frontend/src/components/forms/SeriesUploadForm.tsx103
-rw-r--r--frontend/src/components/forms/uploadFormSelectorTypes.tsx16
-rw-r--r--frontend/src/components/inputs/Selector.tsx5
-rw-r--r--frontend/src/modules/modals/hooks.ts13
-rw-r--r--frontend/src/modules/socketio/reducer.ts52
-rw-r--r--frontend/src/pages/Movies/index.tsx3
-rw-r--r--frontend/src/pages/Series/index.tsx31
-rw-r--r--frontend/src/pages/Settings/General/index.tsx4
-rw-r--r--frontend/src/pages/Settings/Languages/index.tsx46
-rw-r--r--frontend/src/pages/Settings/Languages/table.tsx49
-rw-r--r--frontend/src/pages/Settings/Providers/components.tsx2
-rw-r--r--frontend/src/pages/Settings/Providers/list.ts44
-rw-r--r--frontend/src/pages/Settings/Radarr/index.tsx5
-rw-r--r--frontend/src/pages/Settings/Sonarr/index.tsx5
-rw-r--r--frontend/src/pages/Settings/Subtitles/index.tsx50
-rw-r--r--frontend/src/pages/Settings/components/Card.tsx29
-rw-r--r--frontend/src/pages/Settings/components/forms.test.tsx8
-rw-r--r--frontend/src/pages/Settings/components/forms.tsx26
-rw-r--r--frontend/src/pages/System/Announcements/table.tsx8
-rw-r--r--frontend/src/pages/System/Status/index.tsx2
-rw-r--r--frontend/src/pages/System/Tasks/table.tsx8
-rw-r--r--frontend/src/pages/Wanted/Movies/index.tsx4
-rw-r--r--frontend/src/pages/views/ItemOverview.tsx10
-rw-r--r--frontend/src/types/api.d.ts1
-rw-r--r--frontend/src/types/settings.d.ts1
-rw-r--r--frontend/src/types/system.d.ts2
-rw-r--r--frontend/src/utilities/languages.ts4
-rw-r--r--frontend/vite.config.ts3
-rw-r--r--libs/argparse-1.4.0.dist-info/LICENSE.txt20
-rw-r--r--libs/argparse-1.4.0.dist-info/METADATA84
-rw-r--r--libs/argparse-1.4.0.dist-info/RECORD8
-rw-r--r--libs/argparse-1.4.0.dist-info/WHEEL6
-rw-r--r--libs/argparse-1.4.0.dist-info/top_level.txt1
-rw-r--r--libs/argparse.py2392
-rw-r--r--libs/fese-0.2.9.dist-info/INSTALLER1
-rw-r--r--libs/fese-0.2.9.dist-info/RECORD13
-rw-r--r--libs/fese-0.2.9.dist-info/REQUESTED0
-rw-r--r--libs/fese-0.3.0.dist-info/INSTALLER (renamed from libs/argparse-1.4.0.dist-info/INSTALLER)0
-rwxr-xr-xlibs/fese-0.3.0.dist-info/LICENSE (renamed from libs/fese-0.2.9.dist-info/LICENSE)0
-rw-r--r--libs/fese-0.3.0.dist-info/METADATA (renamed from libs/fese-0.2.9.dist-info/METADATA)2
-rw-r--r--libs/fese-0.3.0.dist-info/RECORD13
-rw-r--r--libs/fese-0.3.0.dist-info/REQUESTED (renamed from libs/argparse-1.4.0.dist-info/REQUESTED)0
-rw-r--r--libs/fese-0.3.0.dist-info/WHEEL (renamed from libs/fese-0.2.9.dist-info/WHEEL)2
-rw-r--r--libs/fese-0.3.0.dist-info/top_level.txt (renamed from libs/fese-0.2.9.dist-info/top_level.txt)0
-rw-r--r--libs/fese/container.py5
-rw-r--r--libs/fese/tags.py4
-rw-r--r--libs/version.txt3
-rw-r--r--migrations/env.py2
-rw-r--r--migrations/versions/452dd0f0b578_.py4
-rw-r--r--migrations/versions/b183a2ac0dd1_.py (renamed from migrations/versions/b183a2ac0dd1)4
-rw-r--r--tests/bazarr/test_all_import.py.to_be_reworked (renamed from tests/bazarr/test_all_import.py)0
-rw-r--r--tests/bazarr/test_logging_filters.py15
-rw-r--r--tests/subliminal_patch/test_embeddedsubtitles.py19
-rw-r--r--tests/subliminal_patch/test_subdl.py36
-rw-r--r--tests/subliminal_patch/test_subf2m.py33
-rw-r--r--tests/subliminal_patch/test_subscene.py50
118 files changed, 2496 insertions, 4512 deletions
diff --git a/README.md b/README.md
index 2520aa9f9..d87fd9aa0 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,7 @@ If you need something that is not already part of Bazarr, feel free to create a
- Karagarga.in
- Ktuvit (Get `hashed_password` using method described [here](https://github.com/XBMCil/service.subtitles.ktuvit))
- LegendasDivx
+- Legendas.net
- Napiprojekt
- Napisy24
- Nekur
diff --git a/bazarr/api/system/settings.py b/bazarr/api/system/settings.py
index 103df6304..126e2f7a5 100644
--- a/bazarr/api/system/settings.py
+++ b/bazarr/api/system/settings.py
@@ -73,6 +73,7 @@ class SystemSettings(Resource):
mustNotContain=str(item['mustNotContain']),
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
None,
+ tag=item['tag'] if 'tag' in item else None,
)
.where(TableLanguagesProfiles.profileId == item['profileId']))
existing.remove(item['profileId'])
@@ -89,6 +90,7 @@ class SystemSettings(Resource):
mustNotContain=str(item['mustNotContain']),
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
None,
+ tag=item['tag'] if 'tag' in item else None,
))
for profileId in existing:
# Remove deleted profiles
diff --git a/bazarr/api/system/status.py b/bazarr/api/system/status.py
index 325f2fb61..cbc54949d 100644
--- a/bazarr/api/system/status.py
+++ b/bazarr/api/system/status.py
@@ -6,10 +6,12 @@ import logging
from flask_restx import Resource, Namespace
from tzlocal import get_localzone_name
+from alembic.migration import MigrationContext
from radarr.info import get_radarr_info
from sonarr.info import get_sonarr_info
from app.get_args import args
+from app.database import engine, database, select
from init import startTime
from ..utils import authenticate
@@ -34,6 +36,16 @@ class SystemStatus(Resource):
timezone = "Exception while getting time zone name."
logging.exception("BAZARR is unable to get configured time zone name.")
+ try:
+ database_version = ".".join([str(x) for x in engine.dialect.server_version_info])
+ except Exception:
+ database_version = ""
+
+ try:
+ database_migration = MigrationContext.configure(engine.connect()).get_current_revision()
+ except Exception:
+ database_migration = "unknown"
+
system_status = {}
system_status.update({'bazarr_version': os.environ["BAZARR_VERSION"]})
system_status.update({'package_version': package_version})
@@ -41,6 +53,8 @@ class SystemStatus(Resource):
system_status.update({'radarr_version': get_radarr_info.version()})
system_status.update({'operating_system': platform.platform()})
system_status.update({'python_version': platform.python_version()})
+ system_status.update({'database_engine': f'{engine.dialect.name.capitalize()} {database_version}'})
+ system_status.update({'database_migration': database_migration})
system_status.update({'bazarr_directory': os.path.dirname(os.path.dirname(os.path.dirname(
os.path.dirname(__file__))))})
system_status.update({'bazarr_config_directory': args.config_dir})
diff --git a/bazarr/app/config.py b/bazarr/app/config.py
index f5203da84..15e9959f6 100644
--- a/bazarr/app/config.py
+++ b/bazarr/app/config.py
@@ -31,12 +31,20 @@ def base_url_slash_cleaner(uri):
def validate_ip_address(ip_string):
+ if ip_string == '*':
+ return True
try:
ip_address(ip_string)
return True
except ValueError:
return False
+def validate_tags(tags):
+ if not tags:
+ return True
+
+ return all(re.match( r'^[a-z0-9_-]+$', item) for item in tags)
+
ONE_HUNDRED_YEARS_IN_MINUTES = 52560000
ONE_HUNDRED_YEARS_IN_HOURS = 876000
@@ -67,7 +75,7 @@ validators = [
# general section
Validator('general.flask_secret_key', must_exist=True, default=hexlify(os.urandom(16)).decode(),
is_type_of=str),
- Validator('general.ip', must_exist=True, default='0.0.0.0', is_type_of=str, condition=validate_ip_address),
+ Validator('general.ip', must_exist=True, default='*', is_type_of=str, condition=validate_ip_address),
Validator('general.port', must_exist=True, default=6767, is_type_of=int, gte=1, lte=65535),
Validator('general.base_url', must_exist=True, default='', is_type_of=str),
Validator('general.path_mappings', must_exist=True, default=[], is_type_of=list),
@@ -88,6 +96,9 @@ validators = [
Validator('general.use_sonarr', must_exist=True, default=False, is_type_of=bool),
Validator('general.use_radarr', must_exist=True, default=False, is_type_of=bool),
Validator('general.path_mappings_movie', must_exist=True, default=[], is_type_of=list),
+ Validator('general.serie_tag_enabled', must_exist=True, default=False, is_type_of=bool),
+ Validator('general.movie_tag_enabled', must_exist=True, default=False, is_type_of=bool),
+ Validator('general.remove_profile_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags),
Validator('general.serie_default_enabled', must_exist=True, default=False, is_type_of=bool),
Validator('general.serie_default_profile', must_exist=True, default='', is_type_of=(int, str)),
Validator('general.movie_default_enabled', must_exist=True, default=False, is_type_of=bool),
@@ -176,7 +187,7 @@ validators = [
Validator('sonarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
Validator('sonarr.series_sync', must_exist=True, default=60, is_type_of=int,
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
- Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
+ Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags),
Validator('sonarr.excluded_series_types', must_exist=True, default=[], is_type_of=list),
Validator('sonarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
Validator('sonarr.exclude_season_zero', must_exist=True, default=False, is_type_of=bool),
@@ -199,7 +210,7 @@ validators = [
Validator('radarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
Validator('radarr.movies_sync', must_exist=True, default=60, is_type_of=int,
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
- Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
+ Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags),
Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool),
Validator('radarr.sync_only_monitored_movies', must_exist=True, default=False, is_type_of=bool),
@@ -271,6 +282,10 @@ validators = [
Validator('legendasdivx.password', must_exist=True, default='', is_type_of=str, cast=str),
Validator('legendasdivx.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool),
+ # legendasnet section
+ Validator('legendasnet.username', must_exist=True, default='', is_type_of=str, cast=str),
+ Validator('legendasnet.password', must_exist=True, default='', is_type_of=str, cast=str),
+
# ktuvit section
Validator('ktuvit.email', must_exist=True, default='', is_type_of=str),
Validator('ktuvit.hashed_password', must_exist=True, default='', is_type_of=str, cast=str),
@@ -298,6 +313,12 @@ validators = [
# analytics section
Validator('analytics.enabled', must_exist=True, default=True, is_type_of=bool),
+
+ # jimaku section
+ Validator('jimaku.api_key', must_exist=True, default='', is_type_of=str),
+ Validator('jimaku.enable_name_search_fallback', must_exist=True, default=True, is_type_of=bool),
+ Validator('jimaku.enable_archives_download', must_exist=True, default=False, is_type_of=bool),
+ Validator('jimaku.enable_ai_subs', must_exist=True, default=False, is_type_of=bool),
# titlovi section
Validator('titlovi.username', must_exist=True, default='', is_type_of=str, cast=str),
@@ -454,6 +475,7 @@ array_keys = ['excluded_tags',
'enabled_integrations',
'path_mappings',
'path_mappings_movie',
+ 'remove_profile_tags',
'language_equals',
'blacklisted_languages',
'blacklisted_providers']
diff --git a/bazarr/app/database.py b/bazarr/app/database.py
index fa612c4eb..b931c5f0f 100644
--- a/bazarr/app/database.py
+++ b/bazarr/app/database.py
@@ -379,6 +379,7 @@ def update_profile_id_list():
'mustContain': ast.literal_eval(x.mustContain) if x.mustContain else [],
'mustNotContain': ast.literal_eval(x.mustNotContain) if x.mustNotContain else [],
'originalFormat': x.originalFormat,
+ 'tag': x.tag,
} for x in database.execute(
select(TableLanguagesProfiles.profileId,
TableLanguagesProfiles.name,
@@ -386,7 +387,8 @@ def update_profile_id_list():
TableLanguagesProfiles.items,
TableLanguagesProfiles.mustContain,
TableLanguagesProfiles.mustNotContain,
- TableLanguagesProfiles.originalFormat))
+ TableLanguagesProfiles.originalFormat,
+ TableLanguagesProfiles.tag))
.all()
]
@@ -421,7 +423,7 @@ def get_profile_cutoff(profile_id):
if profile_id and profile_id != 'null':
cutoff_language = []
for profile in profile_id_list:
- profileId, name, cutoff, items, mustContain, mustNotContain, originalFormat = profile.values()
+ profileId, name, cutoff, items, mustContain, mustNotContain, originalFormat, tag = profile.values()
if cutoff:
if profileId == int(profile_id):
for item in items:
@@ -511,7 +513,8 @@ def upgrade_languages_profile_hi_values():
TableLanguagesProfiles.items,
TableLanguagesProfiles.mustContain,
TableLanguagesProfiles.mustNotContain,
- TableLanguagesProfiles.originalFormat)
+ TableLanguagesProfiles.originalFormat,
+ TableLanguagesProfiles.tag)
))\
.all():
items = json.loads(languages_profile.items)
@@ -525,3 +528,32 @@ def upgrade_languages_profile_hi_values():
.values({"items": json.dumps(items)})
.where(TableLanguagesProfiles.profileId == languages_profile.profileId)
)
+
+
+def fix_languages_profiles_with_duplicate_ids():
+ languages_profiles = database.execute(
+ select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.items, TableLanguagesProfiles.cutoff)).all()
+ for languages_profile in languages_profiles:
+ if languages_profile.cutoff:
+ # ignore profiles that have a cutoff set
+ continue
+ languages_profile_ids = []
+ languages_profile_has_duplicate = False
+ languages_profile_items = json.loads(languages_profile.items)
+ for items in languages_profile_items:
+ if items['id'] in languages_profile_ids:
+ languages_profile_has_duplicate = True
+ break
+ else:
+ languages_profile_ids.append(items['id'])
+
+ if languages_profile_has_duplicate:
+ item_id = 0
+ for items in languages_profile_items:
+ item_id += 1
+ items['id'] = item_id
+ database.execute(
+ update(TableLanguagesProfiles)
+ .values({"items": json.dumps(languages_profile_items)})
+ .where(TableLanguagesProfiles.profileId == languages_profile.profileId)
+ )
diff --git a/bazarr/app/get_providers.py b/bazarr/app/get_providers.py
index b9ce975ff..125b24d40 100644
--- a/bazarr/app/get_providers.py
+++ b/bazarr/app/get_providers.py
@@ -264,6 +264,10 @@ def get_providers_auth():
'password': settings.legendasdivx.password,
'skip_wrong_fps': settings.legendasdivx.skip_wrong_fps,
},
+ 'legendasnet': {
+ 'username': settings.legendasnet.username,
+ 'password': settings.legendasnet.password,
+ },
'xsubs': {
'username': settings.xsubs.username,
'password': settings.xsubs.password,
@@ -285,6 +289,12 @@ def get_providers_auth():
'username': settings.titlovi.username,
'password': settings.titlovi.password,
},
+ 'jimaku': {
+ 'api_key': settings.jimaku.api_key,
+ 'enable_name_search_fallback': settings.jimaku.enable_name_search_fallback,
+ 'enable_archives_download': settings.jimaku.enable_archives_download,
+ 'enable_ai_subs': settings.jimaku.enable_ai_subs,
+ },
'ktuvit': {
'email': settings.ktuvit.email,
'hashed_password': settings.ktuvit.hashed_password,
diff --git a/bazarr/app/logger.py b/bazarr/app/logger.py
index a47acf3dc..8598912c9 100644
--- a/bazarr/app/logger.py
+++ b/bazarr/app/logger.py
@@ -58,10 +58,13 @@ class NoExceptionFormatter(logging.Formatter):
class UnwantedWaitressMessageFilter(logging.Filter):
def filter(self, record):
- if settings.general.debug:
- # no filtering in debug mode
+ if settings.general.debug or "BAZARR" in record.msg:
+ # no filtering in debug mode or if originating from us
return True
+ if record.levelno < logging.ERROR:
+ return False
+
unwantedMessages = [
"Exception while serving /api/socket.io/",
['Session is disconnected', 'Session not found'],
@@ -161,7 +164,7 @@ def configure_logging(debug=False):
logging.getLogger("websocket").setLevel(logging.CRITICAL)
logging.getLogger("ga4mp.ga4mp").setLevel(logging.ERROR)
- logging.getLogger("waitress").setLevel(logging.ERROR)
+ logging.getLogger("waitress").setLevel(logging.INFO)
logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter())
logging.getLogger("knowit").setLevel(logging.CRITICAL)
logging.getLogger("enzyme").setLevel(logging.CRITICAL)
@@ -169,9 +172,14 @@ def configure_logging(debug=False):
logging.getLogger("rebulk").setLevel(logging.WARNING)
logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL)
+def empty_file(filename):
+ # Open the log file in write mode to clear its contents
+ with open(filename, 'w'):
+ pass # Just opening and closing the file will clear it
def empty_log():
fh.doRollover()
+ empty_file(get_log_file_path())
logging.info('BAZARR Log file emptied')
diff --git a/bazarr/app/server.py b/bazarr/app/server.py
index 1def54dab..4ffe436ce 100644
--- a/bazarr/app/server.py
+++ b/bazarr/app/server.py
@@ -50,7 +50,7 @@ class Server:
self.connected = True
except OSError as error:
if error.errno == errno.EADDRNOTAVAIL:
- logging.exception("BAZARR cannot bind to specified IP, trying with default (0.0.0.0)")
+ logging.exception("BAZARR cannot bind to specified IP, trying with 0.0.0.0")
self.address = '0.0.0.0'
self.connected = False
super(Server, self).__init__()
@@ -76,8 +76,7 @@ class Server:
self.shutdown(EXIT_INTERRUPT)
def start(self):
- logging.info(f'BAZARR is started and waiting for request on http://{self.server.effective_host}:'
- f'{self.server.effective_port}')
+ self.server.print_listen("BAZARR is started and waiting for requests on: http://{}:{}")
signal.signal(signal.SIGINT, self.interrupt_handler)
try:
self.server.run()
diff --git a/bazarr/languages/custom_lang.py b/bazarr/languages/custom_lang.py
index bc50a4758..e6f3aa2f3 100644
--- a/bazarr/languages/custom_lang.py
+++ b/bazarr/languages/custom_lang.py
@@ -5,7 +5,8 @@ import os
from subzero.language import Language
-from app.database import database, insert
+from app.database import database, insert, update
+from sqlalchemy.exc import IntegrityError
logger = logging.getLogger(__name__)
@@ -18,7 +19,7 @@ class CustomLanguage:
language = "pt-BR"
official_alpha2 = "pt"
official_alpha3 = "por"
- name = "Brazilian Portuguese"
+ name = "Portuguese (Brazil)"
iso = "BR"
_scripts = []
_possible_matches = ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil")
@@ -50,13 +51,19 @@ class CustomLanguage:
"""Register the custom language subclasses in the database."""
for sub in cls.__subclasses__():
- database.execute(
- insert(table)
- .values(code3=sub.alpha3,
- code2=sub.alpha2,
- name=sub.name,
- enabled=0)
- .on_conflict_do_nothing())
+ try:
+ database.execute(
+ insert(table)
+ .values(code3=sub.alpha3,
+ code2=sub.alpha2,
+ name=sub.name,
+ enabled=0))
+ except IntegrityError:
+ database.execute(
+ update(table)
+ .values(code2=sub.alpha2,
+ name=sub.name)
+ .where(table.code3 == sub.alpha3))
@classmethod
def found_external(cls, subtitle, subtitle_path):
@@ -212,7 +219,7 @@ class LatinAmericanSpanish(CustomLanguage):
language = "es-MX"
official_alpha2 = "es"
official_alpha3 = "spa"
- name = "Latin American Spanish"
+ name = "Spanish (Latino)"
iso = "MX" # Not fair, but ok
_scripts = ("419",)
_possible_matches = (
diff --git a/bazarr/languages/get_languages.py b/bazarr/languages/get_languages.py
index 2fbd52c89..494343f6f 100644
--- a/bazarr/languages/get_languages.py
+++ b/bazarr/languages/get_languages.py
@@ -44,6 +44,12 @@ def create_languages_dict():
.values(name='Chinese Simplified')
.where(TableSettingsLanguages.code3 == 'zho'))
+ # replace Modern Greek by Greek to match Sonarr and Radarr languages
+ database.execute(
+ update(TableSettingsLanguages)
+ .values(name='Greek')
+ .where(TableSettingsLanguages.code3 == 'ell'))
+
languages_dict = [{
'code3': x.code3,
'code2': x.code2,
@@ -55,6 +61,19 @@ def create_languages_dict():
.all()]
+def audio_language_from_name(lang):
+ lang_map = {
+ 'Chinese': 'zh',
+ }
+
+ alpha2_code = lang_map.get(lang, None)
+
+ if alpha2_code is None:
+ return lang
+
+ return language_from_alpha2(alpha2_code)
+
+
def language_from_alpha2(lang):
return next((item['name'] for item in languages_dict if item['code2'] == lang[:2]), None)
diff --git a/bazarr/main.py b/bazarr/main.py
index 8fe9a43fd..c86f5a7b4 100644
--- a/bazarr/main.py
+++ b/bazarr/main.py
@@ -35,7 +35,8 @@ else:
# there's missing embedded packages after a commit
check_if_new_update()
-from app.database import System, database, update, migrate_db, create_db_revision, upgrade_languages_profile_hi_values # noqa E402
+from app.database import (System, database, update, migrate_db, create_db_revision, upgrade_languages_profile_hi_values,
+ fix_languages_profiles_with_duplicate_ids) # noqa E402
from app.notifier import update_notifier # noqa E402
from languages.get_languages import load_language_in_db # noqa E402
from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402
@@ -50,6 +51,7 @@ if args.create_db_revision:
else:
migrate_db(app)
upgrade_languages_profile_hi_values()
+ fix_languages_profiles_with_duplicate_ids()
configure_proxy_func()
diff --git a/bazarr/radarr/sync/movies.py b/bazarr/radarr/sync/movies.py
index 82416cffb..a5ecf0416 100644
--- a/bazarr/radarr/sync/movies.py
+++ b/bazarr/radarr/sync/movies.py
@@ -28,6 +28,11 @@ def trace(message):
logging.debug(FEATURE_PREFIX + message)
+def get_language_profiles():
+ return database.execute(
+ select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.name, TableLanguagesProfiles.tag)).all()
+
+
def update_all_movies():
movies_full_scan_subtitles()
logging.info('BAZARR All existing movie subtitles indexed from disk.')
@@ -59,7 +64,7 @@ def update_movie(updated_movie, send_event):
def get_movie_monitored_status(movie_id):
existing_movie_monitored = database.execute(
select(TableMovies.monitored)
- .where(TableMovies.tmdbId == movie_id))\
+ .where(TableMovies.tmdbId == str(movie_id)))\
.first()
if existing_movie_monitored is None:
return True
@@ -108,6 +113,7 @@ def update_movies(send_event=True):
else:
audio_profiles = get_profile_list()
tagsDict = get_tags()
+ language_profiles = get_language_profiles()
# Get movies data from radarr
movies = get_movies_from_radarr_api(apikey_radarr=apikey_radarr)
@@ -178,6 +184,7 @@ def update_movies(send_event=True):
if str(movie['tmdbId']) in current_movies_id_db:
parsed_movie = movieParser(movie, action='update',
tags_dict=tagsDict,
+ language_profiles=language_profiles,
movie_default_profile=movie_default_profile,
audio_profiles=audio_profiles)
if not any([parsed_movie.items() <= x for x in current_movies_db_kv]):
@@ -186,6 +193,7 @@ def update_movies(send_event=True):
else:
parsed_movie = movieParser(movie, action='insert',
tags_dict=tagsDict,
+ language_profiles=language_profiles,
movie_default_profile=movie_default_profile,
audio_profiles=audio_profiles)
add_movie(parsed_movie, send_event)
@@ -247,6 +255,7 @@ def update_one_movie(movie_id, action, defer_search=False):
audio_profiles = get_profile_list()
tagsDict = get_tags()
+ language_profiles = get_language_profiles()
try:
# Get movie data from radarr api
@@ -256,10 +265,10 @@ def update_one_movie(movie_id, action, defer_search=False):
return
else:
if action == 'updated' and existing_movie:
- movie = movieParser(movie_data, action='update', tags_dict=tagsDict,
+ movie = movieParser(movie_data, action='update', tags_dict=tagsDict, language_profiles=language_profiles,
movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)
elif action == 'updated' and not existing_movie:
- movie = movieParser(movie_data, action='insert', tags_dict=tagsDict,
+ movie = movieParser(movie_data, action='insert', tags_dict=tagsDict, language_profiles=language_profiles,
movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)
except Exception:
logging.exception('BAZARR cannot get movie returned by SignalR feed from Radarr API.')
diff --git a/bazarr/radarr/sync/parser.py b/bazarr/radarr/sync/parser.py
index 9648152c2..cc186df2f 100644
--- a/bazarr/radarr/sync/parser.py
+++ b/bazarr/radarr/sync/parser.py
@@ -3,7 +3,7 @@
import os
from app.config import settings
-from languages.get_languages import language_from_alpha2
+from languages.get_languages import audio_language_from_name
from radarr.info import get_radarr_info
from utilities.video_analyzer import embedded_audio_reader
from utilities.path_mappings import path_mappings
@@ -11,7 +11,17 @@ from utilities.path_mappings import path_mappings
from .converter import RadarrFormatAudioCodec, RadarrFormatVideoCodec
-def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles):
+def get_matching_profile(tags, language_profiles):
+ matching_profile = None
+ if len(tags) > 0:
+ for profileId, name, tag in language_profiles:
+ if tag in tags:
+ matching_profile = profileId
+ break
+ return matching_profile
+
+
+def movieParser(movie, action, tags_dict, language_profiles, movie_default_profile, audio_profiles):
if 'movieFile' in movie:
try:
overview = str(movie['overview'])
@@ -107,9 +117,7 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
for item in movie['movieFile']['languages']:
if isinstance(item, dict):
if 'name' in item:
- language = item['name']
- if item['name'] == 'Portuguese (Brazil)':
- language = language_from_alpha2('pb')
+ language = audio_language_from_name(item['name'])
audio_language.append(language)
tags = [d['label'] for d in tags_dict if d['id'] in movie['tags']]
@@ -140,6 +148,15 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
parsed_movie['subtitles'] = '[]'
parsed_movie['profileId'] = movie_default_profile
+ if settings.general.movie_tag_enabled:
+ tag_profile = get_matching_profile(tags, language_profiles)
+ if tag_profile:
+ parsed_movie['profileId'] = tag_profile
+ remove_profile_tags_list = settings.general.remove_profile_tags
+ if len(remove_profile_tags_list) > 0:
+ if set(tags) & set(remove_profile_tags_list):
+ parsed_movie['profileId'] = None
+
return parsed_movie
diff --git a/bazarr/sonarr/sync/parser.py b/bazarr/sonarr/sync/parser.py
index d8fce1697..27da32117 100644
--- a/bazarr/sonarr/sync/parser.py
+++ b/bazarr/sonarr/sync/parser.py
@@ -5,6 +5,7 @@ import os
from app.config import settings
from app.database import TableShows, database, select
from constants import MINIMUM_VIDEO_SIZE
+from languages.get_languages import audio_language_from_name
from utilities.path_mappings import path_mappings
from utilities.video_analyzer import embedded_audio_reader
from sonarr.info import get_sonarr_info
@@ -12,7 +13,17 @@ from sonarr.info import get_sonarr_info
from .converter import SonarrFormatVideoCodec, SonarrFormatAudioCodec
-def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles):
+def get_matching_profile(tags, language_profiles):
+ matching_profile = None
+ if len(tags) > 0:
+ for profileId, name, tag in language_profiles:
+ if tag in tags:
+ matching_profile = profileId
+ break
+ return matching_profile
+
+
+def seriesParser(show, action, tags_dict, language_profiles, serie_default_profile, audio_profiles):
overview = show['overview'] if 'overview' in show else ''
poster = ''
fanart = ''
@@ -24,9 +35,11 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
if image['coverType'] == 'fanart':
fanart = image['url'].split('?')[0]
- alternate_titles = None
if show['alternateTitles'] is not None:
- alternate_titles = str([item['title'] for item in show['alternateTitles']])
+ alternate_titles = [item['title'] for item in show['alternateTitles'] if 'title' in item and item['title'] not
+ in [None, ''] and item["title"] != show["title"]]
+ else:
+ alternate_titles = []
tags = [d['label'] for d in tags_dict if d['id'] in show['tags']]
@@ -42,39 +55,37 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
else:
audio_language = []
- if action == 'update':
- return {'title': show["title"],
- 'path': show["path"],
- 'tvdbId': int(show["tvdbId"]),
- 'sonarrSeriesId': int(show["id"]),
- 'overview': overview,
- 'poster': poster,
- 'fanart': fanart,
- 'audio_language': str(audio_language),
- 'sortTitle': show['sortTitle'],
- 'year': str(show['year']),
- 'alternativeTitles': alternate_titles,
- 'tags': str(tags),
- 'seriesType': show['seriesType'],
- 'imdbId': imdbId,
- 'monitored': str(bool(show['monitored']))}
- else:
- return {'title': show["title"],
- 'path': show["path"],
- 'tvdbId': show["tvdbId"],
- 'sonarrSeriesId': show["id"],
- 'overview': overview,
- 'poster': poster,
- 'fanart': fanart,
- 'audio_language': str(audio_language),
- 'sortTitle': show['sortTitle'],
- 'year': str(show['year']),
- 'alternativeTitles': alternate_titles,
- 'tags': str(tags),
- 'seriesType': show['seriesType'],
- 'imdbId': imdbId,
- 'profileId': serie_default_profile,
- 'monitored': str(bool(show['monitored']))}
+ parsed_series = {
+ 'title': show["title"],
+ 'path': show["path"],
+ 'tvdbId': int(show["tvdbId"]),
+ 'sonarrSeriesId': int(show["id"]),
+ 'overview': overview,
+ 'poster': poster,
+ 'fanart': fanart,
+ 'audio_language': str(audio_language),
+ 'sortTitle': show['sortTitle'],
+ 'year': str(show['year']),
+ 'alternativeTitles': str(alternate_titles),
+ 'tags': str(tags),
+ 'seriesType': show['seriesType'],
+ 'imdbId': imdbId,
+ 'monitored': str(bool(show['monitored']))
+ }
+
+ if action == 'insert':
+ parsed_series['profileId'] = serie_default_profile
+
+ if settings.general.serie_tag_enabled:
+ tag_profile = get_matching_profile(tags, language_profiles)
+ if tag_profile:
+ parsed_series['profileId'] = tag_profile
+ remove_profile_tags_list = settings.general.remove_profile_tags
+ if len(remove_profile_tags_list) > 0:
+ if set(tags) & set(remove_profile_tags_list):
+ parsed_series['profileId'] = None
+
+ return parsed_series
def profile_id_to_language(id_, profiles):
@@ -111,13 +122,13 @@ def episodeParser(episode):
item = episode['episodeFile']['language']
if isinstance(item, dict):
if 'name' in item:
- audio_language.append(item['name'])
+ audio_language.append(audio_language_from_name(item['name']))
elif 'languages' in episode['episodeFile'] and len(episode['episodeFile']['languages']):
items = episode['episodeFile']['languages']
if isinstance(items, list):
for item in items:
if 'name' in item:
- audio_language.append(item['name'])
+ audio_language.append(audio_language_from_name(item['name']))
else:
audio_language = database.execute(
select(TableShows.audio_language)
diff --git a/bazarr/sonarr/sync/series.py b/bazarr/sonarr/sync/series.py
index 96a00009c..f8bd84990 100644
--- a/bazarr/sonarr/sync/series.py
+++ b/bazarr/sonarr/sync/series.py
@@ -26,6 +26,11 @@ def trace(message):
logging.debug(FEATURE_PREFIX + message)
+def get_language_profiles():
+ return database.execute(
+ select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.name, TableLanguagesProfiles.tag)).all()
+
+
def get_series_monitored_table():
series_monitored = database.execute(
select(TableShows.tvdbId, TableShows.monitored))\
@@ -58,6 +63,7 @@ def update_series(send_event=True):
audio_profiles = get_profile_list()
tagsDict = get_tags()
+ language_profiles = get_language_profiles()
# Get shows data from Sonarr
series = get_series_from_sonarr_api(apikey_sonarr=apikey_sonarr)
@@ -111,6 +117,7 @@ def update_series(send_event=True):
if show['id'] in current_shows_db:
updated_series = seriesParser(show, action='update', tags_dict=tagsDict,
+ language_profiles=language_profiles,
serie_default_profile=serie_default_profile,
audio_profiles=audio_profiles)
@@ -132,6 +139,7 @@ def update_series(send_event=True):
event_stream(type='series', payload=show['id'])
else:
added_series = seriesParser(show, action='insert', tags_dict=tagsDict,
+ language_profiles=language_profiles,
serie_default_profile=serie_default_profile,
audio_profiles=audio_profiles)
@@ -203,7 +211,7 @@ def update_one_series(series_id, action):
audio_profiles = get_profile_list()
tagsDict = get_tags()
-
+ language_profiles = get_language_profiles()
try:
# Get series data from sonarr api
series = None
@@ -215,10 +223,12 @@ def update_one_series(series_id, action):
else:
if action == 'updated' and existing_series:
series = seriesParser(series_data[0], action='update', tags_dict=tagsDict,
+ language_profiles=language_profiles,
serie_default_profile=serie_default_profile,
audio_profiles=audio_profiles)
elif action == 'updated' and not existing_series:
series = seriesParser(series_data[0], action='insert', tags_dict=tagsDict,
+ language_profiles=language_profiles,
serie_default_profile=serie_default_profile,
audio_profiles=audio_profiles)
except Exception:
diff --git a/bazarr/subtitles/indexer/movies.py b/bazarr/subtitles/indexer/movies.py
index ba105efc3..563a68cff 100644
--- a/bazarr/subtitles/indexer/movies.py
+++ b/bazarr/subtitles/indexer/movies.py
@@ -216,7 +216,9 @@ def list_missing_subtitles_movies(no=None, send_event=True):
if cutoff_temp_list:
for cutoff_temp in cutoff_temp_list:
- cutoff_language = [cutoff_temp['language'], cutoff_temp['forced'], cutoff_temp['hi']]
+ cutoff_language = {'language': cutoff_temp['language'],
+ 'forced': cutoff_temp['forced'],
+ 'hi': cutoff_temp['hi']}
if cutoff_temp['audio_exclude'] == 'True' and \
any(x['code2'] == cutoff_temp['language'] for x in
get_audio_profile_languages(movie_subtitles.audio_language)):
@@ -224,7 +226,10 @@ def list_missing_subtitles_movies(no=None, send_event=True):
elif cutoff_language in actual_subtitles_list:
cutoff_met = True
# HI is considered as good as normal
- elif cutoff_language and [cutoff_language[0], 'False', 'True'] in actual_subtitles_list:
+ elif (cutoff_language and
+ {'language': cutoff_language['language'],
+ 'forced': 'False',
+ 'hi': 'True'} in actual_subtitles_list):
cutoff_met = True
if cutoff_met:
diff --git a/bazarr/subtitles/indexer/series.py b/bazarr/subtitles/indexer/series.py
index 6b8501dfe..64af2e4c3 100644
--- a/bazarr/subtitles/indexer/series.py
+++ b/bazarr/subtitles/indexer/series.py
@@ -216,7 +216,9 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
if cutoff_temp_list:
for cutoff_temp in cutoff_temp_list:
- cutoff_language = [cutoff_temp['language'], cutoff_temp['forced'], cutoff_temp['hi']]
+ cutoff_language = {'language': cutoff_temp['language'],
+ 'forced': cutoff_temp['forced'],
+ 'hi': cutoff_temp['hi']}
if cutoff_temp['audio_exclude'] == 'True' and \
any(x['code2'] == cutoff_temp['language'] for x in
get_audio_profile_languages(episode_subtitles.audio_language)):
@@ -224,7 +226,10 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
elif cutoff_language in actual_subtitles_list:
cutoff_met = True
# HI is considered as good as normal
- elif [cutoff_language[0], 'False', 'True'] in actual_subtitles_list:
+ elif (cutoff_language and
+ {'language': cutoff_language['language'],
+ 'forced': 'False',
+ 'hi': 'True'} in actual_subtitles_list):
cutoff_met = True
if cutoff_met:
diff --git a/bazarr/subtitles/indexer/utils.py b/bazarr/subtitles/indexer/utils.py
index 03549d748..63b9dee6d 100644
--- a/bazarr/subtitles/indexer/utils.py
+++ b/bazarr/subtitles/indexer/utils.py
@@ -2,7 +2,6 @@
import os
import logging
-import re
from guess_language import guess_language
from subliminal_patch import core
@@ -136,6 +135,7 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
continue
text = text.decode(encoding)
- if bool(re.search(core.HI_REGEX, text)):
+ if core.parse_for_hi_regex(subtitle_text=text,
+ alpha3_language=language.alpha3 if hasattr(language, 'alpha3') else None):
subtitles[subtitle] = Language.rebuild(subtitles[subtitle], forced=False, hi=True)
return subtitles
diff --git a/bazarr/subtitles/refiners/__init__.py b/bazarr/subtitles/refiners/__init__.py
index ff1e715a0..9fbdecbb2 100644
--- a/bazarr/subtitles/refiners/__init__.py
+++ b/bazarr/subtitles/refiners/__init__.py
@@ -4,10 +4,12 @@ from .ffprobe import refine_from_ffprobe
from .database import refine_from_db
from .arr_history import refine_from_arr_history
from .anidb import refine_from_anidb
+from .anilist import refine_from_anilist
registered = {
"database": refine_from_db,
"ffprobe": refine_from_ffprobe,
"arr_history": refine_from_arr_history,
"anidb": refine_from_anidb,
+ "anilist": refine_from_anilist, # Must run AFTER AniDB
}
diff --git a/bazarr/subtitles/refiners/anidb.py b/bazarr/subtitles/refiners/anidb.py
index f3c61916a..f896697bd 100644
--- a/bazarr/subtitles/refiners/anidb.py
+++ b/bazarr/subtitles/refiners/anidb.py
@@ -20,7 +20,10 @@ except ImportError:
except ImportError:
import xml.etree.ElementTree as etree
-refined_providers = {'animetosho'}
+refined_providers = {'animetosho', 'jimaku'}
+providers_requiring_anidb_api = {'animetosho'}
+
+logger = logging.getLogger(__name__)
api_url = 'http://api.anidb.net:9001/httpapi'
@@ -40,6 +43,10 @@ class AniDBClient(object):
@property
def is_throttled(self):
return self.cache and self.cache.get('is_throttled')
+
+ @property
+ def has_api_credentials(self):
+ return self.api_client_key != '' and self.api_client_key is not None
@property
def daily_api_request_count(self):
@@ -62,7 +69,9 @@ class AniDBClient(object):
return r.content
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
- def get_series_id(self, mappings, tvdb_series_season, tvdb_series_id, episode):
+ def get_show_information(self, tvdb_series_id, tvdb_series_season, episode):
+ mappings = etree.fromstring(self.get_series_mappings())
+
# Enrich the collection of anime with the episode offset
animes = [
self.AnimeInfo(anime, int(anime.attrib.get('episodeoffset', 0)))
@@ -71,37 +80,72 @@ class AniDBClient(object):
)
]
+ is_special_entry = False
if not animes:
- return None, None
+ # Some entries will store TVDB seasons in a nested mapping list, identifiable by the value 'a' as the season
+ special_entries = mappings.findall(
+ f".//anime[@tvdbid='{tvdb_series_id}'][@defaulttvdbseason='a']"
+ )
- # Sort the anime by offset in ascending order
- animes.sort(key=lambda a: a.episode_offset)
+ if not special_entries:
+ return None, None, None
- # Different from Tvdb, Anidb have different ids for the Parts of a season
- anidb_id = None
- offset = 0
+ is_special_entry = True
+ for special_entry in special_entries:
+ mapping_list = special_entry.findall(f".//mapping[@tvdbseason='{tvdb_series_season}']")
+ if len(mapping_list) > 0:
+ anidb_id = int(special_entry.attrib.get('anidbid'))
+ offset = int(mapping_list[0].attrib.get('offset', 0))
- for index, anime_info in enumerate(animes):
- anime, episode_offset = anime_info
- anidb_id = int(anime.attrib.get('anidbid'))
- if episode > episode_offset:
- anidb_id = anidb_id
- offset = episode_offset
+ if not is_special_entry:
+ # Sort the anime by offset in ascending order
+ animes.sort(key=lambda a: a.episode_offset)
- return anidb_id, episode - offset
+ # Different from Tvdb, Anidb have different ids for the Parts of a season
+ anidb_id = None
+ offset = 0
- @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
- def get_series_episodes_ids(self, tvdb_series_id, season, episode):
- mappings = etree.fromstring(self.get_series_mappings())
+ for index, anime_info in enumerate(animes):
+ anime, episode_offset = anime_info
+
+ mapping_list = anime.find('mapping-list')
+
+ # Handle mapping list for Specials
+ if mapping_list:
+ for mapping in mapping_list.findall("mapping"):
+ if mapping.text is None:
+ continue
- series_id, episode_no = self.get_series_id(mappings, season, tvdb_series_id, episode)
+ # Mapping values are usually like ;1-1;2-1;3-1;
+ for episode_ref in mapping.text.split(';'):
+ if not episode_ref:
+ continue
+ anidb_episode, tvdb_episode = map(int, episode_ref.split('-'))
+ if tvdb_episode == episode:
+ anidb_id = int(anime.attrib.get('anidbid'))
+
+ return anidb_id, anidb_episode, 0
+
+ if episode > episode_offset:
+ anidb_id = int(anime.attrib.get('anidbid'))
+ offset = episode_offset
+
+ return anidb_id, episode - offset, offset
+
+ @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
+ def get_episode_ids(self, series_id, episode_no):
if not series_id:
- return None, None
+ return None
episodes = etree.fromstring(self.get_episodes(series_id))
- return series_id, int(episodes.find(f".//episode[epno='{episode_no}']").attrib.get('id'))
+ episode = episodes.find(f".//episode[epno='{episode_no}']")
+
+ if not episode:
+ return series_id, None
+
+ return series_id, int(episode.attrib.get('id'))
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
def get_episodes(self, series_id):
@@ -156,8 +200,6 @@ class AniDBClient(object):
def refine_from_anidb(path, video):
if not isinstance(video, Episode) or not video.series_tvdb_id:
- logging.debug(f'Video is not an Anime TV series, skipping refinement for {video}')
-
return
if refined_providers.intersection(settings.general.enabled_providers) and video.series_anidb_id is None:
@@ -169,27 +211,35 @@ def refine_anidb_ids(video):
season = video.season if video.season else 0
- if anidb_client.is_throttled:
- logging.warning(f'API daily limit reached. Skipping refinement for {video.series}')
-
- return video
-
- try:
- anidb_series_id, anidb_episode_id = anidb_client.get_series_episodes_ids(
- video.series_tvdb_id,
- season, video.episode,
- )
- except TooManyRequests:
- logging.error(f'API daily limit reached while refining {video.series}')
-
- anidb_client.mark_as_throttled()
-
- return video
-
- if not anidb_episode_id:
- logging.error(f'Could not find anime series {video.series}')
-
+ anidb_series_id, anidb_episode_no, anidb_season_episode_offset = anidb_client.get_show_information(
+ video.series_tvdb_id,
+ season,
+ video.episode,
+ )
+
+ if not anidb_series_id:
+ logger.error(f'Could not find anime series {video.series}')
return video
+
+ anidb_episode_id = None
+ if anidb_client.has_api_credentials:
+ if anidb_client.is_throttled:
+ logger.warning(f'API daily limit reached. Skipping episode ID refinement for {video.series}')
+ else:
+ try:
+ anidb_episode_id = anidb_client.get_episode_ids(
+ anidb_series_id,
+ anidb_episode_no
+ )
+ except TooManyRequests:
+ logger.error(f'API daily limit reached while refining {video.series}')
+ anidb_client.mark_as_throttled()
+ else:
+ intersect = providers_requiring_anidb_api.intersection(settings.general.enabled_providers)
+ if len(intersect) >= 1:
+ logger.warn(f'AniDB API credentials are not fully set up, the following providers may not work: {intersect}')
video.series_anidb_id = anidb_series_id
video.series_anidb_episode_id = anidb_episode_id
+ video.series_anidb_episode_no = anidb_episode_no
+ video.series_anidb_season_episode_offset = anidb_season_episode_offset
diff --git a/bazarr/subtitles/refiners/anilist.py b/bazarr/subtitles/refiners/anilist.py
new file mode 100644
index 000000000..4a008a5e1
--- /dev/null
+++ b/bazarr/subtitles/refiners/anilist.py
@@ -0,0 +1,79 @@
+# coding=utf-8
+# fmt: off
+
+import logging
+import time
+import requests
+from collections import namedtuple
+from datetime import timedelta
+
+from app.config import settings
+from subliminal import Episode, region, __short_version__
+
+logger = logging.getLogger(__name__)
+refined_providers = {'jimaku'}
+
+
+class AniListClient(object):
+ def __init__(self, session=None, timeout=10):
+ self.session = session or requests.Session()
+ self.session.timeout = timeout
+ self.session.headers['Content-Type'] = 'application/json'
+ self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
+
+ @region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
+ def get_series_mappings(self):
+ r = self.session.get(
+ 'https://raw.githubusercontent.com/Fribb/anime-lists/master/anime-list-mini.json'
+ )
+
+ r.raise_for_status()
+ return r.json()
+
+ def get_series_id(self, candidate_id_name, candidate_id_value):
+ anime_list = self.get_series_mappings()
+
+ tag_map = {
+ "series_anidb_id": "anidb_id",
+ "imdb_id": "imdb_id"
+ }
+ mapped_tag = tag_map.get(candidate_id_name, candidate_id_name)
+
+ obj = [obj for obj in anime_list if mapped_tag in obj and str(obj[mapped_tag]) == str(candidate_id_value)]
+ logger.debug(f"Based on '{mapped_tag}': '{candidate_id_value}', anime-list matched: {obj}")
+
+ if len(obj) > 0:
+ return obj[0]["anilist_id"]
+ else:
+ logger.debug(f"Could not find corresponding AniList ID with '{mapped_tag}': {candidate_id_value}")
+ return None
+
+
+def refine_from_anilist(path, video):
+ # Safety checks
+ if isinstance(video, Episode):
+ if not video.series_anidb_id:
+ return
+
+ if refined_providers.intersection(settings.general.enabled_providers) and video.anilist_id is None:
+ refine_anilist_ids(video)
+
+
+def refine_anilist_ids(video):
+ anilist_client = AniListClient()
+
+ if isinstance(video, Episode):
+ candidate_id_name = "series_anidb_id"
+ else:
+ candidate_id_name = "imdb_id"
+
+ candidate_id_value = getattr(video, candidate_id_name, None)
+ if not candidate_id_value:
+ logger.error(f"Found no value for property {candidate_id_name} of video.")
+ return video
+
+ anilist_id = anilist_client.get_series_id(candidate_id_name, candidate_id_value)
+ if not anilist_id:
+ return video
+
+ video.anilist_id = anilist_id
diff --git a/bazarr/subtitles/tools/delete.py b/bazarr/subtitles/tools/delete.py
index 8291c2ff9..6345a91f5 100644
--- a/bazarr/subtitles/tools/delete.py
+++ b/bazarr/subtitles/tools/delete.py
@@ -36,40 +36,47 @@ def delete_subtitles(media_type, language, forced, hi, media_path, subtitles_pat
language_log += ':forced'
language_string += ' forced'
+ if media_type == 'series':
+ pr = path_mappings.path_replace
+ prr = path_mappings.path_replace_reverse
+ else:
+ pr = path_mappings.path_replace_movie
+ prr = path_mappings.path_replace_reverse_movie
+
result = ProcessSubtitlesResult(message=f"{language_string} subtitles deleted from disk.",
- reversed_path=path_mappings.path_replace_reverse(media_path),
+ reversed_path=prr(media_path),
downloaded_language_code2=language_log,
downloaded_provider=None,
score=None,
forced=None,
subtitle_id=None,
- reversed_subtitles_path=path_mappings.path_replace_reverse(subtitles_path),
+ reversed_subtitles_path=prr(subtitles_path),
hearing_impaired=None)
if media_type == 'series':
try:
- os.remove(path_mappings.path_replace(subtitles_path))
+ os.remove(pr(subtitles_path))
except OSError:
logging.exception(f'BAZARR cannot delete subtitles file: {subtitles_path}')
- store_subtitles(path_mappings.path_replace_reverse(media_path), media_path)
+ store_subtitles(prr(media_path), media_path)
return False
else:
history_log(0, sonarr_series_id, sonarr_episode_id, result)
- store_subtitles(path_mappings.path_replace_reverse(media_path), media_path)
+ store_subtitles(prr(media_path), media_path)
notify_sonarr(sonarr_series_id)
event_stream(type='series', action='update', payload=sonarr_series_id)
event_stream(type='episode-wanted', action='update', payload=sonarr_episode_id)
return True
else:
try:
- os.remove(path_mappings.path_replace_movie(subtitles_path))
+ os.remove(pr(subtitles_path))
except OSError:
logging.exception(f'BAZARR cannot delete subtitles file: {subtitles_path}')
- store_subtitles_movie(path_mappings.path_replace_reverse_movie(media_path), media_path)
+ store_subtitles_movie(prr(media_path), media_path)
return False
else:
history_log_movie(0, radarr_id, result)
- store_subtitles_movie(path_mappings.path_replace_reverse_movie(media_path), media_path)
+ store_subtitles_movie(prr(media_path), media_path)
notify_radarr(radarr_id)
event_stream(type='movie-wanted', action='update', payload=radarr_id)
return True
diff --git a/bazarr/subtitles/tools/subsyncer.py b/bazarr/subtitles/tools/subsyncer.py
index edde5c774..22b2b4f56 100644
--- a/bazarr/subtitles/tools/subsyncer.py
+++ b/bazarr/subtitles/tools/subsyncer.py
@@ -97,8 +97,7 @@ class SubSyncer:
result = run(self.args)
except Exception:
logging.exception(
- f'BAZARR an exception occurs during the synchronization process for this subtitles: {self.srtin}')
- raise OSError
+ f'BAZARR an exception occurs during the synchronization process for this subtitle file: {self.srtin}')
else:
if settings.subsync.debug:
return result
@@ -113,14 +112,19 @@ class SubSyncer:
f"{offset_seconds} seconds and a framerate scale factor of "
f"{f'{framerate_scale_factor:.2f}'}.")
+ if sonarr_series_id:
+ prr = path_mappings.path_replace_reverse
+ else:
+ prr = path_mappings.path_replace_reverse_movie
+
result = ProcessSubtitlesResult(message=message,
- reversed_path=path_mappings.path_replace_reverse(self.reference),
+ reversed_path=prr(self.reference),
downloaded_language_code2=srt_lang,
downloaded_provider=None,
score=None,
forced=forced,
subtitle_id=None,
- reversed_subtitles_path=srt_path,
+ reversed_subtitles_path=prr(self.srtin),
hearing_impaired=hi)
if sonarr_episode_id:
diff --git a/bazarr/subtitles/tools/translate.py b/bazarr/subtitles/tools/translate.py
index 4dcc25471..2b80bf951 100644
--- a/bazarr/subtitles/tools/translate.py
+++ b/bazarr/subtitles/tools/translate.py
@@ -6,12 +6,17 @@ import pysubs2
from subliminal_patch.core import get_subtitle_path
from subzero.language import Language
from deep_translator import GoogleTranslator
+from deep_translator.exceptions import TooManyRequests, RequestError, TranslationNotFound
+from time import sleep
+from concurrent.futures import ThreadPoolExecutor
from languages.custom_lang import CustomLanguage
from languages.get_languages import alpha3_from_alpha2, language_from_alpha2, language_from_alpha3
from radarr.history import history_log_movie
from sonarr.history import history_log
from subtitles.processing import ProcessSubtitlesResult
+from app.event_handler import show_progress, hide_progress
+from utilities.path_mappings import path_mappings
def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, forced, hi, media_type, sonarr_series_id,
@@ -33,8 +38,6 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
logging.debug(f'BAZARR is translating in {lang_obj} this subtitles {source_srt_file}')
- max_characters = 5000
-
dest_srt_file = get_subtitle_path(video_path,
language=lang_obj if isinstance(lang_obj, Language) else lang_obj.subzero_language(),
extension='.srt',
@@ -44,40 +47,53 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
subs = pysubs2.load(source_srt_file, encoding='utf-8')
subs.remove_miscellaneous_events()
lines_list = [x.plaintext for x in subs]
- joined_lines_str = '\n\n\n'.join(lines_list)
-
- logging.debug(f'BAZARR splitting subtitles into {max_characters} characters blocks')
- lines_block_list = []
- translated_lines_list = []
- while len(joined_lines_str):
- partial_lines_str = joined_lines_str[:max_characters]
+ lines_list_len = len(lines_list)
- if len(joined_lines_str) > max_characters:
- new_partial_lines_str = partial_lines_str.rsplit('\n\n', 1)[0]
+ def translate_line(id, line, attempt):
+ try:
+ translated_text = GoogleTranslator(
+ source='auto',
+ target=language_code_convert_dict.get(lang_obj.alpha2, lang_obj.alpha2)
+ ).translate(text=line)
+ except TooManyRequests:
+ if attempt <= 5:
+ sleep(1)
+ super(translate_line(id, line, attempt+1))
+ else:
+ logging.debug(f'Too many requests while translating {line}')
+ translated_lines.append({'id': id, 'line': line})
+ except (RequestError, TranslationNotFound):
+ logging.debug(f'Unable to translate line {line}')
+ translated_lines.append({'id': id, 'line': line})
else:
- new_partial_lines_str = partial_lines_str
+ translated_lines.append({'id': id, 'line': translated_text})
+ finally:
+ show_progress(id=f'translate_progress_{dest_srt_file}',
+ header=f'Translating subtitles lines to {language_from_alpha3(to_lang)}...',
+ name='',
+ value=len(translated_lines),
+ count=lines_list_len)
- lines_block_list.append(new_partial_lines_str)
- joined_lines_str = joined_lines_str.replace(new_partial_lines_str, '')
+ logging.debug(f'BAZARR is sending {lines_list_len} blocks to Google Translate')
- logging.debug(f'BAZARR is sending {len(lines_block_list)} blocks to Google Translate')
- for block_str in lines_block_list:
- try:
- translated_partial_srt_text = GoogleTranslator(source='auto',
- target=language_code_convert_dict.get(lang_obj.alpha2,
- lang_obj.alpha2)
- ).translate(text=block_str)
- except Exception:
- logging.exception(f'BAZARR Unable to translate subtitles {source_srt_file}')
- return False
- else:
- translated_partial_srt_list = translated_partial_srt_text.split('\n\n')
- translated_lines_list += translated_partial_srt_list
+ pool = ThreadPoolExecutor(max_workers=10)
+
+ translated_lines = []
+
+ for i, line in enumerate(lines_list):
+ pool.submit(translate_line, i, line, 1)
+
+ pool.shutdown(wait=True)
+
+ for i, line in enumerate(translated_lines):
+ lines_list[line['id']] = line['line']
+
+ hide_progress(id=f'translate_progress_{dest_srt_file}')
logging.debug(f'BAZARR saving translated subtitles to {dest_srt_file}')
for i, line in enumerate(subs):
try:
- line.plaintext = translated_lines_list[i]
+ line.plaintext = lines_list[i]
except IndexError:
logging.error(f'BAZARR is unable to translate malformed subtitles: {source_srt_file}')
return False
@@ -89,14 +105,19 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
message = f"{language_from_alpha2(from_lang)} subtitles translated to {language_from_alpha3(to_lang)}."
+ if media_type == 'series':
+ prr = path_mappings.path_replace_reverse
+ else:
+ prr = path_mappings.path_replace_reverse_movie
+
result = ProcessSubtitlesResult(message=message,
- reversed_path=video_path,
+ reversed_path=prr(video_path),
downloaded_language_code2=to_lang,
downloaded_provider=None,
score=None,
forced=forced,
subtitle_id=None,
- reversed_subtitles_path=dest_srt_file,
+ reversed_subtitles_path=prr(dest_srt_file),
hearing_impaired=hi)
if media_type == 'series':
diff --git a/bazarr/utilities/health.py b/bazarr/utilities/health.py
index 36b1625f1..c1d3a6a3d 100644
--- a/bazarr/utilities/health.py
+++ b/bazarr/utilities/health.py
@@ -1,7 +1,9 @@
# coding=utf-8
+import json
+
from app.config import settings
-from app.database import TableShowsRootfolder, TableMoviesRootfolder, database, select
+from app.database import TableShowsRootfolder, TableMoviesRootfolder, TableLanguagesProfiles, database, select
from app.event_handler import event_stream
from .path_mappings import path_mappings
from sonarr.rootfolder import check_sonarr_rootfolder
@@ -47,4 +49,21 @@ def get_health_issues():
health_issues.append({'object': path_mappings.path_replace_movie(item.path),
'issue': item.error})
+ # get languages profiles duplicate ids issues when there's a cutoff set
+ languages_profiles = database.execute(
+ select(TableLanguagesProfiles.items, TableLanguagesProfiles.name, TableLanguagesProfiles.cutoff)).all()
+ for languages_profile in languages_profiles:
+ if not languages_profile.cutoff:
+ # ignore profiles that don't have a cutoff set
+ continue
+ languages_profile_ids = []
+ for items in json.loads(languages_profile.items):
+ if items['id'] in languages_profile_ids:
+ health_issues.append({'object': languages_profile.name,
+ 'issue': 'This languages profile has duplicate IDs. You need to edit this profile'
+ ' and make sure to select the proper cutoff if required.'})
+ break
+ else:
+ languages_profile_ids.append(items['id'])
+
return health_issues
diff --git a/custom_libs/subliminal/video.py b/custom_libs/subliminal/video.py
index 2168d91a9..66c090945 100644
--- a/custom_libs/subliminal/video.py
+++ b/custom_libs/subliminal/video.py
@@ -130,7 +130,8 @@ class Episode(Video):
"""
def __init__(self, name, series, season, episode, title=None, year=None, original_series=True, tvdb_id=None,
series_tvdb_id=None, series_imdb_id=None, alternative_series=None, series_anidb_id=None,
- series_anidb_episode_id=None, **kwargs):
+ series_anidb_episode_id=None, series_anidb_season_episode_offset=None,
+ anilist_id=None, **kwargs):
super(Episode, self).__init__(name, **kwargs)
#: Series of the episode
@@ -163,8 +164,11 @@ class Episode(Video):
#: Alternative names of the series
self.alternative_series = alternative_series or []
+ #: Anime specific information
self.series_anidb_episode_id = series_anidb_episode_id
self.series_anidb_id = series_anidb_id
+ self.series_anidb_season_episode_offset = series_anidb_season_episode_offset
+ self.anilist_id = anilist_id
@classmethod
def fromguess(cls, name, guess):
@@ -207,10 +211,11 @@ class Movie(Video):
:param str title: title of the movie.
:param int year: year of the movie.
:param list alternative_titles: alternative titles of the movie
+ :param int anilist_id: AniList ID of movie (if Anime)
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
- def __init__(self, name, title, year=None, alternative_titles=None, **kwargs):
+ def __init__(self, name, title, year=None, alternative_titles=None, anilist_id=None, **kwargs):
super(Movie, self).__init__(name, **kwargs)
#: Title of the movie
@@ -221,6 +226,9 @@ class Movie(Video):
#: Alternative titles of the movie
self.alternative_titles = alternative_titles or []
+
+ #: AniList ID of the movie
+ self.anilist_id = anilist_id
@classmethod
def fromguess(cls, name, guess):
diff --git a/custom_libs/subliminal_patch/core.py b/custom_libs/subliminal_patch/core.py
index c1629496b..0fc2ac0a7 100644
--- a/custom_libs/subliminal_patch/core.py
+++ b/custom_libs/subliminal_patch/core.py
@@ -49,7 +49,17 @@ SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl', '
_POOL_LIFETIME = datetime.timedelta(hours=12)
-HI_REGEX = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\(\{].{3,}[\]\)\}](?<!{\\an\d})')
+HI_REGEX_WITHOUT_PARENTHESIS = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\{].{3,}[\]\}](?<!{\\an\d})')
+HI_REGEX_WITH_PARENTHESIS = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\(\{].{3,}[\]\)\}](?<!{\\an\d})')
+
+HI_REGEX_PARENTHESIS_EXCLUDED_LANGUAGES = ['ara']
+
+
+def parse_for_hi_regex(subtitle_text, alpha3_language):
+ if alpha3_language in HI_REGEX_PARENTHESIS_EXCLUDED_LANGUAGES:
+ return bool(re.search(HI_REGEX_WITHOUT_PARENTHESIS, subtitle_text))
+ else:
+ return bool(re.search(HI_REGEX_WITH_PARENTHESIS, subtitle_text))
def remove_crap_from_fn(fn):
@@ -1203,7 +1213,10 @@ def save_subtitles(file_path, subtitles, single=False, directory=None, chmod=Non
continue
# create subtitle path
- if subtitle.text and bool(re.search(HI_REGEX, subtitle.text)):
+ if subtitle.text and parse_for_hi_regex(subtitle_text=subtitle.text,
+ alpha3_language=subtitle.language.alpha3 if
+ (hasattr(subtitle, 'language') and hasattr(subtitle.language, 'alpha3'))
+ else None):
subtitle.language.hi = True
subtitle_path = get_subtitle_path(file_path, None if single else subtitle.language,
forced_tag=subtitle.language.forced,
diff --git a/custom_libs/subliminal_patch/providers/animetosho.py b/custom_libs/subliminal_patch/providers/animetosho.py
index 1fb791e86..9cd3d80b9 100644
--- a/custom_libs/subliminal_patch/providers/animetosho.py
+++ b/custom_libs/subliminal_patch/providers/animetosho.py
@@ -141,7 +141,8 @@ class AnimeToshoProvider(Provider, ProviderSubtitleArchiveMixin):
for subtitle_file in subtitle_files:
hex_id = format(subtitle_file['id'], '08x')
- lang = Language.fromalpha3b(subtitle_file['info']['lang'])
+ # Animetosho assumes missing languages as english as fallback when not specified.
+ lang = Language.fromalpha3b(subtitle_file['info'].get('lang', 'eng'))
# For Portuguese and Portuguese Brazilian they both share the same code, the name is the only
# identifier AnimeTosho provides. Also, some subtitles does not have name, in this case it could
diff --git a/custom_libs/subliminal_patch/providers/avistaz_network.py b/custom_libs/subliminal_patch/providers/avistaz_network.py
index 2c5796ce3..61afefcbf 100644
--- a/custom_libs/subliminal_patch/providers/avistaz_network.py
+++ b/custom_libs/subliminal_patch/providers/avistaz_network.py
@@ -5,7 +5,7 @@ from random import randint
import pycountry
from requests.cookies import RequestsCookieJar
-from subliminal.exceptions import AuthenticationError
+from subliminal.exceptions import AuthenticationError, ProviderError
from subliminal.providers import ParserBeautifulSoup
from subliminal_patch.http import RetryingCFSession
from subliminal_patch.pitcher import store_verification
@@ -318,7 +318,7 @@ class AvistazNetworkProviderBase(Provider):
release_name = release['Title'].get_text().strip()
lang = lookup_lang(subtitle_cols['Language'].get_text().strip())
download_link = subtitle_cols['Download'].a['href']
- uploader_name = subtitle_cols['Uploader'].get_text().strip()
+ uploader_name = subtitle_cols['Uploader'].get_text().strip() if 'Uploader' in subtitle_cols else None
if lang not in languages:
continue
@@ -354,7 +354,10 @@ class AvistazNetworkProviderBase(Provider):
def _parse_release_table(self, html):
release_data_table = (ParserBeautifulSoup(html, ['html.parser'])
- .select_one('#content-area > div:nth-child(4) > div.table-responsive > table > tbody'))
+ .select_one('#content-area > div.block > div.table-responsive > table > tbody'))
+
+ if release_data_table is None:
+ raise ProviderError('Unexpected HTML page layout - no release data table found')
rows = {}
for tr in release_data_table.find_all('tr', recursive=False):
diff --git a/custom_libs/subliminal_patch/providers/embeddedsubtitles.py b/custom_libs/subliminal_patch/providers/embeddedsubtitles.py
index 002f439b7..2d8a492c7 100644
--- a/custom_libs/subliminal_patch/providers/embeddedsubtitles.py
+++ b/custom_libs/subliminal_patch/providers/embeddedsubtitles.py
@@ -112,7 +112,11 @@ class EmbeddedSubtitlesProvider(Provider):
# Default is True
container.FFMPEG_STATS = False
- tags.LANGUAGE_FALLBACK = self._fallback_lang if self._unknown_as_fallback and self._fallback_lang else None
+ tags.LANGUAGE_FALLBACK = (
+ self._fallback_lang
+ if self._unknown_as_fallback and self._fallback_lang
+ else None
+ )
logger.debug("Language fallback set: %s", tags.LANGUAGE_FALLBACK)
def initialize(self):
@@ -194,7 +198,7 @@ class EmbeddedSubtitlesProvider(Provider):
def download_subtitle(self, subtitle: EmbeddedSubtitle):
try:
path = self._get_subtitle_path(subtitle)
- except KeyError: # TODO: add MustGetBlacklisted support
+ except KeyError: # TODO: add MustGetBlacklisted support
logger.error("Couldn't get subtitle path")
return None
@@ -229,6 +233,7 @@ class EmbeddedSubtitlesProvider(Provider):
timeout=self._timeout,
fallback_to_convert=True,
basename_callback=_basename_callback,
+ progress_callback=lambda d: logger.debug("Progress: %s", d),
)
# Add the extracted paths to the containter path key
self._cached_paths[container.path] = extracted
diff --git a/custom_libs/subliminal_patch/providers/hdbits.py b/custom_libs/subliminal_patch/providers/hdbits.py
index 19196aecd..87fdfaa82 100644
--- a/custom_libs/subliminal_patch/providers/hdbits.py
+++ b/custom_libs/subliminal_patch/providers/hdbits.py
@@ -96,7 +96,12 @@ class HDBitsProvider(Provider):
"https://hdbits.org/api/torrents", json={**self._def_params, **lookup}
)
response.raise_for_status()
- ids = [item["id"] for item in response.json()["data"]]
+
+ try:
+ ids = [item["id"] for item in response.json()["data"]]
+ except KeyError:
+ logger.debug("No data found")
+ return []
subtitles = []
for torrent_id in ids:
diff --git a/custom_libs/subliminal_patch/providers/jimaku.py b/custom_libs/subliminal_patch/providers/jimaku.py
new file mode 100644
index 000000000..68393821d
--- /dev/null
+++ b/custom_libs/subliminal_patch/providers/jimaku.py
@@ -0,0 +1,419 @@
+from __future__ import absolute_import
+
+from datetime import timedelta
+import logging
+import os
+import re
+import time
+
+from requests import Session
+from subliminal import region, __short_version__
+from subliminal.cache import REFINER_EXPIRATION_TIME
+from subliminal.exceptions import ConfigurationError, AuthenticationError, ServiceUnavailable
+from subliminal.utils import sanitize
+from subliminal.video import Episode, Movie
+from subliminal_patch.providers import Provider
+from subliminal_patch.subtitle import Subtitle
+from subliminal_patch.exceptions import APIThrottled
+from subliminal_patch.providers.utils import get_subtitle_from_archive, get_archive_from_bytes
+from urllib.parse import urlencode, urljoin
+from guessit import guessit
+from subzero.language import Language, FULL_LANGUAGE_LIST
+
+logger = logging.getLogger(__name__)
+
+# Unhandled formats, such files will always get filtered out
+unhandled_archive_formats = (".7z",)
+accepted_archive_formats = (".zip", ".rar")
+
+class JimakuSubtitle(Subtitle):
+ '''Jimaku Subtitle.'''
+ provider_name = 'jimaku'
+
+ hash_verifiable = False
+
+ def __init__(self, language, video, download_url, filename):
+ super(JimakuSubtitle, self).__init__(language, page_link=download_url)
+
+ self.video = video
+ self.download_url = download_url
+ self.filename = filename
+ self.release_info = filename
+ self.is_archive = filename.endswith(accepted_archive_formats)
+
+ @property
+ def id(self):
+ return self.download_url
+
+ def get_matches(self, video):
+ matches = set()
+
+ # Episode/Movie specific matches
+ if isinstance(video, Episode):
+ if sanitize(video.series) and sanitize(self.video.series) in (
+ sanitize(name) for name in [video.series] + video.alternative_series):
+ matches.add('series')
+
+ if video.season and self.video.season is None or video.season and video.season == self.video.season:
+ matches.add('season')
+ elif isinstance(video, Movie):
+ if sanitize(video.title) and sanitize(self.video.title) in (
+ sanitize(name) for name in [video.title] + video.alternative_titles):
+ matches.add('title')
+
+ # General matches
+ if video.year and video.year == self.video.year:
+ matches.add('year')
+
+ video_type = 'movie' if isinstance(video, Movie) else 'episode'
+ matches.add(video_type)
+
+ guess = guessit(self.filename, {'type': video_type})
+ for g in guess:
+ if g[0] == "release_group" or "source":
+ if video.release_group == g[1]:
+ matches.add('release_group')
+ break
+
+ # Prioritize .srt by repurposing the audio_codec match
+ if self.filename.endswith(".srt"):
+ matches.add('audio_codec')
+
+ return matches
+
+class JimakuProvider(Provider):
+ '''Jimaku Provider.'''
+ video_types = (Episode, Movie)
+
+ api_url = 'https://jimaku.cc/api'
+ api_ratelimit_max_delay_seconds = 5
+ api_ratelimit_backoff_limit = 3
+
+ corrupted_file_size_threshold = 500
+
+ languages = {Language.fromietf("ja")}
+
+ def __init__(self, enable_name_search_fallback, enable_archives_download, enable_ai_subs, api_key):
+ if api_key:
+ self.api_key = api_key
+ else:
+ raise ConfigurationError('Missing api_key.')
+
+ self.enable_name_search_fallback = enable_name_search_fallback
+ self.download_archives = enable_archives_download
+ self.enable_ai_subs = enable_ai_subs
+ self.session = None
+
+ def initialize(self):
+ self.session = Session()
+ self.session.headers['Content-Type'] = 'application/json'
+ self.session.headers['Authorization'] = self.api_key
+ self.session.headers['User-Agent'] = os.environ.get("SZ_USER_AGENT")
+
+ def terminate(self):
+ self.session.close()
+
+ def _query(self, video):
+ if isinstance(video, Movie):
+ media_name = video.title.lower()
+ elif isinstance(video, Episode):
+ media_name = video.series.lower()
+
+ # With entries that have a season larger than 1, Jimaku appends the corresponding season number to the name.
+ # We'll reassemble media_name here to account for cases where we can only search by name alone.
+ season_addendum = str(video.season) if video.season > 1 else None
+ media_name = f"{media_name} {season_addendum}" if season_addendum else media_name
+
+ # Search for entry
+ searching_for_entry_attempts = 0
+ additional_url_params = {}
+ while searching_for_entry_attempts < 2:
+ searching_for_entry_attempts += 1
+ url = self._assemble_jimaku_search_url(video, media_name, additional_url_params)
+ if not url:
+ return None
+
+ searching_for_entry = "query" in url
+ data = self._search_for_entry(url)
+
+ if not data:
+ if searching_for_entry and searching_for_entry_attempts < 2:
+ logger.info("Maybe this is live action media? Will retry search without anime parameter...")
+ additional_url_params = {'anime': "false"}
+ else:
+ return None
+ else:
+ break
+
+ # We only go for the first entry
+ entry = data[0]
+
+ entry_id = entry.get('id')
+ anilist_id = entry.get('anilist_id', None)
+ entry_name = entry.get('name')
+ is_movie = entry.get('flags', {}).get('movie', False)
+
+ if isinstance(video, Episode) and is_movie:
+ logger.warn("Bazarr thinks this is a series, but Jimaku says this is a movie! May not be able to match subtitles...")
+
+ logger.info(f"Matched entry: ID: '{entry_id}', anilist_id: '{anilist_id}', name: '{entry_name}', english_name: '{entry.get('english_name')}', movie: {is_movie}")
+ if entry.get("flags").get("unverified"):
+ logger.warning(f"This entry '{entry_id}' is unverified, subtitles might be incomplete or have quality issues!")
+
+ # Get a list of subtitles for entry
+ episode_number = video.episode if "episode" in dir(video) else None
+ url_params = {'episode': episode_number} if isinstance(video, Episode) and not is_movie else {}
+ only_look_for_archives = False
+
+ has_offset = isinstance(video, Episode) and video.series_anidb_season_episode_offset is not None
+
+ retry_count = 0
+ adjusted_ep_num = None
+ while retry_count <= 1:
+ # Account for positive episode offset first
+ if isinstance(video, Episode) and not is_movie and retry_count < 1:
+ if video.season > 1 and has_offset:
+ offset_value = video.series_anidb_season_episode_offset
+ offset_value = offset_value if offset_value > 0 else -offset_value
+
+ if episode_number < offset_value:
+ adjusted_ep_num = episode_number + offset_value
+ logger.warning(f"Will try using adjusted episode number {adjusted_ep_num} first")
+ url_params = {'episode': adjusted_ep_num}
+
+ url = f"entries/{entry_id}/files"
+ data = self._search_for_subtitles(url, url_params)
+
+ if not data:
+ if isinstance(video, Episode) and not is_movie and has_offset and retry_count < 1:
+ logger.warning(f"Found no subtitles for adjusted episode number, but will retry with normal episode number {episode_number}")
+ url_params = {'episode': episode_number}
+ elif isinstance(video, Episode) and not is_movie and retry_count < 1:
+ logger.warning(f"Found no subtitles for episode number {episode_number}, but will retry without 'episode' parameter")
+ url_params = {}
+ only_look_for_archives = True
+ else:
+ return None
+
+ retry_count += 1
+ else:
+ if adjusted_ep_num:
+ video.episode = adjusted_ep_num
+ logger.debug(f"This videos episode attribute has been updated to: {video.episode}")
+ break
+
+ # Filter subtitles
+ list_of_subtitles = []
+
+ data = [item for item in data if not item['name'].endswith(unhandled_archive_formats)]
+
+ # Detect only archives being uploaded
+ archive_entries = [item for item in data if item['name'].endswith(accepted_archive_formats)]
+ subtitle_entries = [item for item in data if not item['name'].endswith(accepted_archive_formats)]
+ has_only_archives = len(archive_entries) > 0 and len(subtitle_entries) == 0
+ if has_only_archives:
+ logger.warning("Have only found archived subtitles")
+
+ elif only_look_for_archives:
+ data = [item for item in data if item['name'].endswith(accepted_archive_formats)]
+
+ for item in data:
+ filename = item.get('name')
+ download_url = item.get('url')
+ is_archive = filename.endswith(accepted_archive_formats)
+
+ # Archives will still be considered if they're the only files available, as is mostly the case for movies.
+ if is_archive and not has_only_archives and not self.download_archives:
+ logger.warning(f"Skipping archive '{filename}' because normal subtitles are available instead")
+ continue
+
+ if not self.enable_ai_subs:
+ p = re.compile(r'[\[\(]?(whisperai)[\]\)]?|[\[\(]whisper[\]\)]', re.IGNORECASE)
+ if p.search(filename):
+ logger.warning(f"Skipping subtitle '{filename}' as it's suspected of being AI generated")
+ continue
+
+ sub_languages = self._try_determine_subtitle_languages(filename)
+ if len(sub_languages) > 1:
+ logger.warning(f"Skipping subtitle '{filename}' as it's suspected of containing multiple languages")
+ continue
+
+ # Check if file is obviously corrupt. If no size is returned, assume OK
+ filesize = item.get('size', self.corrupted_file_size_threshold)
+ if filesize < self.corrupted_file_size_threshold:
+ logger.warning(f"Skipping possibly corrupt file '{filename}': Filesize is just {filesize} bytes")
+ continue
+
+ if not filename.endswith(unhandled_archive_formats):
+ lang = sub_languages[0] if len(sub_languages) > 1 else Language("jpn")
+ list_of_subtitles.append(JimakuSubtitle(lang, video, download_url, filename))
+ else:
+ logger.debug(f"Skipping archive '{filename}' as it's not a supported format")
+
+ return list_of_subtitles
+
+ def list_subtitles(self, video, languages=None):
+ subtitles = self._query(video)
+ if not subtitles:
+ return []
+
+ return [s for s in subtitles]
+
+ def download_subtitle(self, subtitle: JimakuSubtitle):
+ target_url = subtitle.download_url
+ response = self.session.get(target_url, timeout=10)
+ response.raise_for_status()
+
+ if subtitle.is_archive:
+ archive = get_archive_from_bytes(response.content)
+ if archive:
+ if isinstance(subtitle.video, Episode):
+ subtitle.content = get_subtitle_from_archive(
+ archive,
+ episode=subtitle.video.episode,
+ episode_title=subtitle.video.title
+ )
+ else:
+ subtitle.content = get_subtitle_from_archive(
+ archive
+ )
+ else:
+ logger.warning("Archive seems to not be an archive! File possibly corrupt?")
+ return None
+ else:
+ subtitle.content = response.content
+
+ def _do_jimaku_request(self, url_path, url_params={}):
+ url = urljoin(f"{self.api_url}/{url_path}", '?' + urlencode(url_params))
+
+ retry_count = 0
+ while retry_count < self.api_ratelimit_backoff_limit:
+ response = self.session.get(url, timeout=10)
+
+ if response.status_code == 429:
+ reset_time = 5
+ retry_count + 1
+
+ logger.warning(f"Jimaku ratelimit hit, waiting for '{reset_time}' seconds ({retry_count}/{self.api_ratelimit_backoff_limit} tries)")
+ time.sleep(reset_time)
+ continue
+ elif response.status_code == 401:
+ raise AuthenticationError("Unauthorized. API key possibly invalid")
+ else:
+ response.raise_for_status()
+
+ data = response.json()
+ logger.debug(f"Length of response on {url}: {len(data)}")
+ if len(data) == 0:
+ logger.error(f"Jimaku returned no items for our our query: {url}")
+ return None
+ elif 'error' in data:
+ raise ServiceUnavailable(f"Jimaku returned an error: '{data.get('error')}', Code: '{data.get('code')}'")
+ else:
+ return data
+
+ raise APIThrottled(f"Jimaku ratelimit max backoff limit of {self.api_ratelimit_backoff_limit} reached, aborting")
+
+ # Wrapper functions to indirectly call _do_jimaku_request with different cache configs
+ @region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
+ def _search_for_entry(self, url_path, url_params={}):
+ return self._do_jimaku_request(url_path, url_params)
+
+ @region.cache_on_arguments(expiration_time=timedelta(minutes=1).total_seconds())
+ def _search_for_subtitles(self, url_path, url_params={}):
+ return self._do_jimaku_request(url_path, url_params)
+
+ @staticmethod
+ def _try_determine_subtitle_languages(filename):
+ # This is more like a guess and not a 100% fool-proof way of detecting multi-lang subs:
+ # It assumes that language codes, if present, are in the last metadata group of the subs filename.
+ # If such codes are not present, or we failed to match any at all, then we'll just assume that the sub is purely Japanese.
+ default_language = Language("jpn")
+
+ dot_delimit = filename.split(".")
+ bracket_delimit = re.split(r'[\[\]\(\)]+', filename)
+
+ candidate_list = list()
+ if len(dot_delimit) > 2:
+ candidate_list = dot_delimit[-2]
+ elif len(bracket_delimit) > 2:
+ candidate_list = bracket_delimit[-2]
+
+ candidates = [] if len(candidate_list) == 0 else re.split(r'[,\-\+\& ]+', candidate_list)
+
+ # Discard match group if any candidate...
+ # ...contains any numbers, as the group is likely encoding information
+ if any(re.compile(r'\d').search(string) for string in candidates):
+ return [default_language]
+ # ...is >= 5 chars long, as the group is likely other unrelated metadata
+ if any(len(string) >= 5 for string in candidates):
+ return [default_language]
+
+ languages = list()
+ for candidate in candidates:
+ candidate = candidate.lower()
+ if candidate in ["ass", "srt"]:
+ continue
+
+ # Sometimes, languages are hidden in 4 character blocks, i.e. "JPSC"
+ if len(candidate) == 4:
+ for addendum in [candidate[:2], candidate[2:]]:
+ candidates.append(addendum)
+ continue
+
+ # Sometimes, language codes can have additional info such as 'cc' or 'sdh'. For example: "ja[cc]"
+ if len(dot_delimit) > 2 and any(c in candidate for c in '[]()'):
+ candidate = re.split(r'[\[\]\(\)]+', candidate)[0]
+
+ try:
+ language_squash = {
+ "jp": "ja",
+ "jap": "ja",
+ "chs": "zho",
+ "cht": "zho",
+ "zhi": "zho",
+ "cn": "zho"
+ }
+
+ candidate = language_squash[candidate] if candidate in language_squash else candidate
+ if len(candidate) > 2:
+ language = Language(candidate)
+ else:
+ language = Language.fromietf(candidate)
+
+ if not any(l.alpha3 == language.alpha3 for l in languages):
+ languages.append(language)
+ except:
+ if candidate in FULL_LANGUAGE_LIST:
+ # Create a dummy for the unknown language
+ languages.append(Language("zul"))
+
+ if len(languages) > 1:
+ # Sometimes a metadata group that actually contains info about codecs gets processed as valid languages.
+ # To prevent false positives, we'll check if Japanese language codes are in the processed languages list.
+ # If not, then it's likely that we didn't actually match language codes -> Assume Japanese only subtitle.
+ contains_jpn = any([l for l in languages if l.alpha3 == "jpn"])
+
+ return languages if contains_jpn else [Language("jpn")]
+ else:
+ return [default_language]
+
+ def _assemble_jimaku_search_url(self, video, media_name, additional_params={}):
+ endpoint = "entries/search"
+ anilist_id = video.anilist_id
+
+ params = {}
+ if anilist_id:
+ params = {'anilist_id': anilist_id}
+ else:
+ if self.enable_name_search_fallback or isinstance(video, Movie):
+ params = {'query': media_name}
+ else:
+ logger.error(f"Skipping '{media_name}': Got no AniList ID and fuzzy matching using name is disabled")
+ return None
+
+ if additional_params:
+ params.update(additional_params)
+
+ logger.info(f"Will search for entry based on params: {params}")
+ return urljoin(endpoint, '?' + urlencode(params)) \ No newline at end of file
diff --git a/custom_libs/subliminal_patch/providers/legendasnet.py b/custom_libs/subliminal_patch/providers/legendasnet.py
new file mode 100644
index 000000000..b5c8e0cd9
--- /dev/null
+++ b/custom_libs/subliminal_patch/providers/legendasnet.py
@@ -0,0 +1,264 @@
+# -*- coding: utf-8 -*-
+import logging
+import os
+import time
+import io
+import json
+
+from zipfile import ZipFile, is_zipfile
+from urllib.parse import urljoin
+from requests import Session
+
+from subzero.language import Language
+from subliminal import Episode, Movie
+from subliminal.exceptions import ConfigurationError, ProviderError, DownloadLimitExceeded
+from subliminal_patch.exceptions import APIThrottled
+from .mixins import ProviderRetryMixin
+from subliminal_patch.subtitle import Subtitle
+from subliminal.subtitle import fix_line_ending
+from subliminal_patch.providers import Provider
+from subliminal_patch.providers import utils
+
+logger = logging.getLogger(__name__)
+
+retry_amount = 3
+retry_timeout = 5
+
+
+class LegendasNetSubtitle(Subtitle):
+ provider_name = 'legendasnet'
+ hash_verifiable = False
+
+ def __init__(self, language, forced, page_link, download_link, file_id, release_names, uploader,
+ season=None, episode=None):
+ super().__init__(language)
+ language = Language.rebuild(language, forced=forced)
+
+ self.season = season
+ self.episode = episode
+ self.releases = release_names
+ self.release_info = ', '.join(release_names)
+ self.language = language
+ self.forced = forced
+ self.file_id = file_id
+ self.page_link = page_link
+ self.download_link = download_link
+ self.uploader = uploader
+ self.matches = None
+
+ @property
+ def id(self):
+ return self.file_id
+
+ def get_matches(self, video):
+ matches = set()
+
+ # handle movies and series separately
+ if isinstance(video, Episode):
+ # series
+ matches.add('series')
+ # season
+ if video.season == self.season:
+ matches.add('season')
+ # episode
+ if video.episode == self.episode:
+ matches.add('episode')
+ # imdb
+ matches.add('series_imdb_id')
+ else:
+ # title
+ matches.add('title')
+ # imdb
+ matches.add('imdb_id')
+
+ utils.update_matches(matches, video, self.release_info)
+
+ self.matches = matches
+
+ return matches
+
+
+class LegendasNetProvider(ProviderRetryMixin, Provider):
+ """Legendas.Net Provider"""
+ server_hostname = 'legendas.net/api'
+
+ languages = {Language('por', 'BR')}
+ video_types = (Episode, Movie)
+
+ def __init__(self, username, password):
+ self.session = Session()
+ self.session.headers = {'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")}
+ self.username = username
+ self.password = password
+ self.access_token = None
+ self.video = None
+ self._started = None
+ self.login()
+
+ def login(self):
+ headersList = {
+ "Accept": "*/*",
+ "User-Agent": self.session.headers['User-Agent'],
+ "Content-Type": "application/json"
+ }
+
+ payload = json.dumps({
+ "email": self.username,
+ "password": self.password
+ })
+
+ response = self.session.request("POST", self.server_url() + 'login', data=payload, headers=headersList)
+ if response.status_code != 200:
+ raise ConfigurationError('Failed to login and retrieve access token')
+ self.access_token = response.json().get('access_token')
+ if not self.access_token:
+ raise ConfigurationError('Access token not found in login response')
+ self.session.headers.update({'Authorization': f'Bearer {self.access_token}'})
+
+ def initialize(self):
+ self._started = time.time()
+
+ def terminate(self):
+ self.session.close()
+
+ def server_url(self):
+ return f'https://{self.server_hostname}/v1/'
+
+ def query(self, languages, video):
+ self.video = video
+
+ # query the server
+ if isinstance(self.video, Episode):
+ res = self.retry(
+ lambda: self.session.get(self.server_url() + 'search/tv',
+ json={
+ 'name': video.series,
+ 'page': 1,
+ 'per_page': 25,
+ 'tv_episode': video.episode,
+ 'tv_season': video.season,
+ 'imdb_id': video.series_imdb_id
+ },
+ headers={'Content-Type': 'application/json'},
+ timeout=30),
+ amount=retry_amount,
+ retry_timeout=retry_timeout
+ )
+ else:
+ res = self.retry(
+ lambda: self.session.get(self.server_url() + 'search/movie',
+ json={
+ 'name': video.title,
+ 'page': 1,
+ 'per_page': 25,
+ 'imdb_id': video.imdb_id
+ },
+ headers={'Content-Type': 'application/json'},
+ timeout=30),
+ amount=retry_amount,
+ retry_timeout=retry_timeout
+ )
+
+ if res.status_code == 404:
+ logger.error(f"Endpoint not found: {res.url}")
+ raise ProviderError("Endpoint not found")
+ elif res.status_code == 429:
+ raise APIThrottled("Too many requests")
+ elif res.status_code == 403:
+ raise ConfigurationError("Invalid access token")
+ elif res.status_code != 200:
+ res.raise_for_status()
+
+ subtitles = []
+
+ result = res.json()
+
+ if ('success' in result and not result['success']) or ('status' in result and not result['status']):
+ logger.debug(result["error"])
+ return []
+
+ if isinstance(self.video, Episode):
+ if len(result['tv_shows']):
+ for item in result['tv_shows']:
+ subtitle = LegendasNetSubtitle(
+ language=Language('por', 'BR'),
+ forced=self._is_forced(item),
+ page_link=f"https://legendas.net/tv_legenda?movie_id={result['tv_shows'][0]['tmdb_id']}&"
+ f"legenda_id={item['id']}",
+ download_link=item['path'],
+ file_id=item['id'],
+ release_names=[item.get('release_name', '')],
+ uploader=item['uploader'],
+ season=item.get('season', ''),
+ episode=item.get('episode', '')
+ )
+ subtitle.get_matches(self.video)
+ if subtitle.language in languages:
+ subtitles.append(subtitle)
+ else:
+ if len(result['movies']):
+ for item in result['movies']:
+ subtitle = LegendasNetSubtitle(
+ language=Language('por', 'BR'),
+ forced=self._is_forced(item),
+ page_link=f"https://legendas.net/legenda?movie_id={result['movies'][0]['tmdb_id']}&"
+ f"legenda_id={item['id']}",
+ download_link=item['path'],
+ file_id=item['id'],
+ release_names=[item.get('release_name', '')],
+ uploader=item['uploader'],
+ season=None,
+ episode=None
+ )
+ subtitle.get_matches(self.video)
+ if subtitle.language in languages:
+ subtitles.append(subtitle)
+
+ return subtitles
+
+ @staticmethod
+ def _is_forced(item):
+ forced_tags = ['forced', 'foreign']
+ for tag in forced_tags:
+ if tag in item.get('comment', '').lower():
+ return True
+
+ # nothing match so we consider it as normal subtitles
+ return False
+
+ def list_subtitles(self, video, languages):
+ return self.query(languages, video)
+
+ def download_subtitle(self, subtitle):
+ logger.debug('Downloading subtitle %r', subtitle)
+ download_link = urljoin("https://legendas.net", subtitle.download_link)
+
+ r = self.retry(
+ lambda: self.session.get(download_link, timeout=30),
+ amount=retry_amount,
+ retry_timeout=retry_timeout
+ )
+
+ if r.status_code == 429:
+ raise DownloadLimitExceeded("Daily download limit exceeded")
+ elif r.status_code == 403:
+ raise ConfigurationError("Invalid access token")
+ elif r.status_code != 200:
+ r.raise_for_status()
+
+ if not r:
+ logger.error(f'Could not download subtitle from {download_link}')
+ subtitle.content = None
+ return
+ else:
+ archive_stream = io.BytesIO(r.content)
+ if is_zipfile(archive_stream):
+ archive = ZipFile(archive_stream)
+ for name in archive.namelist():
+ subtitle_content = archive.read(name)
+ subtitle.content = fix_line_ending(subtitle_content)
+ return
+ else:
+ subtitle_content = r.content
+ subtitle.content = fix_line_ending(subtitle_content)
+ return
diff --git a/custom_libs/subliminal_patch/providers/podnapisi.py b/custom_libs/subliminal_patch/providers/podnapisi.py
index 18131ff52..d20accb99 100644
--- a/custom_libs/subliminal_patch/providers/podnapisi.py
+++ b/custom_libs/subliminal_patch/providers/podnapisi.py
@@ -209,7 +209,8 @@ class PodnapisiProvider(_PodnapisiProvider, ProviderSubtitleArchiveMixin):
break
# exit if no results
- if not xml.find('pagination/results') or not int(xml.find('pagination/results').text):
+ if (not xml.find('pagination/results') or not xml.find('pagination/results').text or not
+ int(xml.find('pagination/results').text)):
logger.debug('No subtitles found')
break
diff --git a/custom_libs/subliminal_patch/providers/soustitreseu.py b/custom_libs/subliminal_patch/providers/soustitreseu.py
index 945b4a21b..727a70458 100644
--- a/custom_libs/subliminal_patch/providers/soustitreseu.py
+++ b/custom_libs/subliminal_patch/providers/soustitreseu.py
@@ -277,7 +277,11 @@ class SoustitreseuProvider(Provider, ProviderSubtitleArchiveMixin):
release = name[:-4].lower().rstrip('tag').rstrip('en').rstrip('fr')
_guess = guessit(release)
if isinstance(video, Episode):
- if video.episode != _guess['episode'] or video.season != _guess['season']:
+ try:
+ if video.episode != _guess['episode'] or video.season != _guess['season']:
+ continue
+ except KeyError:
+ # episode or season are missing from guessit result
continue
matches = set()
diff --git a/custom_libs/subliminal_patch/providers/subdivx.py b/custom_libs/subliminal_patch/providers/subdivx.py
index a19ae1b5e..720cba3ed 100644
--- a/custom_libs/subliminal_patch/providers/subdivx.py
+++ b/custom_libs/subliminal_patch/providers/subdivx.py
@@ -172,7 +172,7 @@ class SubdivxSubtitlesProvider(Provider):
logger.debug("Query: %s", query)
- response = self.session.post(search_link, data=payload)
+ response = self.session.post(search_link, data=payload, timeout=30)
if response.status_code == 500:
logger.debug(
diff --git a/custom_libs/subliminal_patch/providers/subdl.py b/custom_libs/subliminal_patch/providers/subdl.py
index bf4dc79d2..102125eae 100644
--- a/custom_libs/subliminal_patch/providers/subdl.py
+++ b/custom_libs/subliminal_patch/providers/subdl.py
@@ -17,8 +17,7 @@ from .mixins import ProviderRetryMixin
from subliminal_patch.subtitle import Subtitle
from subliminal.subtitle import fix_line_ending
from subliminal_patch.providers import Provider
-from subliminal_patch.subtitle import guess_matches
-from guessit import guessit
+from subliminal_patch.providers import utils
logger = logging.getLogger(__name__)
@@ -27,8 +26,6 @@ retry_timeout = 5
language_converters.register('subdl = subliminal_patch.converters.subdl:SubdlConverter')
-supported_languages = list(language_converters['subdl'].to_subdl.keys())
-
class SubdlSubtitle(Subtitle):
provider_name = 'subdl'
@@ -59,7 +56,6 @@ class SubdlSubtitle(Subtitle):
def get_matches(self, video):
matches = set()
- type_ = "movie" if isinstance(video, Movie) else "episode"
# handle movies and series separately
if isinstance(video, Episode):
@@ -79,8 +75,7 @@ class SubdlSubtitle(Subtitle):
# imdb
matches.add('imdb_id')
- # other properties
- matches |= guess_matches(video, guessit(self.release_info, {"type": type_}))
+ utils.update_matches(matches, video, self.release_info)
self.matches = matches
@@ -91,7 +86,7 @@ class SubdlProvider(ProviderRetryMixin, Provider):
"""Subdl Provider"""
server_hostname = 'api.subdl.com'
- languages = {Language(*lang) for lang in supported_languages}
+ languages = {Language(*lang) for lang in list(language_converters['subdl'].to_subdl.keys())}
languages.update(set(Language.rebuild(lang, forced=True) for lang in languages))
languages.update(set(Language.rebuild(l, hi=True) for l in languages))
@@ -130,7 +125,8 @@ class SubdlProvider(ProviderRetryMixin, Provider):
imdb_id = self.video.imdb_id
# be sure to remove duplicates using list(set())
- langs_list = sorted(list(set([lang.basename.upper() for lang in languages])))
+ langs_list = sorted(list(set([language_converters['subdl'].convert(lang.alpha3, lang.country, lang.script) for
+ lang in languages])))
langs = ','.join(langs_list)
logger.debug(f'Searching for those languages: {langs}')
@@ -148,7 +144,9 @@ class SubdlProvider(ProviderRetryMixin, Provider):
('subs_per_page', 30),
('type', 'tv'),
('comment', 1),
- ('releases', 1)),
+ ('releases', 1),
+ ('bazarr', 1)), # this argument filter incompatible image based or
+ # txt subtitles
timeout=30),
amount=retry_amount,
retry_timeout=retry_timeout
@@ -163,7 +161,9 @@ class SubdlProvider(ProviderRetryMixin, Provider):
('subs_per_page', 30),
('type', 'movie'),
('comment', 1),
- ('releases', 1)),
+ ('releases', 1),
+ ('bazarr', 1)), # this argument filter incompatible image based or
+ # txt subtitles
timeout=30),
amount=retry_amount,
retry_timeout=retry_timeout
@@ -181,7 +181,8 @@ class SubdlProvider(ProviderRetryMixin, Provider):
result = res.json()
if ('success' in result and not result['success']) or ('status' in result and not result['status']):
- raise ProviderError(result['error'])
+ logger.debug(result["error"])
+ return []
logger.debug(f"Query returned {len(result['subtitles'])} subtitles")
diff --git a/custom_libs/subliminal_patch/providers/subf2m.py b/custom_libs/subliminal_patch/providers/subf2m.py
index 6b3a59129..1ac0b4048 100644
--- a/custom_libs/subliminal_patch/providers/subf2m.py
+++ b/custom_libs/subliminal_patch/providers/subf2m.py
@@ -132,9 +132,9 @@ _DEFAULT_HEADERS = {
class Subf2mProvider(Provider):
provider_name = "subf2m"
- _movie_title_regex = re.compile(r"^(.+?)( \((\d{4})\))?$")
+ _movie_title_regex = re.compile(r"^(.+?)(\s+\((\d{4})\))?$")
_tv_show_title_regex = re.compile(
- r"^(.+?) [-\(]\s?(.*?) (season|series)\)?( \((\d{4})\))?$"
+ r"^(.+?)\s+[-\(]\s?(.*?)\s+(season|series)\)?(\s+\((\d{4})\))?$"
)
_tv_show_title_alt_regex = re.compile(r"(.+)\s(\d{1,2})(?:\s|$)")
_supported_languages = {}
@@ -220,7 +220,7 @@ class Subf2mProvider(Provider):
results = []
for result in self._gen_results(title):
- text = result.text.lower()
+ text = result.text.strip().lower()
match = self._movie_title_regex.match(text)
if not match:
continue
@@ -254,7 +254,7 @@ class Subf2mProvider(Provider):
results = []
for result in self._gen_results(title):
- text = result.text.lower()
+ text = result.text.strip().lower()
match = self._tv_show_title_regex.match(text)
if not match:
diff --git a/custom_libs/subliminal_patch/providers/supersubtitles.py b/custom_libs/subliminal_patch/providers/supersubtitles.py
index c3ecb06a3..cc7752925 100644
--- a/custom_libs/subliminal_patch/providers/supersubtitles.py
+++ b/custom_libs/subliminal_patch/providers/supersubtitles.py
@@ -455,7 +455,13 @@ class SuperSubtitlesProvider(Provider, ProviderSubtitleArchiveMixin):
soup = ParserBeautifulSoup(r, ['lxml'])
tables = soup.find_all("table")
- tables = tables[0].find_all("tr")
+
+ try:
+ tables = tables[0].find_all("tr")
+ except IndexError:
+ logger.debug("No tables found for %s", url)
+ return []
+
i = 0
for table in tables:
diff --git a/custom_libs/subliminal_patch/providers/utils.py b/custom_libs/subliminal_patch/providers/utils.py
index 2feaecea2..206334462 100644
--- a/custom_libs/subliminal_patch/providers/utils.py
+++ b/custom_libs/subliminal_patch/providers/utils.py
@@ -65,7 +65,7 @@ def _get_matching_sub(
guess = guessit(sub_name, options=guess_options)
matched_episode_num = guess.get("episode")
- if matched_episode_num:
+ if not matched_episode_num:
logger.debug("No episode number found in file: %s", sub_name)
if episode_title is not None:
@@ -86,11 +86,13 @@ def _get_matching_sub(
return None
-def _analize_sub_name(sub_name: str, title_):
- titles = re.split(r"[.-]", os.path.splitext(sub_name)[0])
+def _analize_sub_name(sub_name: str, title_: str):
+ titles = re.split(r"[\s_\.\+]?[.-][\s_\.\+]?", os.path.splitext(sub_name)[0])
+
for title in titles:
title = title.strip()
- ratio = SequenceMatcher(None, title, title_).ratio()
+ ratio = SequenceMatcher(None, title.lower(), title_.lower()).ratio()
+
if ratio > 0.85:
logger.debug(
"Episode title matched: '%s' -> '%s' [%s]", title, sub_name, ratio
diff --git a/custom_libs/subliminal_patch/providers/whisperai.py b/custom_libs/subliminal_patch/providers/whisperai.py
index d427f8ad2..866585cdb 100644
--- a/custom_libs/subliminal_patch/providers/whisperai.py
+++ b/custom_libs/subliminal_patch/providers/whisperai.py
@@ -143,7 +143,7 @@ def encode_audio_stream(path, ffmpeg_path, audio_stream_language=None):
logger.debug(f"Whisper will only use the {audio_stream_language} audio stream for {path}")
inp = inp[f'a:m:language:{audio_stream_language}']
- out, _ = inp.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=16000) \
+ out, _ = inp.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=16000, af="aresample=async=1") \
.run(cmd=[ffmpeg_path, "-nostdin"], capture_stdout=True, capture_stderr=True)
except ffmpeg.Error as e:
diff --git a/custom_libs/subliminal_patch/providers/zimuku.py b/custom_libs/subliminal_patch/providers/zimuku.py
index 7c66bfc6a..9fc97eba0 100644
--- a/custom_libs/subliminal_patch/providers/zimuku.py
+++ b/custom_libs/subliminal_patch/providers/zimuku.py
@@ -316,7 +316,7 @@ class ZimukuProvider(Provider):
r = self.yunsuo_bypass(download_link, headers={'Referer': subtitle.page_link}, timeout=30)
r.raise_for_status()
try:
- filename = r.headers["Content-Disposition"]
+ filename = r.headers["Content-Disposition"].lower()
except KeyError:
logger.debug("Unable to parse subtitles filename. Dropping this subtitles.")
return
diff --git a/custom_libs/subliminal_patch/subtitle.py b/custom_libs/subliminal_patch/subtitle.py
index c19acd1ec..c65f8cdd2 100644
--- a/custom_libs/subliminal_patch/subtitle.py
+++ b/custom_libs/subliminal_patch/subtitle.py
@@ -12,6 +12,7 @@ import chardet
import pysrt
import pysubs2
from bs4 import UnicodeDammit
+from copy import deepcopy
from pysubs2 import SSAStyle
from pysubs2.formats.subrip import parse_tags, MAX_REPRESENTABLE_TIME
from pysubs2.time import ms_to_times
@@ -65,6 +66,11 @@ class Subtitle(Subtitle_):
# format = "srt" # default format is srt
def __init__(self, language, hearing_impaired=False, page_link=None, encoding=None, mods=None, original_format=False):
+ # language needs to be cloned because it is actually a reference to the provider language object
+ # if a new copy is not created then all subsequent subtitles for this provider will incorrectly be modified
+ # at least until Bazarr is restarted or the provider language object is recreated somehow
+ language = deepcopy(language)
+
# set subtitle language to hi if it's hearing_impaired
if hearing_impaired:
language = Language.rebuild(language, hi=True)
@@ -275,7 +281,7 @@ class Subtitle(Subtitle_):
return encoding
def is_valid(self):
- """Check if a :attr:`text` is a valid SubRip format. Note that orignal format will pypass the checking
+ """Check if a :attr:`text` is a valid SubRip format. Note that original format will bypass the checking
:return: whether or not the subtitle is valid.
:rtype: bool
diff --git a/custom_libs/subliminal_patch/video.py b/custom_libs/subliminal_patch/video.py
index f5df0c92e..96101cf54 100644
--- a/custom_libs/subliminal_patch/video.py
+++ b/custom_libs/subliminal_patch/video.py
@@ -35,6 +35,8 @@ class Video(Video_):
info_url=None,
series_anidb_id=None,
series_anidb_episode_id=None,
+ series_anidb_season_episode_offset=None,
+ anilist_id=None,
**kwargs
):
super(Video, self).__init__(
@@ -61,3 +63,5 @@ class Video(Video_):
self.info_url = info_url
self.series_anidb_series_id = series_anidb_id,
self.series_anidb_episode_id = series_anidb_episode_id,
+ self.series_anidb_season_episode_offset = series_anidb_season_episode_offset,
+ self.anilist_id = anilist_id,
diff --git a/custom_libs/subzero/language.py b/custom_libs/subzero/language.py
index 3d556c0e1..99b64211c 100644
--- a/custom_libs/subzero/language.py
+++ b/custom_libs/subzero/language.py
@@ -162,14 +162,4 @@ class Language(Language_):
return Language(*Language_.fromalpha3b(s).__getstate__())
-IETF_MATCH = ".+\.([^-.]+)(?:-[A-Za-z]+)?$"
-ENDSWITH_LANGUAGECODE_RE = re.compile("\.([^-.]{2,3})(?:-[A-Za-z]{2,})?$")
-
-
-def match_ietf_language(s, ietf=False):
- language_match = re.match(".+\.([^\.]+)$" if not ietf
- else IETF_MATCH, s)
- if language_match and len(language_match.groups()) == 1:
- language = language_match.groups()[0]
- return language
- return s
+ENDSWITH_LANGUAGECODE_RE = re.compile(r"\.([^-.]{2,3})(?:-[A-Za-z]{2,})?$")
diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json
index 97b3d3de5..f1418bfdf 100644
--- a/frontend/.eslintrc.json
+++ b/frontend/.eslintrc.json
@@ -15,12 +15,11 @@
"@typescript-eslint/no-unused-vars": "warn"
},
"extends": [
- "react-app",
- "plugin:react-hooks/recommended",
"eslint:recommended",
+ "plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended"
],
- "plugins": ["testing-library", "simple-import-sort"],
+ "plugins": ["testing-library", "simple-import-sort", "react-refresh"],
"overrides": [
{
"files": [
@@ -63,6 +62,7 @@
}
}
],
+ "parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest"
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 38d3e3f76..e189952d2 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,12 +9,12 @@
"version": "1.0.0",
"license": "GPL-3",
"dependencies": {
- "@mantine/core": "^7.11.0",
- "@mantine/dropzone": "^7.11.0",
- "@mantine/form": "^7.11.0",
- "@mantine/hooks": "^7.11.0",
- "@mantine/modals": "^7.11.0",
- "@mantine/notifications": "^7.11.0",
+ "@mantine/core": "^7.12.2",
+ "@mantine/dropzone": "^7.12.2",
+ "@mantine/form": "^7.12.2",
+ "@mantine/hooks": "^7.12.2",
+ "@mantine/modals": "^7.12.2",
+ "@mantine/notifications": "^7.12.2",
"@tanstack/react-query": "^5.40.1",
"@tanstack/react-table": "^8.19.2",
"axios": "^1.6.8",
@@ -26,10 +26,10 @@
},
"devDependencies": {
"@fontsource/roboto": "^5.0.12",
- "@fortawesome/fontawesome-svg-core": "^6.5.2",
- "@fortawesome/free-brands-svg-icons": "^6.5.2",
- "@fortawesome/free-regular-svg-icons": "^6.5.2",
- "@fortawesome/free-solid-svg-icons": "^6.5.2",
+ "@fortawesome/fontawesome-svg-core": "^6.6.0",
+ "@fortawesome/free-brands-svg-icons": "^6.6.0",
+ "@fortawesome/free-regular-svg-icons": "^6.6.0",
+ "@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/react-query-devtools": "^5.40.1",
"@testing-library/jest-dom": "^6.4.2",
@@ -38,16 +38,18 @@
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.1",
"@types/node": "^20.12.6",
- "@types/react": "^18.3.3",
+ "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
+ "@typescript-eslint/eslint-plugin": "^7.16.0",
+ "@typescript-eslint/parser": "^7.16.0",
"@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.2.2",
"clsx": "^2.1.0",
"eslint": "^8.57.0",
- "eslint-config-react-app": "^7.0.1",
"eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-simple-import-sort": "^12.1.0",
"eslint-plugin-testing-library": "^6.2.0",
"husky": "^9.0.11",
@@ -58,7 +60,7 @@
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"pretty-quick": "^4.0.0",
- "recharts": "^2.12.6",
+ "recharts": "^2.12.7",
"sass": "^1.74.1",
"typescript": "^5.4.4",
"vite": "^5.2.8",
@@ -164,33 +166,6 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
- "node_modules/@babel/eslint-parser": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.1.tgz",
- "integrity": "sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ==",
- "dev": true,
- "dependencies": {
- "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
- "eslint-visitor-keys": "^2.1.0",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": "^10.13.0 || ^12.13.0 || >=14.0.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.11.0",
- "eslint": "^7.5.0 || ^8.0.0"
- }
- },
- "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
- "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
- "dev": true,
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@babel/generator": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz",
@@ -687,109 +662,6 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/plugin-proposal-class-properties": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz",
- "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==",
- "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.",
- "dev": true,
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.18.6",
- "@babel/helper-plugin-utils": "^7.18.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-decorators": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz",
- "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==",
- "dev": true,
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.24.1",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/plugin-syntax-decorators": "^7.24.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
- "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
- "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.18.6",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-numeric-separator": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz",
- "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==",
- "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.18.6",
- "@babel/plugin-syntax-numeric-separator": "^7.10.4"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-optional-chaining": {
- "version": "7.21.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
- "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
- "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.20.2",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-private-methods": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz",
- "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==",
- "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.",
- "dev": true,
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.18.6",
- "@babel/helper-plugin-utils": "^7.18.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
@@ -841,21 +713,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-syntax-decorators": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz",
- "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-syntax-dynamic-import": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
@@ -880,21 +737,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-syntax-flow": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.1.tgz",
- "integrity": "sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-syntax-import-assertions": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz",
@@ -949,21 +791,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz",
- "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-syntax-logical-assignment-operators": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
@@ -1066,21 +893,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz",
- "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
@@ -1342,22 +1154,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-flow-strip-types": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.1.tgz",
- "integrity": "sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/plugin-syntax-flow": "^7.24.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-for-of": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz",
@@ -1714,55 +1510,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-react-display-name": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz",
- "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz",
- "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==",
- "dev": true,
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-module-imports": "^7.22.15",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-jsx": "^7.23.3",
- "@babel/types": "^7.23.4"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-development": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz",
- "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==",
- "dev": true,
- "dependencies": {
- "@babel/plugin-transform-react-jsx": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz",
@@ -1793,22 +1540,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-react-pure-annotations": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz",
- "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==",
- "dev": true,
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.24.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz",
@@ -1840,26 +1571,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-runtime": {
- "version": "7.24.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz",
- "integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==",
- "dev": true,
- "dependencies": {
- "@babel/helper-module-imports": "^7.24.3",
- "@babel/helper-plugin-utils": "^7.24.0",
- "babel-plugin-polyfill-corejs2": "^0.4.10",
- "babel-plugin-polyfill-corejs3": "^0.10.1",
- "babel-plugin-polyfill-regenerator": "^0.6.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-shorthand-properties": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz",
@@ -1936,24 +1647,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-typescript": {
- "version": "7.24.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz",
- "integrity": "sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g==",
- "dev": true,
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-create-class-features-plugin": "^7.24.4",
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/plugin-syntax-typescript": "^7.24.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz",
@@ -2126,45 +1819,6 @@
"@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
}
},
- "node_modules/@babel/preset-react": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz",
- "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-validator-option": "^7.23.5",
- "@babel/plugin-transform-react-display-name": "^7.24.1",
- "@babel/plugin-transform-react-jsx": "^7.23.4",
- "@babel/plugin-transform-react-jsx-development": "^7.22.5",
- "@babel/plugin-transform-react-pure-annotations": "^7.24.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-typescript": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz",
- "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==",
- "dev": true,
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0",
- "@babel/helper-validator-option": "^7.23.5",
- "@babel/plugin-syntax-jsx": "^7.24.1",
- "@babel/plugin-transform-modules-commonjs": "^7.24.1",
- "@babel/plugin-transform-typescript": "^7.24.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/regjsgen": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz",
@@ -2750,62 +2404,57 @@
"dev": true
},
"node_modules/@fortawesome/fontawesome-common-types": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
- "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
+ "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
"dev": true,
- "hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
- "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
+ "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"dev": true,
- "hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.5.2"
+ "@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz",
- "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
+ "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
"dev": true,
- "hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.5.2"
+ "@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz",
- "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
+ "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
"dev": true,
- "hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.5.2"
+ "@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
- "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
+ "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"dev": true,
- "hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.5.2"
+ "@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
@@ -2967,9 +2616,9 @@
}
},
"node_modules/@mantine/core": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.11.0.tgz",
- "integrity": "sha512-yw2Llww9mw8rDWZtucdEuvkqqjHdreUibos7JCUpejL721FW1Tn9L91nsxO/YQFSS7jn4Q0CP+1YbQ/PMULmwA==",
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.12.2.tgz",
+ "integrity": "sha512-FrMHOKq4s3CiPIxqZ9xnVX7H4PEGNmbtHMvWO/0YlfPgoV0Er/N/DNJOFW1ys4WSnidPTayYeB41riyxxGOpRQ==",
"dependencies": {
"@floating-ui/react": "^0.26.9",
"clsx": "^2.1.1",
@@ -2979,7 +2628,7 @@
"type-fest": "^4.12.0"
},
"peerDependencies": {
- "@mantine/hooks": "7.11.0",
+ "@mantine/hooks": "7.12.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
@@ -2996,23 +2645,23 @@
}
},
"node_modules/@mantine/dropzone": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.11.0.tgz",
- "integrity": "sha512-8vZgm8+NlBrQFJlWckaoqz55zjk8GVX0GDn1bZUunUtIJ5uv/wJPAInq3IlRdzvWVfz5MA+4oxd32fa5oxsBSA==",
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.12.2.tgz",
+ "integrity": "sha512-VXKpgFBfRfci6eQEyrmNSsTR7LdtErDhWloVw7W6YRsCqJxJHg9e3luG+yIk+tokzSyLoLOVZRX/mESDEso3PQ==",
"dependencies": {
"react-dropzone-esm": "15.0.1"
},
"peerDependencies": {
- "@mantine/core": "7.11.0",
- "@mantine/hooks": "7.11.0",
+ "@mantine/core": "7.12.2",
+ "@mantine/hooks": "7.12.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/@mantine/form": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.11.0.tgz",
- "integrity": "sha512-BmkzRp57O1zZuxCYH76w6zeBNhczq7OeRtkG/zvMo35BJp1K5u8eetN3AC1WwkGLmrNid2BCIsvTFHDP9DYnaQ==",
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.12.2.tgz",
+ "integrity": "sha512-MknzDN5F7u/V24wVrL5VIXNvE7/6NMt40K6w3p7wbKFZiLhdh/tDWdMcRN7PkkWF1j2+eoVCBAOCL74U3BzNag==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"klona": "^2.0.6"
@@ -3022,78 +2671,47 @@
}
},
"node_modules/@mantine/hooks": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.11.0.tgz",
- "integrity": "sha512-T3472GhUXFhuhXUHlxjHv0wfb73lFyNuaw631c7Ddtgvewq0WKtNqYd7j/Zz/k02DuS3r0QXA7e12/XgqHBZjg==",
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.2.tgz",
+ "integrity": "sha512-dVMw8jpM0hAzc8e7/GNvzkk9N0RN/m+PKycETB3H6lJGuXJJSRR4wzzgQKpEhHwPccktDpvb4rkukKDq2jA8Fg==",
"peerDependencies": {
"react": "^18.2.0"
}
},
"node_modules/@mantine/modals": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.11.0.tgz",
- "integrity": "sha512-I4bxdXirLNvVbmVcS9lhU9z1bknE8XlteGeSxAZ00SLUk9EowG+AX/9nK0TrSG2GBNDX82fFxp2z98/o7bTw5w==",
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.12.2.tgz",
+ "integrity": "sha512-ffnu9MtUHceoaLlhrwq+J+eojidEPkq3m2Rrt5HfcZv3vAP8RtqPnTfgk99WOB3vyCtdu8r4I9P3ckuYtPRtAg==",
"peerDependencies": {
- "@mantine/core": "7.11.0",
- "@mantine/hooks": "7.11.0",
+ "@mantine/core": "7.12.2",
+ "@mantine/hooks": "7.12.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/@mantine/notifications": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.11.0.tgz",
- "integrity": "sha512-UtAHJoSi4s+lfVZrkUDWMlg6j0w1LZaiMEOBMG9p5MV5dP38W75LeCy2cio2Znji2S5YzXaZolOkHBT5ZONKAw==",
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.12.2.tgz",
+ "integrity": "sha512-gTvLHkoAZ42v5bZxibP9A50djp5ndEwumVhHSa7mxQ8oSS23tt3It/6hOqH7M+9kHY0a8s+viMiflUzTByA9qg==",
"dependencies": {
- "@mantine/store": "7.11.0",
+ "@mantine/store": "7.12.2",
"react-transition-group": "4.4.5"
},
"peerDependencies": {
- "@mantine/core": "7.11.0",
- "@mantine/hooks": "7.11.0",
+ "@mantine/core": "7.12.2",
+ "@mantine/hooks": "7.12.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/@mantine/store": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.11.0.tgz",
- "integrity": "sha512-zPmOpdFgvkUqYKSK7NNKbhgXsh2QPw51m3iypTaj0mw+rZbk3WSH9vZvaEx59X0QG+ahwUg2/HezbjfXFUbvrA==",
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.12.2.tgz",
+ "integrity": "sha512-NqL31sO/KcAETEWP/CiXrQOQNoE4168vZsxyXacQHGBueVMJa64WIDQtKLHrCnFRMws3vsXF02/OO4bH4XGcMQ==",
"peerDependencies": {
"react": "^18.2.0"
}
},
- "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
- "version": "5.1.1-v1",
- "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
- "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
- "dev": true,
- "dependencies": {
- "eslint-scope": "5.1.1"
- }
- },
- "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
- "dev": true,
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3417,12 +3035,6 @@
"win32"
]
},
- "node_modules/@rushstack/eslint-patch": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz",
- "integrity": "sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==",
- "dev": true
- },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -3847,12 +3459,6 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
- "node_modules/@types/json5": {
- "version": "0.0.29",
- "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true
- },
"node_modules/@types/lodash": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz",
@@ -3868,12 +3474,6 @@
"undici-types": "~5.26.4"
}
},
- "node_modules/@types/parse-json": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
- "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
- "dev": true
- },
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
@@ -3881,9 +3481,9 @@
"devOptional": true
},
"node_modules/@types/react": {
- "version": "18.3.3",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
- "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
+ "version": "18.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
+ "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
@@ -3941,32 +3541,32 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
- "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
+ "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/type-utils": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
- "debug": "^4.3.4",
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "7.16.0",
+ "@typescript-eslint/type-utils": "7.16.0",
+ "@typescript-eslint/utils": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0",
"graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^5.0.0",
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "@typescript-eslint/parser": "^7.0.0",
+ "eslint": "^8.56.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -3974,26 +3574,140 @@
}
}
},
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
+ "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "yallist": "^4.0.0"
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0"
},
"engines": {
- "node": ">=10"
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
- "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
+ "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
+ "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "lru-cache": "^6.0.0"
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
+ "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.16.0",
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/typescript-estree": "7.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
+ "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.16.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
},
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+ "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+ "dev": true,
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -4001,51 +3715,89 @@
"node": ">=10"
}
},
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- },
- "node_modules/@typescript-eslint/experimental-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz",
- "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==",
+ "node_modules/@typescript-eslint/parser": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
+ "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/utils": "5.62.0"
+ "@typescript-eslint/scope-manager": "7.16.0",
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/typescript-estree": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0",
+ "debug": "^4.3.4"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
}
},
- "node_modules/@typescript-eslint/parser": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
- "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
+ "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
+ "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
},
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
+ "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
@@ -4053,6 +3805,63 @@
}
}
},
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
+ "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.16.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/semver": {
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+ "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
@@ -4071,25 +3880,26 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
- "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
+ "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@typescript-eslint/typescript-estree": "7.16.0",
+ "@typescript-eslint/utils": "7.16.0",
"debug": "^4.3.4",
- "tsutils": "^3.21.0"
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "*"
+ "eslint": "^8.56.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -4097,6 +3907,147 @@
}
}
},
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
+ "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
+ "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
+ "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/visitor-keys": "7.16.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
+ "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.16.0",
+ "@typescript-eslint/types": "7.16.0",
+ "@typescript-eslint/typescript-estree": "7.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
+ "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.16.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/semver": {
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+ "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@typescript-eslint/types": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
@@ -4667,26 +4618,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/array-includes": {
- "version": "3.1.8",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
- "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.4",
- "is-string": "^1.0.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -4696,107 +4627,6 @@
"node": ">=8"
}
},
- "node_modules/array.prototype.findlast": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
- "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.findlastindex": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz",
- "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.flat": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
- "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.flatmap": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
- "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.toreversed": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz",
- "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- }
- },
- "node_modules/array.prototype.tosorted": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz",
- "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.5",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.22.3",
- "es-errors": "^1.1.0",
- "es-shim-unscopables": "^1.0.2"
- }
- },
"node_modules/arraybuffer.prototype.slice": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
@@ -4828,12 +4658,6 @@
"node": "*"
}
},
- "node_modules/ast-types-flow": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
- "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
- "dev": true
- },
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
@@ -4871,15 +4695,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/axe-core": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
- "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
@@ -4890,15 +4705,6 @@
"proxy-from-env": "^1.1.0"
}
},
- "node_modules/axobject-query": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
- "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
- "dev": true,
- "dependencies": {
- "dequal": "^2.0.3"
- }
- },
"node_modules/b4a": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz",
@@ -4906,21 +4712,6 @@
"dev": true,
"license": "Apache-2.0"
},
- "node_modules/babel-plugin-macros": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
- "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
- "dev": true,
- "dependencies": {
- "@babel/runtime": "^7.12.5",
- "cosmiconfig": "^7.0.0",
- "resolve": "^1.19.0"
- },
- "engines": {
- "node": ">=10",
- "npm": ">=6"
- }
- },
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.10",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz",
@@ -4960,36 +4751,6 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
- "node_modules/babel-plugin-transform-react-remove-prop-types": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
- "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==",
- "dev": true
- },
- "node_modules/babel-preset-react-app": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz",
- "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==",
- "dev": true,
- "dependencies": {
- "@babel/core": "^7.16.0",
- "@babel/plugin-proposal-class-properties": "^7.16.0",
- "@babel/plugin-proposal-decorators": "^7.16.4",
- "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0",
- "@babel/plugin-proposal-numeric-separator": "^7.16.0",
- "@babel/plugin-proposal-optional-chaining": "^7.16.0",
- "@babel/plugin-proposal-private-methods": "^7.16.0",
- "@babel/plugin-transform-flow-strip-types": "^7.16.0",
- "@babel/plugin-transform-react-display-name": "^7.16.0",
- "@babel/plugin-transform-runtime": "^7.16.4",
- "@babel/preset-env": "^7.16.4",
- "@babel/preset-react": "^7.16.0",
- "@babel/preset-typescript": "^7.16.0",
- "@babel/runtime": "^7.16.3",
- "babel-plugin-macros": "^3.1.0",
- "babel-plugin-transform-react-remove-prop-types": "^0.4.24"
- }
- },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -5455,12 +5216,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
- "node_modules/confusing-browser-globals": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
- "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
- "dev": true
- },
"node_modules/consola": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz",
@@ -5484,31 +5239,6 @@
"url": "https://opencollective.com/core-js"
}
},
- "node_modules/cosmiconfig": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
- "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
- "dev": true,
- "dependencies": {
- "@types/parse-json": "^4.0.0",
- "import-fresh": "^3.2.1",
- "parse-json": "^5.0.0",
- "path-type": "^4.0.0",
- "yaml": "^1.10.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/cosmiconfig/node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
- "dev": true,
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -5689,12 +5419,6 @@
"node": ">=12"
}
},
- "node_modules/damerau-levenshtein": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
- "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
- "dev": true
- },
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
@@ -6013,12 +5737,6 @@
"integrity": "sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==",
"dev": true
},
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true
- },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -6062,15 +5780,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dev": true,
- "dependencies": {
- "is-arrayish": "^0.2.1"
- }
- },
"node_modules/es-abstract": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@@ -6152,31 +5861,6 @@
"node": ">= 0.4"
}
},
- "node_modules/es-iterator-helpers": {
- "version": "1.0.18",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz",
- "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.0",
- "es-errors": "^1.3.0",
- "es-set-tostringtag": "^2.0.3",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.4",
- "globalthis": "^1.0.3",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.0.3",
- "has-symbols": "^1.0.3",
- "internal-slot": "^1.0.7",
- "iterator.prototype": "^1.1.2",
- "safe-array-concat": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
@@ -6203,15 +5887,6 @@
"node": ">= 0.4"
}
},
- "node_modules/es-shim-unscopables": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
- "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
- "dev": true,
- "dependencies": {
- "hasown": "^2.0.0"
- }
- },
"node_modules/es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
@@ -6343,252 +6018,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint-config-react-app": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
- "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==",
- "dev": true,
- "dependencies": {
- "@babel/core": "^7.16.0",
- "@babel/eslint-parser": "^7.16.3",
- "@rushstack/eslint-patch": "^1.1.0",
- "@typescript-eslint/eslint-plugin": "^5.5.0",
- "@typescript-eslint/parser": "^5.5.0",
- "babel-preset-react-app": "^10.0.1",
- "confusing-browser-globals": "^1.0.11",
- "eslint-plugin-flowtype": "^8.0.3",
- "eslint-plugin-import": "^2.25.3",
- "eslint-plugin-jest": "^25.3.0",
- "eslint-plugin-jsx-a11y": "^6.5.1",
- "eslint-plugin-react": "^7.27.1",
- "eslint-plugin-react-hooks": "^4.3.0",
- "eslint-plugin-testing-library": "^5.0.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "eslint": "^8.0.0"
- }
- },
- "node_modules/eslint-config-react-app/node_modules/eslint-plugin-testing-library": {
- "version": "5.11.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz",
- "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/utils": "^5.58.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0",
- "npm": ">=6"
- },
- "peerDependencies": {
- "eslint": "^7.5.0 || ^8.0.0"
- }
- },
- "node_modules/eslint-import-resolver-node": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
- "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
- "dev": true,
- "dependencies": {
- "debug": "^3.2.7",
- "is-core-module": "^2.13.0",
- "resolve": "^1.22.4"
- }
- },
- "node_modules/eslint-import-resolver-node/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-module-utils": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
- "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
- "dev": true,
- "dependencies": {
- "debug": "^3.2.7"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-module-utils/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-plugin-flowtype": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz",
- "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==",
- "dev": true,
- "dependencies": {
- "lodash": "^4.17.21",
- "string-natural-compare": "^3.0.1"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "@babel/plugin-syntax-flow": "^7.14.5",
- "@babel/plugin-transform-react-jsx": "^7.14.9",
- "eslint": "^8.1.0"
- }
- },
- "node_modules/eslint-plugin-import": {
- "version": "2.29.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
- "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
- "dev": true,
- "dependencies": {
- "array-includes": "^3.1.7",
- "array.prototype.findlastindex": "^1.2.3",
- "array.prototype.flat": "^1.3.2",
- "array.prototype.flatmap": "^1.3.2",
- "debug": "^3.2.7",
- "doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.9",
- "eslint-module-utils": "^2.8.0",
- "hasown": "^2.0.0",
- "is-core-module": "^2.13.1",
- "is-glob": "^4.0.3",
- "minimatch": "^3.1.2",
- "object.fromentries": "^2.0.7",
- "object.groupby": "^1.0.1",
- "object.values": "^1.1.7",
- "semver": "^6.3.1",
- "tsconfig-paths": "^3.15.0"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eslint-plugin-jest": {
- "version": "25.7.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz",
- "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/experimental-utils": "^5.0.0"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
- },
- "peerDependencies": {
- "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0",
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "@typescript-eslint/eslint-plugin": {
- "optional": true
- },
- "jest": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-plugin-jsx-a11y": {
- "version": "6.8.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
- "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
- "dev": true,
- "dependencies": {
- "@babel/runtime": "^7.23.2",
- "aria-query": "^5.3.0",
- "array-includes": "^3.1.7",
- "array.prototype.flatmap": "^1.3.2",
- "ast-types-flow": "^0.0.8",
- "axe-core": "=4.7.0",
- "axobject-query": "^3.2.1",
- "damerau-levenshtein": "^1.0.8",
- "emoji-regex": "^9.2.2",
- "es-iterator-helpers": "^1.0.15",
- "hasown": "^2.0.0",
- "jsx-ast-utils": "^3.3.5",
- "language-tags": "^1.0.9",
- "minimatch": "^3.1.2",
- "object.entries": "^1.1.7",
- "object.fromentries": "^2.0.7"
- },
- "engines": {
- "node": ">=4.0"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
- }
- },
- "node_modules/eslint-plugin-react": {
- "version": "7.34.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz",
- "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==",
- "dev": true,
- "dependencies": {
- "array-includes": "^3.1.7",
- "array.prototype.findlast": "^1.2.4",
- "array.prototype.flatmap": "^1.3.2",
- "array.prototype.toreversed": "^1.1.2",
- "array.prototype.tosorted": "^1.1.3",
- "doctrine": "^2.1.0",
- "es-iterator-helpers": "^1.0.17",
- "estraverse": "^5.3.0",
- "jsx-ast-utils": "^2.4.1 || ^3.0.0",
- "minimatch": "^3.1.2",
- "object.entries": "^1.1.7",
- "object.fromentries": "^2.0.7",
- "object.hasown": "^1.1.3",
- "object.values": "^1.1.7",
- "prop-types": "^15.8.1",
- "resolve": "^2.0.0-next.5",
- "semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.10"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
- }
- },
"node_modules/eslint-plugin-react-hooks": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
@@ -6601,33 +6030,14 @@
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
}
},
- "node_modules/eslint-plugin-react/node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/resolve": {
- "version": "2.0.0-next.5",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
- "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz",
+ "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==",
"dev": true,
- "dependencies": {
- "is-core-module": "^2.13.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=7"
}
},
"node_modules/eslint-plugin-simple-import-sort": {
@@ -7633,27 +7043,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
- "dev": true
- },
- "node_modules/is-async-function": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
- "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
- "dev": true,
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-bigint": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
@@ -7773,33 +7162,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-finalizationregistry": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
- "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-generator-function": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
- "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
- "dev": true,
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -7812,18 +7174,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-map": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
- "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
@@ -7918,18 +7268,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-set": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
- "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-shared-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
@@ -8002,18 +7340,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-weakmap": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
- "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-weakref": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -8026,22 +7352,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-weakset": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
- "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "get-intrinsic": "^1.2.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -8104,19 +7414,6 @@
"node": ">=8"
}
},
- "node_modules/iterator.prototype": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
- "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
- "dev": true,
- "dependencies": {
- "define-properties": "^1.2.1",
- "get-intrinsic": "^1.2.1",
- "has-symbols": "^1.0.3",
- "reflect.getprototypeof": "^1.0.4",
- "set-function-name": "^2.0.1"
- }
- },
"node_modules/jake": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz",
@@ -8393,12 +7690,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
- "node_modules/json-parse-even-better-errors": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "dev": true
- },
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
@@ -8467,21 +7758,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/jsx-ast-utils": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
- "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
- "dev": true,
- "dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flat": "^1.3.1",
- "object.assign": "^4.1.4",
- "object.values": "^1.1.6"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8499,24 +7775,6 @@
"node": ">= 8"
}
},
- "node_modules/language-subtag-registry": {
- "version": "0.3.22",
- "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
- "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
- "dev": true
- },
- "node_modules/language-tags": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
- "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
- "dev": true,
- "dependencies": {
- "language-subtag-registry": "^0.3.20"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -8540,12 +7798,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true
- },
"node_modules/local-pkg": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz",
@@ -8883,12 +8135,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
- "node_modules/natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
- },
"node_modules/node-abi": {
"version": "3.65.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz",
@@ -8999,86 +8245,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/object.entries": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
- "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.fromentries": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
- "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.groupby": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
- "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.hasown": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz",
- "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==",
- "dev": true,
- "dependencies": {
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.values": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
- "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -9162,24 +8328,6 @@
"node": ">=6"
}
},
- "node_modules/parse-json": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
- "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "dev": true,
- "dependencies": {
- "@babel/code-frame": "^7.0.0",
- "error-ex": "^1.3.1",
- "json-parse-even-better-errors": "^2.3.0",
- "lines-and-columns": "^1.1.6"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -9943,9 +9091,9 @@
}
},
"node_modules/recharts": {
- "version": "2.12.6",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz",
- "integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==",
+ "version": "2.12.7",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz",
+ "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==",
"dev": true,
"dependencies": {
"clsx": "^2.0.0",
@@ -9987,27 +9135,6 @@
"node": ">=8"
}
},
- "node_modules/reflect.getprototypeof": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
- "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.1",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4",
- "globalthis": "^1.0.3",
- "which-builtin-type": "^1.1.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -10734,12 +9861,6 @@
"safe-buffer": "~5.2.0"
}
},
- "node_modules/string-natural-compare": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz",
- "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==",
- "dev": true
- },
"node_modules/string.prototype.matchall": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
@@ -10842,15 +9963,6 @@
"node": ">=8"
}
},
- "node_modules/strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/strip-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
@@ -11182,28 +10294,17 @@
"node": ">=18"
}
},
- "node_modules/tsconfig-paths": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
- "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
- "dev": true,
- "dependencies": {
- "@types/json5": "^0.0.29",
- "json5": "^1.0.2",
- "minimist": "^1.2.6",
- "strip-bom": "^3.0.0"
- }
- },
- "node_modules/tsconfig-paths/node_modules/json5": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
- "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "node_modules/ts-api-utils": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+ "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
"dev": true,
- "dependencies": {
- "minimist": "^1.2.0"
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
},
- "bin": {
- "json5": "lib/cli.js"
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
}
},
"node_modules/tslib": {
@@ -12239,50 +11340,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/which-builtin-type": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
- "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
- "dev": true,
- "dependencies": {
- "function.prototype.name": "^1.1.5",
- "has-tostringtag": "^1.0.0",
- "is-async-function": "^2.0.0",
- "is-date-object": "^1.0.5",
- "is-finalizationregistry": "^1.0.2",
- "is-generator-function": "^1.0.10",
- "is-regex": "^1.1.4",
- "is-weakref": "^1.0.2",
- "isarray": "^2.0.5",
- "which-boxed-primitive": "^1.0.2",
- "which-collection": "^1.0.1",
- "which-typed-array": "^1.1.9"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-collection": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
- "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
- "dev": true,
- "dependencies": {
- "is-map": "^2.0.3",
- "is-set": "^2.0.3",
- "is-weakmap": "^2.0.2",
- "is-weakset": "^2.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/which-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 0d3c0546e..57a8cdd55 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,12 +13,12 @@
},
"private": true,
"dependencies": {
- "@mantine/core": "^7.11.0",
- "@mantine/dropzone": "^7.11.0",
- "@mantine/form": "^7.11.0",
- "@mantine/hooks": "^7.11.0",
- "@mantine/modals": "^7.11.0",
- "@mantine/notifications": "^7.11.0",
+ "@mantine/core": "^7.12.2",
+ "@mantine/dropzone": "^7.12.2",
+ "@mantine/form": "^7.12.2",
+ "@mantine/hooks": "^7.12.2",
+ "@mantine/modals": "^7.12.2",
+ "@mantine/notifications": "^7.12.2",
"@tanstack/react-query": "^5.40.1",
"@tanstack/react-table": "^8.19.2",
"axios": "^1.6.8",
@@ -30,10 +30,10 @@
},
"devDependencies": {
"@fontsource/roboto": "^5.0.12",
- "@fortawesome/fontawesome-svg-core": "^6.5.2",
- "@fortawesome/free-brands-svg-icons": "^6.5.2",
- "@fortawesome/free-regular-svg-icons": "^6.5.2",
- "@fortawesome/free-solid-svg-icons": "^6.5.2",
+ "@fortawesome/fontawesome-svg-core": "^6.6.0",
+ "@fortawesome/free-brands-svg-icons": "^6.6.0",
+ "@fortawesome/free-regular-svg-icons": "^6.6.0",
+ "@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/react-query-devtools": "^5.40.1",
"@testing-library/jest-dom": "^6.4.2",
@@ -42,16 +42,18 @@
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.1",
"@types/node": "^20.12.6",
- "@types/react": "^18.3.3",
+ "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
+ "@typescript-eslint/eslint-plugin": "^7.16.0",
+ "@typescript-eslint/parser": "^7.16.0",
"@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.2.2",
"clsx": "^2.1.0",
"eslint": "^8.57.0",
- "eslint-config-react-app": "^7.0.1",
"eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-simple-import-sort": "^12.1.0",
"eslint-plugin-testing-library": "^6.2.0",
"husky": "^9.0.11",
@@ -62,7 +64,7 @@
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"pretty-quick": "^4.0.0",
- "recharts": "^2.12.6",
+ "recharts": "^2.12.7",
"sass": "^1.74.1",
"typescript": "^5.4.4",
"vite": "^5.2.8",
diff --git a/frontend/src/Router/index.tsx b/frontend/src/Router/index.tsx
index d600fc87d..8ccea87f9 100644
--- a/frontend/src/Router/index.tsx
+++ b/frontend/src/Router/index.tsx
@@ -270,6 +270,7 @@ function useRoutes(): CustomRouteObject[] {
{
path: "status",
name: "Status",
+ badge: data?.status,
element: (
<Lazy>
<SystemStatusView></SystemStatusView>
@@ -309,6 +310,7 @@ function useRoutes(): CustomRouteObject[] {
data?.sonarr_signalr,
data?.radarr_signalr,
data?.announcements,
+ data?.status,
radarr,
sonarr,
],
diff --git a/frontend/src/apis/hooks/episodes.ts b/frontend/src/apis/hooks/episodes.ts
index 6a489e938..956fd103f 100644
--- a/frontend/src/apis/hooks/episodes.ts
+++ b/frontend/src/apis/hooks/episodes.ts
@@ -25,23 +25,6 @@ const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
});
};
-export function useEpisodesByIds(ids: number[]) {
- const client = useQueryClient();
-
- const query = useQuery({
- queryKey: [QueryKeys.Series, QueryKeys.Episodes, ids],
- queryFn: () => api.episodes.byEpisodeId(ids),
- });
-
- useEffect(() => {
- if (query.isSuccess && query.data) {
- cacheEpisodes(client, query.data);
- }
- }, [query.isSuccess, query.data, client]);
-
- return query;
-}
-
export function useEpisodesBySeriesId(id: number) {
const client = useQueryClient();
@@ -87,10 +70,11 @@ export function useEpisodeAddBlacklist() {
},
onSuccess: (_, { seriesId }) => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.Series, seriesId],
});
},
@@ -105,8 +89,8 @@ export function useEpisodeDeleteBlacklist() {
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
api.episodes.deleteBlacklist(param.all, param.form),
- onSuccess: (_) => {
- client.invalidateQueries({
+ onSuccess: () => {
+ void client.invalidateQueries({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
});
},
diff --git a/frontend/src/apis/hooks/movies.ts b/frontend/src/apis/hooks/movies.ts
index cf4594cbe..6b1c5c2a5 100644
--- a/frontend/src/apis/hooks/movies.ts
+++ b/frontend/src/apis/hooks/movies.ts
@@ -15,23 +15,6 @@ const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
});
};
-export function useMoviesByIds(ids: number[]) {
- const client = useQueryClient();
-
- const query = useQuery({
- queryKey: [QueryKeys.Movies, ...ids],
- queryFn: () => api.movies.movies(ids),
- });
-
- useEffect(() => {
- if (query.isSuccess && query.data) {
- cacheMovies(client, query.data);
- }
- }, [query.isSuccess, query.data, client]);
-
- return query;
-}
-
export function useMovieById(id: number) {
return useQuery({
queryKey: [QueryKeys.Movies, id],
@@ -74,12 +57,13 @@ export function useMovieModification() {
onSuccess: (_, form) => {
form.id.forEach((v) => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.Movies, v],
});
});
+
// TODO: query less
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
},
@@ -93,7 +77,7 @@ export function useMovieAction() {
mutationFn: (form: FormType.MoviesAction) => api.movies.action(form),
onSuccess: () => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
},
@@ -125,10 +109,11 @@ export function useMovieAddBlacklist() {
},
onSuccess: (_, { id }) => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.Movies, id],
});
},
@@ -143,8 +128,8 @@ export function useMovieDeleteBlacklist() {
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
api.movies.deleteBlacklist(param.all, param.form),
- onSuccess: (_, param) => {
- client.invalidateQueries({
+ onSuccess: () => {
+ void client.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
},
diff --git a/frontend/src/apis/hooks/system.ts b/frontend/src/apis/hooks/system.ts
index 109e77105..a0ce17fb9 100644
--- a/frontend/src/apis/hooks/system.ts
+++ b/frontend/src/apis/hooks/system.ts
@@ -54,22 +54,27 @@ export function useSettingsMutation() {
mutationFn: (data: LooseObject) => api.system.updateSettings(data),
onSuccess: () => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.System],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.Series],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.Episodes],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.Wanted],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.Badges],
});
},
@@ -101,7 +106,7 @@ export function useDeleteLogs() {
mutationFn: () => api.system.deleteLogs(),
onSuccess: () => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Logs],
});
},
@@ -128,11 +133,12 @@ export function useSystemAnnouncementsAddDismiss() {
return api.system.addAnnouncementsDismiss(hash);
},
- onSuccess: (_, { hash }) => {
- client.invalidateQueries({
+ onSuccess: () => {
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Announcements],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Badges],
});
},
@@ -156,10 +162,11 @@ export function useRunTask() {
mutationFn: (id: string) => api.system.runTask(id),
onSuccess: () => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Tasks],
});
- client.invalidateQueries({
+
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
@@ -180,7 +187,7 @@ export function useCreateBackups() {
mutationFn: () => api.system.createBackups(),
onSuccess: () => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
@@ -194,7 +201,7 @@ export function useRestoreBackups() {
mutationFn: (filename: string) => api.system.restoreBackups(filename),
onSuccess: () => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
@@ -208,7 +215,7 @@ export function useDeleteBackups() {
mutationFn: (filename: string) => api.system.deleteBackups(filename),
onSuccess: () => {
- client.invalidateQueries({
+ void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
diff --git a/frontend/src/assets/badge.module.scss b/frontend/src/assets/badge.module.scss
index 4b8717fe3..5b31be59e 100644
--- a/frontend/src/assets/badge.module.scss
+++ b/frontend/src/assets/badge.module.scss
@@ -47,4 +47,8 @@
}
}
}
+
+ .label {
+ overflow: visible;
+ }
}
diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx
index b506afee3..c0dde3bef 100644
--- a/frontend/src/components/Search.tsx
+++ b/frontend/src/components/Search.tsx
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import { Autocomplete, ComboboxItem, OptionsFilter, Text } from "@mantine/core";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { chain, includes } from "lodash";
import { useServerSearch } from "@/apis/hooks";
import { useDebouncedValue } from "@/utilities";
@@ -15,23 +16,45 @@ function useSearch(query: string) {
const debouncedQuery = useDebouncedValue(query, 500);
const { data } = useServerSearch(debouncedQuery, debouncedQuery.length >= 0);
+ const duplicates = chain(data)
+ .groupBy((item) => `${item.title} (${item.year})`)
+ .filter((group) => group.length > 1)
+ .map((group) => `${group[0].title} (${group[0].year})`)
+ .value();
+
return useMemo<SearchResultItem[]>(
() =>
data?.map((v) => {
- let link: string;
- if (v.sonarrSeriesId) {
- link = `/series/${v.sonarrSeriesId}`;
- } else if (v.radarrId) {
- link = `/movies/${v.radarrId}`;
- } else {
+ const { link, displayName } = (() => {
+ const hasDuplicate = includes(duplicates, `${v.title} (${v.year})`);
+
+ if (v.sonarrSeriesId) {
+ return {
+ link: `/series/${v.sonarrSeriesId}`,
+ displayName: hasDuplicate
+ ? `${v.title} (${v.year}) (S)`
+ : `${v.title} (${v.year})`,
+ };
+ }
+
+ if (v.radarrId) {
+ return {
+ link: `/movies/${v.radarrId}`,
+ displayName: hasDuplicate
+ ? `${v.title} (${v.year}) (M)`
+ : `${v.title} (${v.year})`,
+ };
+ }
+
throw new Error("Unknown search result");
- }
+ })();
+
return {
- value: `${v.title} (${v.year})`,
+ value: displayName,
link,
};
}) ?? [],
- [data],
+ [data, duplicates],
);
}
diff --git a/frontend/src/components/TextPopover.tsx b/frontend/src/components/TextPopover.tsx
index 974c0d0c0..03dd58700 100644
--- a/frontend/src/components/TextPopover.tsx
+++ b/frontend/src/components/TextPopover.tsx
@@ -25,7 +25,7 @@ const TextPopover: FunctionComponent<TextPopoverProps> = ({
opened={hovered}
label={text}
{...tooltip}
- style={{ textWrap: "pretty" }}
+ style={{ textWrap: "wrap" }}
>
<div ref={ref}>{children}</div>
</Tooltip>
diff --git a/frontend/src/components/async/MutateAction.tsx b/frontend/src/components/async/MutateAction.tsx
index 6fff0dbb7..92c102ea9 100644
--- a/frontend/src/components/async/MutateAction.tsx
+++ b/frontend/src/components/async/MutateAction.tsx
@@ -16,7 +16,6 @@ type MutateActionProps<DATA, VAR> = Omit<
function MutateAction<DATA, VAR>({
mutation,
- noReset,
onSuccess,
onError,
args,
diff --git a/frontend/src/components/async/MutateButton.tsx b/frontend/src/components/async/MutateButton.tsx
index 9197e2d50..908c2dfda 100644
--- a/frontend/src/components/async/MutateButton.tsx
+++ b/frontend/src/components/async/MutateButton.tsx
@@ -15,7 +15,6 @@ type MutateButtonProps<DATA, VAR> = Omit<
function MutateButton<DATA, VAR>({
mutation,
- noReset,
onSuccess,
onError,
args,
diff --git a/frontend/src/components/async/QueryOverlay.tsx b/frontend/src/components/async/QueryOverlay.tsx
index 2a5848cf2..1672989ff 100644
--- a/frontend/src/components/async/QueryOverlay.tsx
+++ b/frontend/src/components/async/QueryOverlay.tsx
@@ -12,7 +12,7 @@ interface QueryOverlayProps {
const QueryOverlay: FunctionComponent<QueryOverlayProps> = ({
children,
global = false,
- result: { isLoading, isError, error },
+ result: { isLoading },
}) => {
return (
<LoadingProvider value={isLoading}>
diff --git a/frontend/src/components/bazarr/AudioList.tsx b/frontend/src/components/bazarr/AudioList.tsx
index f1af7ff3c..f0dc07c0d 100644
--- a/frontend/src/components/bazarr/AudioList.tsx
+++ b/frontend/src/components/bazarr/AudioList.tsx
@@ -1,6 +1,7 @@
import { FunctionComponent } from "react";
import { Badge, BadgeProps, Group, GroupProps } from "@mantine/core";
import { BuildKey } from "@/utilities";
+import { normalizeAudioLanguage } from "@/utilities/languages";
export type AudioListProps = GroupProps & {
audios: Language.Info[];
@@ -16,7 +17,7 @@ const AudioList: FunctionComponent<AudioListProps> = ({
<Group gap="xs" {...group}>
{audios.map((audio, idx) => (
<Badge color="blue" key={BuildKey(idx, audio.code2)} {...badgeProps}>
- {audio.name}
+ {normalizeAudioLanguage(audio.name)}
</Badge>
))}
</Group>
diff --git a/frontend/src/components/forms/MovieUploadForm.tsx b/frontend/src/components/forms/MovieUploadForm.tsx
index 8e318d7ad..f7f8f47c5 100644
--- a/frontend/src/components/forms/MovieUploadForm.tsx
+++ b/frontend/src/components/forms/MovieUploadForm.tsx
@@ -1,9 +1,9 @@
-import { FunctionComponent, useEffect, useMemo } from "react";
+import React, { FunctionComponent, useEffect, useMemo } from "react";
import {
Button,
- Checkbox,
Divider,
MantineColor,
+ Select,
Stack,
Text,
} from "@mantine/core";
@@ -17,8 +17,9 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
-import { isString } from "lodash";
+import { isString, uniqBy } from "lodash";
import { useMovieSubtitleModification } from "@/apis/hooks";
+import { subtitlesTypeOptions } from "@/components/forms/uploadFormSelectorTypes";
import { Action, Selector } from "@/components/inputs";
import SimpleTable from "@/components/tables/SimpleTable";
import TextPopover from "@/components/TextPopover";
@@ -88,7 +89,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
const languages = useProfileItemsToLanguages(profile);
const languageOptions = useSelectorOptions(
- languages,
+ uniqBy(languages, "code2"),
(v) => v.name,
(v) => v.code2,
);
@@ -208,34 +209,6 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
},
{
- header: "Forced",
- accessorKey: "forced",
- cell: ({ row: { original, index } }) => {
- return (
- <Checkbox
- checked={original.forced}
- onChange={({ currentTarget: { checked } }) => {
- action.mutate(index, { ...original, forced: checked });
- }}
- ></Checkbox>
- );
- },
- },
- {
- header: "HI",
- accessorKey: "hi",
- cell: ({ row: { original, index } }) => {
- return (
- <Checkbox
- checked={original.hi}
- onChange={({ currentTarget: { checked } }) => {
- action.mutate(index, { ...original, hi: checked });
- }}
- ></Checkbox>
- );
- },
- },
- {
header: "Language",
accessorKey: "language",
cell: ({ row: { original, index } }) => {
@@ -252,6 +225,61 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
},
{
+ header: () => (
+ <Selector
+ options={subtitlesTypeOptions}
+ value={null}
+ placeholder="Type"
+ onChange={(value) => {
+ if (value) {
+ action.update((item) => {
+ switch (value) {
+ case "hi":
+ return { ...item, hi: true, forced: false };
+ case "forced":
+ return { ...item, hi: false, forced: true };
+ case "normal":
+ return { ...item, hi: false, forced: false };
+ default:
+ return item;
+ }
+ });
+ }
+ }}
+ ></Selector>
+ ),
+ accessorKey: "type",
+ cell: ({ row: { original, index } }) => {
+ return (
+ <Select
+ value={
+ subtitlesTypeOptions.find((s) => {
+ if (original.hi) {
+ return s.value === "hi";
+ }
+
+ if (original.forced) {
+ return s.value === "forced";
+ }
+
+ return s.value === "normal";
+ })?.value
+ }
+ data={subtitlesTypeOptions}
+ onChange={(value) => {
+ if (value) {
+ action.mutate(index, {
+ ...original,
+ hi: value === "hi",
+ forced: value === "forced",
+ });
+ }
+ }}
+ ></Select>
+ );
+ },
+ },
+ {
id: "action",
cell: ({ row: { index } }) => {
return (
diff --git a/frontend/src/components/forms/ProfileEditForm.module.scss b/frontend/src/components/forms/ProfileEditForm.module.scss
index d98b850ff..3d4a8e177 100644
--- a/frontend/src/components/forms/ProfileEditForm.module.scss
+++ b/frontend/src/components/forms/ProfileEditForm.module.scss
@@ -3,3 +3,11 @@
padding: 0;
}
}
+
+.evenly {
+ flex-wrap: wrap;
+
+ & > div {
+ flex: 1;
+ }
+}
diff --git a/frontend/src/components/forms/ProfileEditForm.tsx b/frontend/src/components/forms/ProfileEditForm.tsx
index 75e2f9df7..267951fcb 100644
--- a/frontend/src/components/forms/ProfileEditForm.tsx
+++ b/frontend/src/components/forms/ProfileEditForm.tsx
@@ -3,6 +3,7 @@ import {
Accordion,
Button,
Checkbox,
+ Flex,
Select,
Stack,
Switch,
@@ -72,9 +73,16 @@ const ProfileEditForm: FunctionComponent<Props> = ({
(value) => value.length > 0,
"Must have a name",
),
+ tag: FormUtils.validation((value) => {
+ if (!value) {
+ return true;
+ }
+
+ return /^[a-z_0-9-]+$/.test(value);
+ }, "Only lowercase alphanumeric characters, underscores (_) and hyphens (-) are allowed"),
items: FormUtils.validation(
(value) => value.length > 0,
- "Must contain at lease 1 language",
+ "Must contain at least 1 language",
),
},
});
@@ -265,7 +273,24 @@ const ProfileEditForm: FunctionComponent<Props> = ({
})}
>
<Stack>
- <TextInput label="Name" {...form.getInputProps("name")}></TextInput>
+ <Flex
+ direction={{ base: "column", sm: "row" }}
+ gap="sm"
+ className={styles.evenly}
+ >
+ <TextInput label="Name" {...form.getInputProps("name")}></TextInput>
+ <TextInput
+ label="Tag"
+ {...form.getInputProps("tag")}
+ onBlur={() =>
+ form.setFieldValue(
+ "tag",
+ (prev) =>
+ prev?.toLowerCase().trim().replace(/\s+/g, "_") ?? undefined,
+ )
+ }
+ ></TextInput>
+ </Flex>
<Accordion
multiple
chevronPosition="right"
@@ -274,7 +299,6 @@ const ProfileEditForm: FunctionComponent<Props> = ({
>
<Accordion.Item value="Languages">
<Stack>
- {form.errors.items}
<SimpleTable
columns={columns}
data={form.values.items}
@@ -282,6 +306,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
<Button fullWidth onClick={addItem}>
Add Language
</Button>
+ <Text c="var(--mantine-color-error)">{form.errors.items}</Text>
<Selector
clearable
label="Cutoff"
diff --git a/frontend/src/components/forms/SeriesUploadForm.tsx b/frontend/src/components/forms/SeriesUploadForm.tsx
index e4482cab4..9ae6308c9 100644
--- a/frontend/src/components/forms/SeriesUploadForm.tsx
+++ b/frontend/src/components/forms/SeriesUploadForm.tsx
@@ -1,9 +1,9 @@
-import { FunctionComponent, useEffect, useMemo } from "react";
+import React, { FunctionComponent, useEffect, useMemo } from "react";
import {
Button,
- Checkbox,
Divider,
MantineColor,
+ Select,
Stack,
Text,
} from "@mantine/core";
@@ -17,12 +17,13 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
-import { isString } from "lodash";
+import { isString, uniqBy } from "lodash";
import {
useEpisodesBySeriesId,
useEpisodeSubtitleModification,
useSubtitleInfos,
} from "@/apis/hooks";
+import { subtitlesTypeOptions } from "@/components/forms/uploadFormSelectorTypes";
import { Action, Selector } from "@/components/inputs";
import SimpleTable from "@/components/tables/SimpleTable";
import TextPopover from "@/components/TextPopover";
@@ -100,7 +101,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
const profile = useLanguageProfileBy(series.profileId);
const languages = useProfileItemsToLanguages(profile);
const languageOptions = useSelectorOptions(
- languages,
+ uniqBy(languages, "code2"),
(v) => v.name,
(v) => v.code2,
);
@@ -236,42 +237,6 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
},
{
- header: "Forced",
- accessorKey: "forced",
- cell: ({ row: { original, index } }) => {
- return (
- <Checkbox
- checked={original.forced}
- onChange={({ currentTarget: { checked } }) => {
- action.mutate(index, {
- ...original,
- forced: checked,
- hi: checked ? false : original.hi,
- });
- }}
- ></Checkbox>
- );
- },
- },
- {
- header: "HI",
- accessorKey: "hi",
- cell: ({ row: { original, index } }) => {
- return (
- <Checkbox
- checked={original.hi}
- onChange={({ currentTarget: { checked } }) => {
- action.mutate(index, {
- ...original,
- hi: checked,
- forced: checked ? false : original.forced,
- });
- }}
- ></Checkbox>
- );
- },
- },
- {
header: () => (
<Selector
{...languageOptions}
@@ -280,8 +245,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
onChange={(value) => {
if (value) {
action.update((item) => {
- item.language = value;
- return item;
+ return { ...item, language: value };
});
}
}}
@@ -302,6 +266,61 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
},
{
+ header: () => (
+ <Selector
+ options={subtitlesTypeOptions}
+ value={null}
+ placeholder="Type"
+ onChange={(value) => {
+ if (value) {
+ action.update((item) => {
+ switch (value) {
+ case "hi":
+ return { ...item, hi: true, forced: false };
+ case "forced":
+ return { ...item, hi: false, forced: true };
+ case "normal":
+ return { ...item, hi: false, forced: false };
+ default:
+ return item;
+ }
+ });
+ }
+ }}
+ ></Selector>
+ ),
+ accessorKey: "type",
+ cell: ({ row: { original, index } }) => {
+ return (
+ <Select
+ value={
+ subtitlesTypeOptions.find((s) => {
+ if (original.hi) {
+ return s.value === "hi";
+ }
+
+ if (original.forced) {
+ return s.value === "forced";
+ }
+
+ return s.value === "normal";
+ })?.value
+ }
+ data={subtitlesTypeOptions}
+ onChange={(value) => {
+ if (value) {
+ action.mutate(index, {
+ ...original,
+ hi: value === "hi",
+ forced: value === "forced",
+ });
+ }
+ }}
+ ></Select>
+ );
+ },
+ },
+ {
id: "episode",
header: "Episode",
accessorKey: "episode",
diff --git a/frontend/src/components/forms/uploadFormSelectorTypes.tsx b/frontend/src/components/forms/uploadFormSelectorTypes.tsx
new file mode 100644
index 000000000..168cdddb1
--- /dev/null
+++ b/frontend/src/components/forms/uploadFormSelectorTypes.tsx
@@ -0,0 +1,16 @@
+import { SelectorOption } from "@/components";
+
+export const subtitlesTypeOptions: SelectorOption<string>[] = [
+ {
+ label: "Normal",
+ value: "normal",
+ },
+ {
+ label: "Hearing-Impaired",
+ value: "hi",
+ },
+ {
+ label: "Forced",
+ value: "forced",
+ },
+];
diff --git a/frontend/src/components/inputs/Selector.tsx b/frontend/src/components/inputs/Selector.tsx
index 1825d314a..092fd24e7 100644
--- a/frontend/src/components/inputs/Selector.tsx
+++ b/frontend/src/components/inputs/Selector.tsx
@@ -7,7 +7,7 @@ import {
Select,
SelectProps,
} from "@mantine/core";
-import { isNull, isUndefined, noop } from "lodash";
+import { isNull, isUndefined } from "lodash";
import { LOG } from "@/utilities/console";
export type SelectorOption<T> = Override<
@@ -49,10 +49,7 @@ export type GroupedSelectorProps<T> = Override<
>;
export function GroupedSelector<T>({
- value,
options,
- getkey = DefaultKeyBuilder,
- onOptionSubmit = noop,
...select
}: GroupedSelectorProps<T>) {
return (
diff --git a/frontend/src/modules/modals/hooks.ts b/frontend/src/modules/modals/hooks.ts
index 09855ac51..667e429d3 100644
--- a/frontend/src/modules/modals/hooks.ts
+++ b/frontend/src/modules/modals/hooks.ts
@@ -5,11 +5,8 @@ import { ModalSettings } from "@mantine/modals/lib/context";
import { ModalComponent, ModalIdContext } from "./WithModal";
export function useModals() {
- const {
- openContextModal: openMantineContextModal,
- closeContextModal: closeContextModalRaw,
- ...rest
- } = useMantineModals();
+ const { openContextModal: openMantineContextModal, ...rest } =
+ useMantineModals();
const openContextModal = useCallback(
<ARGS extends {}>(
@@ -26,7 +23,7 @@ export function useModals() {
[openMantineContextModal],
);
- const closeContextModal = useCallback(
+ const closeContext = useCallback(
(modal: ModalComponent) => {
rest.closeModal(modal.modalKey);
},
@@ -43,7 +40,7 @@ export function useModals() {
// TODO: Performance
return useMemo(
- () => ({ openContextModal, closeContextModal, closeSelf, ...rest }),
- [closeContextModal, closeSelf, openContextModal, rest],
+ () => ({ openContextModal, closeContext, closeSelf, ...rest }),
+ [closeContext, closeSelf, openContextModal, rest],
);
}
diff --git a/frontend/src/modules/socketio/reducer.ts b/frontend/src/modules/socketio/reducer.ts
index d19ff87c4..378ab12fc 100644
--- a/frontend/src/modules/socketio/reducer.ts
+++ b/frontend/src/modules/socketio/reducer.ts
@@ -40,13 +40,17 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: (ids) => {
LOG("info", "Invalidating series", ids);
ids.forEach((id) => {
- queryClient.invalidateQueries({ queryKey: [QueryKeys.Series, id] });
+ void queryClient.invalidateQueries({
+ queryKey: [QueryKeys.Series, id],
+ });
});
},
delete: (ids) => {
LOG("info", "Invalidating series", ids);
ids.forEach((id) => {
- queryClient.invalidateQueries({ queryKey: [QueryKeys.Series, id] });
+ void queryClient.invalidateQueries({
+ queryKey: [QueryKeys.Series, id],
+ });
});
},
},
@@ -55,13 +59,17 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: (ids) => {
LOG("info", "Invalidating movies", ids);
ids.forEach((id) => {
- queryClient.invalidateQueries({ queryKey: [QueryKeys.Movies, id] });
+ void queryClient.invalidateQueries({
+ queryKey: [QueryKeys.Movies, id],
+ });
});
},
delete: (ids) => {
LOG("info", "Invalidating movies", ids);
ids.forEach((id) => {
- queryClient.invalidateQueries({ queryKey: [QueryKeys.Movies, id] });
+ void queryClient.invalidateQueries({
+ queryKey: [QueryKeys.Movies, id],
+ });
});
},
},
@@ -78,7 +86,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
id,
]);
if (episode !== undefined) {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, episode.sonarrSeriesId],
});
}
@@ -92,7 +100,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
id,
]);
if (episode !== undefined) {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, episode.sonarrSeriesId],
});
}
@@ -101,28 +109,28 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
},
{
key: "episode-wanted",
- update: (ids) => {
+ update: () => {
// Find a better way to update wanted
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
delete: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
},
{
key: "movie-wanted",
- update: (ids) => {
+ update: () => {
// Find a better way to update wanted
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
delete: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
@@ -130,13 +138,13 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "settings",
any: () => {
- queryClient.invalidateQueries({ queryKey: [QueryKeys.System] });
+ void queryClient.invalidateQueries({ queryKey: [QueryKeys.System] });
},
},
{
key: "languages",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Languages],
});
},
@@ -144,7 +152,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "badges",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Badges],
});
},
@@ -152,7 +160,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "movie-history",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.History],
});
},
@@ -160,7 +168,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "movie-blacklist",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
},
@@ -168,7 +176,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "episode-history",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.History],
});
},
@@ -176,7 +184,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "episode-blacklist",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Blacklist],
});
},
@@ -184,7 +192,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "reset-episode-wanted",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
@@ -192,7 +200,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "reset-movie-wanted",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
@@ -200,7 +208,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "task",
any: () => {
- queryClient.invalidateQueries({
+ void queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Tasks],
});
},
diff --git a/frontend/src/pages/Movies/index.tsx b/frontend/src/pages/Movies/index.tsx
index 0429e1fdd..ef5a1ec0d 100644
--- a/frontend/src/pages/Movies/index.tsx
+++ b/frontend/src/pages/Movies/index.tsx
@@ -6,6 +6,7 @@ import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
+import { uniqueId } from "lodash";
import { useMovieModification, useMoviesPagination } from "@/apis/hooks";
import { Action } from "@/components";
import { AudioList } from "@/components/bazarr";
@@ -95,7 +96,7 @@ const MovieView: FunctionComponent = () => {
<Badge
mr="xs"
color="yellow"
- key={BuildKey(v.code2, v.hi, v.forced)}
+ key={uniqueId(`${BuildKey(v.code2, v.hi, v.forced)}_`)}
>
<Language.Text value={v}></Language.Text>
</Badge>
diff --git a/frontend/src/pages/Series/index.tsx b/frontend/src/pages/Series/index.tsx
index 229082444..c142a6767 100644
--- a/frontend/src/pages/Series/index.tsx
+++ b/frontend/src/pages/Series/index.tsx
@@ -65,25 +65,34 @@ const SeriesView: FunctionComponent = () => {
cell: (row) => {
const { episodeFileCount, episodeMissingCount, profileId, title } =
row.row.original;
- let progress = 0;
- let label = "";
- if (episodeFileCount === 0 || !profileId) {
- progress = 0.0;
- } else {
- progress = (1.0 - episodeMissingCount / episodeFileCount) * 100.0;
- label = `${
- episodeFileCount - episodeMissingCount
- }/${episodeFileCount}`;
- }
+ const label = `${episodeFileCount - episodeMissingCount}/${episodeFileCount}`;
return (
<Progress.Root key={title} size="xl">
<Progress.Section
- value={progress}
+ value={
+ episodeFileCount === 0 || !profileId
+ ? 0
+ : (1.0 - episodeMissingCount / episodeFileCount) * 100.0
+ }
color={episodeMissingCount === 0 ? "brand" : "yellow"}
>
<Progress.Label>{label}</Progress.Label>
</Progress.Section>
+ {episodeMissingCount === episodeFileCount && (
+ <Progress.Label
+ styles={{
+ label: {
+ position: "absolute",
+ top: "3px",
+ left: "50%",
+ transform: "translateX(-50%)",
+ },
+ }}
+ >
+ {label}
+ </Progress.Label>
+ )}
</Progress.Root>
);
},
diff --git a/frontend/src/pages/Settings/General/index.tsx b/frontend/src/pages/Settings/General/index.tsx
index 312d09d1f..6db3ee7fc 100644
--- a/frontend/src/pages/Settings/General/index.tsx
+++ b/frontend/src/pages/Settings/General/index.tsx
@@ -43,10 +43,10 @@ const SettingsGeneralView: FunctionComponent = () => {
<Section header="Host">
<Text
label="Address"
- placeholder="0.0.0.0"
+ placeholder="*"
settingKey="settings-general-ip"
></Text>
- <Message>Valid IPv4 address or '0.0.0.0' for all interfaces</Message>
+ <Message>Valid IP address or '*' for all interfaces</Message>
<Number
label="Port"
placeholder="6767"
diff --git a/frontend/src/pages/Settings/Languages/index.tsx b/frontend/src/pages/Settings/Languages/index.tsx
index 9fe562920..1bd9d72a8 100644
--- a/frontend/src/pages/Settings/Languages/index.tsx
+++ b/frontend/src/pages/Settings/Languages/index.tsx
@@ -1,7 +1,9 @@
import { FunctionComponent } from "react";
+import { Text as MantineText } from "@mantine/core";
import { useLanguageProfiles, useLanguages } from "@/apis/hooks";
import {
Check,
+ Chips,
CollapseBox,
Layout,
Message,
@@ -115,6 +117,50 @@ const SettingsLanguagesView: FunctionComponent = () => {
<Section header="Languages Profile">
<Table></Table>
</Section>
+ <Section header="Tag-Based Automatic Language Profile Selection Settings">
+ <Message>
+ If enabled, Bazarr will look at the names of all tags of a Series from
+ Sonarr (or a Movie from Radarr) to find a matching Bazarr language
+ profile tag. It will use as the language profile the FIRST tag from
+ Sonarr/Radarr that matches the tag of a Bazarr language profile
+ EXACTLY. If multiple tags match, there is no guarantee as to which one
+ will be used, so choose your tag names carefully. Also, if you update
+ the tag names in Sonarr/Radarr, Bazarr will detect this and repeat the
+ matching process for the affected shows. However, if a show's only
+ matching tag is removed from Sonarr/Radarr, Bazarr will NOT remove the
+ show's existing language profile for that reason. But if you wish to
+ have language profiles removed automatically by tag value, simply
+ enter a list of one or more tags in the{" "}
+ <MantineText fw={700} span>
+ Remove Profile Tags
+ </MantineText>{" "}
+ entry list below. If your video tag matches one of the tags in that
+ list, then Bazarr will remove the language profile for that video. If
+ there is a conflict between profile selection and profile removal,
+ then profile removal wins out and is performed.
+ </Message>
+ <Check
+ label="Series"
+ settingKey="settings-general-serie_tag_enabled"
+ ></Check>
+ <Check
+ label="Movies"
+ settingKey="settings-general-movie_tag_enabled"
+ ></Check>
+ <Chips
+ label="Remove Profile Tags"
+ settingKey="settings-general-remove_profile_tags"
+ sanitizeFn={(values: string[] | null) =>
+ values?.map((item) =>
+ item.replace(/[^a-z0-9_-]/gi, "").toLowerCase(),
+ )
+ }
+ ></Chips>
+ <Message>
+ Enter tag values that will trigger a language profile removal. Leave
+ empty if you don't want Bazarr to remove language profiles.
+ </Message>
+ </Section>
<Section header="Default Settings">
<Check
label="Series"
diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx
index c32300628..5cfefdfa9 100644
--- a/frontend/src/pages/Settings/Languages/table.tsx
+++ b/frontend/src/pages/Settings/Languages/table.tsx
@@ -2,7 +2,7 @@ import { FunctionComponent, useCallback, useMemo } from "react";
import { Badge, Button, Group } from "@mantine/core";
import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
-import { cloneDeep } from "lodash";
+import { cloneDeep, includes, maxBy } from "lodash";
import { Action } from "@/components";
import {
anyCutoff,
@@ -66,6 +66,10 @@ const Table: FunctionComponent = () => {
accessorKey: "name",
},
{
+ header: "Tag",
+ accessorKey: "tag",
+ },
+ {
header: "Languages",
accessorKey: "items",
cell: ({
@@ -75,10 +79,10 @@ const Table: FunctionComponent = () => {
}) => {
return (
<Group gap="xs" wrap="nowrap">
- {items.map((v) => {
+ {items.map((v, i) => {
const isCutoff = v.id === cutoff || cutoff === anyCutoff;
return (
- <ItemBadge key={v.id} cutoff={isCutoff} item={v}></ItemBadge>
+ <ItemBadge key={i} cutoff={isCutoff} item={v}></ItemBadge>
);
})}
</Group>
@@ -144,9 +148,45 @@ const Table: FunctionComponent = () => {
icon={faWrench}
c="gray"
onClick={() => {
+ const lastId = maxBy(profile.items, "id")?.id || 0;
+
+ // We once had an issue on the past where there were duplicated
+ // item ids that needs to become unique upon editing.
+ const sanitizedProfile = {
+ ...cloneDeep(profile),
+ items: profile.items.reduce(
+ (acc, value) => {
+ const { ids, duplicatedIds, items } = acc;
+
+ // We once had an issue on the past where there were duplicated
+ // item ids that needs to become unique upon editing.
+ if (includes(ids, value.id)) {
+ duplicatedIds.push(value.id);
+ items.push({
+ ...value,
+ id: lastId + duplicatedIds.length,
+ });
+
+ return acc;
+ }
+
+ ids.push(value.id);
+ items.push(value);
+
+ return acc;
+ },
+ {
+ ids: [] as number[],
+ duplicatedIds: [] as number[],
+ items: [] as typeof profile.items,
+ },
+ ).items,
+ tag: profile.tag || undefined,
+ };
+
modals.openContextModal(ProfileEditModal, {
languages,
- profile: cloneDeep(profile),
+ profile: sanitizedProfile,
onComplete: updateProfile,
});
}}
@@ -178,6 +218,7 @@ const Table: FunctionComponent = () => {
const profile = {
profileId: nextProfileId,
name: "",
+ tag: undefined,
items: [],
cutoff: null,
mustContain: [],
diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx
index 72e2c3b1f..acae15261 100644
--- a/frontend/src/pages/Settings/Providers/components.tsx
+++ b/frontend/src/pages/Settings/Providers/components.tsx
@@ -108,10 +108,12 @@ export const ProviderView: FunctionComponent<ProviderViewProps> = ({
})
.map((v, idx) => (
<Card
+ titleStyles={{ overflow: "hidden", textOverflow: "ellipsis" }}
key={BuildKey(v.key, idx)}
header={v.name ?? capitalize(v.key)}
description={v.description}
onClick={() => select(v)}
+ lineClamp={2}
></Card>
));
} else {
diff --git a/frontend/src/pages/Settings/Providers/list.ts b/frontend/src/pages/Settings/Providers/list.ts
index b2f9a33c7..8f0e46a56 100644
--- a/frontend/src/pages/Settings/Providers/list.ts
+++ b/frontend/src/pages/Settings/Providers/list.ts
@@ -218,6 +218,35 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
},
],
},
+ {
+ key: "jimaku",
+ name: "Jimaku.cc",
+ description: "Japanese Subtitles Provider",
+ message:
+ "API key required. Subtitles stem from various sources and might have quality/timing issues.",
+ inputs: [
+ {
+ type: "password",
+ key: "api_key",
+ name: "API key",
+ },
+ {
+ type: "switch",
+ key: "enable_name_search_fallback",
+ name: "Search by name if no AniList ID was determined (Less accurate, required for live action)",
+ },
+ {
+ type: "switch",
+ key: "enable_archives_download",
+ name: "Also consider archives alongside uncompressed subtitles",
+ },
+ {
+ type: "switch",
+ key: "enable_ai_subs",
+ name: "Download AI generated subtitles",
+ },
+ ],
+ },
{ key: "hosszupuska", description: "Hungarian Subtitles Provider" },
{
key: "karagarga",
@@ -276,6 +305,21 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
{ type: "switch", key: "skip_wrong_fps", name: "Skip Wrong FPS" },
],
},
+ {
+ key: "legendasnet",
+ name: "Legendas.net",
+ description: "Brazilian Subtitles Provider",
+ inputs: [
+ {
+ type: "text",
+ key: "username",
+ },
+ {
+ type: "password",
+ key: "password",
+ },
+ ],
+ },
{ key: "napiprojekt", description: "Polish Subtitles Provider" },
{
key: "napisy24",
diff --git a/frontend/src/pages/Settings/Radarr/index.tsx b/frontend/src/pages/Settings/Radarr/index.tsx
index b2e858178..264c78924 100644
--- a/frontend/src/pages/Settings/Radarr/index.tsx
+++ b/frontend/src/pages/Settings/Radarr/index.tsx
@@ -54,6 +54,11 @@ const SettingsRadarrView: FunctionComponent = () => {
<Chips
label="Excluded Tags"
settingKey="settings-radarr-excluded_tags"
+ sanitizeFn={(values: string[] | null) =>
+ values?.map((item) =>
+ item.replace(/[^a-z0-9_-]/gi, "").toLowerCase(),
+ )
+ }
></Chips>
<Message>
Movies with those tags (case sensitive) in Radarr will be excluded
diff --git a/frontend/src/pages/Settings/Sonarr/index.tsx b/frontend/src/pages/Settings/Sonarr/index.tsx
index ed66ef679..ff4ac6ca2 100644
--- a/frontend/src/pages/Settings/Sonarr/index.tsx
+++ b/frontend/src/pages/Settings/Sonarr/index.tsx
@@ -56,6 +56,11 @@ const SettingsSonarrView: FunctionComponent = () => {
<Chips
label="Excluded Tags"
settingKey="settings-sonarr-excluded_tags"
+ sanitizeFn={(values: string[] | null) =>
+ values?.map((item) =>
+ item.replace(/[^a-z0-9_-]/gi, "").toLowerCase(),
+ )
+ }
></Chips>
<Message>
Episodes from series with those tags (case sensitive) in Sonarr will
diff --git a/frontend/src/pages/Settings/Subtitles/index.tsx b/frontend/src/pages/Settings/Subtitles/index.tsx
index a2250e5a9..a2e05a5c5 100644
--- a/frontend/src/pages/Settings/Subtitles/index.tsx
+++ b/frontend/src/pages/Settings/Subtitles/index.tsx
@@ -1,5 +1,5 @@
-import { FunctionComponent } from "react";
-import { Code, Space, Table } from "@mantine/core";
+import React, { FunctionComponent } from "react";
+import { Code, Space, Table, Text as MantineText } from "@mantine/core";
import {
Check,
CollapseBox,
@@ -115,14 +115,16 @@ const commandOptions: CommandOption[] = [
},
];
-const commandOptionElements: JSX.Element[] = commandOptions.map((op, idx) => (
- <tr key={idx}>
- <td>
- <Code>{op.option}</Code>
- </td>
- <td>{op.description}</td>
- </tr>
-));
+const commandOptionElements: React.JSX.Element[] = commandOptions.map(
+ (op, idx) => (
+ <tr key={idx}>
+ <td>
+ <Code>{op.option}</Code>
+ </td>
+ <td>{op.description}</td>
+ </tr>
+ ),
+);
const SettingsSubtitlesView: FunctionComponent = () => {
return (
@@ -436,8 +438,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-subsync-subsync_threshold"></Slider>
<Space />
<Message>
- Only series subtitles with scores <b>below</b> this value will be
- automatically synchronized.
+ Only series subtitles with scores{" "}
+ <MantineText fw={700} span>
+ below
+ </MantineText>{" "}
+ this value will be automatically synchronized.
</Message>
</CollapseBox>
<Check
@@ -451,8 +456,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-subsync-subsync_movie_threshold"></Slider>
<Space />
<Message>
- Only movie subtitles with scores <b>below</b> this value will be
- automatically synchronized.
+ Only movie subtitles with scores{" "}
+ <MantineText fw={700} span>
+ below
+ </MantineText>{" "}
+ this value will be automatically synchronized.
</Message>
</CollapseBox>
</CollapseBox>
@@ -478,8 +486,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-general-postprocessing_threshold"></Slider>
<Space />
<Message>
- Only series subtitles with scores <b>below</b> this value will be
- automatically post-processed.
+ Only series subtitles with scores{" "}
+ <MantineText fw={700} span>
+ below
+ </MantineText>{" "}
+ this value will be automatically post-processed.
</Message>
</CollapseBox>
<Check
@@ -493,8 +504,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-general-postprocessing_threshold_movie"></Slider>
<Space />
<Message>
- Only movie subtitles with scores <b>below</b> this value will be
- automatically post-processed.
+ Only movie subtitles with scores{" "}
+ <MantineText fw={700} span>
+ below
+ </MantineText>{" "}
+ this value will be automatically post-processed.
</Message>
</CollapseBox>
<Text
diff --git a/frontend/src/pages/Settings/components/Card.tsx b/frontend/src/pages/Settings/components/Card.tsx
index 69df15636..a8a33eec3 100644
--- a/frontend/src/pages/Settings/components/Card.tsx
+++ b/frontend/src/pages/Settings/components/Card.tsx
@@ -1,14 +1,23 @@
import { FunctionComponent } from "react";
-import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
+import {
+ Center,
+ MantineStyleProp,
+ Stack,
+ Text,
+ UnstyledButton,
+} from "@mantine/core";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import TextPopover from "@/components/TextPopover";
import styles from "./Card.module.scss";
interface CardProps {
- header?: string;
description?: string;
- plus?: boolean;
+ header?: string;
+ lineClamp?: number | undefined;
onClick?: () => void;
+ plus?: boolean;
+ titleStyles?: MantineStyleProp | undefined;
}
export const Card: FunctionComponent<CardProps> = ({
@@ -16,6 +25,8 @@ export const Card: FunctionComponent<CardProps> = ({
description,
plus,
onClick,
+ lineClamp,
+ titleStyles,
}) => {
return (
<UnstyledButton p="lg" onClick={onClick} className={styles.card}>
@@ -24,9 +35,15 @@ export const Card: FunctionComponent<CardProps> = ({
<FontAwesomeIcon size="2x" icon={faPlus}></FontAwesomeIcon>
</Center>
) : (
- <Stack h="100%" gap={0} align="flex-start">
- <Text fw="bold">{header}</Text>
- <Text hidden={description === undefined}>{description}</Text>
+ <Stack h="100%" gap={0}>
+ <Text fw="bold" style={titleStyles}>
+ {header}
+ </Text>
+ <TextPopover text={description}>
+ <Text hidden={description === undefined} lineClamp={lineClamp}>
+ {description}
+ </Text>
+ </TextPopover>
</Stack>
)}
</UnstyledButton>
diff --git a/frontend/src/pages/Settings/components/forms.test.tsx b/frontend/src/pages/Settings/components/forms.test.tsx
index a88d2bec7..4ec60699b 100644
--- a/frontend/src/pages/Settings/components/forms.test.tsx
+++ b/frontend/src/pages/Settings/components/forms.test.tsx
@@ -2,7 +2,7 @@ import { FunctionComponent, PropsWithChildren, ReactElement } from "react";
import { useForm } from "@mantine/form";
import { describe, it } from "vitest";
import { FormContext, FormValues } from "@/pages/Settings/utilities/FormValues";
-import { render, RenderOptions, screen } from "@/tests";
+import { render, screen } from "@/tests";
import { Number, Text } from "./forms";
const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
@@ -15,10 +15,8 @@ const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
};
-const formRender = (
- ui: ReactElement,
- options?: Omit<RenderOptions, "wrapper">,
-) => render(<FormSupport>{ui}</FormSupport>);
+const formRender = (ui: ReactElement) =>
+ render(<FormSupport>{ui}</FormSupport>);
describe("Settings form", () => {
describe("number component", () => {
diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx
index 95134db92..43b559736 100644
--- a/frontend/src/pages/Settings/components/forms.tsx
+++ b/frontend/src/pages/Settings/components/forms.tsx
@@ -1,4 +1,4 @@
-import { FunctionComponent, ReactNode, ReactText } from "react";
+import { FunctionComponent, ReactNode } from "react";
import {
Input,
NumberInput,
@@ -49,7 +49,7 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
);
};
-export type TextProps = BaseInput<ReactText> & TextInputProps;
+export type TextProps = BaseInput<string | number> & TextInputProps;
export const Text: FunctionComponent<TextProps> = (props) => {
const { value, update, rest } = useBaseInput(props);
@@ -86,11 +86,7 @@ export interface CheckProps extends BaseInput<boolean> {
inline?: boolean;
}
-export const Check: FunctionComponent<CheckProps> = ({
- label,
- inline,
- ...props
-}) => {
+export const Check: FunctionComponent<CheckProps> = ({ label, ...props }) => {
const { value, update, rest } = useBaseInput(props);
return (
@@ -160,13 +156,25 @@ export const Slider: FunctionComponent<SliderProps> = (props) => {
};
type ChipsProp = BaseInput<string[]> &
- Omit<ChipInputProps, "onChange" | "data">;
+ Omit<ChipInputProps, "onChange" | "data"> & {
+ sanitizeFn?: (values: string[] | null) => string[] | undefined;
+ };
export const Chips: FunctionComponent<ChipsProp> = (props) => {
const { value, update, rest } = useBaseInput(props);
+ const handleChange = (value: string[] | null) => {
+ const sanitizedValues = props.sanitizeFn?.(value) ?? value;
+
+ update(sanitizedValues || null);
+ };
+
return (
- <ChipInput {...rest} value={value ?? []} onChange={update}></ChipInput>
+ <ChipInput
+ {...rest}
+ value={value ?? []}
+ onChange={handleChange}
+ ></ChipInput>
);
};
diff --git a/frontend/src/pages/System/Announcements/table.tsx b/frontend/src/pages/System/Announcements/table.tsx
index febb32fa1..910fb4bd5 100644
--- a/frontend/src/pages/System/Announcements/table.tsx
+++ b/frontend/src/pages/System/Announcements/table.tsx
@@ -19,7 +19,7 @@ const Table: FunctionComponent<Props> = ({ announcements }) => {
() => [
{
header: "Since",
- accessor: "timestamp",
+ accessorKey: "timestamp",
cell: ({
row: {
original: { timestamp },
@@ -30,7 +30,7 @@ const Table: FunctionComponent<Props> = ({ announcements }) => {
},
{
header: "Announcement",
- accessor: "text",
+ accessorKey: "text",
cell: ({
row: {
original: { text },
@@ -41,7 +41,7 @@ const Table: FunctionComponent<Props> = ({ announcements }) => {
},
{
header: "More Info",
- accessor: "link",
+ accessorKey: "link",
cell: ({
row: {
original: { link },
@@ -56,7 +56,7 @@ const Table: FunctionComponent<Props> = ({ announcements }) => {
},
{
header: "Dismiss",
- accessor: "hash",
+ accessorKey: "hash",
cell: ({
row: {
original: { dismissible, hash },
diff --git a/frontend/src/pages/System/Status/index.tsx b/frontend/src/pages/System/Status/index.tsx
index bcd0e175d..157935dfb 100644
--- a/frontend/src/pages/System/Status/index.tsx
+++ b/frontend/src/pages/System/Status/index.tsx
@@ -144,6 +144,8 @@ const SystemStatusView: FunctionComponent = () => {
<Row title="Radarr Version">{status?.radarr_version}</Row>
<Row title="Operating System">{status?.operating_system}</Row>
<Row title="Python Version">{status?.python_version}</Row>
+ <Row title="Database Engine">{status?.database_engine}</Row>
+ <Row title="Database Version">{status?.database_migration}</Row>
<Row title="Bazarr Directory">{status?.bazarr_directory}</Row>
<Row title="Bazarr Config Directory">
{status?.bazarr_config_directory}
diff --git a/frontend/src/pages/System/Tasks/table.tsx b/frontend/src/pages/System/Tasks/table.tsx
index ed3248b6f..5e1b045bd 100644
--- a/frontend/src/pages/System/Tasks/table.tsx
+++ b/frontend/src/pages/System/Tasks/table.tsx
@@ -17,7 +17,7 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
() => [
{
header: "Name",
- accessor: "name",
+ accessorKey: "name",
cell: ({
row: {
original: { name },
@@ -28,7 +28,7 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
},
{
header: "Interval",
- accessor: "interval",
+ accessorKey: "interval",
cell: ({
row: {
original: { interval },
@@ -39,11 +39,11 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
},
{
header: "Next Execution",
- accessor: "next_run_in",
+ accessorKey: "next_run_in",
},
{
header: "Run",
- accessor: "job_running",
+ accessorKey: "job_running",
cell: ({
row: {
original: { job_id: jobId, job_running: jobRunning },
diff --git a/frontend/src/pages/Wanted/Movies/index.tsx b/frontend/src/pages/Wanted/Movies/index.tsx
index 7c497f799..c05cfb7c3 100644
--- a/frontend/src/pages/Wanted/Movies/index.tsx
+++ b/frontend/src/pages/Wanted/Movies/index.tsx
@@ -21,7 +21,7 @@ const WantedMoviesView: FunctionComponent = () => {
() => [
{
header: "Name",
- accessor: "title",
+ accessorKey: "title",
cell: ({
row: {
original: { title, radarrId },
@@ -37,7 +37,7 @@ const WantedMoviesView: FunctionComponent = () => {
},
{
header: "Missing",
- accessor: "missing_subtitles",
+ accessorKey: "missing_subtitles",
cell: ({
row: {
original: { radarrId, missing_subtitles: missingSubtitles },
diff --git a/frontend/src/pages/views/ItemOverview.tsx b/frontend/src/pages/views/ItemOverview.tsx
index 15b43aab1..36d296850 100644
--- a/frontend/src/pages/views/ItemOverview.tsx
+++ b/frontend/src/pages/views/ItemOverview.tsx
@@ -31,6 +31,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Language } from "@/components/bazarr";
import { BuildKey } from "@/utilities";
import {
+ normalizeAudioLanguage,
useLanguageProfileBy,
useProfileItemsToLanguages,
} from "@/utilities/languages";
@@ -87,7 +88,7 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
icon={faMusic}
title="Audio Language"
>
- {v.name}
+ {normalizeAudioLanguage(v.name)}
</ItemBadge>
)) ?? [],
[item?.audio_language],
@@ -142,12 +143,7 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
}}
>
<Grid.Col span={3} visibleFrom="sm">
- <Image
- src={item?.poster}
- mx="auto"
- maw="250px"
- fallbackSrc="https://placehold.co/250x250?text=Placeholder"
- ></Image>
+ <Image src={item?.poster} mx="auto" maw="250px"></Image>
</Grid.Col>
<Grid.Col span={8} maw="100%" style={{ overflow: "hidden" }}>
<Stack align="flex-start" gap="xs" mx={6}>
diff --git a/frontend/src/types/api.d.ts b/frontend/src/types/api.d.ts
index 069be3029..e8bd4483e 100644
--- a/frontend/src/types/api.d.ts
+++ b/frontend/src/types/api.d.ts
@@ -40,6 +40,7 @@ declare namespace Language {
mustContain: string[];
mustNotContain: string[];
originalFormat: boolean | null;
+ tag: string | undefined;
}
}
diff --git a/frontend/src/types/settings.d.ts b/frontend/src/types/settings.d.ts
index 9ae6d8454..7b57f10cc 100644
--- a/frontend/src/types/settings.d.ts
+++ b/frontend/src/types/settings.d.ts
@@ -62,6 +62,7 @@ declare namespace Settings {
postprocessing_cmd?: string;
postprocessing_threshold: number;
postprocessing_threshold_movie: number;
+ remove_profile_tags: string[];
single_language: boolean;
subfolder: string;
subfolder_custom?: string;
diff --git a/frontend/src/types/system.d.ts b/frontend/src/types/system.d.ts
index 544d969ae..5a477fb54 100644
--- a/frontend/src/types/system.d.ts
+++ b/frontend/src/types/system.d.ts
@@ -20,6 +20,8 @@ declare namespace System {
bazarr_config_directory: string;
bazarr_directory: string;
bazarr_version: string;
+ database_engine: string;
+ database_migration: string;
operating_system: string;
package_version: string;
python_version: string;
diff --git a/frontend/src/utilities/languages.ts b/frontend/src/utilities/languages.ts
index 1b59aa4e7..7885e9667 100644
--- a/frontend/src/utilities/languages.ts
+++ b/frontend/src/utilities/languages.ts
@@ -51,3 +51,7 @@ export function useLanguageFromCode3(code3: string) {
[data, code3],
);
}
+
+export const normalizeAudioLanguage = (name: string) => {
+ return name === "Chinese Simplified" ? "Chinese" : name;
+};
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index d6c625441..fbae83c88 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -36,6 +36,9 @@ export default defineConfig(async ({ mode, command }) => {
enableBuild: false,
}),
VitePWA({
+ workbox: {
+ globIgnores: ["index.html"],
+ },
registerType: "autoUpdate",
includeAssets: [
`${imagesFolder}/favicon.ico`,
diff --git a/libs/argparse-1.4.0.dist-info/LICENSE.txt b/libs/argparse-1.4.0.dist-info/LICENSE.txt
deleted file mode 100644
index 640bc7809..000000000
--- a/libs/argparse-1.4.0.dist-info/LICENSE.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-argparse is (c) 2006-2009 Steven J. Bethard <[email protected]>.
-
-The argparse module was contributed to Python as of Python 2.7 and thus
-was licensed under the Python license. Same license applies to all files in
-the argparse package project.
-
-For details about the Python License, please see doc/Python-License.txt.
-
-History
--------
-
-Before (and including) argparse 1.1, the argparse package was licensed under
-Apache License v2.0.
-
-After argparse 1.1, all project files from the argparse project were deleted
-due to license compatibility issues between Apache License 2.0 and GNU GPL v2.
-
-The project repository then had a clean start with some files taken from
-Python 2.7.1, so definitely all files are under Python License now.
-
diff --git a/libs/argparse-1.4.0.dist-info/METADATA b/libs/argparse-1.4.0.dist-info/METADATA
deleted file mode 100644
index 39d06b1a7..000000000
--- a/libs/argparse-1.4.0.dist-info/METADATA
+++ /dev/null
@@ -1,84 +0,0 @@
-Metadata-Version: 2.1
-Name: argparse
-Version: 1.4.0
-Summary: Python command-line parsing library
-Home-page: https://github.com/ThomasWaldmann/argparse/
-Author: Thomas Waldmann
-Author-email: [email protected]
-License: Python Software Foundation License
-Keywords: argparse command line parser parsing
-Platform: any
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Console
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: Python Software Foundation License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 2.3
-Classifier: Programming Language :: Python :: 2.4
-Classifier: Programming Language :: Python :: 2.5
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.0
-Classifier: Programming Language :: Python :: 3.1
-Classifier: Programming Language :: Python :: 3.2
-Classifier: Programming Language :: Python :: 3.3
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Topic :: Software Development
-License-File: LICENSE.txt
-
-The argparse module makes it easy to write user friendly command line
-interfaces.
-
-The program defines what arguments it requires, and argparse will figure out
-how to parse those out of sys.argv. The argparse module also automatically
-generates help and usage messages and issues errors when users give the
-program invalid arguments.
-
-As of Python >= 2.7 and >= 3.2, the argparse module is maintained within the
-Python standard library. For users who still need to support Python < 2.7 or
-< 3.2, it is also provided as a separate package, which tries to stay
-compatible with the module in the standard library, but also supports older
-Python versions.
-
-Also, we can fix bugs here for users who are stuck on some non-current python
-version, like e.g. 3.2.3 (which has bugs that were fixed in a later 3.2.x
-release).
-
-argparse is licensed under the Python license, for details see LICENSE.txt.
-
-
-Compatibility
--------------
-
-argparse should work on Python >= 2.3, it was tested on:
-
-* 2.3, 2.4, 2.5, 2.6 and 2.7
-* 3.1, 3.2, 3.3, 3.4
-
-
-Installation
-------------
-
-Try one of these:
-
- python setup.py install
-
- easy_install argparse
-
- pip install argparse
-
- putting argparse.py in some directory listed in sys.path should also work
-
-
-Bugs
-----
-
-If you find a bug in argparse (pypi), please try to reproduce it with latest
-python 2.7 and 3.4 (and use argparse from stdlib).
-
-If it happens there also, please file a bug in the python.org issue tracker.
-If it does not happen there, file a bug in the argparse package issue tracker.
-
diff --git a/libs/argparse-1.4.0.dist-info/RECORD b/libs/argparse-1.4.0.dist-info/RECORD
deleted file mode 100644
index 8b6f6f6c9..000000000
--- a/libs/argparse-1.4.0.dist-info/RECORD
+++ /dev/null
@@ -1,8 +0,0 @@
-argparse-1.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
-argparse-1.4.0.dist-info/LICENSE.txt,sha256=bVBNRcTRCfkl7wWJYLbRzicSu2tXk-kmv8FRcWrHQEg,741
-argparse-1.4.0.dist-info/METADATA,sha256=yZGPMA4uvkui2P7qaaiI89zqwjDbyFcehJG4j5Pk8Yk,2816
-argparse-1.4.0.dist-info/RECORD,,
-argparse-1.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-argparse-1.4.0.dist-info/WHEEL,sha256=P2T-6epvtXQ2cBOE_U1K4_noqlJFN3tj15djMgEu4NM,110
-argparse-1.4.0.dist-info/top_level.txt,sha256=TgiWrQsF0mKWwqS2KHLORD0ZtqYHPRGdCAAzKwtVvJ4,9
-argparse.py,sha256=0ksYqisQDQvhoiuo19JERCSpg51tc641GFJIx7pTA0g,89214
diff --git a/libs/argparse-1.4.0.dist-info/WHEEL b/libs/argparse-1.4.0.dist-info/WHEEL
deleted file mode 100644
index f31e450fd..000000000
--- a/libs/argparse-1.4.0.dist-info/WHEEL
+++ /dev/null
@@ -1,6 +0,0 @@
-Wheel-Version: 1.0
-Generator: bdist_wheel (0.41.3)
-Root-Is-Purelib: true
-Tag: py2-none-any
-Tag: py3-none-any
-
diff --git a/libs/argparse-1.4.0.dist-info/top_level.txt b/libs/argparse-1.4.0.dist-info/top_level.txt
deleted file mode 100644
index 1352d5e6f..000000000
--- a/libs/argparse-1.4.0.dist-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-argparse
diff --git a/libs/argparse.py b/libs/argparse.py
deleted file mode 100644
index 70a77cc02..000000000
--- a/libs/argparse.py
+++ /dev/null
@@ -1,2392 +0,0 @@
-# Author: Steven J. Bethard <[email protected]>.
-# Maintainer: Thomas Waldmann <[email protected]>
-
-"""Command-line parsing library
-
-This module is an optparse-inspired command-line parsing library that:
-
- - handles both optional and positional arguments
- - produces highly informative usage messages
- - supports parsers that dispatch to sub-parsers
-
-The following is a simple usage example that sums integers from the
-command-line and writes the result to a file::
-
- parser = argparse.ArgumentParser(
- description='sum the integers at the command line')
- parser.add_argument(
- 'integers', metavar='int', nargs='+', type=int,
- help='an integer to be summed')
- parser.add_argument(
- '--log', default=sys.stdout, type=argparse.FileType('w'),
- help='the file where the sum should be written')
- args = parser.parse_args()
- args.log.write('%s' % sum(args.integers))
- args.log.close()
-
-The module contains the following public classes:
-
- - ArgumentParser -- The main entry point for command-line parsing. As the
- example above shows, the add_argument() method is used to populate
- the parser with actions for optional and positional arguments. Then
- the parse_args() method is invoked to convert the args at the
- command-line into an object with attributes.
-
- - ArgumentError -- The exception raised by ArgumentParser objects when
- there are errors with the parser's actions. Errors raised while
- parsing the command-line are caught by ArgumentParser and emitted
- as command-line messages.
-
- - FileType -- A factory for defining types of files to be created. As the
- example above shows, instances of FileType are typically passed as
- the type= argument of add_argument() calls.
-
- - Action -- The base class for parser actions. Typically actions are
- selected by passing strings like 'store_true' or 'append_const' to
- the action= argument of add_argument(). However, for greater
- customization of ArgumentParser actions, subclasses of Action may
- be defined and passed as the action= argument.
-
- - HelpFormatter, RawDescriptionHelpFormatter, RawTextHelpFormatter,
- ArgumentDefaultsHelpFormatter -- Formatter classes which
- may be passed as the formatter_class= argument to the
- ArgumentParser constructor. HelpFormatter is the default,
- RawDescriptionHelpFormatter and RawTextHelpFormatter tell the parser
- not to change the formatting for help text, and
- ArgumentDefaultsHelpFormatter adds information about argument defaults
- to the help.
-
-All other classes in this module are considered implementation details.
-(Also note that HelpFormatter and RawDescriptionHelpFormatter are only
-considered public as object names -- the API of the formatter objects is
-still considered an implementation detail.)
-"""
-
-__version__ = '1.4.0' # we use our own version number independant of the
- # one in stdlib and we release this on pypi.
-
-__external_lib__ = True # to make sure the tests really test THIS lib,
- # not the builtin one in Python stdlib
-
-__all__ = [
- 'ArgumentParser',
- 'ArgumentError',
- 'ArgumentTypeError',
- 'FileType',
- 'HelpFormatter',
- 'ArgumentDefaultsHelpFormatter',
- 'RawDescriptionHelpFormatter',
- 'RawTextHelpFormatter',
- 'Namespace',
- 'Action',
- 'ONE_OR_MORE',
- 'OPTIONAL',
- 'PARSER',
- 'REMAINDER',
- 'SUPPRESS',
- 'ZERO_OR_MORE',
-]
-
-
-import copy as _copy
-import os as _os
-import re as _re
-import sys as _sys
-import textwrap as _textwrap
-
-from gettext import gettext as _
-
-try:
- set
-except NameError:
- # for python < 2.4 compatibility (sets module is there since 2.3):
- from sets import Set as set
-
-try:
- basestring
-except NameError:
- basestring = str
-
-try:
- sorted
-except NameError:
- # for python < 2.4 compatibility:
- def sorted(iterable, reverse=False):
- result = list(iterable)
- result.sort()
- if reverse:
- result.reverse()
- return result
-
-
-def _callable(obj):
- return hasattr(obj, '__call__') or hasattr(obj, '__bases__')
-
-
-SUPPRESS = '==SUPPRESS=='
-
-OPTIONAL = '?'
-ZERO_OR_MORE = '*'
-ONE_OR_MORE = '+'
-PARSER = 'A...'
-REMAINDER = '...'
-_UNRECOGNIZED_ARGS_ATTR = '_unrecognized_args'
-
-# =============================
-# Utility functions and classes
-# =============================
-
-class _AttributeHolder(object):
- """Abstract base class that provides __repr__.
-
- The __repr__ method returns a string in the format::
- ClassName(attr=name, attr=name, ...)
- The attributes are determined either by a class-level attribute,
- '_kwarg_names', or by inspecting the instance __dict__.
- """
-
- def __repr__(self):
- type_name = type(self).__name__
- arg_strings = []
- for arg in self._get_args():
- arg_strings.append(repr(arg))
- for name, value in self._get_kwargs():
- arg_strings.append('%s=%r' % (name, value))
- return '%s(%s)' % (type_name, ', '.join(arg_strings))
-
- def _get_kwargs(self):
- return sorted(self.__dict__.items())
-
- def _get_args(self):
- return []
-
-
-def _ensure_value(namespace, name, value):
- if getattr(namespace, name, None) is None:
- setattr(namespace, name, value)
- return getattr(namespace, name)
-
-
-# ===============
-# Formatting Help
-# ===============
-
-class HelpFormatter(object):
- """Formatter for generating usage messages and argument help strings.
-
- Only the name of this class is considered a public API. All the methods
- provided by the class are considered an implementation detail.
- """
-
- def __init__(self,
- prog,
- indent_increment=2,
- max_help_position=24,
- width=None):
-
- # default setting for width
- if width is None:
- try:
- width = int(_os.environ['COLUMNS'])
- except (KeyError, ValueError):
- width = 80
- width -= 2
-
- self._prog = prog
- self._indent_increment = indent_increment
- self._max_help_position = max_help_position
- self._width = width
-
- self._current_indent = 0
- self._level = 0
- self._action_max_length = 0
-
- self._root_section = self._Section(self, None)
- self._current_section = self._root_section
-
- self._whitespace_matcher = _re.compile(r'\s+')
- self._long_break_matcher = _re.compile(r'\n\n\n+')
-
- # ===============================
- # Section and indentation methods
- # ===============================
- def _indent(self):
- self._current_indent += self._indent_increment
- self._level += 1
-
- def _dedent(self):
- self._current_indent -= self._indent_increment
- assert self._current_indent >= 0, 'Indent decreased below 0.'
- self._level -= 1
-
- class _Section(object):
-
- def __init__(self, formatter, parent, heading=None):
- self.formatter = formatter
- self.parent = parent
- self.heading = heading
- self.items = []
-
- def format_help(self):
- # format the indented section
- if self.parent is not None:
- self.formatter._indent()
- join = self.formatter._join_parts
- for func, args in self.items:
- func(*args)
- item_help = join([func(*args) for func, args in self.items])
- if self.parent is not None:
- self.formatter._dedent()
-
- # return nothing if the section was empty
- if not item_help:
- return ''
-
- # add the heading if the section was non-empty
- if self.heading is not SUPPRESS and self.heading is not None:
- current_indent = self.formatter._current_indent
- heading = '%*s%s:\n' % (current_indent, '', self.heading)
- else:
- heading = ''
-
- # join the section-initial newline, the heading and the help
- return join(['\n', heading, item_help, '\n'])
-
- def _add_item(self, func, args):
- self._current_section.items.append((func, args))
-
- # ========================
- # Message building methods
- # ========================
- def start_section(self, heading):
- self._indent()
- section = self._Section(self, self._current_section, heading)
- self._add_item(section.format_help, [])
- self._current_section = section
-
- def end_section(self):
- self._current_section = self._current_section.parent
- self._dedent()
-
- def add_text(self, text):
- if text is not SUPPRESS and text is not None:
- self._add_item(self._format_text, [text])
-
- def add_usage(self, usage, actions, groups, prefix=None):
- if usage is not SUPPRESS:
- args = usage, actions, groups, prefix
- self._add_item(self._format_usage, args)
-
- def add_argument(self, action):
- if action.help is not SUPPRESS:
-
- # find all invocations
- get_invocation = self._format_action_invocation
- invocations = [get_invocation(action)]
- for subaction in self._iter_indented_subactions(action):
- invocations.append(get_invocation(subaction))
-
- # update the maximum item length
- invocation_length = max([len(s) for s in invocations])
- action_length = invocation_length + self._current_indent
- self._action_max_length = max(self._action_max_length,
- action_length)
-
- # add the item to the list
- self._add_item(self._format_action, [action])
-
- def add_arguments(self, actions):
- for action in actions:
- self.add_argument(action)
-
- # =======================
- # Help-formatting methods
- # =======================
- def format_help(self):
- help = self._root_section.format_help()
- if help:
- help = self._long_break_matcher.sub('\n\n', help)
- help = help.strip('\n') + '\n'
- return help
-
- def _join_parts(self, part_strings):
- return ''.join([part
- for part in part_strings
- if part and part is not SUPPRESS])
-
- def _format_usage(self, usage, actions, groups, prefix):
- if prefix is None:
- prefix = _('usage: ')
-
- # if usage is specified, use that
- if usage is not None:
- usage = usage % dict(prog=self._prog)
-
- # if no optionals or positionals are available, usage is just prog
- elif usage is None and not actions:
- usage = '%(prog)s' % dict(prog=self._prog)
-
- # if optionals and positionals are available, calculate usage
- elif usage is None:
- prog = '%(prog)s' % dict(prog=self._prog)
-
- # split optionals from positionals
- optionals = []
- positionals = []
- for action in actions:
- if action.option_strings:
- optionals.append(action)
- else:
- positionals.append(action)
-
- # build full usage string
- format = self._format_actions_usage
- action_usage = format(optionals + positionals, groups)
- usage = ' '.join([s for s in [prog, action_usage] if s])
-
- # wrap the usage parts if it's too long
- text_width = self._width - self._current_indent
- if len(prefix) + len(usage) > text_width:
-
- # break usage into wrappable parts
- part_regexp = r'\(.*?\)+|\[.*?\]+|\S+'
- opt_usage = format(optionals, groups)
- pos_usage = format(positionals, groups)
- opt_parts = _re.findall(part_regexp, opt_usage)
- pos_parts = _re.findall(part_regexp, pos_usage)
- assert ' '.join(opt_parts) == opt_usage
- assert ' '.join(pos_parts) == pos_usage
-
- # helper for wrapping lines
- def get_lines(parts, indent, prefix=None):
- lines = []
- line = []
- if prefix is not None:
- line_len = len(prefix) - 1
- else:
- line_len = len(indent) - 1
- for part in parts:
- if line_len + 1 + len(part) > text_width:
- lines.append(indent + ' '.join(line))
- line = []
- line_len = len(indent) - 1
- line.append(part)
- line_len += len(part) + 1
- if line:
- lines.append(indent + ' '.join(line))
- if prefix is not None:
- lines[0] = lines[0][len(indent):]
- return lines
-
- # if prog is short, follow it with optionals or positionals
- if len(prefix) + len(prog) <= 0.75 * text_width:
- indent = ' ' * (len(prefix) + len(prog) + 1)
- if opt_parts:
- lines = get_lines([prog] + opt_parts, indent, prefix)
- lines.extend(get_lines(pos_parts, indent))
- elif pos_parts:
- lines = get_lines([prog] + pos_parts, indent, prefix)
- else:
- lines = [prog]
-
- # if prog is long, put it on its own line
- else:
- indent = ' ' * len(prefix)
- parts = opt_parts + pos_parts
- lines = get_lines(parts, indent)
- if len(lines) > 1:
- lines = []
- lines.extend(get_lines(opt_parts, indent))
- lines.extend(get_lines(pos_parts, indent))
- lines = [prog] + lines
-
- # join lines into usage
- usage = '\n'.join(lines)
-
- # prefix with 'usage:'
- return '%s%s\n\n' % (prefix, usage)
-
- def _format_actions_usage(self, actions, groups):
- # find group indices and identify actions in groups
- group_actions = set()
- inserts = {}
- for group in groups:
- try:
- start = actions.index(group._group_actions[0])
- except ValueError:
- continue
- else:
- end = start + len(group._group_actions)
- if actions[start:end] == group._group_actions:
- for action in group._group_actions:
- group_actions.add(action)
- if not group.required:
- if start in inserts:
- inserts[start] += ' ['
- else:
- inserts[start] = '['
- inserts[end] = ']'
- else:
- if start in inserts:
- inserts[start] += ' ('
- else:
- inserts[start] = '('
- inserts[end] = ')'
- for i in range(start + 1, end):
- inserts[i] = '|'
-
- # collect all actions format strings
- parts = []
- for i, action in enumerate(actions):
-
- # suppressed arguments are marked with None
- # remove | separators for suppressed arguments
- if action.help is SUPPRESS:
- parts.append(None)
- if inserts.get(i) == '|':
- inserts.pop(i)
- elif inserts.get(i + 1) == '|':
- inserts.pop(i + 1)
-
- # produce all arg strings
- elif not action.option_strings:
- part = self._format_args(action, action.dest)
-
- # if it's in a group, strip the outer []
- if action in group_actions:
- if part[0] == '[' and part[-1] == ']':
- part = part[1:-1]
-
- # add the action string to the list
- parts.append(part)
-
- # produce the first way to invoke the option in brackets
- else:
- option_string = action.option_strings[0]
-
- # if the Optional doesn't take a value, format is:
- # -s or --long
- if action.nargs == 0:
- part = '%s' % option_string
-
- # if the Optional takes a value, format is:
- # -s ARGS or --long ARGS
- else:
- default = action.dest.upper()
- args_string = self._format_args(action, default)
- part = '%s %s' % (option_string, args_string)
-
- # make it look optional if it's not required or in a group
- if not action.required and action not in group_actions:
- part = '[%s]' % part
-
- # add the action string to the list
- parts.append(part)
-
- # insert things at the necessary indices
- for i in sorted(inserts, reverse=True):
- parts[i:i] = [inserts[i]]
-
- # join all the action items with spaces
- text = ' '.join([item for item in parts if item is not None])
-
- # clean up separators for mutually exclusive groups
- open = r'[\[(]'
- close = r'[\])]'
- text = _re.sub(r'(%s) ' % open, r'\1', text)
- text = _re.sub(r' (%s)' % close, r'\1', text)
- text = _re.sub(r'%s *%s' % (open, close), r'', text)
- text = _re.sub(r'\(([^|]*)\)', r'\1', text)
- text = text.strip()
-
- # return the text
- return text
-
- def _format_text(self, text):
- if '%(prog)' in text:
- text = text % dict(prog=self._prog)
- text_width = self._width - self._current_indent
- indent = ' ' * self._current_indent
- return self._fill_text(text, text_width, indent) + '\n\n'
-
- def _format_action(self, action):
- # determine the required width and the entry label
- help_position = min(self._action_max_length + 2,
- self._max_help_position)
- help_width = self._width - help_position
- action_width = help_position - self._current_indent - 2
- action_header = self._format_action_invocation(action)
-
- # ho nelp; start on same line and add a final newline
- if not action.help:
- tup = self._current_indent, '', action_header
- action_header = '%*s%s\n' % tup
-
- # short action name; start on the same line and pad two spaces
- elif len(action_header) <= action_width:
- tup = self._current_indent, '', action_width, action_header
- action_header = '%*s%-*s ' % tup
- indent_first = 0
-
- # long action name; start on the next line
- else:
- tup = self._current_indent, '', action_header
- action_header = '%*s%s\n' % tup
- indent_first = help_position
-
- # collect the pieces of the action help
- parts = [action_header]
-
- # if there was help for the action, add lines of help text
- if action.help:
- help_text = self._expand_help(action)
- help_lines = self._split_lines(help_text, help_width)
- parts.append('%*s%s\n' % (indent_first, '', help_lines[0]))
- for line in help_lines[1:]:
- parts.append('%*s%s\n' % (help_position, '', line))
-
- # or add a newline if the description doesn't end with one
- elif not action_header.endswith('\n'):
- parts.append('\n')
-
- # if there are any sub-actions, add their help as well
- for subaction in self._iter_indented_subactions(action):
- parts.append(self._format_action(subaction))
-
- # return a single string
- return self._join_parts(parts)
-
- def _format_action_invocation(self, action):
- if not action.option_strings:
- metavar, = self._metavar_formatter(action, action.dest)(1)
- return metavar
-
- else:
- parts = []
-
- # if the Optional doesn't take a value, format is:
- # -s, --long
- if action.nargs == 0:
- parts.extend(action.option_strings)
-
- # if the Optional takes a value, format is:
- # -s ARGS, --long ARGS
- else:
- default = action.dest.upper()
- args_string = self._format_args(action, default)
- for option_string in action.option_strings:
- parts.append('%s %s' % (option_string, args_string))
-
- return ', '.join(parts)
-
- def _metavar_formatter(self, action, default_metavar):
- if action.metavar is not None:
- result = action.metavar
- elif action.choices is not None:
- choice_strs = [str(choice) for choice in action.choices]
- result = '{%s}' % ','.join(choice_strs)
- else:
- result = default_metavar
-
- def format(tuple_size):
- if isinstance(result, tuple):
- return result
- else:
- return (result, ) * tuple_size
- return format
-
- def _format_args(self, action, default_metavar):
- get_metavar = self._metavar_formatter(action, default_metavar)
- if action.nargs is None:
- result = '%s' % get_metavar(1)
- elif action.nargs == OPTIONAL:
- result = '[%s]' % get_metavar(1)
- elif action.nargs == ZERO_OR_MORE:
- result = '[%s [%s ...]]' % get_metavar(2)
- elif action.nargs == ONE_OR_MORE:
- result = '%s [%s ...]' % get_metavar(2)
- elif action.nargs == REMAINDER:
- result = '...'
- elif action.nargs == PARSER:
- result = '%s ...' % get_metavar(1)
- else:
- formats = ['%s' for _ in range(action.nargs)]
- result = ' '.join(formats) % get_metavar(action.nargs)
- return result
-
- def _expand_help(self, action):
- params = dict(vars(action), prog=self._prog)
- for name in list(params):
- if params[name] is SUPPRESS:
- del params[name]
- for name in list(params):
- if hasattr(params[name], '__name__'):
- params[name] = params[name].__name__
- if params.get('choices') is not None:
- choices_str = ', '.join([str(c) for c in params['choices']])
- params['choices'] = choices_str
- return self._get_help_string(action) % params
-
- def _iter_indented_subactions(self, action):
- try:
- get_subactions = action._get_subactions
- except AttributeError:
- pass
- else:
- self._indent()
- for subaction in get_subactions():
- yield subaction
- self._dedent()
-
- def _split_lines(self, text, width):
- text = self._whitespace_matcher.sub(' ', text).strip()
- return _textwrap.wrap(text, width)
-
- def _fill_text(self, text, width, indent):
- text = self._whitespace_matcher.sub(' ', text).strip()
- return _textwrap.fill(text, width, initial_indent=indent,
- subsequent_indent=indent)
-
- def _get_help_string(self, action):
- return action.help
-
-
-class RawDescriptionHelpFormatter(HelpFormatter):
- """Help message formatter which retains any formatting in descriptions.
-
- Only the name of this class is considered a public API. All the methods
- provided by the class are considered an implementation detail.
- """
-
- def _fill_text(self, text, width, indent):
- return ''.join([indent + line for line in text.splitlines(True)])
-
-
-class RawTextHelpFormatter(RawDescriptionHelpFormatter):
- """Help message formatter which retains formatting of all help text.
-
- Only the name of this class is considered a public API. All the methods
- provided by the class are considered an implementation detail.
- """
-
- def _split_lines(self, text, width):
- return text.splitlines()
-
-
-class ArgumentDefaultsHelpFormatter(HelpFormatter):
- """Help message formatter which adds default values to argument help.
-
- Only the name of this class is considered a public API. All the methods
- provided by the class are considered an implementation detail.
- """
-
- def _get_help_string(self, action):
- help = action.help
- if '%(default)' not in action.help:
- if action.default is not SUPPRESS:
- defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
- if action.option_strings or action.nargs in defaulting_nargs:
- help += ' (default: %(default)s)'
- return help
-
-
-# =====================
-# Options and Arguments
-# =====================
-
-def _get_action_name(argument):
- if argument is None:
- return None
- elif argument.option_strings:
- return '/'.join(argument.option_strings)
- elif argument.metavar not in (None, SUPPRESS):
- return argument.metavar
- elif argument.dest not in (None, SUPPRESS):
- return argument.dest
- else:
- return None
-
-
-class ArgumentError(Exception):
- """An error from creating or using an argument (optional or positional).
-
- The string value of this exception is the message, augmented with
- information about the argument that caused it.
- """
-
- def __init__(self, argument, message):
- self.argument_name = _get_action_name(argument)
- self.message = message
-
- def __str__(self):
- if self.argument_name is None:
- format = '%(message)s'
- else:
- format = 'argument %(argument_name)s: %(message)s'
- return format % dict(message=self.message,
- argument_name=self.argument_name)
-
-
-class ArgumentTypeError(Exception):
- """An error from trying to convert a command line string to a type."""
- pass
-
-
-# ==============
-# Action classes
-# ==============
-
-class Action(_AttributeHolder):
- """Information about how to convert command line strings to Python objects.
-
- Action objects are used by an ArgumentParser to represent the information
- needed to parse a single argument from one or more strings from the
- command line. The keyword arguments to the Action constructor are also
- all attributes of Action instances.
-
- Keyword Arguments:
-
- - option_strings -- A list of command-line option strings which
- should be associated with this action.
-
- - dest -- The name of the attribute to hold the created object(s)
-
- - nargs -- The number of command-line arguments that should be
- consumed. By default, one argument will be consumed and a single
- value will be produced. Other values include:
- - N (an integer) consumes N arguments (and produces a list)
- - '?' consumes zero or one arguments
- - '*' consumes zero or more arguments (and produces a list)
- - '+' consumes one or more arguments (and produces a list)
- Note that the difference between the default and nargs=1 is that
- with the default, a single value will be produced, while with
- nargs=1, a list containing a single value will be produced.
-
- - const -- The value to be produced if the option is specified and the
- option uses an action that takes no values.
-
- - default -- The value to be produced if the option is not specified.
-
- - type -- The type which the command-line arguments should be converted
- to, should be one of 'string', 'int', 'float', 'complex' or a
- callable object that accepts a single string argument. If None,
- 'string' is assumed.
-
- - choices -- A container of values that should be allowed. If not None,
- after a command-line argument has been converted to the appropriate
- type, an exception will be raised if it is not a member of this
- collection.
-
- - required -- True if the action must always be specified at the
- command line. This is only meaningful for optional command-line
- arguments.
-
- - help -- The help string describing the argument.
-
- - metavar -- The name to be used for the option's argument with the
- help string. If None, the 'dest' value will be used as the name.
- """
-
- def __init__(self,
- option_strings,
- dest,
- nargs=None,
- const=None,
- default=None,
- type=None,
- choices=None,
- required=False,
- help=None,
- metavar=None):
- self.option_strings = option_strings
- self.dest = dest
- self.nargs = nargs
- self.const = const
- self.default = default
- self.type = type
- self.choices = choices
- self.required = required
- self.help = help
- self.metavar = metavar
-
- def _get_kwargs(self):
- names = [
- 'option_strings',
- 'dest',
- 'nargs',
- 'const',
- 'default',
- 'type',
- 'choices',
- 'help',
- 'metavar',
- ]
- return [(name, getattr(self, name)) for name in names]
-
- def __call__(self, parser, namespace, values, option_string=None):
- raise NotImplementedError(_('.__call__() not defined'))
-
-
-class _StoreAction(Action):
-
- def __init__(self,
- option_strings,
- dest,
- nargs=None,
- const=None,
- default=None,
- type=None,
- choices=None,
- required=False,
- help=None,
- metavar=None):
- if nargs == 0:
- raise ValueError('nargs for store actions must be > 0; if you '
- 'have nothing to store, actions such as store '
- 'true or store const may be more appropriate')
- if const is not None and nargs != OPTIONAL:
- raise ValueError('nargs must be %r to supply const' % OPTIONAL)
- super(_StoreAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- nargs=nargs,
- const=const,
- default=default,
- type=type,
- choices=choices,
- required=required,
- help=help,
- metavar=metavar)
-
- def __call__(self, parser, namespace, values, option_string=None):
- setattr(namespace, self.dest, values)
-
-
-class _StoreConstAction(Action):
-
- def __init__(self,
- option_strings,
- dest,
- const,
- default=None,
- required=False,
- help=None,
- metavar=None):
- super(_StoreConstAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- nargs=0,
- const=const,
- default=default,
- required=required,
- help=help)
-
- def __call__(self, parser, namespace, values, option_string=None):
- setattr(namespace, self.dest, self.const)
-
-
-class _StoreTrueAction(_StoreConstAction):
-
- def __init__(self,
- option_strings,
- dest,
- default=False,
- required=False,
- help=None):
- super(_StoreTrueAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- const=True,
- default=default,
- required=required,
- help=help)
-
-
-class _StoreFalseAction(_StoreConstAction):
-
- def __init__(self,
- option_strings,
- dest,
- default=True,
- required=False,
- help=None):
- super(_StoreFalseAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- const=False,
- default=default,
- required=required,
- help=help)
-
-
-class _AppendAction(Action):
-
- def __init__(self,
- option_strings,
- dest,
- nargs=None,
- const=None,
- default=None,
- type=None,
- choices=None,
- required=False,
- help=None,
- metavar=None):
- if nargs == 0:
- raise ValueError('nargs for append actions must be > 0; if arg '
- 'strings are not supplying the value to append, '
- 'the append const action may be more appropriate')
- if const is not None and nargs != OPTIONAL:
- raise ValueError('nargs must be %r to supply const' % OPTIONAL)
- super(_AppendAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- nargs=nargs,
- const=const,
- default=default,
- type=type,
- choices=choices,
- required=required,
- help=help,
- metavar=metavar)
-
- def __call__(self, parser, namespace, values, option_string=None):
- items = _copy.copy(_ensure_value(namespace, self.dest, []))
- items.append(values)
- setattr(namespace, self.dest, items)
-
-
-class _AppendConstAction(Action):
-
- def __init__(self,
- option_strings,
- dest,
- const,
- default=None,
- required=False,
- help=None,
- metavar=None):
- super(_AppendConstAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- nargs=0,
- const=const,
- default=default,
- required=required,
- help=help,
- metavar=metavar)
-
- def __call__(self, parser, namespace, values, option_string=None):
- items = _copy.copy(_ensure_value(namespace, self.dest, []))
- items.append(self.const)
- setattr(namespace, self.dest, items)
-
-
-class _CountAction(Action):
-
- def __init__(self,
- option_strings,
- dest,
- default=None,
- required=False,
- help=None):
- super(_CountAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- nargs=0,
- default=default,
- required=required,
- help=help)
-
- def __call__(self, parser, namespace, values, option_string=None):
- new_count = _ensure_value(namespace, self.dest, 0) + 1
- setattr(namespace, self.dest, new_count)
-
-
-class _HelpAction(Action):
-
- def __init__(self,
- option_strings,
- dest=SUPPRESS,
- default=SUPPRESS,
- help=None):
- super(_HelpAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- default=default,
- nargs=0,
- help=help)
-
- def __call__(self, parser, namespace, values, option_string=None):
- parser.print_help()
- parser.exit()
-
-
-class _VersionAction(Action):
-
- def __init__(self,
- option_strings,
- version=None,
- dest=SUPPRESS,
- default=SUPPRESS,
- help="show program's version number and exit"):
- super(_VersionAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- default=default,
- nargs=0,
- help=help)
- self.version = version
-
- def __call__(self, parser, namespace, values, option_string=None):
- version = self.version
- if version is None:
- version = parser.version
- formatter = parser._get_formatter()
- formatter.add_text(version)
- parser.exit(message=formatter.format_help())
-
-
-class _SubParsersAction(Action):
-
- class _ChoicesPseudoAction(Action):
-
- def __init__(self, name, aliases, help):
- metavar = dest = name
- if aliases:
- metavar += ' (%s)' % ', '.join(aliases)
- sup = super(_SubParsersAction._ChoicesPseudoAction, self)
- sup.__init__(option_strings=[], dest=dest, help=help,
- metavar=metavar)
-
- def __init__(self,
- option_strings,
- prog,
- parser_class,
- dest=SUPPRESS,
- help=None,
- metavar=None):
-
- self._prog_prefix = prog
- self._parser_class = parser_class
- self._name_parser_map = {}
- self._choices_actions = []
-
- super(_SubParsersAction, self).__init__(
- option_strings=option_strings,
- dest=dest,
- nargs=PARSER,
- choices=self._name_parser_map,
- help=help,
- metavar=metavar)
-
- def add_parser(self, name, **kwargs):
- # set prog from the existing prefix
- if kwargs.get('prog') is None:
- kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
-
- aliases = kwargs.pop('aliases', ())
-
- # create a pseudo-action to hold the choice help
- if 'help' in kwargs:
- help = kwargs.pop('help')
- choice_action = self._ChoicesPseudoAction(name, aliases, help)
- self._choices_actions.append(choice_action)
-
- # create the parser and add it to the map
- parser = self._parser_class(**kwargs)
- self._name_parser_map[name] = parser
-
- # make parser available under aliases also
- for alias in aliases:
- self._name_parser_map[alias] = parser
-
- return parser
-
- def _get_subactions(self):
- return self._choices_actions
-
- def __call__(self, parser, namespace, values, option_string=None):
- parser_name = values[0]
- arg_strings = values[1:]
-
- # set the parser name if requested
- if self.dest is not SUPPRESS:
- setattr(namespace, self.dest, parser_name)
-
- # select the parser
- try:
- parser = self._name_parser_map[parser_name]
- except KeyError:
- tup = parser_name, ', '.join(self._name_parser_map)
- msg = _('unknown parser %r (choices: %s)' % tup)
- raise ArgumentError(self, msg)
-
- # parse all the remaining options into the namespace
- # store any unrecognized options on the object, so that the top
- # level parser can decide what to do with them
- namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)
- if arg_strings:
- vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
- getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
-
-
-# ==============
-# Type classes
-# ==============
-
-class FileType(object):
- """Factory for creating file object types
-
- Instances of FileType are typically passed as type= arguments to the
- ArgumentParser add_argument() method.
-
- Keyword Arguments:
- - mode -- A string indicating how the file is to be opened. Accepts the
- same values as the builtin open() function.
- - bufsize -- The file's desired buffer size. Accepts the same values as
- the builtin open() function.
- """
-
- def __init__(self, mode='r', bufsize=None):
- self._mode = mode
- self._bufsize = bufsize
-
- def __call__(self, string):
- # the special argument "-" means sys.std{in,out}
- if string == '-':
- if 'r' in self._mode:
- return _sys.stdin
- elif 'w' in self._mode:
- return _sys.stdout
- else:
- msg = _('argument "-" with mode %r' % self._mode)
- raise ValueError(msg)
-
- try:
- # all other arguments are used as file names
- if self._bufsize:
- return open(string, self._mode, self._bufsize)
- else:
- return open(string, self._mode)
- except IOError:
- err = _sys.exc_info()[1]
- message = _("can't open '%s': %s")
- raise ArgumentTypeError(message % (string, err))
-
- def __repr__(self):
- args = [self._mode, self._bufsize]
- args_str = ', '.join([repr(arg) for arg in args if arg is not None])
- return '%s(%s)' % (type(self).__name__, args_str)
-
-# ===========================
-# Optional and Positional Parsing
-# ===========================
-
-class Namespace(_AttributeHolder):
- """Simple object for storing attributes.
-
- Implements equality by attribute names and values, and provides a simple
- string representation.
- """
-
- def __init__(self, **kwargs):
- for name in kwargs:
- setattr(self, name, kwargs[name])
-
- __hash__ = None
-
- def __eq__(self, other):
- return vars(self) == vars(other)
-
- def __ne__(self, other):
- return not (self == other)
-
- def __contains__(self, key):
- return key in self.__dict__
-
-
-class _ActionsContainer(object):
-
- def __init__(self,
- description,
- prefix_chars,
- argument_default,
- conflict_handler):
- super(_ActionsContainer, self).__init__()
-
- self.description = description
- self.argument_default = argument_default
- self.prefix_chars = prefix_chars
- self.conflict_handler = conflict_handler
-
- # set up registries
- self._registries = {}
-
- # register actions
- self.register('action', None, _StoreAction)
- self.register('action', 'store', _StoreAction)
- self.register('action', 'store_const', _StoreConstAction)
- self.register('action', 'store_true', _StoreTrueAction)
- self.register('action', 'store_false', _StoreFalseAction)
- self.register('action', 'append', _AppendAction)
- self.register('action', 'append_const', _AppendConstAction)
- self.register('action', 'count', _CountAction)
- self.register('action', 'help', _HelpAction)
- self.register('action', 'version', _VersionAction)
- self.register('action', 'parsers', _SubParsersAction)
-
- # raise an exception if the conflict handler is invalid
- self._get_handler()
-
- # action storage
- self._actions = []
- self._option_string_actions = {}
-
- # groups
- self._action_groups = []
- self._mutually_exclusive_groups = []
-
- # defaults storage
- self._defaults = {}
-
- # determines whether an "option" looks like a negative number
- self._negative_number_matcher = _re.compile(r'^-\d+$|^-\d*\.\d+$')
-
- # whether or not there are any optionals that look like negative
- # numbers -- uses a list so it can be shared and edited
- self._has_negative_number_optionals = []
-
- # ====================
- # Registration methods
- # ====================
- def register(self, registry_name, value, object):
- registry = self._registries.setdefault(registry_name, {})
- registry[value] = object
-
- def _registry_get(self, registry_name, value, default=None):
- return self._registries[registry_name].get(value, default)
-
- # ==================================
- # Namespace default accessor methods
- # ==================================
- def set_defaults(self, **kwargs):
- self._defaults.update(kwargs)
-
- # if these defaults match any existing arguments, replace
- # the previous default on the object with the new one
- for action in self._actions:
- if action.dest in kwargs:
- action.default = kwargs[action.dest]
-
- def get_default(self, dest):
- for action in self._actions:
- if action.dest == dest and action.default is not None:
- return action.default
- return self._defaults.get(dest, None)
-
-
- # =======================
- # Adding argument actions
- # =======================
- def add_argument(self, *args, **kwargs):
- """
- add_argument(dest, ..., name=value, ...)
- add_argument(option_string, option_string, ..., name=value, ...)
- """
-
- # if no positional args are supplied or only one is supplied and
- # it doesn't look like an option string, parse a positional
- # argument
- chars = self.prefix_chars
- if not args or len(args) == 1 and args[0][0] not in chars:
- if args and 'dest' in kwargs:
- raise ValueError('dest supplied twice for positional argument')
- kwargs = self._get_positional_kwargs(*args, **kwargs)
-
- # otherwise, we're adding an optional argument
- else:
- kwargs = self._get_optional_kwargs(*args, **kwargs)
-
- # if no default was supplied, use the parser-level default
- if 'default' not in kwargs:
- dest = kwargs['dest']
- if dest in self._defaults:
- kwargs['default'] = self._defaults[dest]
- elif self.argument_default is not None:
- kwargs['default'] = self.argument_default
-
- # create the action object, and add it to the parser
- action_class = self._pop_action_class(kwargs)
- if not _callable(action_class):
- raise ValueError('unknown action "%s"' % action_class)
- action = action_class(**kwargs)
-
- # raise an error if the action type is not callable
- type_func = self._registry_get('type', action.type, action.type)
- if not _callable(type_func):
- raise ValueError('%r is not callable' % type_func)
-
- return self._add_action(action)
-
- def add_argument_group(self, *args, **kwargs):
- group = _ArgumentGroup(self, *args, **kwargs)
- self._action_groups.append(group)
- return group
-
- def add_mutually_exclusive_group(self, **kwargs):
- group = _MutuallyExclusiveGroup(self, **kwargs)
- self._mutually_exclusive_groups.append(group)
- return group
-
- def _add_action(self, action):
- # resolve any conflicts
- self._check_conflict(action)
-
- # add to actions list
- self._actions.append(action)
- action.container = self
-
- # index the action by any option strings it has
- for option_string in action.option_strings:
- self._option_string_actions[option_string] = action
-
- # set the flag if any option strings look like negative numbers
- for option_string in action.option_strings:
- if self._negative_number_matcher.match(option_string):
- if not self._has_negative_number_optionals:
- self._has_negative_number_optionals.append(True)
-
- # return the created action
- return action
-
- def _remove_action(self, action):
- self._actions.remove(action)
-
- def _add_container_actions(self, container):
- # collect groups by titles
- title_group_map = {}
- for group in self._action_groups:
- if group.title in title_group_map:
- msg = _('cannot merge actions - two groups are named %r')
- raise ValueError(msg % (group.title))
- title_group_map[group.title] = group
-
- # map each action to its group
- group_map = {}
- for group in container._action_groups:
-
- # if a group with the title exists, use that, otherwise
- # create a new group matching the container's group
- if group.title not in title_group_map:
- title_group_map[group.title] = self.add_argument_group(
- title=group.title,
- description=group.description,
- conflict_handler=group.conflict_handler)
-
- # map the actions to their new group
- for action in group._group_actions:
- group_map[action] = title_group_map[group.title]
-
- # add container's mutually exclusive groups
- # NOTE: if add_mutually_exclusive_group ever gains title= and
- # description= then this code will need to be expanded as above
- for group in container._mutually_exclusive_groups:
- mutex_group = self.add_mutually_exclusive_group(
- required=group.required)
-
- # map the actions to their new mutex group
- for action in group._group_actions:
- group_map[action] = mutex_group
-
- # add all actions to this container or their group
- for action in container._actions:
- group_map.get(action, self)._add_action(action)
-
- def _get_positional_kwargs(self, dest, **kwargs):
- # make sure required is not specified
- if 'required' in kwargs:
- msg = _("'required' is an invalid argument for positionals")
- raise TypeError(msg)
-
- # mark positional arguments as required if at least one is
- # always required
- if kwargs.get('nargs') not in [OPTIONAL, ZERO_OR_MORE]:
- kwargs['required'] = True
- if kwargs.get('nargs') == ZERO_OR_MORE and 'default' not in kwargs:
- kwargs['required'] = True
-
- # return the keyword arguments with no option strings
- return dict(kwargs, dest=dest, option_strings=[])
-
- def _get_optional_kwargs(self, *args, **kwargs):
- # determine short and long option strings
- option_strings = []
- long_option_strings = []
- for option_string in args:
- # error on strings that don't start with an appropriate prefix
- if not option_string[0] in self.prefix_chars:
- msg = _('invalid option string %r: '
- 'must start with a character %r')
- tup = option_string, self.prefix_chars
- raise ValueError(msg % tup)
-
- # strings starting with two prefix characters are long options
- option_strings.append(option_string)
- if option_string[0] in self.prefix_chars:
- if len(option_string) > 1:
- if option_string[1] in self.prefix_chars:
- long_option_strings.append(option_string)
-
- # infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
- dest = kwargs.pop('dest', None)
- if dest is None:
- if long_option_strings:
- dest_option_string = long_option_strings[0]
- else:
- dest_option_string = option_strings[0]
- dest = dest_option_string.lstrip(self.prefix_chars)
- if not dest:
- msg = _('dest= is required for options like %r')
- raise ValueError(msg % option_string)
- dest = dest.replace('-', '_')
-
- # return the updated keyword arguments
- return dict(kwargs, dest=dest, option_strings=option_strings)
-
- def _pop_action_class(self, kwargs, default=None):
- action = kwargs.pop('action', default)
- return self._registry_get('action', action, action)
-
- def _get_handler(self):
- # determine function from conflict handler string
- handler_func_name = '_handle_conflict_%s' % self.conflict_handler
- try:
- return getattr(self, handler_func_name)
- except AttributeError:
- msg = _('invalid conflict_resolution value: %r')
- raise ValueError(msg % self.conflict_handler)
-
- def _check_conflict(self, action):
-
- # find all options that conflict with this option
- confl_optionals = []
- for option_string in action.option_strings:
- if option_string in self._option_string_actions:
- confl_optional = self._option_string_actions[option_string]
- confl_optionals.append((option_string, confl_optional))
-
- # resolve any conflicts
- if confl_optionals:
- conflict_handler = self._get_handler()
- conflict_handler(action, confl_optionals)
-
- def _handle_conflict_error(self, action, conflicting_actions):
- message = _('conflicting option string(s): %s')
- conflict_string = ', '.join([option_string
- for option_string, action
- in conflicting_actions])
- raise ArgumentError(action, message % conflict_string)
-
- def _handle_conflict_resolve(self, action, conflicting_actions):
-
- # remove all conflicting options
- for option_string, action in conflicting_actions:
-
- # remove the conflicting option
- action.option_strings.remove(option_string)
- self._option_string_actions.pop(option_string, None)
-
- # if the option now has no option string, remove it from the
- # container holding it
- if not action.option_strings:
- action.container._remove_action(action)
-
-
-class _ArgumentGroup(_ActionsContainer):
-
- def __init__(self, container, title=None, description=None, **kwargs):
- # add any missing keyword arguments by checking the container
- update = kwargs.setdefault
- update('conflict_handler', container.conflict_handler)
- update('prefix_chars', container.prefix_chars)
- update('argument_default', container.argument_default)
- super_init = super(_ArgumentGroup, self).__init__
- super_init(description=description, **kwargs)
-
- # group attributes
- self.title = title
- self._group_actions = []
-
- # share most attributes with the container
- self._registries = container._registries
- self._actions = container._actions
- self._option_string_actions = container._option_string_actions
- self._defaults = container._defaults
- self._has_negative_number_optionals = \
- container._has_negative_number_optionals
-
- def _add_action(self, action):
- action = super(_ArgumentGroup, self)._add_action(action)
- self._group_actions.append(action)
- return action
-
- def _remove_action(self, action):
- super(_ArgumentGroup, self)._remove_action(action)
- self._group_actions.remove(action)
-
-
-class _MutuallyExclusiveGroup(_ArgumentGroup):
-
- def __init__(self, container, required=False):
- super(_MutuallyExclusiveGroup, self).__init__(container)
- self.required = required
- self._container = container
-
- def _add_action(self, action):
- if action.required:
- msg = _('mutually exclusive arguments must be optional')
- raise ValueError(msg)
- action = self._container._add_action(action)
- self._group_actions.append(action)
- return action
-
- def _remove_action(self, action):
- self._container._remove_action(action)
- self._group_actions.remove(action)
-
-
-class ArgumentParser(_AttributeHolder, _ActionsContainer):
- """Object for parsing command line strings into Python objects.
-
- Keyword Arguments:
- - prog -- The name of the program (default: sys.argv[0])
- - usage -- A usage message (default: auto-generated from arguments)
- - description -- A description of what the program does
- - epilog -- Text following the argument descriptions
- - parents -- Parsers whose arguments should be copied into this one
- - formatter_class -- HelpFormatter class for printing help messages
- - prefix_chars -- Characters that prefix optional arguments
- - fromfile_prefix_chars -- Characters that prefix files containing
- additional arguments
- - argument_default -- The default value for all arguments
- - conflict_handler -- String indicating how to handle conflicts
- - add_help -- Add a -h/-help option
- """
-
- def __init__(self,
- prog=None,
- usage=None,
- description=None,
- epilog=None,
- version=None,
- parents=[],
- formatter_class=HelpFormatter,
- prefix_chars='-',
- fromfile_prefix_chars=None,
- argument_default=None,
- conflict_handler='error',
- add_help=True):
-
- if version is not None:
- import warnings
- warnings.warn(
- """The "version" argument to ArgumentParser is deprecated. """
- """Please use """
- """"add_argument(..., action='version', version="N", ...)" """
- """instead""", DeprecationWarning)
-
- superinit = super(ArgumentParser, self).__init__
- superinit(description=description,
- prefix_chars=prefix_chars,
- argument_default=argument_default,
- conflict_handler=conflict_handler)
-
- # default setting for prog
- if prog is None:
- prog = _os.path.basename(_sys.argv[0])
-
- self.prog = prog
- self.usage = usage
- self.epilog = epilog
- self.version = version
- self.formatter_class = formatter_class
- self.fromfile_prefix_chars = fromfile_prefix_chars
- self.add_help = add_help
-
- add_group = self.add_argument_group
- self._positionals = add_group(_('positional arguments'))
- self._optionals = add_group(_('optional arguments'))
- self._subparsers = None
-
- # register types
- def identity(string):
- return string
- self.register('type', None, identity)
-
- # add help and version arguments if necessary
- # (using explicit default to override global argument_default)
- if '-' in prefix_chars:
- default_prefix = '-'
- else:
- default_prefix = prefix_chars[0]
- if self.add_help:
- self.add_argument(
- default_prefix+'h', default_prefix*2+'help',
- action='help', default=SUPPRESS,
- help=_('show this help message and exit'))
- if self.version:
- self.add_argument(
- default_prefix+'v', default_prefix*2+'version',
- action='version', default=SUPPRESS,
- version=self.version,
- help=_("show program's version number and exit"))
-
- # add parent arguments and defaults
- for parent in parents:
- self._add_container_actions(parent)
- try:
- defaults = parent._defaults
- except AttributeError:
- pass
- else:
- self._defaults.update(defaults)
-
- # =======================
- # Pretty __repr__ methods
- # =======================
- def _get_kwargs(self):
- names = [
- 'prog',
- 'usage',
- 'description',
- 'version',
- 'formatter_class',
- 'conflict_handler',
- 'add_help',
- ]
- return [(name, getattr(self, name)) for name in names]
-
- # ==================================
- # Optional/Positional adding methods
- # ==================================
- def add_subparsers(self, **kwargs):
- if self._subparsers is not None:
- self.error(_('cannot have multiple subparser arguments'))
-
- # add the parser class to the arguments if it's not present
- kwargs.setdefault('parser_class', type(self))
-
- if 'title' in kwargs or 'description' in kwargs:
- title = _(kwargs.pop('title', 'subcommands'))
- description = _(kwargs.pop('description', None))
- self._subparsers = self.add_argument_group(title, description)
- else:
- self._subparsers = self._positionals
-
- # prog defaults to the usage message of this parser, skipping
- # optional arguments and with no "usage:" prefix
- if kwargs.get('prog') is None:
- formatter = self._get_formatter()
- positionals = self._get_positional_actions()
- groups = self._mutually_exclusive_groups
- formatter.add_usage(self.usage, positionals, groups, '')
- kwargs['prog'] = formatter.format_help().strip()
-
- # create the parsers action and add it to the positionals list
- parsers_class = self._pop_action_class(kwargs, 'parsers')
- action = parsers_class(option_strings=[], **kwargs)
- self._subparsers._add_action(action)
-
- # return the created parsers action
- return action
-
- def _add_action(self, action):
- if action.option_strings:
- self._optionals._add_action(action)
- else:
- self._positionals._add_action(action)
- return action
-
- def _get_optional_actions(self):
- return [action
- for action in self._actions
- if action.option_strings]
-
- def _get_positional_actions(self):
- return [action
- for action in self._actions
- if not action.option_strings]
-
- # =====================================
- # Command line argument parsing methods
- # =====================================
- def parse_args(self, args=None, namespace=None):
- args, argv = self.parse_known_args(args, namespace)
- if argv:
- msg = _('unrecognized arguments: %s')
- self.error(msg % ' '.join(argv))
- return args
-
- def parse_known_args(self, args=None, namespace=None):
- # args default to the system args
- if args is None:
- args = _sys.argv[1:]
-
- # default Namespace built from parser defaults
- if namespace is None:
- namespace = Namespace()
-
- # add any action defaults that aren't present
- for action in self._actions:
- if action.dest is not SUPPRESS:
- if not hasattr(namespace, action.dest):
- if action.default is not SUPPRESS:
- setattr(namespace, action.dest, action.default)
-
- # add any parser defaults that aren't present
- for dest in self._defaults:
- if not hasattr(namespace, dest):
- setattr(namespace, dest, self._defaults[dest])
-
- # parse the arguments and exit if there are any errors
- try:
- namespace, args = self._parse_known_args(args, namespace)
- if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
- args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
- delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
- return namespace, args
- except ArgumentError:
- err = _sys.exc_info()[1]
- self.error(str(err))
-
- def _parse_known_args(self, arg_strings, namespace):
- # replace arg strings that are file references
- if self.fromfile_prefix_chars is not None:
- arg_strings = self._read_args_from_files(arg_strings)
-
- # map all mutually exclusive arguments to the other arguments
- # they can't occur with
- action_conflicts = {}
- for mutex_group in self._mutually_exclusive_groups:
- group_actions = mutex_group._group_actions
- for i, mutex_action in enumerate(mutex_group._group_actions):
- conflicts = action_conflicts.setdefault(mutex_action, [])
- conflicts.extend(group_actions[:i])
- conflicts.extend(group_actions[i + 1:])
-
- # find all option indices, and determine the arg_string_pattern
- # which has an 'O' if there is an option at an index,
- # an 'A' if there is an argument, or a '-' if there is a '--'
- option_string_indices = {}
- arg_string_pattern_parts = []
- arg_strings_iter = iter(arg_strings)
- for i, arg_string in enumerate(arg_strings_iter):
-
- # all args after -- are non-options
- if arg_string == '--':
- arg_string_pattern_parts.append('-')
- for arg_string in arg_strings_iter:
- arg_string_pattern_parts.append('A')
-
- # otherwise, add the arg to the arg strings
- # and note the index if it was an option
- else:
- option_tuple = self._parse_optional(arg_string)
- if option_tuple is None:
- pattern = 'A'
- else:
- option_string_indices[i] = option_tuple
- pattern = 'O'
- arg_string_pattern_parts.append(pattern)
-
- # join the pieces together to form the pattern
- arg_strings_pattern = ''.join(arg_string_pattern_parts)
-
- # converts arg strings to the appropriate and then takes the action
- seen_actions = set()
- seen_non_default_actions = set()
-
- def take_action(action, argument_strings, option_string=None):
- seen_actions.add(action)
- argument_values = self._get_values(action, argument_strings)
-
- # error if this argument is not allowed with other previously
- # seen arguments, assuming that actions that use the default
- # value don't really count as "present"
- if argument_values is not action.default:
- seen_non_default_actions.add(action)
- for conflict_action in action_conflicts.get(action, []):
- if conflict_action in seen_non_default_actions:
- msg = _('not allowed with argument %s')
- action_name = _get_action_name(conflict_action)
- raise ArgumentError(action, msg % action_name)
-
- # take the action if we didn't receive a SUPPRESS value
- # (e.g. from a default)
- if argument_values is not SUPPRESS:
- action(self, namespace, argument_values, option_string)
-
- # function to convert arg_strings into an optional action
- def consume_optional(start_index):
-
- # get the optional identified at this index
- option_tuple = option_string_indices[start_index]
- action, option_string, explicit_arg = option_tuple
-
- # identify additional optionals in the same arg string
- # (e.g. -xyz is the same as -x -y -z if no args are required)
- match_argument = self._match_argument
- action_tuples = []
- while True:
-
- # if we found no optional action, skip it
- if action is None:
- extras.append(arg_strings[start_index])
- return start_index + 1
-
- # if there is an explicit argument, try to match the
- # optional's string arguments to only this
- if explicit_arg is not None:
- arg_count = match_argument(action, 'A')
-
- # if the action is a single-dash option and takes no
- # arguments, try to parse more single-dash options out
- # of the tail of the option string
- chars = self.prefix_chars
- if arg_count == 0 and option_string[1] not in chars:
- action_tuples.append((action, [], option_string))
- char = option_string[0]
- option_string = char + explicit_arg[0]
- new_explicit_arg = explicit_arg[1:] or None
- optionals_map = self._option_string_actions
- if option_string in optionals_map:
- action = optionals_map[option_string]
- explicit_arg = new_explicit_arg
- else:
- msg = _('ignored explicit argument %r')
- raise ArgumentError(action, msg % explicit_arg)
-
- # if the action expect exactly one argument, we've
- # successfully matched the option; exit the loop
- elif arg_count == 1:
- stop = start_index + 1
- args = [explicit_arg]
- action_tuples.append((action, args, option_string))
- break
-
- # error if a double-dash option did not use the
- # explicit argument
- else:
- msg = _('ignored explicit argument %r')
- raise ArgumentError(action, msg % explicit_arg)
-
- # if there is no explicit argument, try to match the
- # optional's string arguments with the following strings
- # if successful, exit the loop
- else:
- start = start_index + 1
- selected_patterns = arg_strings_pattern[start:]
- arg_count = match_argument(action, selected_patterns)
- stop = start + arg_count
- args = arg_strings[start:stop]
- action_tuples.append((action, args, option_string))
- break
-
- # add the Optional to the list and return the index at which
- # the Optional's string args stopped
- assert action_tuples
- for action, args, option_string in action_tuples:
- take_action(action, args, option_string)
- return stop
-
- # the list of Positionals left to be parsed; this is modified
- # by consume_positionals()
- positionals = self._get_positional_actions()
-
- # function to convert arg_strings into positional actions
- def consume_positionals(start_index):
- # match as many Positionals as possible
- match_partial = self._match_arguments_partial
- selected_pattern = arg_strings_pattern[start_index:]
- arg_counts = match_partial(positionals, selected_pattern)
-
- # slice off the appropriate arg strings for each Positional
- # and add the Positional and its args to the list
- for action, arg_count in zip(positionals, arg_counts):
- args = arg_strings[start_index: start_index + arg_count]
- start_index += arg_count
- take_action(action, args)
-
- # slice off the Positionals that we just parsed and return the
- # index at which the Positionals' string args stopped
- positionals[:] = positionals[len(arg_counts):]
- return start_index
-
- # consume Positionals and Optionals alternately, until we have
- # passed the last option string
- extras = []
- start_index = 0
- if option_string_indices:
- max_option_string_index = max(option_string_indices)
- else:
- max_option_string_index = -1
- while start_index <= max_option_string_index:
-
- # consume any Positionals preceding the next option
- next_option_string_index = min([
- index
- for index in option_string_indices
- if index >= start_index])
- if start_index != next_option_string_index:
- positionals_end_index = consume_positionals(start_index)
-
- # only try to parse the next optional if we didn't consume
- # the option string during the positionals parsing
- if positionals_end_index > start_index:
- start_index = positionals_end_index
- continue
- else:
- start_index = positionals_end_index
-
- # if we consumed all the positionals we could and we're not
- # at the index of an option string, there were extra arguments
- if start_index not in option_string_indices:
- strings = arg_strings[start_index:next_option_string_index]
- extras.extend(strings)
- start_index = next_option_string_index
-
- # consume the next optional and any arguments for it
- start_index = consume_optional(start_index)
-
- # consume any positionals following the last Optional
- stop_index = consume_positionals(start_index)
-
- # if we didn't consume all the argument strings, there were extras
- extras.extend(arg_strings[stop_index:])
-
- # if we didn't use all the Positional objects, there were too few
- # arg strings supplied.
- if positionals:
- self.error(_('too few arguments'))
-
- # make sure all required actions were present, and convert defaults.
- for action in self._actions:
- if action not in seen_actions:
- if action.required:
- name = _get_action_name(action)
- self.error(_('argument %s is required') % name)
- else:
- # Convert action default now instead of doing it before
- # parsing arguments to avoid calling convert functions
- # twice (which may fail) if the argument was given, but
- # only if it was defined already in the namespace
- if (action.default is not None and
- isinstance(action.default, basestring) and
- hasattr(namespace, action.dest) and
- action.default is getattr(namespace, action.dest)):
- setattr(namespace, action.dest,
- self._get_value(action, action.default))
-
- # make sure all required groups had one option present
- for group in self._mutually_exclusive_groups:
- if group.required:
- for action in group._group_actions:
- if action in seen_non_default_actions:
- break
-
- # if no actions were used, report the error
- else:
- names = [_get_action_name(action)
- for action in group._group_actions
- if action.help is not SUPPRESS]
- msg = _('one of the arguments %s is required')
- self.error(msg % ' '.join(names))
-
- # return the updated namespace and the extra arguments
- return namespace, extras
-
- def _read_args_from_files(self, arg_strings):
- # expand arguments referencing files
- new_arg_strings = []
- for arg_string in arg_strings:
-
- # for regular arguments, just add them back into the list
- if arg_string[0] not in self.fromfile_prefix_chars:
- new_arg_strings.append(arg_string)
-
- # replace arguments referencing files with the file content
- else:
- try:
- args_file = open(arg_string[1:])
- try:
- arg_strings = []
- for arg_line in args_file.read().splitlines():
- for arg in self.convert_arg_line_to_args(arg_line):
- arg_strings.append(arg)
- arg_strings = self._read_args_from_files(arg_strings)
- new_arg_strings.extend(arg_strings)
- finally:
- args_file.close()
- except IOError:
- err = _sys.exc_info()[1]
- self.error(str(err))
-
- # return the modified argument list
- return new_arg_strings
-
- def convert_arg_line_to_args(self, arg_line):
- return [arg_line]
-
- def _match_argument(self, action, arg_strings_pattern):
- # match the pattern for this action to the arg strings
- nargs_pattern = self._get_nargs_pattern(action)
- match = _re.match(nargs_pattern, arg_strings_pattern)
-
- # raise an exception if we weren't able to find a match
- if match is None:
- nargs_errors = {
- None: _('expected one argument'),
- OPTIONAL: _('expected at most one argument'),
- ONE_OR_MORE: _('expected at least one argument'),
- }
- default = _('expected %s argument(s)') % action.nargs
- msg = nargs_errors.get(action.nargs, default)
- raise ArgumentError(action, msg)
-
- # return the number of arguments matched
- return len(match.group(1))
-
- def _match_arguments_partial(self, actions, arg_strings_pattern):
- # progressively shorten the actions list by slicing off the
- # final actions until we find a match
- result = []
- for i in range(len(actions), 0, -1):
- actions_slice = actions[:i]
- pattern = ''.join([self._get_nargs_pattern(action)
- for action in actions_slice])
- match = _re.match(pattern, arg_strings_pattern)
- if match is not None:
- result.extend([len(string) for string in match.groups()])
- break
-
- # return the list of arg string counts
- return result
-
- def _parse_optional(self, arg_string):
- # if it's an empty string, it was meant to be a positional
- if not arg_string:
- return None
-
- # if it doesn't start with a prefix, it was meant to be positional
- if not arg_string[0] in self.prefix_chars:
- return None
-
- # if the option string is present in the parser, return the action
- if arg_string in self._option_string_actions:
- action = self._option_string_actions[arg_string]
- return action, arg_string, None
-
- # if it's just a single character, it was meant to be positional
- if len(arg_string) == 1:
- return None
-
- # if the option string before the "=" is present, return the action
- if '=' in arg_string:
- option_string, explicit_arg = arg_string.split('=', 1)
- if option_string in self._option_string_actions:
- action = self._option_string_actions[option_string]
- return action, option_string, explicit_arg
-
- # search through all possible prefixes of the option string
- # and all actions in the parser for possible interpretations
- option_tuples = self._get_option_tuples(arg_string)
-
- # if multiple actions match, the option string was ambiguous
- if len(option_tuples) > 1:
- options = ', '.join([option_string
- for action, option_string, explicit_arg in option_tuples])
- tup = arg_string, options
- self.error(_('ambiguous option: %s could match %s') % tup)
-
- # if exactly one action matched, this segmentation is good,
- # so return the parsed action
- elif len(option_tuples) == 1:
- option_tuple, = option_tuples
- return option_tuple
-
- # if it was not found as an option, but it looks like a negative
- # number, it was meant to be positional
- # unless there are negative-number-like options
- if self._negative_number_matcher.match(arg_string):
- if not self._has_negative_number_optionals:
- return None
-
- # if it contains a space, it was meant to be a positional
- if ' ' in arg_string:
- return None
-
- # it was meant to be an optional but there is no such option
- # in this parser (though it might be a valid option in a subparser)
- return None, arg_string, None
-
- def _get_option_tuples(self, option_string):
- result = []
-
- # option strings starting with two prefix characters are only
- # split at the '='
- chars = self.prefix_chars
- if option_string[0] in chars and option_string[1] in chars:
- if '=' in option_string:
- option_prefix, explicit_arg = option_string.split('=', 1)
- else:
- option_prefix = option_string
- explicit_arg = None
- for option_string in self._option_string_actions:
- if option_string.startswith(option_prefix):
- action = self._option_string_actions[option_string]
- tup = action, option_string, explicit_arg
- result.append(tup)
-
- # single character options can be concatenated with their arguments
- # but multiple character options always have to have their argument
- # separate
- elif option_string[0] in chars and option_string[1] not in chars:
- option_prefix = option_string
- explicit_arg = None
- short_option_prefix = option_string[:2]
- short_explicit_arg = option_string[2:]
-
- for option_string in self._option_string_actions:
- if option_string == short_option_prefix:
- action = self._option_string_actions[option_string]
- tup = action, option_string, short_explicit_arg
- result.append(tup)
- elif option_string.startswith(option_prefix):
- action = self._option_string_actions[option_string]
- tup = action, option_string, explicit_arg
- result.append(tup)
-
- # shouldn't ever get here
- else:
- self.error(_('unexpected option string: %s') % option_string)
-
- # return the collected option tuples
- return result
-
- def _get_nargs_pattern(self, action):
- # in all examples below, we have to allow for '--' args
- # which are represented as '-' in the pattern
- nargs = action.nargs
-
- # the default (None) is assumed to be a single argument
- if nargs is None:
- nargs_pattern = '(-*A-*)'
-
- # allow zero or one arguments
- elif nargs == OPTIONAL:
- nargs_pattern = '(-*A?-*)'
-
- # allow zero or more arguments
- elif nargs == ZERO_OR_MORE:
- nargs_pattern = '(-*[A-]*)'
-
- # allow one or more arguments
- elif nargs == ONE_OR_MORE:
- nargs_pattern = '(-*A[A-]*)'
-
- # allow any number of options or arguments
- elif nargs == REMAINDER:
- nargs_pattern = '([-AO]*)'
-
- # allow one argument followed by any number of options or arguments
- elif nargs == PARSER:
- nargs_pattern = '(-*A[-AO]*)'
-
- # all others should be integers
- else:
- nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs)
-
- # if this is an optional action, -- is not allowed
- if action.option_strings:
- nargs_pattern = nargs_pattern.replace('-*', '')
- nargs_pattern = nargs_pattern.replace('-', '')
-
- # return the pattern
- return nargs_pattern
-
- # ========================
- # Value conversion methods
- # ========================
- def _get_values(self, action, arg_strings):
- # for everything but PARSER args, strip out '--'
- if action.nargs not in [PARSER, REMAINDER]:
- arg_strings = [s for s in arg_strings if s != '--']
-
- # optional argument produces a default when not present
- if not arg_strings and action.nargs == OPTIONAL:
- if action.option_strings:
- value = action.const
- else:
- value = action.default
- if isinstance(value, basestring):
- value = self._get_value(action, value)
- self._check_value(action, value)
-
- # when nargs='*' on a positional, if there were no command-line
- # args, use the default if it is anything other than None
- elif (not arg_strings and action.nargs == ZERO_OR_MORE and
- not action.option_strings):
- if action.default is not None:
- value = action.default
- else:
- value = arg_strings
- self._check_value(action, value)
-
- # single argument or optional argument produces a single value
- elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]:
- arg_string, = arg_strings
- value = self._get_value(action, arg_string)
- self._check_value(action, value)
-
- # REMAINDER arguments convert all values, checking none
- elif action.nargs == REMAINDER:
- value = [self._get_value(action, v) for v in arg_strings]
-
- # PARSER arguments convert all values, but check only the first
- elif action.nargs == PARSER:
- value = [self._get_value(action, v) for v in arg_strings]
- self._check_value(action, value[0])
-
- # all other types of nargs produce a list
- else:
- value = [self._get_value(action, v) for v in arg_strings]
- for v in value:
- self._check_value(action, v)
-
- # return the converted value
- return value
-
- def _get_value(self, action, arg_string):
- type_func = self._registry_get('type', action.type, action.type)
- if not _callable(type_func):
- msg = _('%r is not callable')
- raise ArgumentError(action, msg % type_func)
-
- # convert the value to the appropriate type
- try:
- result = type_func(arg_string)
-
- # ArgumentTypeErrors indicate errors
- except ArgumentTypeError:
- name = getattr(action.type, '__name__', repr(action.type))
- msg = str(_sys.exc_info()[1])
- raise ArgumentError(action, msg)
-
- # TypeErrors or ValueErrors also indicate errors
- except (TypeError, ValueError):
- name = getattr(action.type, '__name__', repr(action.type))
- msg = _('invalid %s value: %r')
- raise ArgumentError(action, msg % (name, arg_string))
-
- # return the converted value
- return result
-
- def _check_value(self, action, value):
- # converted value must be one of the choices (if specified)
- if action.choices is not None and value not in action.choices:
- tup = value, ', '.join(map(repr, action.choices))
- msg = _('invalid choice: %r (choose from %s)') % tup
- raise ArgumentError(action, msg)
-
- # =======================
- # Help-formatting methods
- # =======================
- def format_usage(self):
- formatter = self._get_formatter()
- formatter.add_usage(self.usage, self._actions,
- self._mutually_exclusive_groups)
- return formatter.format_help()
-
- def format_help(self):
- formatter = self._get_formatter()
-
- # usage
- formatter.add_usage(self.usage, self._actions,
- self._mutually_exclusive_groups)
-
- # description
- formatter.add_text(self.description)
-
- # positionals, optionals and user-defined groups
- for action_group in self._action_groups:
- formatter.start_section(action_group.title)
- formatter.add_text(action_group.description)
- formatter.add_arguments(action_group._group_actions)
- formatter.end_section()
-
- # epilog
- formatter.add_text(self.epilog)
-
- # determine help from format above
- return formatter.format_help()
-
- def format_version(self):
- import warnings
- warnings.warn(
- 'The format_version method is deprecated -- the "version" '
- 'argument to ArgumentParser is no longer supported.',
- DeprecationWarning)
- formatter = self._get_formatter()
- formatter.add_text(self.version)
- return formatter.format_help()
-
- def _get_formatter(self):
- return self.formatter_class(prog=self.prog)
-
- # =====================
- # Help-printing methods
- # =====================
- def print_usage(self, file=None):
- if file is None:
- file = _sys.stdout
- self._print_message(self.format_usage(), file)
-
- def print_help(self, file=None):
- if file is None:
- file = _sys.stdout
- self._print_message(self.format_help(), file)
-
- def print_version(self, file=None):
- import warnings
- warnings.warn(
- 'The print_version method is deprecated -- the "version" '
- 'argument to ArgumentParser is no longer supported.',
- DeprecationWarning)
- self._print_message(self.format_version(), file)
-
- def _print_message(self, message, file=None):
- if message:
- if file is None:
- file = _sys.stderr
- file.write(message)
-
- # ===============
- # Exiting methods
- # ===============
- def exit(self, status=0, message=None):
- if message:
- self._print_message(message, _sys.stderr)
- _sys.exit(status)
-
- def error(self, message):
- """error(message: string)
-
- Prints a usage message incorporating the message to stderr and
- exits.
-
- If you override this in a subclass, it should not return -- it
- should either exit or raise an exception.
- """
- self.print_usage(_sys.stderr)
- self.exit(2, _('%s: error: %s\n') % (self.prog, message))
diff --git a/libs/fese-0.2.9.dist-info/INSTALLER b/libs/fese-0.2.9.dist-info/INSTALLER
deleted file mode 100644
index a1b589e38..000000000
--- a/libs/fese-0.2.9.dist-info/INSTALLER
+++ /dev/null
@@ -1 +0,0 @@
-pip
diff --git a/libs/fese-0.2.9.dist-info/RECORD b/libs/fese-0.2.9.dist-info/RECORD
deleted file mode 100644
index e30fae26a..000000000
--- a/libs/fese-0.2.9.dist-info/RECORD
+++ /dev/null
@@ -1,13 +0,0 @@
-fese-0.2.9.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
-fese-0.2.9.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
-fese-0.2.9.dist-info/METADATA,sha256=nJz9q6FwX7fqmsO3jgM0ZgV0gsCeILWoxVRUqCbJkFI,655
-fese-0.2.9.dist-info/RECORD,,
-fese-0.2.9.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-fese-0.2.9.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
-fese-0.2.9.dist-info/top_level.txt,sha256=ra2BuARVEUZpk76YpHnjVoqjR2FxvzhCdmW2OyBWGzE,5
-fese/__init__.py,sha256=_YUpx7sq26ioEp5LZOEKa-0MrRHQUuRuDCs0EQ6Amv4,150
-fese/container.py,sha256=sLuxP0vlba4iGVohGfYtd-QcjQ-YxMU6lqMOM-Wtqlc,10340
-fese/disposition.py,sha256=hv4YmXpsvKmUdpeWvSrZkhKgtZLZ8t56dmwMddsqxus,2156
-fese/exceptions.py,sha256=VZaubpq8SPpkUGp28Ryebsf9YzqbKK62nni6YZgDPYI,372
-fese/stream.py,sha256=Hgf6-amksHpuhSoY6SL6C3q4YtGCuRHl4fusBWE9nBE,4866
-fese/tags.py,sha256=qKkcjJmCKgnXIbZ9x-nngCNYAfv5cbJZ4A6EP0ckZME,5454
diff --git a/libs/fese-0.2.9.dist-info/REQUESTED b/libs/fese-0.2.9.dist-info/REQUESTED
deleted file mode 100644
index e69de29bb..000000000
--- a/libs/fese-0.2.9.dist-info/REQUESTED
+++ /dev/null
diff --git a/libs/argparse-1.4.0.dist-info/INSTALLER b/libs/fese-0.3.0.dist-info/INSTALLER
index a1b589e38..a1b589e38 100644
--- a/libs/argparse-1.4.0.dist-info/INSTALLER
+++ b/libs/fese-0.3.0.dist-info/INSTALLER
diff --git a/libs/fese-0.2.9.dist-info/LICENSE b/libs/fese-0.3.0.dist-info/LICENSE
index f288702d2..f288702d2 100755
--- a/libs/fese-0.2.9.dist-info/LICENSE
+++ b/libs/fese-0.3.0.dist-info/LICENSE
diff --git a/libs/fese-0.2.9.dist-info/METADATA b/libs/fese-0.3.0.dist-info/METADATA
index 85f1cd160..8c8782450 100644
--- a/libs/fese-0.2.9.dist-info/METADATA
+++ b/libs/fese-0.3.0.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fese
-Version: 0.2.9
+Version: 0.3.0
Summary: A library to extract FFmpeg subtitle streams
Author-email: Vitiko Nogales <[email protected]>
Requires-Python: >=3.7
diff --git a/libs/fese-0.3.0.dist-info/RECORD b/libs/fese-0.3.0.dist-info/RECORD
new file mode 100644
index 000000000..55deffc5a
--- /dev/null
+++ b/libs/fese-0.3.0.dist-info/RECORD
@@ -0,0 +1,13 @@
+fese-0.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+fese-0.3.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
+fese-0.3.0.dist-info/METADATA,sha256=Y6rn3cPjHc2ySJrSnUAuXxahgSFs2YoAivSIgJqi40M,655
+fese-0.3.0.dist-info/RECORD,,
+fese-0.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+fese-0.3.0.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
+fese-0.3.0.dist-info/top_level.txt,sha256=ra2BuARVEUZpk76YpHnjVoqjR2FxvzhCdmW2OyBWGzE,5
+fese/__init__.py,sha256=_YUpx7sq26ioEp5LZOEKa-0MrRHQUuRuDCs0EQ6Amv4,150
+fese/container.py,sha256=zhUNLut9Tdy_oPa6gCophUekTQegGWbHdbd1prR5aHg,10443
+fese/disposition.py,sha256=hv4YmXpsvKmUdpeWvSrZkhKgtZLZ8t56dmwMddsqxus,2156
+fese/exceptions.py,sha256=VZaubpq8SPpkUGp28Ryebsf9YzqbKK62nni6YZgDPYI,372
+fese/stream.py,sha256=Hgf6-amksHpuhSoY6SL6C3q4YtGCuRHl4fusBWE9nBE,4866
+fese/tags.py,sha256=1v-CLuyEZ2tL-TVtOXY8nbo7a3jFwq6fTFDHtFjdnow,5554
diff --git a/libs/argparse-1.4.0.dist-info/REQUESTED b/libs/fese-0.3.0.dist-info/REQUESTED
index e69de29bb..e69de29bb 100644
--- a/libs/argparse-1.4.0.dist-info/REQUESTED
+++ b/libs/fese-0.3.0.dist-info/REQUESTED
diff --git a/libs/fese-0.2.9.dist-info/WHEEL b/libs/fese-0.3.0.dist-info/WHEEL
index 98c0d20b7..5bea5450d 100644
--- a/libs/fese-0.2.9.dist-info/WHEEL
+++ b/libs/fese-0.3.0.dist-info/WHEEL
@@ -1,5 +1,5 @@
Wheel-Version: 1.0
-Generator: bdist_wheel (0.42.0)
+Generator: setuptools (70.3.0)
Root-Is-Purelib: true
Tag: py3-none-any
diff --git a/libs/fese-0.2.9.dist-info/top_level.txt b/libs/fese-0.3.0.dist-info/top_level.txt
index ee8bdad9b..ee8bdad9b 100644
--- a/libs/fese-0.2.9.dist-info/top_level.txt
+++ b/libs/fese-0.3.0.dist-info/top_level.txt
diff --git a/libs/fese/container.py b/libs/fese/container.py
index 21dfc20da..5f2c70371 100644
--- a/libs/fese/container.py
+++ b/libs/fese/container.py
@@ -54,7 +54,10 @@ def _ffmpeg_call(command, log_callback=None, progress_callback=None, timeout=100
if match:
size, time_, bitrate, speed = match.groups()
info = {"size": size, "time": time_, "bitrate": bitrate, "speed": speed}
- progress_callback(info)
+ else:
+ info = {"size": "n/a", "time": "n/a", "bitrate": "n/a", "speed": "n/a"}
+
+ progress_callback(info)
if timeout is not None and time.time() - start > timeout:
proc.kill()
diff --git a/libs/fese/tags.py b/libs/fese/tags.py
index b846fffea..3c4625eb1 100644
--- a/libs/fese/tags.py
+++ b/libs/fese/tags.py
@@ -192,4 +192,8 @@ _extra_languages = {
"matches": ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil"),
"language_args": ("por", "BR"),
},
+ "fil": {
+ "matches": ("fil", "filipino"),
+ "language_args": ("tgl", "PH"),
+ },
}
diff --git a/libs/version.txt b/libs/version.txt
index 1eaf9a16a..fd884e307 100644
--- a/libs/version.txt
+++ b/libs/version.txt
@@ -1,7 +1,6 @@
# Bazarr dependencies
alembic==1.13.1
aniso8601==9.0.1
-argparse==1.4.0
apprise==1.7.6
apscheduler<=3.10.4
attrs==23.2.0
@@ -10,7 +9,7 @@ charset-normalizer==3.3.2
deep-translator==1.11.4
dogpile.cache==1.3.2
dynaconf==3.2.4
-fese==0.2.9
+fese==0.3.0
ffsubsync==0.4.25
flask-cors==4.0.0
flask-migrate==4.0.5
diff --git a/migrations/env.py b/migrations/env.py
index beddf9710..e591f1a9f 100644
--- a/migrations/env.py
+++ b/migrations/env.py
@@ -110,8 +110,6 @@ def run_migrations_online():
elif bind.engine.name == 'postgresql':
bind.execute(text("SET CONSTRAINTS ALL IMMEDIATE;"))
- bind.close()
-
if context.is_offline_mode():
run_migrations_offline()
diff --git a/migrations/versions/452dd0f0b578_.py b/migrations/versions/452dd0f0b578_.py
index 13e623988..a4e7b2966 100644
--- a/migrations/versions/452dd0f0b578_.py
+++ b/migrations/versions/452dd0f0b578_.py
@@ -1,7 +1,7 @@
"""empty message
Revision ID: 452dd0f0b578
-Revises: 30f37e2e15e1
+Revises: b183a2ac0dd1
Create Date: 2024-05-06 20:27:15.618027
"""
@@ -11,7 +11,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '452dd0f0b578'
-down_revision = '30f37e2e15e1'
+down_revision = 'b183a2ac0dd1'
branch_labels = None
depends_on = None
diff --git a/migrations/versions/b183a2ac0dd1 b/migrations/versions/b183a2ac0dd1_.py
index 3a8c16339..a36a0caac 100644
--- a/migrations/versions/b183a2ac0dd1
+++ b/migrations/versions/b183a2ac0dd1_.py
@@ -7,6 +7,7 @@ Create Date: 2024-02-16 10:32:39.123456
"""
from alembic import op
import sqlalchemy as sa
+from app.database import TableLanguagesProfiles
# revision identifiers, used by Alembic.
@@ -19,6 +20,9 @@ bind = op.get_context().bind
def upgrade():
+ op.execute(sa.update(TableLanguagesProfiles)
+ .values({TableLanguagesProfiles.originalFormat: 0})
+ .where(TableLanguagesProfiles.originalFormat.is_(None)))
if bind.engine.name == 'postgresql':
with op.batch_alter_table('table_languages_profiles') as batch_op:
batch_op.alter_column('originalFormat', type_=sa.Integer())
diff --git a/tests/bazarr/test_all_import.py b/tests/bazarr/test_all_import.py.to_be_reworked
index 1fd1feee5..1fd1feee5 100644
--- a/tests/bazarr/test_all_import.py
+++ b/tests/bazarr/test_all_import.py.to_be_reworked
diff --git a/tests/bazarr/test_logging_filters.py b/tests/bazarr/test_logging_filters.py
new file mode 100644
index 000000000..bf01979e7
--- /dev/null
+++ b/tests/bazarr/test_logging_filters.py
@@ -0,0 +1,15 @@
+import logging
+
+from bazarr.app.logger import UnwantedWaitressMessageFilter
+
+def test_true_for_bazarr():
+ record = logging.LogRecord("", logging.INFO, "", 0, "a message from BAZARR for logging", (), None)
+ assert UnwantedWaitressMessageFilter().filter(record)
+
+def test_false_below_error():
+ record = logging.LogRecord("", logging.INFO, "", 0, "", (), None)
+ assert not UnwantedWaitressMessageFilter().filter(record)
+
+def test_true_above_error():
+ record = logging.LogRecord("", logging.CRITICAL, "", 0, "", (), None)
+ assert UnwantedWaitressMessageFilter().filter(record)
diff --git a/tests/subliminal_patch/test_embeddedsubtitles.py b/tests/subliminal_patch/test_embeddedsubtitles.py
index b7bfaa0a8..7326a368b 100644
--- a/tests/subliminal_patch/test_embeddedsubtitles.py
+++ b/tests/subliminal_patch/test_embeddedsubtitles.py
@@ -107,6 +107,13 @@ def fake_streams():
"tags": {"language": "eng", "title": "English"},
}
),
+ "tg": FFprobeSubtitleStream(
+ {
+ "index": 3,
+ "codec_name": "subrip",
+ "tags": {"language": "fil", "title": "Filipino"},
+ }
+ ),
"es_hi": FFprobeSubtitleStream(
{
"index": 3,
@@ -192,6 +199,18 @@ def test_list_subtitles_hi_fallback_one_stream(
assert subs[0].hearing_impaired == False
+def test_list_subtitles_custom_language_from_fese(
+ video_single_language, fake_streams, mocker
+):
+ with EmbeddedSubtitlesProvider(hi_fallback=True) as provider:
+ language = Language("tgl", "PH")
+ mocker.patch(
+ "subliminal_patch.providers.embeddedsubtitles._MemoizedFFprobeVideoContainer.get_subtitles",
+ return_value=[fake_streams["tg"]],
+ )
+ assert provider.list_subtitles(video_single_language, {language})
+
+
def test_list_subtitles_hi_fallback_multiple_streams(
video_single_language, fake_streams, mocker
):
diff --git a/tests/subliminal_patch/test_subdl.py b/tests/subliminal_patch/test_subdl.py
new file mode 100644
index 000000000..47fe7577d
--- /dev/null
+++ b/tests/subliminal_patch/test_subdl.py
@@ -0,0 +1,36 @@
+import os
+
+import pytest
+from subliminal_patch.providers.subdl import SubdlProvider
+from subliminal_patch.providers.subdl import SubdlSubtitle
+
+
[email protected](scope="session")
+def provider():
+ with SubdlProvider(os.environ["SUBDL_TOKEN"]) as provider:
+ yield provider
+
+
+def test_list_subtitles_movie(provider, movies, languages):
+ for sub in provider.list_subtitles(movies["dune"], {languages["en"]}):
+ assert sub.language == languages["en"]
+
+
+def test_download_subtitle(provider, languages):
+ data = {
+ "language": languages["en"],
+ "forced": False,
+ "hearing_impaired": False,
+ "page_link": "https://subdl.com/s/info/ebC6BrLCOC",
+ "download_link": "/subtitle/2808552-2770424.zip",
+ "file_id": "SUBDL::dune-2021-2770424.zip",
+ "release_names": ["Dune Part 1 WebDl"],
+ "uploader": "makoto77",
+ "season": 0,
+ "episode": None,
+ }
+
+ sub = SubdlSubtitle(**data)
+ provider.download_subtitle(sub)
+
+ assert sub.is_valid()
diff --git a/tests/subliminal_patch/test_subf2m.py b/tests/subliminal_patch/test_subf2m.py
index e8369d2fa..b8901179b 100644
--- a/tests/subliminal_patch/test_subf2m.py
+++ b/tests/subliminal_patch/test_subf2m.py
@@ -184,15 +184,36 @@ def test_download_subtitle_episode(provider, subtitle_episode):
assert subtitle_episode.is_valid()
-def test_download_subtitle_episode_with_title(provider):
+ "language,page_link,release_info,episode_number,episode_title",
+ [
+ (
+ "en",
+ "https://subf2m.co/subtitles/courage-the-cowardly-dog/english/2232402",
+ "Season 3 complete.",
+ 13,
+ "Feast of the Bullfrogs",
+ ),
+ (
+ "en",
+ "https://subf2m.co/subtitles/rick-and-morty-sixth-season/english/3060783",
+ "Used Subtitle Tools to convert from SUP to SRT, then ran the cleaner to remove HI. Grabbed subs from Rick.and.Morty.S06.1080p.BluRay.x264-STORiES.",
+ 7,
+ "Full Meta Jackrick",
+ ),
+ ],
+)
+def test_download_subtitle_episode_with_title(
+ provider, language, page_link, release_info, episode_number, episode_title
+):
sub = Subf2mSubtitle(
- Language.fromalpha2("en"),
- "https://subf2m.co/subtitles/courage-the-cowardly-dog/english/2232402",
- "Season 3 complete.",
- 13,
+ Language.fromalpha2(language),
+ page_link,
+ release_info,
+ episode_number,
)
- sub.episode_title = "Feast of the Bullfrogs"
+ sub.episode_title = episode_title
provider.download_subtitle(sub)
assert sub.is_valid()
diff --git a/tests/subliminal_patch/test_subscene.py b/tests/subliminal_patch/test_subscene.py
deleted file mode 100644
index 72063aae3..000000000
--- a/tests/subliminal_patch/test_subscene.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from subliminal_patch.providers import subscene_cloudscraper as subscene
-
-
-def test_provider_scraper_call():
- with subscene.SubsceneProvider() as provider:
- result = provider._scraper_call(
- "https://subscene.com/subtitles/breaking-bad-fifth-season"
- )
- assert result.status_code == 200
-
-
-def test_provider_gen_results():
- with subscene.SubsceneProvider() as provider:
- assert list(provider._gen_results("Breaking Bad"))
-
-
-def test_provider_search_movie():
- with subscene.SubsceneProvider() as provider:
- result = provider._search_movie("Taxi Driver", 1976)
- assert result == "/subtitles/taxi-driver"
-
-
-def test_provider_find_movie_subtitles(languages):
- with subscene.SubsceneProvider() as provider:
- result = provider._find_movie_subtitles(
- "/subtitles/taxi-driver", languages["en"]
- )
- assert result
-
-
-def test_provider_search_tv_show_season():
- with subscene.SubsceneProvider() as provider:
- result = provider._search_tv_show_season("The Wire", 1)
- assert result == "/subtitles/the-wire--first-season"
-
-
-def test_provider_find_episode_subtitles(languages):
- with subscene.SubsceneProvider() as provider:
- result = provider._find_episode_subtitles(
- "/subtitles/the-wire--first-season", 1, 1, languages["en"]
- )
- assert result
-
-
-def test_provider_download_subtitle(languages):
- path = "https://subscene.com/subtitles/the-wire--first-season/english/115904"
- subtitle = subscene.SubsceneSubtitle(languages["en"], path, "", 1)
- with subscene.SubsceneProvider() as provider:
- provider.download_subtitle(subtitle)
- assert subtitle.is_valid()