Merge branch 'development'

# Conflicts:
#	frontend/package-lock.json
#	frontend/package.json
This commit is contained in:
morpheus65535 2023-06-24 18:17:40 -04:00
commit 080710e7e1
399 changed files with 11476 additions and 5647 deletions

View File

@ -57,7 +57,6 @@ If you need something that is not already part of Bazarr, feel free to create a
- GreekSubtitles - GreekSubtitles
- Hosszupuska - Hosszupuska
- LegendasDivx - LegendasDivx
- LegendasTV
- Karagarga.in - Karagarga.in
- Ktuvit (Get `hashed_password` using method described [here](https://github.com/XBMCil/service.subtitles.ktuvit)) - Ktuvit (Get `hashed_password` using method described [here](https://github.com/XBMCil/service.subtitles.ktuvit))
- Napiprojekt - Napiprojekt

View File

@ -20,8 +20,8 @@ def check_python_version():
print("Python " + minimum_py3_str + " or greater required. " print("Python " + minimum_py3_str + " or greater required. "
"Current version is " + platform.python_version() + ". Please upgrade Python.") "Current version is " + platform.python_version() + ". Please upgrade Python.")
sys.exit(1) sys.exit(1)
elif int(python_version[0]) == 3 and int(python_version[1]) > 10: elif int(python_version[0]) == 3 and int(python_version[1]) > 11:
print("Python version greater than 3.10.x is unsupported. Current version is " + platform.python_version() + print("Python version greater than 3.11.x is unsupported. Current version is " + platform.python_version() +
". Keep in mind that even if it works, you're on your own.") ". Keep in mind that even if it works, you're on your own.")
elif (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \ elif (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \
(int(python_version[0]) != minimum_py3_tuple[0]): (int(python_version[0]) != minimum_py3_tuple[0]):

View File

@ -4,7 +4,7 @@ from flask_restx import Resource, Namespace, reqparse
from operator import itemgetter from operator import itemgetter
from app.database import TableHistory, TableHistoryMovie, TableSettingsLanguages from app.database import TableHistory, TableHistoryMovie, TableSettingsLanguages
from languages.get_languages import alpha2_from_alpha3, language_from_alpha2 from languages.get_languages import alpha2_from_alpha3, language_from_alpha2, alpha3_from_alpha2
from ..utils import authenticate, False_Keys from ..utils import authenticate, False_Keys
@ -46,6 +46,7 @@ class Languages(Resource):
try: try:
languages_dicts.append({ languages_dicts.append({
'code2': code2, 'code2': code2,
'code3': alpha3_from_alpha2(code2),
'name': language_from_alpha2(code2), 'name': language_from_alpha2(code2),
# Compatibility: Use false temporarily # Compatibility: Use false temporarily
'enabled': False 'enabled': False
@ -55,6 +56,7 @@ class Languages(Resource):
else: else:
languages_dicts = TableSettingsLanguages.select(TableSettingsLanguages.name, languages_dicts = TableSettingsLanguages.select(TableSettingsLanguages.name,
TableSettingsLanguages.code2, TableSettingsLanguages.code2,
TableSettingsLanguages.code3,
TableSettingsLanguages.enabled)\ TableSettingsLanguages.enabled)\
.order_by(TableSettingsLanguages.name).dicts() .order_by(TableSettingsLanguages.name).dicts()
languages_dicts = list(languages_dicts) languages_dicts = list(languages_dicts)

View File

@ -2,6 +2,7 @@
from flask import Flask, redirect from flask import Flask, redirect
from flask_compress import Compress
from flask_cors import CORS from flask_cors import CORS
from flask_socketio import SocketIO from flask_socketio import SocketIO
@ -15,6 +16,8 @@ socketio = SocketIO()
def create_app(): def create_app():
# Flask Setup # Flask Setup
app = Flask(__name__) app = Flask(__name__)
app.config['COMPRESS_ALGORITHM'] = 'gzip'
Compress(app)
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
app.config["SECRET_KEY"] = settings.general.flask_secret_key app.config["SECRET_KEY"] = settings.general.flask_secret_key

View File

@ -52,6 +52,7 @@ defaults = {
'movie_default_enabled': 'False', 'movie_default_enabled': 'False',
'movie_default_profile': '', 'movie_default_profile': '',
'page_size': '25', 'page_size': '25',
'theme': 'auto',
'page_size_manual_search': '10', 'page_size_manual_search': '10',
'minimum_score_movie': '70', 'minimum_score_movie': '70',
'use_embedded_subs': 'True', 'use_embedded_subs': 'True',
@ -83,7 +84,8 @@ defaults = {
'default_und_audio_lang': '', 'default_und_audio_lang': '',
'default_und_embedded_subtitles_lang': '', 'default_und_embedded_subtitles_lang': '',
'parse_embedded_audio_track': 'False', 'parse_embedded_audio_track': 'False',
'skip_hashing': 'False' 'skip_hashing': 'False',
'language_equals': '[]',
}, },
'auth': { 'auth': {
'type': 'None', 'type': 'None',
@ -168,7 +170,8 @@ defaults = {
'verify_ssl': 'True' 'verify_ssl': 'True'
}, },
'subf2m': { 'subf2m': {
'verify_ssl': 'True' 'verify_ssl': 'True',
'user_agent': ''
}, },
'whisperai': { 'whisperai': {
'endpoint': 'http://127.0.0.1:9000', 'endpoint': 'http://127.0.0.1:9000',
@ -183,11 +186,6 @@ defaults = {
'email': '', 'email': '',
'hashed_password': '' 'hashed_password': ''
}, },
'legendastv': {
'username': '',
'password': '',
'featured_only': 'False'
},
'xsubs': { 'xsubs': {
'username': '', 'username': '',
'password': '' 'password': ''
@ -300,7 +298,8 @@ array_keys = ['excluded_tags',
'excluded_series_types', 'excluded_series_types',
'enabled_providers', 'enabled_providers',
'path_mappings', 'path_mappings',
'path_mappings_movie'] 'path_mappings_movie',
'language_equals']
str_keys = ['chmod'] str_keys = ['chmod']

View File

@ -20,6 +20,7 @@ from subliminal_patch.extensions import provider_registry
from app.get_args import args from app.get_args import args
from app.config import settings, get_array_from from app.config import settings, get_array_from
from languages.get_languages import CustomLanguage
from app.event_handler import event_stream from app.event_handler import event_stream
from utilities.binaries import get_binary from utilities.binaries import get_binary
from radarr.blacklist import blacklist_log_movie from radarr.blacklist import blacklist_log_movie
@ -103,7 +104,7 @@ def provider_throttle_map():
} }
PROVIDERS_FORCED_OFF = ["addic7ed", "tvsubtitles", "legendasdivx", "legendastv", "napiprojekt", "shooter", PROVIDERS_FORCED_OFF = ["addic7ed", "tvsubtitles", "legendasdivx", "napiprojekt", "shooter",
"hosszupuska", "supersubtitles", "titlovi", "argenteam", "assrt", "subscene"] "hosszupuska", "supersubtitles", "titlovi", "argenteam", "assrt", "subscene"]
throttle_count = {} throttle_count = {}
@ -115,6 +116,49 @@ def provider_pool():
return subliminal_patch.core.SZProviderPool return subliminal_patch.core.SZProviderPool
def _lang_from_str(content: str):
" Formats: es-MX en@hi es-MX@forced "
extra_info = content.split("@")
if len(extra_info) > 1:
kwargs = {extra_info[-1]: True}
else:
kwargs = {}
content = extra_info[0]
try:
code, country = content.split("-")
except ValueError:
lang = CustomLanguage.from_value(content)
if lang is not None:
lang = lang.subzero_language()
return lang.rebuild(lang, **kwargs)
code, country = content, None
return subliminal_patch.core.Language(code, country, **kwargs)
def get_language_equals(settings_=None):
settings_ = settings_ or settings
equals = get_array_from(settings_.general.language_equals)
if not equals:
return []
items = []
for equal in equals:
try:
from_, to_ = equal.split(":")
from_, to_ = _lang_from_str(from_), _lang_from_str(to_)
except Exception as error:
logging.info("Invalid equal value: '%s' [%s]", equal, error)
else:
items.append((from_, to_))
return items
def get_providers(): def get_providers():
providers_list = [] providers_list = []
existing_providers = provider_registry.names() existing_providers = provider_registry.names()
@ -202,13 +246,6 @@ def get_providers_auth():
'skip_wrong_fps' 'skip_wrong_fps'
), ),
}, },
'legendastv': {
'username': settings.legendastv.username,
'password': settings.legendastv.password,
'featured_only': settings.legendastv.getboolean(
'featured_only'
),
},
'xsubs': { 'xsubs': {
'username': settings.xsubs.username, 'username': settings.xsubs.username,
'password': settings.xsubs.password, 'password': settings.xsubs.password,
@ -250,11 +287,13 @@ def get_providers_auth():
'f_password': settings.karagarga.f_password, 'f_password': settings.karagarga.f_password,
}, },
'subf2m': { 'subf2m': {
'verify_ssl': settings.subf2m.getboolean('verify_ssl') 'verify_ssl': settings.subf2m.getboolean('verify_ssl'),
'user_agent': settings.subf2m.user_agent,
}, },
'whisperai': { 'whisperai': {
'endpoint': settings.whisperai.endpoint, 'endpoint': settings.whisperai.endpoint,
'timeout': settings.whisperai.timeout 'timeout': settings.whisperai.timeout,
'ffmpeg_path': _FFMPEG_BINARY,
} }
} }

View File

@ -24,9 +24,14 @@ ui_bp = Blueprint('ui', __name__,
'build', 'assets'), 'build', 'assets'),
static_url_path='/assets') static_url_path='/assets')
static_bp = Blueprint('images', __name__, if os.path.exists(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend', 'build',
static_folder=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'images')):
'frontend', 'build', 'images'), static_url_path='/images') static_directory = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend', 'build',
'images')
else:
static_directory = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend', 'public',
'images')
static_bp = Blueprint('images', __name__, static_folder=static_directory, static_url_path='/images')
ui_bp.register_blueprint(static_bp) ui_bp.register_blueprint(static_bp)

View File

@ -18,6 +18,7 @@ class CustomLanguage:
official_alpha3 = "por" official_alpha3 = "por"
name = "Brazilian Portuguese" name = "Brazilian Portuguese"
iso = "BR" iso = "BR"
_scripts = []
_possible_matches = ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil") _possible_matches = ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil")
_extensions = (".pt-br", ".pob", ".pb") _extensions = (".pt-br", ".pob", ".pb")
_extensions_forced = (".pt-br.forced", ".pob.forced", ".pb.forced") _extensions_forced = (".pt-br.forced", ".pob.forced", ".pb.forced")
@ -86,6 +87,15 @@ class CustomLanguage:
return any(ext in name for ext in self._possible_matches) return any(ext in name for ext in self._possible_matches)
def language_found(self, language: Language):
if str(language.country) == self.iso:
return True
if language.script and str(language.script) in self._scripts:
return True
return False
class BrazilianPortuguese(CustomLanguage): class BrazilianPortuguese(CustomLanguage):
# Same attributes as base class # Same attributes as base class
@ -100,6 +110,9 @@ class ChineseTraditional(CustomLanguage):
official_alpha3 = "zho" official_alpha3 = "zho"
name = "Chinese Traditional" name = "Chinese Traditional"
iso = "TW" iso = "TW"
# _scripts = (Script("Hant"),)
# We'll use literals for now
_scripts = ("Hant",)
_extensions = ( _extensions = (
".cht", ".cht",
".tc", ".tc",
@ -211,6 +224,7 @@ class LatinAmericanSpanish(CustomLanguage):
official_alpha3 = "spa" official_alpha3 = "spa"
name = "Latin American Spanish" name = "Latin American Spanish"
iso = "MX" # Not fair, but ok iso = "MX" # Not fair, but ok
_scripts = ("419",)
_possible_matches = ( _possible_matches = (
"es-la", "es-la",
"spa-la", "spa-la",

View File

@ -7,7 +7,7 @@ import re
from guess_language import guess_language from guess_language import guess_language
from subliminal_patch import core from subliminal_patch import core
from subzero.language import Language from subzero.language import Language
from charamel import Detector from chardet import detect
from app.config import settings from app.config import settings
from constants import hi_regex from constants import hi_regex
@ -76,45 +76,34 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
with open(subtitle_path, 'rb') as f: with open(subtitle_path, 'rb') as f:
text = f.read() text = f.read()
try: encoding = detect(text)
text = text.decode('utf-8') if encoding and 'encoding' in encoding:
detected_language = guess_language(text) encoding = detect(text)['encoding']
# add simplified and traditional chinese detection else:
if detected_language == 'zh': logging.debug("BAZARR skipping this subtitles because we can't guess the encoding. "
traditional_chinese_fuzzy = [u"", u"雙語"] "It's probably a binary file: " + subtitle_path)
traditional_chinese = [".cht", ".tc", ".zh-tw", ".zht", ".zh-hant", ".zhhant", ".zh_hant", continue
".hant", ".big5", ".traditional"] text = text.decode(encoding)
if str(os.path.splitext(subtitle)[0]).lower().endswith(tuple(traditional_chinese)) or (str(subtitle_path).lower())[:-5] in traditional_chinese_fuzzy:
detected_language == 'zt' detected_language = guess_language(text)
except UnicodeDecodeError:
detector = Detector() # add simplified and traditional chinese detection
if detected_language == 'zh':
traditional_chinese_fuzzy = [u"", u"雙語"]
traditional_chinese = [".cht", ".tc", ".zh-tw", ".zht", ".zh-hant", ".zhhant", ".zh_hant",
".hant", ".big5", ".traditional"]
if str(os.path.splitext(subtitle)[0]).lower().endswith(tuple(traditional_chinese)) or \
(str(subtitle_path).lower())[:-5] in traditional_chinese_fuzzy:
detected_language = 'zt'
if detected_language:
logging.debug("BAZARR external subtitles detected and guessed this language: " + str(
detected_language))
try: try:
guess = detector.detect(text) subtitles[subtitle] = Language.rebuild(Language.fromietf(detected_language), forced=forced,
hi=False)
except Exception: except Exception:
logging.debug("BAZARR skipping this subtitles because we can't guess the encoding. " pass
"It's probably a binary file: " + subtitle_path)
continue
else:
logging.debug('BAZARR detected encoding %r', guess)
try:
text = text.decode(guess)
except Exception:
logging.debug(
"BAZARR skipping this subtitles because we can't decode the file using the "
"guessed encoding. It's probably a binary file: " + subtitle_path)
continue
detected_language = guess_language(text)
except Exception:
logging.debug('BAZARR was unable to detect encoding for this subtitles file: %r', subtitle_path)
finally:
if detected_language:
logging.debug("BAZARR external subtitles detected and guessed this language: " + str(
detected_language))
try:
subtitles[subtitle] = Language.rebuild(Language.fromietf(detected_language), forced=forced,
hi=False)
except Exception:
pass
# If language is still None (undetected), skip it # If language is still None (undetected), skip it
if hasattr(subtitles[subtitle], 'basename') and not subtitles[subtitle].basename: if hasattr(subtitles[subtitle], 'basename') and not subtitles[subtitle].basename:
@ -139,24 +128,14 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
with open(subtitle_path, 'rb') as f: with open(subtitle_path, 'rb') as f:
text = f.read() text = f.read()
try: encoding = detect(text)
text = text.decode('utf-8') if encoding and 'encoding' in encoding:
except UnicodeDecodeError: encoding = detect(text)['encoding']
detector = Detector() else:
try: logging.debug("BAZARR skipping this subtitles because we can't guess the encoding. "
guess = detector.detect(text) "It's probably a binary file: " + subtitle_path)
except Exception: continue
logging.debug("BAZARR skipping this subtitles because we can't guess the encoding. " text = text.decode(encoding)
"It's probably a binary file: " + subtitle_path)
continue
else:
logging.debug('BAZARR detected encoding %r', guess)
try:
text = text.decode(guess)
except Exception:
logging.debug("BAZARR skipping this subtitles because we can't decode the file using the "
"guessed encoding. It's probably a binary file: " + subtitle_path)
continue
if bool(re.search(hi_regex, text)): if bool(re.search(hi_regex, text)):
subtitles[subtitle] = Language.rebuild(subtitles[subtitle], forced=False, hi=True) subtitles[subtitle] = Language.rebuild(subtitles[subtitle], forced=False, hi=True)

View File

@ -87,8 +87,7 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type):
logging.debug(f"BAZARR Skipping {s}, because it doesn't match our series/episode") logging.debug(f"BAZARR Skipping {s}, because it doesn't match our series/episode")
except TypeError: except TypeError:
logging.debug("BAZARR Ignoring invalid subtitles") logging.debug("BAZARR Ignoring invalid subtitles")
finally: continue
continue
initial_hi = None initial_hi = None
initial_hi_match = False initial_hi_match = False

View File

@ -8,7 +8,7 @@ from inspect import getfullargspec
from radarr.blacklist import get_blacklist_movie from radarr.blacklist import get_blacklist_movie
from sonarr.blacklist import get_blacklist from sonarr.blacklist import get_blacklist
from app.get_providers import get_providers, get_providers_auth, provider_throttle, provider_pool from app.get_providers import get_providers, get_providers_auth, provider_throttle, provider_pool, get_language_equals
from .utils import get_ban_list from .utils import get_ban_list
@ -19,10 +19,11 @@ def _init_pool(media_type, profile_id=None, providers=None):
return pool( return pool(
providers=providers or get_providers(), providers=providers or get_providers(),
provider_configs=get_providers_auth(), provider_configs=get_providers_auth(),
blacklist=get_blacklist() if media_type == 'series' else get_blacklist_movie(), blacklist=get_blacklist() if media_type == "series" else get_blacklist_movie(),
throttle_callback=provider_throttle, throttle_callback=provider_throttle,
ban_list=get_ban_list(profile_id), ban_list=get_ban_list(profile_id),
language_hook=None, language_hook=None,
language_equals=get_language_equals(),
) )
@ -54,8 +55,19 @@ def _update_pool(media_type, profile_id=None):
return pool.update( return pool.update(
get_providers(), get_providers(),
get_providers_auth(), get_providers_auth(),
get_blacklist() if media_type == 'series' else get_blacklist_movie(), get_blacklist() if media_type == "series" else get_blacklist_movie(),
get_ban_list(profile_id), get_ban_list(profile_id),
get_language_equals(),
)
def _pool_update(pool, media_type, profile_id=None):
return pool.update(
get_providers(),
get_providers_auth(),
get_blacklist() if media_type == "series" else get_blacklist_movie(),
get_ban_list(profile_id),
get_language_equals(),
) )

View File

@ -18,8 +18,8 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
sonarr_episode_id, radarr_id): sonarr_episode_id, radarr_id):
language_code_convert_dict = { language_code_convert_dict = {
'he': 'iw', 'he': 'iw',
'zt': 'zh-CN', 'zh': 'zh-CN',
'zh': 'zh-TW', 'zt': 'zh-TW',
} }
to_lang = alpha3_from_alpha2(to_lang) to_lang = alpha3_from_alpha2(to_lang)

View File

@ -9,7 +9,7 @@ from functools import reduce
from app.config import settings from app.config import settings
from app.database import get_exclusion_clause, get_audio_profile_languages, TableShows, TableEpisodes, TableMovies, \ from app.database import get_exclusion_clause, get_audio_profile_languages, TableShows, TableEpisodes, TableMovies, \
TableHistory, TableHistoryMovie TableHistory, TableHistoryMovie, get_profiles_list
from app.event_handler import show_progress, hide_progress from app.event_handler import show_progress, hide_progress
from app.get_providers import get_providers from app.get_providers import get_providers
from app.notifier import send_notifications, send_notifications_movie from app.notifier import send_notifications, send_notifications_movie
@ -217,7 +217,7 @@ def get_upgradable_episode_subtitles():
if not upgradable_episodes: if not upgradable_episodes:
return [] return []
else: else:
upgradable_episodes = list(upgradable_episodes) upgradable_episodes = [x for x in upgradable_episodes if _language_still_desired(x['language'], x['profileId'])]
logging.debug(f"{len(upgradable_episodes)} potentially upgradable episode subtitles have been found, let's " logging.debug(f"{len(upgradable_episodes)} potentially upgradable episode subtitles have been found, let's "
f"filter them...") f"filter them...")
@ -252,8 +252,32 @@ def get_upgradable_movies_subtitles():
if not upgradable_movies: if not upgradable_movies:
return [] return []
else: else:
upgradable_movies = list(upgradable_movies) upgradable_movies = [x for x in upgradable_movies if _language_still_desired(x['language'], x['profileId'])]
logging.debug(f"{len(upgradable_movies)} potentially upgradable movie subtitles have been found, let's filter " logging.debug(f"{len(upgradable_movies)} potentially upgradable movie subtitles have been found, let's filter "
f"them...") f"them...")
return parse_upgradable_list(upgradable_list=upgradable_movies, perfect_score=117, media_type='movie') return parse_upgradable_list(upgradable_list=upgradable_movies, perfect_score=117, media_type='movie')
def _language_still_desired(language, profile_id):
if not profile_id:
return False
profile = get_profiles_list(profile_id)
if profile and language in _language_from_items(profile['items']):
return True
else:
return False
def _language_from_items(items):
results = []
for item in items:
if item['forced'] == 'True':
results.append(item['language'] + ':forced')
elif item['hi'] == 'True':
results.append(item['language'] + ':hi')
else:
results.append(item['language'])
results.append(item['language'] + ':hi')
return results

View File

@ -4,7 +4,7 @@ import os
import logging import logging
import hashlib import hashlib
from charamel import Detector from chardet import detect
from bs4 import UnicodeDammit from bs4 import UnicodeDammit
from app.config import settings from app.config import settings
@ -64,8 +64,7 @@ def force_unicode(s):
try: try:
s = s.decode("utf-8") s = s.decode("utf-8")
except UnicodeDecodeError: except UnicodeDecodeError:
detector = Detector() t = detect(s)['encoding']
t = detector.detect(s)
try: try:
s = s.decode(t) s = s.decode(t)
except UnicodeDecodeError: except UnicodeDecodeError:

View File

@ -16,13 +16,19 @@ def _handle_alpha3(detected_language: dict):
alpha3 = detected_language["language"].alpha3 alpha3 = detected_language["language"].alpha3
custom = CustomLanguage.from_value(alpha3, "official_alpha3") custom = CustomLanguage.from_value(alpha3, "official_alpha3")
if custom and custom.ffprobe_found(detected_language): if not custom:
return alpha3
found = custom.language_found(detected_language["language"])
if not found:
found = custom.ffprobe_found(detected_language)
if found:
logging.debug("Custom embedded language found: %s", custom.name) logging.debug("Custom embedded language found: %s", custom.name)
return custom.alpha3 return custom.alpha3
return alpha3 return alpha3
def embedded_subs_reader(file, file_size, episode_file_id=None, movie_file_id=None, use_cache=True): def embedded_subs_reader(file, file_size, episode_file_id=None, movie_file_id=None, use_cache=True):
data = parse_video_metadata(file, file_size, episode_file_id, movie_file_id, use_cache=use_cache) data = parse_video_metadata(file, file_size, episode_file_id, movie_file_id, use_cache=use_cache)
und_default_language = alpha3_from_alpha2(settings.general.default_und_embedded_subtitles_lang) und_default_language = alpha3_from_alpha2(settings.general.default_und_embedded_subtitles_lang)
@ -33,7 +39,7 @@ def embedded_subs_reader(file, file_size, episode_file_id=None, movie_file_id=No
return subtitles_list return subtitles_list
cache_provider = None cache_provider = None
if data["ffprobe"] and "subtitle" in data["ffprobe"]: if "ffprobe" in data and data["ffprobe"] and "subtitle" in data["ffprobe"]:
cache_provider = 'ffprobe' cache_provider = 'ffprobe'
elif 'mediainfo' in data and data["mediainfo"] and "subtitle" in data["mediainfo"]: elif 'mediainfo' in data and data["mediainfo"] and "subtitle" in data["mediainfo"]:
cache_provider = 'mediainfo' cache_provider = 'mediainfo'
@ -75,7 +81,7 @@ def embedded_audio_reader(file, file_size, episode_file_id=None, movie_file_id=N
return audio_list return audio_list
cache_provider = None cache_provider = None
if data["ffprobe"] and "audio" in data["ffprobe"]: if "ffprobe" in data and data["ffprobe"] and "audio" in data["ffprobe"]:
cache_provider = 'ffprobe' cache_provider = 'ffprobe'
elif 'mediainfo' in data and data["mediainfo"] and "audio" in data["mediainfo"]: elif 'mediainfo' in data and data["mediainfo"] and "audio" in data["mediainfo"]:
cache_provider = 'mediainfo' cache_provider = 'mediainfo'
@ -86,7 +92,8 @@ def embedded_audio_reader(file, file_size, episode_file_id=None, movie_file_id=N
audio_list.append(None) audio_list.append(None)
continue continue
language = language_from_alpha3(detected_language["language"].alpha3) alpha3 = _handle_alpha3(detected_language)
language = language_from_alpha3(alpha3)
if language not in audio_list: if language not in audio_list:
audio_list.append(language) audio_list.append(language)

View File

@ -7,3 +7,4 @@ pytest-cov
pytest-vcr pytest-vcr
pytest-mock pytest-mock
requests-mock requests-mock
setuptools

File diff suppressed because it is too large Load Diff

View File

@ -13,17 +13,17 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@mantine/core": "^5.6.0", "@mantine/core": "^6.0.0",
"@mantine/dropzone": "^5.6.0", "@mantine/dropzone": "^6.0.0",
"@mantine/form": "^5.6.0", "@mantine/form": "^6.0.0",
"@mantine/hooks": "^5.6.0", "@mantine/hooks": "^6.0.0",
"@mantine/modals": "^5.6.0", "@mantine/modals": "^6.0.0",
"@mantine/notifications": "^5.6.0", "@mantine/notifications": "^6.0.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-query": "^3.39.2", "react-query": "^3.39.2",
"react-router-dom": "~6.3.0", "react-router-dom": "~6.10.0",
"socket.io-client": "^4.5.3" "socket.io-client": "^4.5.3"
}, },
"devDependencies": { "devDependencies": {
@ -34,36 +34,35 @@
"@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.0", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/lodash": "^4.14.0", "@types/lodash": "^4.14.0",
"@types/node": "^18.11.7", "@types/node": "^18.16.0",
"@types/react": "^17.0.0", "@types/react": "^18.2.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^18.2.0",
"@types/react-table": "^7.7.0", "@types/react-table": "^7.7.0",
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-c8": "^0.25.0", "vitest": "^0.30.1",
"@vitest/ui": "^0.29.1", "@vitest/coverage-c8": "^0.30.0",
"@vitest/ui": "^0.30.0",
"clsx": "^1.2.0", "clsx": "^1.2.0",
"eslint": "^8.26.0", "eslint": "^8.39.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.9.0", "eslint-plugin-testing-library": "^5.9.0",
"husky": "^8.0.2", "husky": "^8.0.2",
"jsdom": "^20.0.1", "jsdom": "^21.0.0",
"lodash": "^4.17.0", "lodash": "^4.17.0",
"moment": "^2.29", "moment": "^2.29",
"prettier": "^2.7.0", "prettier": "^2.8.0",
"prettier-plugin-organize-imports": "^3.1.0", "prettier-plugin-organize-imports": "^3.1.0",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"react-table": "^7.8.0", "react-table": "^7.8.0",
"recharts": "~2.4.3", "recharts": "~2.5.0",
"sass": "^1.55.0", "sass": "^1.62.0",
"typescript": "^4", "typescript": "^5",
"vite": "^3.2.1", "vite": "^4.3.0",
"vite-plugin-checker": "^0.5.5", "vite-plugin-checker": "^0.5.5"
"vitest": "^0.25.0"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",

View File

@ -15,12 +15,12 @@ import {
Avatar, Avatar,
Badge, Badge,
Burger, Burger,
createStyles,
Divider, Divider,
Group, Group,
Header, Header,
MediaQuery, MediaQuery,
Menu, Menu,
createStyles,
} from "@mantine/core"; } from "@mantine/core";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";

View File

@ -357,9 +357,11 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
></FontAwesomeIcon> ></FontAwesomeIcon>
)} )}
{name} {name}
<Badge className={classes.badge} radius="xs" hidden={shouldHideBadge}> {shouldHideBadge === false && (
{badge} <Badge className={classes.badge} radius="xs">
</Badge> {badge}
</Badge>
)}
</Text> </Text>
</NavLink> </NavLink>
); );

View File

@ -1,11 +1,11 @@
import AppNavbar from "@/App/Navbar"; import AppNavbar from "@/App/Navbar";
import { RouterNames } from "@/Router/RouterNames";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import { Layout } from "@/constants"; import { Layout } from "@/constants";
import NavbarProvider from "@/contexts/Navbar"; import NavbarProvider from "@/contexts/Navbar";
import OnlineProvider from "@/contexts/Online"; import OnlineProvider from "@/contexts/Online";
import { notification } from "@/modules/task"; import { notification } from "@/modules/task";
import CriticalError from "@/pages/errors/CriticalError"; import CriticalError from "@/pages/errors/CriticalError";
import { RouterNames } from "@/Router/RouterNames";
import { Environment } from "@/utilities"; import { Environment } from "@/utilities";
import { AppShell } from "@mantine/core"; import { AppShell } from "@mantine/core";
import { useWindowEvent } from "@mantine/hooks"; import { useWindowEvent } from "@mantine/hooks";

View File

@ -1,3 +1,4 @@
import { useSystemSettings } from "@/apis/hooks";
import { import {
ColorScheme, ColorScheme,
ColorSchemeProvider, ColorSchemeProvider,
@ -6,7 +7,13 @@ import {
MantineThemeOverride, MantineThemeOverride,
} from "@mantine/core"; } from "@mantine/core";
import { useColorScheme } from "@mantine/hooks"; import { useColorScheme } from "@mantine/hooks";
import { FunctionComponent, useCallback, useEffect, useState } from "react"; import {
FunctionComponent,
PropsWithChildren,
useCallback,
useEffect,
useState,
} from "react";
const theme: MantineThemeOverride = { const theme: MantineThemeOverride = {
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif", fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
@ -28,7 +35,19 @@ const theme: MantineThemeOverride = {
}; };
function useAutoColorScheme() { function useAutoColorScheme() {
const preferredColorScheme = useColorScheme(); const settings = useSystemSettings();
const settingsColorScheme = settings.data?.general.theme;
let preferredColorScheme: ColorScheme = useColorScheme();
switch (settingsColorScheme) {
case "light":
preferredColorScheme = "light" as ColorScheme;
break;
case "dark":
preferredColorScheme = "dark" as ColorScheme;
break;
}
const [colorScheme, setColorScheme] = useState(preferredColorScheme); const [colorScheme, setColorScheme] = useState(preferredColorScheme);
// automatically switch dark/light theme // automatically switch dark/light theme
@ -45,7 +64,7 @@ function useAutoColorScheme() {
const emotionCache = createEmotionCache({ key: "bazarr" }); const emotionCache = createEmotionCache({ key: "bazarr" });
const ThemeProvider: FunctionComponent = ({ children }) => { const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
const { colorScheme, toggleColorScheme } = useAutoColorScheme(); const { colorScheme, toggleColorScheme } = useAutoColorScheme();
return ( return (

View File

@ -1,12 +1,11 @@
import App from "@/App";
import { useBadges } from "@/apis/hooks"; import { useBadges } from "@/apis/hooks";
import { useEnabledStatus } from "@/apis/hooks/site"; import { useEnabledStatus } from "@/apis/hooks/site";
import App from "@/App";
import { Lazy } from "@/components/async"; import { Lazy } from "@/components/async";
import Authentication from "@/pages/Authentication"; import Authentication from "@/pages/Authentication";
import BlacklistMoviesView from "@/pages/Blacklist/Movies"; import BlacklistMoviesView from "@/pages/Blacklist/Movies";
import BlacklistSeriesView from "@/pages/Blacklist/Series"; import BlacklistSeriesView from "@/pages/Blacklist/Series";
import Episodes from "@/pages/Episodes"; import Episodes from "@/pages/Episodes";
import NotFound from "@/pages/errors/NotFound";
import MoviesHistoryView from "@/pages/History/Movies"; import MoviesHistoryView from "@/pages/History/Movies";
import SeriesHistoryView from "@/pages/History/Series"; import SeriesHistoryView from "@/pages/History/Series";
import MovieView from "@/pages/Movies"; import MovieView from "@/pages/Movies";
@ -31,6 +30,7 @@ import SystemReleasesView from "@/pages/System/Releases";
import SystemTasksView from "@/pages/System/Tasks"; import SystemTasksView from "@/pages/System/Tasks";
import WantedMoviesView from "@/pages/Wanted/Movies"; import WantedMoviesView from "@/pages/Wanted/Movies";
import WantedSeriesView from "@/pages/Wanted/Series"; import WantedSeriesView from "@/pages/Wanted/Series";
import NotFound from "@/pages/errors/NotFound";
import { Environment } from "@/utilities"; import { Environment } from "@/utilities";
import { import {
faClock, faClock,
@ -42,13 +42,13 @@ import {
faPlay, faPlay,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { import {
createContext,
FunctionComponent, FunctionComponent,
createContext,
lazy, lazy,
useContext, useContext,
useMemo, useMemo,
} from "react"; } from "react";
import { BrowserRouter } from "react-router-dom"; import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Redirector from "./Redirector"; import Redirector from "./Redirector";
import { RouterNames } from "./RouterNames"; import { RouterNames } from "./RouterNames";
import { CustomRouteObject } from "./type"; import { CustomRouteObject } from "./type";
@ -315,12 +315,18 @@ function useRoutes(): CustomRouteObject[] {
const RouterItemContext = createContext<CustomRouteObject[]>([]); const RouterItemContext = createContext<CustomRouteObject[]>([]);
export const Router: FunctionComponent = ({ children }) => { export const Router: FunctionComponent = () => {
const routes = useRoutes(); const routes = useRoutes();
// TODO: Move this outside the function component scope
const router = useMemo(
() => createBrowserRouter(routes, { basename: Environment.baseUrl }),
[routes]
);
return ( return (
<RouterItemContext.Provider value={routes}> <RouterItemContext.Provider value={routes}>
<BrowserRouter basename={Environment.baseUrl}>{children}</BrowserRouter> <RouterProvider router={router}></RouterProvider>
</RouterItemContext.Provider> </RouterItemContext.Provider>
); );
}; };

View File

@ -3,9 +3,9 @@ import { usePageSize } from "@/utilities/storage";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { import {
QueryKey, QueryKey,
UseQueryResult,
useQuery, useQuery,
useQueryClient, useQueryClient,
UseQueryResult,
} from "react-query"; } from "react-query";
import { QueryKeys } from "./keys"; import { QueryKeys } from "./keys";

View File

@ -1,11 +1,11 @@
import UIError from "@/pages/errors/UIError"; import UIError from "@/pages/errors/UIError";
import { Component } from "react"; import { Component, PropsWithChildren } from "react";
interface State { interface State {
error: Error | null; error: Error | null;
} }
class ErrorBoundary extends Component<object, State> { class ErrorBoundary extends Component<PropsWithChildren, State> {
constructor(props: object) { constructor(props: object) {
super(props); super(props);
this.state = { error: null }; this.state = { error: null };

View File

@ -1,7 +1,7 @@
import { LoadingOverlay } from "@mantine/core"; import { LoadingOverlay } from "@mantine/core";
import { FunctionComponent, Suspense } from "react"; import { FunctionComponent, PropsWithChildren, Suspense } from "react";
const Lazy: FunctionComponent = ({ children }) => { const Lazy: FunctionComponent<PropsWithChildren> = ({ children }) => {
return <Suspense fallback={<LoadingOverlay visible />}>{children}</Suspense>; return <Suspense fallback={<LoadingOverlay visible />}>{children}</Suspense>;
}; };

View File

@ -0,0 +1,34 @@
import { useLanguages } from "@/apis/hooks";
import { Selector, SelectorProps } from "@/components/inputs";
import { useSelectorOptions } from "@/utilities";
import { FunctionComponent, useMemo } from "react";
interface LanguageSelectorProps
extends Omit<SelectorProps<Language.Server>, "options" | "getkey"> {
enabled?: boolean;
}
const LanguageSelector: FunctionComponent<LanguageSelectorProps> = ({
enabled = false,
...selector
}) => {
const { data } = useLanguages();
const filteredData = useMemo(() => {
if (enabled) {
return data?.filter((value) => value.enabled);
} else {
return data;
}
}, [data, enabled]);
const options = useSelectorOptions(
filteredData ?? [],
(value) => value.name,
(value) => value.code3
);
return <Selector {...options} searchable {...selector}></Selector>;
};
export default LanguageSelector;

View File

@ -1,6 +1,6 @@
import { useMovieSubtitleModification } from "@/apis/hooks"; import { useMovieSubtitleModification } from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals"; import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task"; import { TaskGroup, task } from "@/modules/task";
import { useTableStyles } from "@/styles"; import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities"; import { useArrayAction, useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form"; import FormUtils from "@/utilities/form";
@ -28,9 +28,9 @@ import { useForm } from "@mantine/form";
import { isString } from "lodash"; import { isString } from "lodash";
import { FunctionComponent, useEffect, useMemo } from "react"; import { FunctionComponent, useEffect, useMemo } from "react";
import { Column } from "react-table"; import { Column } from "react-table";
import TextPopover from "../TextPopover";
import { Action, Selector } from "../inputs"; import { Action, Selector } from "../inputs";
import { SimpleTable } from "../tables"; import { SimpleTable } from "../tables";
import TextPopover from "../TextPopover";
type SubtitleFile = { type SubtitleFile = {
file: File; file: File;

View File

@ -1,4 +1,4 @@
export * from "./inputs";
export { default as Search } from "./Search"; export { default as Search } from "./Search";
export * from "./inputs";
export * from "./tables"; export * from "./tables";
export { default as Toolbox } from "./toolbox"; export { default as Toolbox } from "./toolbox";

View File

@ -4,7 +4,7 @@ import {
faXmark, faXmark,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createStyles, Group, Stack, Text } from "@mantine/core"; import { Group, Stack, Text, createStyles } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";

View File

@ -83,6 +83,9 @@ export const FileBrowser: FunctionComponent<FileBrowserProps> = ({
placeholder="Click to start" placeholder="Click to start"
data={data} data={data}
value={value} value={value}
// Temporary solution of infinite dropdown items, fix later
limit={NaN}
maxDropdownHeight={240}
filter={(value, item) => { filter={(value, item) => {
if (item.value === backKey) { if (item.value === backKey) {
return true; return true;

View File

@ -93,6 +93,7 @@ export function Selector<T>({
return ( return (
<Select <Select
withinPortal={true}
data={data} data={data}
defaultValue={wrappedDefaultValue} defaultValue={wrappedDefaultValue}
value={wrappedValue} value={wrappedValue}

View File

@ -12,11 +12,11 @@ import { Badge, Center, Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table"; import { Column } from "react-table";
import { PageTable } from ".."; import { PageTable } from "..";
import TextPopover from "../TextPopover";
import MutateAction from "../async/MutateAction"; import MutateAction from "../async/MutateAction";
import QueryOverlay from "../async/QueryOverlay"; import QueryOverlay from "../async/QueryOverlay";
import { HistoryIcon } from "../bazarr"; import { HistoryIcon } from "../bazarr";
import Language from "../bazarr/Language"; import Language from "../bazarr/Language";
import TextPopover from "../TextPopover";
interface MovieHistoryViewProps { interface MovieHistoryViewProps {
movie: Item.Movie; movie: Item.Movie;

View File

@ -105,7 +105,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
</Anchor> </Anchor>
); );
} else { } else {
return value; return <Text>{value}</Text>;
} }
}, },
}, },
@ -220,12 +220,12 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
export const MovieSearchModal = withModal<Props<Item.Movie>>( export const MovieSearchModal = withModal<Props<Item.Movie>>(
ManualSearchView, ManualSearchView,
"movie-manual-search", "movie-manual-search",
{ title: "Search Subtitles", size: "xl" } { title: "Search Subtitles", size: "calc(100vw - 4rem)" }
); );
export const EpisodeSearchModal = withModal<Props<Item.Episode>>( export const EpisodeSearchModal = withModal<Props<Item.Episode>>(
ManualSearchView, ManualSearchView,
"episode-manual-search", "episode-manual-search",
{ title: "Search Subtitles", size: "xl" } { title: "Search Subtitles", size: "calc(100vw - 4rem)" }
); );
const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({ const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
@ -237,7 +237,7 @@ const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
const { ref, hovered } = useHover(); const { ref, hovered } = useHover();
return ( return (
<Popover opened={hovered} position="top" width={360} withArrow> <Popover opened={hovered} position="top" width={360} withArrow withinPortal>
<Popover.Target> <Popover.Target>
<Text color={hasIssues ? "yellow" : "green"} ref={ref}> <Text color={hasIssues ? "yellow" : "green"} ref={ref}>
<FontAwesomeIcon <FontAwesomeIcon

View File

@ -4,7 +4,7 @@ import { SimpleTable } from "@/components/tables";
import { useCustomSelection } from "@/components/tables/plugins"; import { useCustomSelection } from "@/components/tables/plugins";
import { withModal } from "@/modules/modals"; import { withModal } from "@/modules/modals";
import { isMovie } from "@/utilities"; import { isMovie } from "@/utilities";
import { Badge, Button, Divider, Group, Stack } from "@mantine/core"; import { Badge, Button, Divider, Group, Stack, Text } from "@mantine/core";
import { FunctionComponent, useMemo, useState } from "react"; import { FunctionComponent, useMemo, useState } from "react";
import { Column, useRowSelect } from "react-table"; import { Column, useRowSelect } from "react-table";
@ -60,9 +60,9 @@ const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
} }
if (idx !== -1) { if (idx !== -1) {
return path.slice(idx + 1); return <Text>{path.slice(idx + 1)}</Text>;
} else { } else {
return path; return <Text>{path}</Text>;
} }
}, },
}, },

View File

@ -30,7 +30,7 @@ const PageControl: FunctionComponent<Props> = ({
<Pagination <Pagination
size="sm" size="sm"
color={isLoading ? "gray" : "primary"} color={isLoading ? "gray" : "primary"}
page={index + 1} value={index + 1}
onChange={(page) => goto(page - 1)} onChange={(page) => goto(page - 1)}
hidden={count <= 1} hidden={count <= 1}
total={count} total={count}

View File

@ -3,8 +3,8 @@ import { useEffect } from "react";
import { usePagination, useTable } from "react-table"; import { usePagination, useTable } from "react-table";
import BaseTable from "./BaseTable"; import BaseTable from "./BaseTable";
import PageControl from "./PageControl"; import PageControl from "./PageControl";
import { useDefaultSettings } from "./plugins";
import { SimpleTableProps } from "./SimpleTable"; import { SimpleTableProps } from "./SimpleTable";
import { useDefaultSettings } from "./plugins";
type Props<T extends object> = SimpleTableProps<T> & { type Props<T extends object> = SimpleTableProps<T> & {
autoScroll?: boolean; autoScroll?: boolean;

View File

@ -4,12 +4,12 @@ import {
CellProps, CellProps,
Column, Column,
ColumnInstance, ColumnInstance,
ensurePluginOrder,
HeaderProps, HeaderProps,
Hooks, Hooks,
MetaBase, MetaBase,
TableInstance, TableInstance,
TableToggleCommonProps, TableToggleCommonProps,
ensurePluginOrder,
} from "react-table"; } from "react-table";
const pluginName = "useCustomSelection"; const pluginName = "useCustomSelection";

View File

@ -1,5 +1,5 @@
import { createStyles, Group } from "@mantine/core"; import { createStyles, Group } from "@mantine/core";
import { FunctionComponent } from "react"; import { FunctionComponent, PropsWithChildren } from "react";
import ToolboxButton, { ToolboxMutateButton } from "./Button"; import ToolboxButton, { ToolboxMutateButton } from "./Button";
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
@ -11,7 +11,7 @@ const useStyles = createStyles((theme) => ({
}, },
})); }));
declare type ToolboxComp = FunctionComponent & { declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
Button: typeof ToolboxButton; Button: typeof ToolboxButton;
MutateButton: typeof ToolboxMutateButton; MutateButton: typeof ToolboxMutateButton;
}; };

View File

@ -1,20 +1,19 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import { useRoutes } from "react-router-dom"; import { Router } from "./Router";
import { AllProviders } from "./providers"; import { AllProviders } from "./providers";
import { useRouteItems } from "./Router";
const RouteApp = () => { const container = document.getElementById("root");
const items = useRouteItems();
return useRoutes(items); if (container === null) {
}; Error("Cannot initialize app, root not found");
} else {
ReactDOM.render( const root = createRoot(container);
<StrictMode> root.render(
<AllProviders> <StrictMode>
<RouteApp /> <AllProviders>
</AllProviders> <Router />
</StrictMode>, </AllProviders>
document.getElementById("root") </StrictMode>
); );
}

View File

@ -2,19 +2,19 @@ import {
ModalsProvider as MantineModalsProvider, ModalsProvider as MantineModalsProvider,
ModalsProviderProps as MantineModalsProviderProps, ModalsProviderProps as MantineModalsProviderProps,
} from "@mantine/modals"; } from "@mantine/modals";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, PropsWithChildren, useMemo } from "react";
import { ModalComponent, StaticModals } from "./WithModal"; import { ModalComponent, StaticModals } from "./WithModal";
const DefaultModalProps: MantineModalsProviderProps["modalProps"] = { const DefaultModalProps: MantineModalsProviderProps["modalProps"] = {
centered: true, centered: true,
styles: { styles: {
modal: { // modals: {
maxWidth: "100%", // maxWidth: "100%",
}, // },
}, },
}; };
const ModalsProvider: FunctionComponent = ({ children }) => { const ModalsProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
const modals = useMemo( const modals = useMemo(
() => () =>
StaticModals.reduce<Record<string, ModalComponent>>((prev, curr) => { StaticModals.reduce<Record<string, ModalComponent>>((prev, curr) => {

View File

@ -5,8 +5,11 @@ import { useCallback, useContext, useMemo } from "react";
import { ModalComponent, ModalIdContext } from "./WithModal"; import { ModalComponent, ModalIdContext } from "./WithModal";
export function useModals() { export function useModals() {
const { openContextModal: openMantineContextModal, ...rest } = const {
useMantineModals(); openContextModal: openMantineContextModal,
closeContextModal: closeContextModalRaw,
...rest
} = useMantineModals();
const openContextModal = useCallback( const openContextModal = useCallback(
<ARGS extends {}>( <ARGS extends {}>(

View File

@ -1,3 +1,3 @@
export * from "./hooks";
export { default as ModalsProvider } from "./ModalsProvider"; export { default as ModalsProvider } from "./ModalsProvider";
export { default as withModal } from "./WithModal"; export { default as withModal } from "./WithModal";
export * from "./hooks";

View File

@ -1,6 +1,6 @@
import { debounce, forIn, remove, uniq } from "lodash"; import { debounce, forIn, remove, uniq } from "lodash";
import { onlineManager } from "react-query"; import { onlineManager } from "react-query";
import { io, Socket } from "socket.io-client"; import { Socket, io } from "socket.io-client";
import { Environment, isDevEnv, isTestEnv } from "../../utilities"; import { Environment, isDevEnv, isTestEnv } from "../../utilities";
import { ENSURE, GROUP, LOG } from "../../utilities/console"; import { ENSURE, GROUP, LOG } from "../../utilities/console";
import { createDefaultReducer } from "./reducer"; import { createDefaultReducer } from "./reducer";

View File

@ -1,3 +1,4 @@
import { RouterNames } from "@/Router/RouterNames";
import { import {
useEpisodesBySeriesId, useEpisodesBySeriesId,
useIsAnyActionRunning, useIsAnyActionRunning,
@ -11,9 +12,8 @@ import { ItemEditModal } from "@/components/forms/ItemEditForm";
import { SeriesUploadModal } from "@/components/forms/SeriesUploadForm"; import { SeriesUploadModal } from "@/components/forms/SeriesUploadForm";
import { SubtitleToolsModal } from "@/components/modals"; import { SubtitleToolsModal } from "@/components/modals";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { notification, task, TaskGroup } from "@/modules/task"; import { TaskGroup, notification, task } from "@/modules/task";
import ItemOverview from "@/pages/views/ItemOverview"; import ItemOverview from "@/pages/views/ItemOverview";
import { RouterNames } from "@/Router/RouterNames";
import { useLanguageProfileBy } from "@/utilities/languages"; import { useLanguageProfileBy } from "@/utilities/languages";
import { import {
faAdjust, faAdjust,

View File

@ -1,10 +1,10 @@
import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks"; import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks";
import { useShowOnlyDesired } from "@/apis/hooks/site"; import { useShowOnlyDesired } from "@/apis/hooks/site";
import { Action, GroupTable } from "@/components"; import { Action, GroupTable } from "@/components";
import TextPopover from "@/components/TextPopover";
import { AudioList } from "@/components/bazarr"; import { AudioList } from "@/components/bazarr";
import { EpisodeHistoryModal } from "@/components/modals"; import { EpisodeHistoryModal } from "@/components/modals";
import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal"; import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal";
import TextPopover from "@/components/TextPopover";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { useTableStyles } from "@/styles"; import { useTableStyles } from "@/styles";
import { BuildKey, filterSubtitleBy } from "@/utilities"; import { BuildKey, filterSubtitleBy } from "@/utilities";
@ -84,7 +84,7 @@ const Table: FunctionComponent<Props> = ({ episodes, profile, disabled }) => {
{ {
accessor: "season", accessor: "season",
Cell: (row) => { Cell: (row) => {
return `Season ${row.value}`; return <Text>Season {row.value}</Text>;
}, },
}, },
{ {

View File

@ -11,8 +11,8 @@ import { useSelectorOptions } from "@/utilities";
import { import {
Box, Box,
Container, Container,
createStyles,
SimpleGrid, SimpleGrid,
createStyles,
useMantineTheme, useMantineTheme,
} from "@mantine/core"; } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";

View File

@ -1,3 +1,4 @@
import { RouterNames } from "@/Router/RouterNames";
import { import {
useDownloadMovieSubtitles, useDownloadMovieSubtitles,
useIsMovieActionRunning, useIsMovieActionRunning,
@ -15,9 +16,8 @@ import { MovieUploadModal } from "@/components/forms/MovieUploadForm";
import { MovieHistoryModal, SubtitleToolsModal } from "@/components/modals"; import { MovieHistoryModal, SubtitleToolsModal } from "@/components/modals";
import { MovieSearchModal } from "@/components/modals/ManualSearchModal"; import { MovieSearchModal } from "@/components/modals/ManualSearchModal";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { notification, task, TaskGroup } from "@/modules/task"; import { TaskGroup, notification, task } from "@/modules/task";
import ItemOverview from "@/pages/views/ItemOverview"; import ItemOverview from "@/pages/views/ItemOverview";
import { RouterNames } from "@/Router/RouterNames";
import { useLanguageProfileBy } from "@/utilities/languages"; import { useLanguageProfileBy } from "@/utilities/languages";
import { import {
faCloudUploadAlt, faCloudUploadAlt,

View File

@ -65,15 +65,19 @@ const MovieView: FunctionComponent = () => {
accessor: "missing_subtitles", accessor: "missing_subtitles",
Cell: (row) => { Cell: (row) => {
const missing = row.value; const missing = row.value;
return missing.map((v) => ( return (
<Badge <>
mr="xs" {missing.map((v) => (
color="yellow" <Badge
key={BuildKey(v.code2, v.hi, v.forced)} mr="xs"
> color="yellow"
<Language.Text value={v}></Language.Text> key={BuildKey(v.code2, v.hi, v.forced)}
</Badge> >
)); <Language.Text value={v}></Language.Text>
</Badge>
))}
</>
);
}, },
}, },
{ {

View File

@ -0,0 +1,196 @@
import {
decodeEqualData,
encodeEqualData,
LanguageEqualData,
LanguageEqualImmediateData,
} from "@/pages/Settings/Languages/equals";
import { describe, expect, it } from "vitest";
describe("Equals Parser", () => {
it("should parse from string correctly", () => {
interface TestData {
text: string;
expected: LanguageEqualImmediateData;
}
function testParsedResult(
text: string,
expected: LanguageEqualImmediateData
) {
const result = decodeEqualData(text);
if (result === undefined) {
expect(false, `Cannot parse '${text}' as language equal data`);
return;
}
expect(
result,
`${text} does not match with the expected equal data`
).toStrictEqual(expected);
}
const testValues: TestData[] = [
{
text: "spa-MX:spa",
expected: {
source: {
content: "spa-MX",
hi: false,
forced: false,
},
target: {
content: "spa",
hi: false,
forced: false,
},
},
},
{
text: "zho@hi:zht",
expected: {
source: {
content: "zho",
hi: true,
forced: false,
},
target: {
content: "zht",
hi: false,
forced: false,
},
},
},
{
text: "es-MX@forced:es-MX",
expected: {
source: {
content: "es-MX",
hi: false,
forced: true,
},
target: {
content: "es-MX",
hi: false,
forced: false,
},
},
},
{
text: "en:en@hi",
expected: {
source: {
content: "en",
hi: false,
forced: false,
},
target: {
content: "en",
hi: true,
forced: false,
},
},
},
];
testValues.forEach((data) => {
testParsedResult(data.text, data.expected);
});
});
it("should encode to string correctly", () => {
interface TestData {
source: LanguageEqualData;
expected: string;
}
const testValues: TestData[] = [
{
source: {
source: {
content: {
name: "Abkhazian",
code2: "ab",
code3: "abk",
enabled: false,
},
hi: false,
forced: false,
},
target: {
content: {
name: "Aragonese",
code2: "an",
code3: "arg",
enabled: false,
},
hi: false,
forced: false,
},
},
expected: "abk:arg",
},
{
source: {
source: {
content: {
name: "Abkhazian",
code2: "ab",
code3: "abk",
enabled: false,
},
hi: true,
forced: false,
},
target: {
content: {
name: "Aragonese",
code2: "an",
code3: "arg",
enabled: false,
},
hi: false,
forced: false,
},
},
expected: "abk@hi:arg",
},
{
source: {
source: {
content: {
name: "Abkhazian",
code2: "ab",
code3: "abk",
enabled: false,
},
hi: false,
forced: true,
},
target: {
content: {
name: "Aragonese",
code2: "an",
code3: "arg",
enabled: false,
},
hi: false,
forced: false,
},
},
expected: "abk@forced:arg",
},
];
function testEncodeResult({ source, expected }: TestData) {
const encoded = encodeEqualData(source);
expect(
encoded,
`Encoded result '${encoded}' is not matched to '${expected}'`
).toEqual(expected);
}
testValues.forEach(testEncodeResult);
});
});

View File

@ -0,0 +1,365 @@
import { useLanguages } from "@/apis/hooks";
import { Action, SimpleTable } from "@/components";
import LanguageSelector from "@/components/bazarr/LanguageSelector";
import { languageEqualsKey } from "@/pages/Settings/keys";
import { useFormActions } from "@/pages/Settings/utilities/FormValues";
import { useSettingValue } from "@/pages/Settings/utilities/hooks";
import { LOG } from "@/utilities/console";
import { faEquals, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Checkbox } from "@mantine/core";
import { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table";
interface GenericEqualTarget<T> {
content: T;
hi: boolean;
forced: boolean;
}
interface LanguageEqualGenericData<T> {
source: GenericEqualTarget<T>;
target: GenericEqualTarget<T>;
}
export type LanguageEqualImmediateData =
LanguageEqualGenericData<Language.CodeType>;
export type LanguageEqualData = LanguageEqualGenericData<Language.Server>;
function decodeEqualTarget(
text: string
): GenericEqualTarget<Language.CodeType> | undefined {
const [code, decoration] = text.split("@");
if (code.length === 0) {
return undefined;
}
const forced = decoration === "forced";
const hi = decoration === "hi";
return {
content: code,
forced,
hi,
};
}
export function decodeEqualData(
text: string
): LanguageEqualImmediateData | undefined {
const [first, second] = text.split(":");
const source = decodeEqualTarget(first);
const target = decodeEqualTarget(second);
if (source === undefined || target === undefined) {
return undefined;
}
return {
source,
target,
};
}
function encodeEqualTarget(data: GenericEqualTarget<Language.Server>): string {
let text = data.content.code3;
if (data.hi) {
text += "@hi";
} else if (data.forced) {
text += "@forced";
}
return text;
}
export function encodeEqualData(data: LanguageEqualData): string {
const source = encodeEqualTarget(data.source);
const target = encodeEqualTarget(data.target);
return `${source}:${target}`;
}
export function useLatestLanguageEquals(): LanguageEqualData[] {
const { data } = useLanguages();
const latest = useSettingValue<string[]>(languageEqualsKey);
return useMemo(
() =>
latest
?.map(decodeEqualData)
.map((parsed) => {
if (parsed === undefined) {
return undefined;
}
const source = data?.find(
(value) => value.code3 === parsed.source.content
);
const target = data?.find(
(value) => value.code3 === parsed.target.content
);
if (source === undefined || target === undefined) {
return undefined;
}
return {
source: { ...parsed.source, content: source },
target: { ...parsed.target, content: target },
};
})
.filter((v): v is LanguageEqualData => v !== undefined) ?? [],
[data, latest]
);
}
interface EqualsTableProps {}
const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
const { data: languages } = useLanguages();
const canAdd = languages !== undefined;
const equals = useLatestLanguageEquals();
const { setValue } = useFormActions();
const setEquals = useCallback(
(values: LanguageEqualData[]) => {
const encodedValues = values.map(encodeEqualData);
LOG("info", "updating language equals data", values);
setValue(encodedValues, languageEqualsKey);
},
[setValue]
);
const add = useCallback(() => {
if (languages === undefined) {
return;
}
const enabled = languages.find((value) => value.enabled);
if (enabled === undefined) {
return;
}
const newValue: LanguageEqualData[] = [
...equals,
{
source: {
content: enabled,
hi: false,
forced: false,
},
target: {
content: enabled,
hi: false,
forced: false,
},
},
];
setEquals(newValue);
}, [equals, languages, setEquals]);
const update = useCallback(
(index: number, value: LanguageEqualData) => {
if (index < 0 || index >= equals.length) {
return;
}
const newValue: LanguageEqualData[] = [...equals];
newValue[index] = { ...value };
setEquals(newValue);
},
[equals, setEquals]
);
const remove = useCallback(
(index: number) => {
if (index < 0 || index >= equals.length) {
return;
}
const newValue: LanguageEqualData[] = [...equals];
newValue.splice(index, 1);
setEquals(newValue);
},
[equals, setEquals]
);
const columns = useMemo<Column<LanguageEqualData>[]>(
() => [
{
Header: "Source",
id: "source-lang",
accessor: "source",
Cell: ({ value: { content }, row }) => {
return (
<LanguageSelector
enabled
value={content}
onChange={(result) => {
if (result !== null) {
update(row.index, {
...row.original,
source: { ...row.original.source, content: result },
});
}
}}
></LanguageSelector>
);
},
},
{
id: "source-hi",
accessor: "source",
Cell: ({ value: { hi }, row }) => {
return (
<Checkbox
label="HI"
checked={hi}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
source: {
...row.original.source,
hi: checked,
forced: checked ? false : row.original.source.forced,
},
});
}}
></Checkbox>
);
},
},
{
id: "source-forced",
accessor: "source",
Cell: ({ value: { forced }, row }) => {
return (
<Checkbox
label="Forced"
checked={forced}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
source: {
...row.original.source,
forced: checked,
hi: checked ? false : row.original.source.hi,
},
});
}}
></Checkbox>
);
},
},
{
id: "equal-icon",
Cell: () => {
return <FontAwesomeIcon icon={faEquals} />;
},
},
{
Header: "Target",
id: "target-lang",
accessor: "target",
Cell: ({ value: { content }, row }) => {
return (
<LanguageSelector
enabled
value={content}
onChange={(result) => {
if (result !== null) {
update(row.index, {
...row.original,
target: { ...row.original.target, content: result },
});
}
}}
></LanguageSelector>
);
},
},
{
id: "target-hi",
accessor: "target",
Cell: ({ value: { hi }, row }) => {
return (
<Checkbox
label="HI"
checked={hi}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
target: {
...row.original.target,
hi: checked,
forced: checked ? false : row.original.target.forced,
},
});
}}
></Checkbox>
);
},
},
{
id: "target-forced",
accessor: "target",
Cell: ({ value: { forced }, row }) => {
return (
<Checkbox
label="Forced"
checked={forced}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
target: {
...row.original.target,
forced: checked,
hi: checked ? false : row.original.target.hi,
},
});
}}
></Checkbox>
);
},
},
{
id: "action",
accessor: "target",
Cell: ({ row }) => {
return (
<Action
label="Remove"
icon={faTrash}
color="red"
onClick={() => remove(row.index)}
></Action>
);
},
},
],
[remove, update]
);
return (
<>
<SimpleTable data={equals} columns={columns}></SimpleTable>
<Button fullWidth disabled={!canAdd} color="light" onClick={add}>
{canAdd ? "Add Equal" : "No Enabled Languages"}
</Button>
</>
);
};
export default EqualsTable;

View File

@ -17,6 +17,7 @@ import {
} from "../keys"; } from "../keys";
import { useSettingValue } from "../utilities/hooks"; import { useSettingValue } from "../utilities/hooks";
import { LanguageSelector, ProfileSelector } from "./components"; import { LanguageSelector, ProfileSelector } from "./components";
import EqualsTable from "./equals";
import Table from "./table"; import Table from "./table";
export function useLatestEnabledLanguages() { export function useLatestEnabledLanguages() {
@ -69,6 +70,13 @@ const SettingsLanguagesView: FunctionComponent = () => {
></LanguageSelector> ></LanguageSelector>
</Section> </Section>
<Section header="Language Equals">
<Message>
Treat the following languages as equal across all providers.
</Message>
<EqualsTable></EqualsTable>
</Section>
<Section header="Embedded Tracks Language"> <Section header="Embedded Tracks Language">
<Check <Check
label="Deep analyze media file to get audio tracks language." label="Deep analyze media file to get audio tracks language."
@ -91,7 +99,6 @@ const SettingsLanguagesView: FunctionComponent = () => {
}} }}
></Selector> ></Selector>
</CollapseBox> </CollapseBox>
<Selector <Selector
clearable clearable
settingKey={defaultUndEmbeddedSubtitlesLang} settingKey={defaultUndEmbeddedSubtitlesLang}

View File

@ -1,7 +1,7 @@
import { Action, SimpleTable } from "@/components"; import { Action, SimpleTable } from "@/components";
import { import {
anyCutoff,
ProfileEditModal, ProfileEditModal,
anyCutoff,
} from "@/components/forms/ProfileEditForm"; } from "@/components/forms/ProfileEditForm";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { BuildKey, useArrayAction } from "@/utilities"; import { BuildKey, useArrayAction } from "@/utilities";
@ -87,15 +87,19 @@ const Table: FunctionComponent = () => {
Cell: (row) => { Cell: (row) => {
const items = row.value; const items = row.value;
if (!items) { if (!items) {
return false; return null;
} }
return items.map((v, idx) => { return (
return ( <>
<Badge key={BuildKey(idx, v)} color="gray"> {items.map((v, idx) => {
{v} return (
</Badge> <Badge key={BuildKey(idx, v)} color="gray">
); {v}
}); </Badge>
);
})}
</>
);
}, },
}, },
{ {
@ -104,15 +108,19 @@ const Table: FunctionComponent = () => {
Cell: (row) => { Cell: (row) => {
const items = row.value; const items = row.value;
if (!items) { if (!items) {
return false; return null;
} }
return items.map((v, idx) => { return (
return ( <>
<Badge key={BuildKey(idx, v)} color="gray"> {items.map((v, idx) => {
{v} return (
</Badge> <Badge key={BuildKey(idx, v)} color="gray">
); {v}
}); </Badge>
);
})}
</>
);
}, },
}, },
{ {

View File

@ -123,6 +123,7 @@ export const NotificationView: FunctionComponent = () => {
const update = useUpdateArray<Settings.NotificationInfo>( const update = useUpdateArray<Settings.NotificationInfo>(
notificationsKey, notificationsKey,
notifications ?? [],
"name" "name"
); );

View File

@ -6,15 +6,15 @@ import {
Button, Button,
Divider, Divider,
Group, Group,
Text as MantineText,
SimpleGrid, SimpleGrid,
Stack, Stack,
Text as MantineText,
} from "@mantine/core"; } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { capitalize } from "lodash"; import { capitalize } from "lodash";
import { import {
forwardRef,
FunctionComponent, FunctionComponent,
forwardRef,
useCallback, useCallback,
useMemo, useMemo,
useRef, useRef,
@ -28,8 +28,8 @@ import {
useFormActions, useFormActions,
useStagedValues, useStagedValues,
} from "../utilities/FormValues"; } from "../utilities/FormValues";
import { useSettingValue } from "../utilities/hooks";
import { SettingsProvider, useSettings } from "../utilities/SettingsProvider"; import { SettingsProvider, useSettings } from "../utilities/SettingsProvider";
import { useSettingValue } from "../utilities/hooks";
import { ProviderInfo, ProviderList } from "./list"; import { ProviderInfo, ProviderList } from "./list";
const ProviderKey = "settings-general-enabled_providers"; const ProviderKey = "settings-general-enabled_providers";

View File

@ -185,26 +185,6 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
}, },
], ],
}, },
{
key: "legendastv",
name: "LegendasTV",
description: "Brazilian / Portuguese Subtitles Provider",
inputs: [
{
type: "text",
key: "username",
},
{
type: "password",
key: "password",
},
{
type: "switch",
key: "featured_only",
name: "Only Download Featured",
},
],
},
{ key: "napiprojekt", description: "Polish Subtitles Provider" }, { key: "napiprojekt", description: "Polish Subtitles Provider" },
{ {
key: "whisperai", key: "whisperai",
@ -329,7 +309,13 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
name: "Verify SSL", name: "Verify SSL",
defaultValue: true, defaultValue: true,
}, },
{
type: "text",
key: "user_agent",
name: "User-agent header",
},
], ],
message: "Make sure to use a unique and credible user agent.",
}, },
{ {
key: "subs4free", key: "subs4free",

View File

@ -1,12 +1,12 @@
import { uiPageSizeKey } from "@/utilities/storage"; import { uiPageSizeKey } from "@/utilities/storage";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { Layout, Section, Selector } from "../components"; import { Layout, Section, Selector } from "../components";
import { pageSizeOptions } from "./options"; import { colorSchemeOptions, pageSizeOptions } from "./options";
const SettingsUIView: FunctionComponent = () => { const SettingsUIView: FunctionComponent = () => {
return ( return (
<Layout name="Interface"> <Layout name="Interface">
<Section header="UI"> <Section header="List View">
<Selector <Selector
label="Page Size" label="Page Size"
options={pageSizeOptions} options={pageSizeOptions}
@ -14,6 +14,14 @@ const SettingsUIView: FunctionComponent = () => {
defaultValue={50} defaultValue={50}
></Selector> ></Selector>
</Section> </Section>
<Section header="Style">
<Selector
label="Theme"
options={colorSchemeOptions}
settingKey="settings-general-theme"
defaultValue={"auto"}
></Selector>
</Section>
</Layout> </Layout>
); );
}; };

View File

@ -26,3 +26,18 @@ export const pageSizeOptions: SelectorOption<number>[] = [
value: 1000, value: 1000,
}, },
]; ];
export const colorSchemeOptions: SelectorOption<string>[] = [
{
label: "Auto",
value: "auto",
},
{
label: "Light",
value: "light",
},
{
label: "Dark",
value: "dark",
},
];

View File

@ -1,9 +1,16 @@
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { FunctionComponent } from "react"; import { FunctionComponent, PropsWithChildren } from "react";
export const Message: FunctionComponent<{ interface MessageProps {
type?: "warning" | "info"; type?: "warning" | "info";
}> = ({ type = "info", children }) => { }
type Props = PropsWithChildren<MessageProps>;
export const Message: FunctionComponent<Props> = ({
type = "info",
children,
}) => {
return ( return (
<Text size="sm" color={type === "info" ? "dimmed" : "yellow"} my={0}> <Text size="sm" color={type === "info" ? "dimmed" : "yellow"} my={0}>
{children} {children}

View File

@ -1,12 +1,14 @@
import { Divider, Stack, Title } from "@mantine/core"; import { Divider, Stack, Title } from "@mantine/core";
import { FunctionComponent } from "react"; import { FunctionComponent, PropsWithChildren } from "react";
interface SectionProps { interface SectionProps {
header: string; header: string;
hidden?: boolean; hidden?: boolean;
} }
export const Section: FunctionComponent<SectionProps> = ({ type Props = PropsWithChildren<SectionProps>;
export const Section: FunctionComponent<Props> = ({
header, header,
hidden, hidden,
children, children,

View File

@ -1,5 +1,5 @@
import { Collapse, Stack } from "@mantine/core"; import { Collapse, Stack } from "@mantine/core";
import { FunctionComponent, useMemo, useRef } from "react"; import { FunctionComponent, PropsWithChildren, useMemo, useRef } from "react";
import { useSettingValue } from "../utilities/hooks"; import { useSettingValue } from "../utilities/hooks";
interface ContentProps { interface ContentProps {
@ -8,7 +8,9 @@ interface ContentProps {
indent?: boolean; indent?: boolean;
} }
const CollapseBox: FunctionComponent<ContentProps> = ({ type Props = PropsWithChildren<ContentProps>;
const CollapseBox: FunctionComponent<Props> = ({
on, on,
indent, indent,
children, children,

View File

@ -1,11 +1,11 @@
import { rawRender, RenderOptions, screen } from "@/tests"; import { rawRender, RenderOptions, screen } from "@/tests";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { FunctionComponent, ReactElement } from "react"; import { FunctionComponent, PropsWithChildren, ReactElement } from "react";
import { describe, it } from "vitest"; import { describe, it } from "vitest";
import { FormContext, FormValues } from "../utilities/FormValues"; import { FormContext, FormValues } from "../utilities/FormValues";
import { Number, Text } from "./forms"; import { Number, Text } from "./forms";
const FormSupport: FunctionComponent = ({ children }) => { const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
const form = useForm<FormValues>({ const form = useForm<FormValues>({
initialValues: { initialValues: {
settings: {}, settings: {},

View File

@ -1,7 +1,7 @@
import { import {
Action as GlobalAction,
FileBrowser, FileBrowser,
FileBrowserProps, FileBrowserProps,
Action as GlobalAction,
MultiSelector as GlobalMultiSelector, MultiSelector as GlobalMultiSelector,
MultiSelectorProps as GlobalMultiSelectorProps, MultiSelectorProps as GlobalMultiSelectorProps,
Selector as GlobalSelector, Selector as GlobalSelector,
@ -12,17 +12,17 @@ import ChipInput, { ChipInputProps } from "@/components/inputs/ChipInput";
import { useSliderMarks } from "@/utilities"; import { useSliderMarks } from "@/utilities";
import { import {
Input, Input,
Slider as MantineSlider,
SliderProps as MantineSliderProps,
NumberInput, NumberInput,
NumberInputProps, NumberInputProps,
PasswordInput, PasswordInput,
PasswordInputProps, PasswordInputProps,
Slider as MantineSlider,
SliderProps as MantineSliderProps,
Switch, Switch,
TextInput, TextInput,
TextInputProps, TextInputProps,
} from "@mantine/core"; } from "@mantine/core";
import { FunctionComponent, ReactText } from "react"; import { FunctionComponent, ReactNode, ReactText } from "react";
import { BaseInput, useBaseInput } from "../utilities/hooks"; import { BaseInput, useBaseInput } from "../utilities/hooks";
export type NumberProps = BaseInput<number> & NumberInputProps; export type NumberProps = BaseInput<number> & NumberInputProps;
@ -34,7 +34,10 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
<NumberInput <NumberInput
{...rest} {...rest}
value={value ?? 0} value={value ?? 0}
onChange={(val = 0) => { onChange={(val) => {
if (val === "") {
val = 0;
}
update(val); update(val);
}} }}
></NumberInput> ></NumberInput>
@ -126,7 +129,9 @@ export function MultiSelector<T extends string | number>(
} }
type SliderProps = BaseInput<number> & type SliderProps = BaseInput<number> &
Omit<MantineSliderProps, "onChange" | "onChangeEnd" | "marks">; Omit<MantineSliderProps, "onChange" | "onChangeEnd" | "marks" | "label"> & {
label?: ReactNode;
};
export const Slider: FunctionComponent<SliderProps> = (props) => { export const Slider: FunctionComponent<SliderProps> = (props) => {
const { value, update, rest } = useBaseInput(props); const { value, update, rest } = useBaseInput(props);

View File

@ -63,11 +63,11 @@ export const URLTestButton: FunctionComponent<{
}; };
export * from "./Card"; export * from "./Card";
export * from "./collapse";
export { default as CollapseBox } from "./collapse";
export * from "./forms";
export * from "./Layout"; export * from "./Layout";
export { default as Layout } from "./Layout"; export { default as Layout } from "./Layout";
export * from "./Message"; export * from "./Message";
export * from "./pathMapper";
export * from "./Section"; export * from "./Section";
export * from "./collapse";
export { default as CollapseBox } from "./collapse";
export * from "./forms";
export * from "./pathMapper";

View File

@ -5,6 +5,8 @@ export const defaultUndEmbeddedSubtitlesLang =
export const languageProfileKey = "languages-profiles"; export const languageProfileKey = "languages-profiles";
export const notificationsKey = "notifications-providers"; export const notificationsKey = "notifications-providers";
export const languageEqualsKey = "settings-general-language_equals";
export const pathMappingsKey = "settings-general-path_mappings"; export const pathMappingsKey = "settings-general-path_mappings";
export const pathMappingsMovieKey = "settings-general-path_mappings_movie"; export const pathMappingsMovieKey = "settings-general-path_mappings_movie";

View File

@ -1,4 +1,9 @@
import { createContext, FunctionComponent, useContext } from "react"; import {
createContext,
FunctionComponent,
PropsWithChildren,
useContext,
} from "react";
const SettingsContext = createContext<Settings | null>(null); const SettingsContext = createContext<Settings | null>(null);
@ -12,7 +17,9 @@ type SettingsProviderProps = {
value: Settings | null; value: Settings | null;
}; };
export const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({ type Props = PropsWithChildren<SettingsProviderProps>;
export const SettingsProvider: FunctionComponent<Props> = ({
value, value,
children, children,
}) => { }) => {

View File

@ -82,7 +82,11 @@ export function useSettingValue<T>(
} }
} }
export function useUpdateArray<T>(key: string, compare: keyof T) { export function useUpdateArray<T>(
key: string,
current: Readonly<T[]>,
compare: keyof T
) {
const { setValue } = useFormActions(); const { setValue } = useFormActions();
const stagedValue = useStagedValues(); const stagedValue = useStagedValues();
@ -93,9 +97,9 @@ export function useUpdateArray<T>(key: string, compare: keyof T) {
if (key in stagedValue) { if (key in stagedValue) {
return stagedValue[key]; return stagedValue[key];
} else { } else {
return []; return current;
} }
}, [key, stagedValue]); }, [key, stagedValue, current]);
return useCallback( return useCallback(
(v: T, hook?: HookType) => { (v: T, hook?: HookType) => {

View File

@ -52,9 +52,7 @@ const ReleaseCard: FunctionComponent<ReleaseInfo> = ({
<Badge color={prerelease ? "yellow" : "green"}> <Badge color={prerelease ? "yellow" : "green"}>
{prerelease ? "Development" : "Master"} {prerelease ? "Development" : "Master"}
</Badge> </Badge>
<Badge hidden={!current} color="indigo"> {current && <Badge color="indigo">Installed</Badge>}
Installed
</Badge>
</Group> </Group>
<Divider my="sm"></Divider> <Divider my="sm"></Divider>
<Text>From newest to oldest:</Text> <Text>From newest to oldest:</Text>

View File

@ -13,7 +13,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Anchor, Container, Divider, Grid, Stack, Text } from "@mantine/core"; import { Anchor, Container, Divider, Grid, Stack, Text } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import moment from "moment"; import moment from "moment";
import { FunctionComponent, ReactNode, useCallback, useState } from "react"; import {
FunctionComponent,
PropsWithChildren,
ReactNode,
useCallback,
useState,
} from "react";
import Table from "./table"; import Table from "./table";
interface InfoProps { interface InfoProps {
@ -53,10 +59,13 @@ function Label(props: IconProps): JSX.Element {
); );
} }
const InfoContainer: FunctionComponent<{ title: string }> = ({ interface InfoContainerProps {
title, title: string;
children, }
}) => {
const InfoContainer: FunctionComponent<
PropsWithChildren<InfoContainerProps>
> = ({ title, children }) => {
return ( return (
<Stack> <Stack>
<h4>{title}</h4> <h4>{title}</h4>

View File

@ -4,7 +4,7 @@ import {
useMovieWantedPagination, useMovieWantedPagination,
} from "@/apis/hooks"; } from "@/apis/hooks";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import { task, TaskGroup } from "@/modules/task"; import { TaskGroup, task } from "@/modules/task";
import WantedView from "@/pages/views/WantedView"; import WantedView from "@/pages/views/WantedView";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faSearch } from "@fortawesome/free-solid-svg-icons";

View File

@ -4,7 +4,7 @@ import {
useSeriesAction, useSeriesAction,
} from "@/apis/hooks"; } from "@/apis/hooks";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import { task, TaskGroup } from "@/modules/task"; import { TaskGroup, task } from "@/modules/task";
import WantedView from "@/pages/views/WantedView"; import WantedView from "@/pages/views/WantedView";
import { useTableStyles } from "@/styles"; import { useTableStyles } from "@/styles";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";

View File

@ -5,17 +5,17 @@ import {
useProfileItemsToLanguages, useProfileItemsToLanguages,
} from "@/utilities/languages"; } from "@/utilities/languages";
import { import {
faBookmark as farBookmark,
faFolder, faFolder,
faBookmark as farBookmark,
} from "@fortawesome/free-regular-svg-icons"; } from "@fortawesome/free-regular-svg-icons";
import { import {
IconDefinition,
faBookmark, faBookmark,
faClone, faClone,
faLanguage, faLanguage,
faMusic, faMusic,
faStream, faStream,
faTags, faTags,
IconDefinition,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@ -23,7 +23,6 @@ import {
Badge, Badge,
BadgeProps, BadgeProps,
Box, Box,
createStyles,
Grid, Grid,
Group, Group,
HoverCard, HoverCard,
@ -33,6 +32,7 @@ import {
Stack, Stack,
Text, Text,
Title, Title,
createStyles,
} from "@mantine/core"; } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";

View File

@ -1,28 +1,24 @@
import queryClient from "@/apis/queries";
import ThemeProvider from "@/App/theme"; import ThemeProvider from "@/App/theme";
import queryClient from "@/apis/queries";
import { ModalsProvider } from "@/modules/modals"; import { ModalsProvider } from "@/modules/modals";
import "@fontsource/roboto/300.css"; import "@fontsource/roboto/300.css";
import { NotificationsProvider } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import { FunctionComponent } from "react"; import { FunctionComponent, PropsWithChildren } from "react";
import { QueryClientProvider } from "react-query"; import { QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools"; import { ReactQueryDevtools } from "react-query/devtools";
import { Router } from "./Router";
import { Environment } from "./utilities"; import { Environment } from "./utilities";
export const AllProviders: FunctionComponent = ({ children }) => { export const AllProviders: FunctionComponent<PropsWithChildren> = ({
children,
}) => {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider> <ThemeProvider>
<ModalsProvider> <ModalsProvider>
<NotificationsProvider limit={5}> <Notifications limit={5} />
<Router> {/* c8 ignore next 3 */}
{/* c8 ignore next 3 */} {Environment.queryDev && <ReactQueryDevtools initialIsOpen={false} />}
{Environment.queryDev && ( {children}
<ReactQueryDevtools initialIsOpen={false} />
)}
{children}
</Router>
</NotificationsProvider>
</ModalsProvider> </ModalsProvider>
</ThemeProvider> </ThemeProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@ -1,11 +1,33 @@
import { AllProviders } from "@/providers"; import { AllProviders } from "@/providers";
import { render, RenderOptions } from "@testing-library/react"; import { render, RenderOptions } from "@testing-library/react";
import { FunctionComponent, ReactElement, StrictMode } from "react"; import {
FunctionComponent,
PropsWithChildren,
ReactElement,
StrictMode,
} from "react";
import {
createBrowserRouter,
RouteObject,
RouterProvider,
} from "react-router-dom";
const AllProvidersWithStrictMode: FunctionComponent<PropsWithChildren> = ({
children,
}) => {
const route: RouteObject = {
path: "/",
element: children,
};
// TODO: Update router system
const router = createBrowserRouter([route]);
const AllProvidersWithStrictMode: FunctionComponent = ({ children }) => {
return ( return (
<StrictMode> <StrictMode>
<AllProviders>{children}</AllProviders> <AllProviders>
<RouterProvider router={router} />
</AllProviders>
</StrictMode> </StrictMode>
); );
}; };

View File

@ -12,6 +12,7 @@ declare namespace Language {
type CodeType = string; type CodeType = string;
interface Server { interface Server {
code2: CodeType; code2: CodeType;
code3: CodeType;
name: string; name: string;
enabled: boolean; enabled: boolean;
} }

View File

@ -16,7 +16,6 @@ interface Settings {
opensubtitlescom: Settings.OpenSubtitlesCom; opensubtitlescom: Settings.OpenSubtitlesCom;
addic7ed: Settings.Addic7ed; addic7ed: Settings.Addic7ed;
legendasdivx: Settings.Legandasdivx; legendasdivx: Settings.Legandasdivx;
legendastv: Settings.Legendastv;
xsubs: Settings.XSubs; xsubs: Settings.XSubs;
assrt: Settings.Assrt; assrt: Settings.Assrt;
napisy24: Settings.Napisy24; napisy24: Settings.Napisy24;
@ -25,6 +24,7 @@ interface Settings {
titlovi: Settings.Titlovi; titlovi: Settings.Titlovi;
ktuvit: Settings.Ktuvit; ktuvit: Settings.Ktuvit;
notifications: Settings.Notifications; notifications: Settings.Notifications;
language_equals: string[];
} }
declare namespace Settings { declare namespace Settings {
@ -56,6 +56,7 @@ declare namespace Settings {
path_mappings: [string, string][]; path_mappings: [string, string][];
path_mappings_movie: [string, string][]; path_mappings_movie: [string, string][];
page_size: number; page_size: number;
theme: string;
port: number; port: number;
upgrade_subs: boolean; upgrade_subs: boolean;
postprocessing_cmd?: string; postprocessing_cmd?: string;
@ -196,10 +197,6 @@ declare namespace Settings {
skip_wrong_fps: boolean; skip_wrong_fps: boolean;
} }
interface Legendastv extends BaseProvider {
featured_only: boolean;
}
interface XSubs extends BaseProvider {} interface XSubs extends BaseProvider {}
interface Napisy24 extends BaseProvider {} interface Napisy24 extends BaseProvider {}

View File

@ -42,3 +42,12 @@ export function useProfileItemsToLanguages(profile?: Language.Profile) {
[data, profile?.items] [data, profile?.items]
); );
} }
export function useLanguageFromCode3(code3: string) {
const { data } = useLanguages();
return useMemo(
() => data?.find((value) => value.code3 === code3),
[data, code3]
);
}

View File

@ -1,41 +1,9 @@
// A workaround of built-in hooks in React-Router v6 // A workaround of built-in hooks in React-Router v6
// https://gist.github.com/rmorse/426ffcc579922a82749934826fa9f743 // https://gist.github.com/rmorse/426ffcc579922a82749934826fa9f743
import type { Blocker, History, Transition } from "history"; import { unstable_usePrompt as useUnstablePrompt } from "react-router-dom";
import { useContext, useEffect } from "react";
// eslint-disable-next-line camelcase
import { UNSAFE_NavigationContext } from "react-router-dom";
export function useBlocker(blocker: Blocker, when = true) {
const navigator = useContext(UNSAFE_NavigationContext).navigator as History;
useEffect(() => {
if (!when) return;
const unblock = navigator.block((tx: Transition) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
// TODO: Replace with Mantine's confirmation modal // TODO: Replace with Mantine's confirmation modal
export function usePrompt(when: boolean, message: string) { export function usePrompt(when: boolean, message: string) {
useBlocker((tx) => { useUnstablePrompt({ when, message });
if (window.confirm(message)) {
tx.retry();
}
}, when);
} }

View File

@ -1,28 +1,37 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import asyncio
import concurrent.futures as cf
import os import os
from itertools import chain from itertools import chain
from . import common from . import common
@ -43,11 +52,6 @@ from .plugins.NotifyBase import NotifyBase
from . import plugins from . import plugins
from . import __version__ from . import __version__
# Python v3+ support code made importable, so it can remain backwards
# compatible with Python v2
# TODO: Review after dropping support for Python 2.
from . import py3compat
class Apprise: class Apprise:
""" """
@ -369,91 +373,83 @@ class Apprise:
such as turning a \n into an actual new line, etc. such as turning a \n into an actual new line, etc.
""" """
return py3compat.asyncio.tosync( try:
self.async_notify( # Process arguments and build synchronous and asynchronous calls
# (this step can throw internal errors).
sequential_calls, parallel_calls = self._create_notify_calls(
body, title, body, title,
notify_type=notify_type, body_format=body_format, notify_type=notify_type, body_format=body_format,
tag=tag, match_always=match_always, attach=attach, tag=tag, match_always=match_always, attach=attach,
interpret_escapes=interpret_escapes, interpret_escapes=interpret_escapes
), )
debug=self.debug
)
def async_notify(self, *args, **kwargs): except TypeError:
# No notifications sent, and there was an internal error.
return False
if not sequential_calls and not parallel_calls:
# Nothing to send
return None
sequential_result = Apprise._notify_sequential(*sequential_calls)
parallel_result = Apprise._notify_parallel_threadpool(*parallel_calls)
return sequential_result and parallel_result
async def async_notify(self, *args, **kwargs):
""" """
Send a notification to all the plugins previously loaded, for Send a notification to all the plugins previously loaded, for
asynchronous callers. This method is an async method that should be asynchronous callers.
awaited on, even if it is missing the async keyword in its signature.
(This is omitted to preserve syntax compatibility with Python 2.)
The arguments are identical to those of Apprise.notify(). The arguments are identical to those of Apprise.notify().
""" """
try: try:
coroutines = list( # Process arguments and build synchronous and asynchronous calls
self._notifyall( # (this step can throw internal errors).
Apprise._notifyhandlerasync, *args, **kwargs)) sequential_calls, parallel_calls = self._create_notify_calls(
*args, **kwargs)
except TypeError: except TypeError:
# No notifications sent, and there was an internal error. # No notifications sent, and there was an internal error.
return py3compat.asyncio.toasyncwrapvalue(False)
else:
if len(coroutines) > 0:
# All notifications sent, return False if any failed.
return py3compat.asyncio.notify(coroutines)
else:
# No notifications sent.
return py3compat.asyncio.toasyncwrapvalue(None)
@staticmethod
def _notifyhandler(server, **kwargs):
"""
The synchronous notification sender. Returns True if the notification
sent successfully.
"""
try:
# Send notification
return server.notify(**kwargs)
except TypeError:
# These our our internally thrown notifications
return False return False
except Exception: if not sequential_calls and not parallel_calls:
# A catch all so we don't have to abort early # Nothing to send
# just because one of our plugins has a bug in it. return None
logger.exception("Unhandled Notification Exception")
return False
@staticmethod sequential_result = Apprise._notify_sequential(*sequential_calls)
def _notifyhandlerasync(server, **kwargs): parallel_result = \
""" await Apprise._notify_parallel_asyncio(*parallel_calls)
The asynchronous notification sender. Returns a coroutine that yields return sequential_result and parallel_result
True if the notification sent successfully.
"""
if server.asset.async_mode: def _create_notify_calls(self, *args, **kwargs):
return server.async_notify(**kwargs)
else:
# Send the notification immediately, and wrap the result in a
# coroutine.
status = Apprise._notifyhandler(server, **kwargs)
return py3compat.asyncio.toasyncwrapvalue(status)
def _notifyall(self, handler, body, title='',
notify_type=common.NotifyType.INFO, body_format=None,
tag=common.MATCH_ALL_TAG, match_always=True, attach=None,
interpret_escapes=None):
""" """
Creates notifications for all the plugins loaded. Creates notifications for all the plugins loaded.
Returns a generator that calls handler for each notification. The first Returns a list of (server, notify() kwargs) tuples for plugins with
and only argument supplied to handler is the server, and the keyword parallelism disabled and another list for plugins with parallelism
arguments are exactly as they would be passed to server.notify(). enabled.
"""
all_calls = list(self._create_notify_gen(*args, **kwargs))
# Split into sequential and parallel notify() calls.
sequential, parallel = [], []
for (server, notify_kwargs) in all_calls:
if server.asset.async_mode:
parallel.append((server, notify_kwargs))
else:
sequential.append((server, notify_kwargs))
return sequential, parallel
def _create_notify_gen(self, body, title='',
notify_type=common.NotifyType.INFO,
body_format=None, tag=common.MATCH_ALL_TAG,
match_always=True, attach=None,
interpret_escapes=None):
"""
Internal generator function for _create_notify_calls().
""" """
if len(self) == 0: if len(self) == 0:
@ -546,14 +542,121 @@ class Apprise:
logger.error(msg) logger.error(msg)
raise TypeError(msg) raise TypeError(msg)
yield handler( kwargs = dict(
server,
body=conversion_body_map[server.notify_format], body=conversion_body_map[server.notify_format],
title=conversion_title_map[server.notify_format], title=conversion_title_map[server.notify_format],
notify_type=notify_type, notify_type=notify_type,
attach=attach, attach=attach,
body_format=body_format, body_format=body_format
) )
yield (server, kwargs)
@staticmethod
def _notify_sequential(*servers_kwargs):
"""
Process a list of notify() calls sequentially and synchronously.
"""
success = True
for (server, kwargs) in servers_kwargs:
try:
# Send notification
result = server.notify(**kwargs)
success = success and result
except TypeError:
# These are our internally thrown notifications.
success = False
except Exception:
# A catch all so we don't have to abort early
# just because one of our plugins has a bug in it.
logger.exception("Unhandled Notification Exception")
success = False
return success
@staticmethod
def _notify_parallel_threadpool(*servers_kwargs):
"""
Process a list of notify() calls in parallel and synchronously.
"""
n_calls = len(servers_kwargs)
# 0-length case
if n_calls == 0:
return True
# There's no need to use a thread pool for just a single notification
if n_calls == 1:
return Apprise._notify_sequential(servers_kwargs[0])
# Create log entry
logger.info(
'Notifying %d service(s) with threads.', len(servers_kwargs))
with cf.ThreadPoolExecutor() as executor:
success = True
futures = [executor.submit(server.notify, **kwargs)
for (server, kwargs) in servers_kwargs]
for future in cf.as_completed(futures):
try:
result = future.result()
success = success and result
except TypeError:
# These are our internally thrown notifications.
success = False
except Exception:
# A catch all so we don't have to abort early
# just because one of our plugins has a bug in it.
logger.exception("Unhandled Notification Exception")
success = False
return success
@staticmethod
async def _notify_parallel_asyncio(*servers_kwargs):
"""
Process a list of async_notify() calls in parallel and asynchronously.
"""
n_calls = len(servers_kwargs)
# 0-length case
if n_calls == 0:
return True
# (Unlike with the thread pool, we don't optimize for the single-
# notification case because asyncio can do useful work while waiting
# for that thread to complete)
# Create log entry
logger.info(
'Notifying %d service(s) asynchronously.', len(servers_kwargs))
async def do_call(server, kwargs):
return await server.async_notify(**kwargs)
cors = (do_call(server, kwargs) for (server, kwargs) in servers_kwargs)
results = await asyncio.gather(*cors, return_exceptions=True)
if any(isinstance(status, Exception)
and not isinstance(status, TypeError) for status in results):
# A catch all so we don't have to abort early just because
# one of our plugins has a bug in it.
logger.exception("Unhandled Notification Exception")
return False
if any(isinstance(status, TypeError) for status in results):
# These are our internally thrown notifications.
return False
return all(results)
def details(self, lang=None, show_requirements=False, show_disabled=False): def details(self, lang=None, show_requirements=False, show_disabled=False):
""" """
@ -581,6 +684,7 @@ class Apprise:
'setup_url': getattr(plugin, 'setup_url', None), 'setup_url': getattr(plugin, 'setup_url', None),
# Placeholder - populated below # Placeholder - populated below
'details': None, 'details': None,
# Differentiat between what is a custom loaded plugin and # Differentiat between what is a custom loaded plugin and
# which is native. # which is native.
'category': getattr(plugin, 'category', None) 'category': getattr(plugin, 'category', None)

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
from uuid import uuid4 from uuid import uuid4

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from . import attachment from . import attachment
from . import URLBase from . import URLBase
@ -170,6 +177,11 @@ class AppriseAttachment:
return_status = False return_status = False
continue continue
elif isinstance(_attachment, AppriseAttachment):
# We were provided a list of Apprise Attachments
# append our content together
instance = _attachment.attachments
elif not isinstance(_attachment, attachment.AttachBase): elif not isinstance(_attachment, attachment.AttachBase):
logger.warning( logger.warning(
"An invalid attachment (type={}) was specified.".format( "An invalid attachment (type={}) was specified.".format(
@ -196,7 +208,11 @@ class AppriseAttachment:
continue continue
# Add our initialized plugin to our server listings # Add our initialized plugin to our server listings
self.attachments.append(instance) if isinstance(instance, list):
self.attachments.extend(instance)
else:
self.attachments.append(instance)
# Return our status # Return our status
return return_status return return_status

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from . import config from . import config
from . import ConfigBase from . import ConfigBase

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import ctypes import ctypes
import locale import locale
@ -67,7 +74,7 @@ class LazyTranslation:
""" """
self.text = text self.text = text
super(LazyTranslation, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def __str__(self): def __str__(self):
return gettext.gettext(self.text) return gettext.gettext(self.text)

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
from .logger import logger from .logger import logger
@ -194,7 +201,7 @@ class URLBase:
asset if isinstance(asset, AppriseAsset) else AppriseAsset() asset if isinstance(asset, AppriseAsset) else AppriseAsset()
# Certificate Verification (for SSL calls); default to being enabled # Certificate Verification (for SSL calls); default to being enabled
self.verify_certificate = kwargs.get('verify', True) self.verify_certificate = parse_bool(kwargs.get('verify', True))
# Secure Mode # Secure Mode
self.secure = kwargs.get('secure', False) self.secure = kwargs.get('secure', False)
@ -222,24 +229,22 @@ class URLBase:
self.password = URLBase.unquote(self.password) self.password = URLBase.unquote(self.password)
# Store our Timeout Variables # Store our Timeout Variables
if 'socket_read_timeout' in kwargs: if 'rto' in kwargs:
try: try:
self.socket_read_timeout = \ self.socket_read_timeout = float(kwargs.get('rto'))
float(kwargs.get('socket_read_timeout'))
except (TypeError, ValueError): except (TypeError, ValueError):
self.logger.warning( self.logger.warning(
'Invalid socket read timeout (rto) was specified {}' 'Invalid socket read timeout (rto) was specified {}'
.format(kwargs.get('socket_read_timeout'))) .format(kwargs.get('rto')))
if 'socket_connect_timeout' in kwargs: if 'cto' in kwargs:
try: try:
self.socket_connect_timeout = \ self.socket_connect_timeout = float(kwargs.get('cto'))
float(kwargs.get('socket_connect_timeout'))
except (TypeError, ValueError): except (TypeError, ValueError):
self.logger.warning( self.logger.warning(
'Invalid socket connect timeout (cto) was specified {}' 'Invalid socket connect timeout (cto) was specified {}'
.format(kwargs.get('socket_connect_timeout'))) .format(kwargs.get('cto')))
if 'tag' in kwargs: if 'tag' in kwargs:
# We want to associate some tags with our notification service. # We want to associate some tags with our notification service.
@ -598,7 +603,7 @@ class URLBase:
} }
@staticmethod @staticmethod
def parse_url(url, verify_host=True): def parse_url(url, verify_host=True, plus_to_space=False):
"""Parses the URL and returns it broken apart into a dictionary. """Parses the URL and returns it broken apart into a dictionary.
This is very specific and customized for Apprise. This is very specific and customized for Apprise.
@ -618,7 +623,8 @@ class URLBase:
""" """
results = parse_url( results = parse_url(
url, default_schema='unknown', verify_host=verify_host) url, default_schema='unknown', verify_host=verify_host,
plus_to_space=plus_to_space)
if not results: if not results:
# We're done; we failed to parse our url # We're done; we failed to parse our url
@ -646,11 +652,11 @@ class URLBase:
# Store our socket read timeout if specified # Store our socket read timeout if specified
if 'rto' in results['qsd']: if 'rto' in results['qsd']:
results['socket_read_timeout'] = results['qsd']['rto'] results['rto'] = results['qsd']['rto']
# Store our socket connect timeout if specified # Store our socket connect timeout if specified
if 'cto' in results['qsd']: if 'cto' in results['qsd']:
results['socket_connect_timeout'] = results['qsd']['cto'] results['cto'] = results['qsd']['cto']
if 'port' in results['qsd']: if 'port' in results['qsd']:
results['port'] = results['qsd']['port'] results['port'] = results['qsd']['port']
@ -679,6 +685,15 @@ class URLBase:
return response return response
def __len__(self):
"""
Should be over-ridden and allows the tracking of how many targets
are associated with each URLBase object.
Default is always 1
"""
return 1
def schemas(self): def schemas(self):
"""A simple function that returns a set of all schemas associated """A simple function that returns a set of all schemas associated
with this object based on the object.protocol and with this object based on the object.protocol and

View File

@ -1,33 +1,40 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2022 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
__title__ = 'Apprise' __title__ = 'Apprise'
__version__ = '1.1.0' __version__ = '1.4.0'
__author__ = 'Chris Caron' __author__ = 'Chris Caron'
__license__ = 'MIT' __license__ = 'BSD'
__copywrite__ = 'Copyright (C) 2022 Chris Caron <lead2gold@gmail.com>' __copywrite__ = 'Copyright (C) 2023 Chris Caron <lead2gold@gmail.com>'
__email__ = 'lead2gold@gmail.com' __email__ = 'lead2gold@gmail.com'
__status__ = 'Production' __status__ = 'Production'

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os import os
import time import time
@ -120,7 +127,7 @@ class AttachBase(URLBase):
should be considered expired. should be considered expired.
""" """
super(AttachBase, self).__init__(**kwargs) super().__init__(**kwargs)
if not mimetypes.inited: if not mimetypes.inited:
# Ensure mimetypes has been initialized # Ensure mimetypes has been initialized

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
import os import os
@ -50,7 +57,7 @@ class AttachFile(AttachBase):
Initialize Local File Attachment Object Initialize Local File Attachment Object
""" """
super(AttachFile, self).__init__(**kwargs) super().__init__(**kwargs)
# Store path but mark it dirty since we have not performed any # Store path but mark it dirty since we have not performed any
# verification at this point. # verification at this point.

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
import os import os
@ -61,7 +68,7 @@ class AttachHTTP(AttachBase):
additionally include as part of the server headers to post with additionally include as part of the server headers to post with
""" """
super(AttachHTTP, self).__init__(**kwargs) super().__init__(**kwargs)
self.schema = 'https' if self.secure else 'http' self.schema = 'https' if self.secure else 'http'
@ -254,7 +261,7 @@ class AttachHTTP(AttachBase):
self._temp_file.close() self._temp_file.close()
self._temp_file = None self._temp_file = None
super(AttachHTTP, self).invalidate() super().invalidate()
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
""" """

View File

@ -1,30 +1,36 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
from os import listdir from os import listdir
from os.path import dirname from os.path import dirname
from os.path import abspath from os.path import abspath

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
# #
# This code is licensed under the MIT License. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Redistribution and use in source and binary forms, with or without
# of this software and associated documentation files(the "Software"), to deal # modification, are permitted provided that the following conditions are met:
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 1. Redistributions of source code must retain the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 2. Redistributions in binary form must reproduce the above copyright notice,
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # this list of conditions and the following disclaimer in the documentation
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # and/or other materials provided with the distribution.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 3. Neither the name of the copyright holder nor the names of its
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # contributors may be used to endorse or promote products derived from
# THE SOFTWARE. # this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import click import click
import logging import logging
@ -73,28 +80,64 @@ DEFAULT_CONFIG_PATHS = (
'~/.apprise/apprise.yml', '~/.apprise/apprise.yml',
'~/.config/apprise/apprise', '~/.config/apprise/apprise',
'~/.config/apprise/apprise.yml', '~/.config/apprise/apprise.yml',
# Global Configuration Support
'/etc/apprise',
'/etc/apprise.yml',
'/etc/apprise/apprise',
'/etc/apprise/apprise.yml',
) )
# Define our paths to search for plugins # Define our paths to search for plugins
DEFAULT_PLUGIN_PATHS = ( DEFAULT_PLUGIN_PATHS = (
'~/.apprise/plugins', '~/.apprise/plugins',
'~/.config/apprise/plugins', '~/.config/apprise/plugins',
# Global Plugin Support
'/var/lib/apprise/plugins',
) )
# Detect Windows # Detect Windows
if platform.system() == 'Windows': if platform.system() == 'Windows':
# Default Config Search Path for Windows Users # Default Config Search Path for Windows Users
DEFAULT_CONFIG_PATHS = ( DEFAULT_CONFIG_PATHS = (
expandvars('%APPDATA%/Apprise/apprise'), expandvars('%APPDATA%\\Apprise\\apprise'),
expandvars('%APPDATA%/Apprise/apprise.yml'), expandvars('%APPDATA%\\Apprise\\apprise.yml'),
expandvars('%LOCALAPPDATA%/Apprise/apprise'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise'),
expandvars('%LOCALAPPDATA%/Apprise/apprise.yml'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'),
#
# Global Support
#
# C:\ProgramData\Apprise\
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'),
# C:\Program Files\Apprise
expandvars('%PROGRAMFILES%\\Apprise\\apprise'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'),
# C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'),
) )
# Default Plugin Search Path for Windows Users # Default Plugin Search Path for Windows Users
DEFAULT_PLUGIN_PATHS = ( DEFAULT_PLUGIN_PATHS = (
expandvars('%APPDATA%/Apprise/plugins'), expandvars('%APPDATA%\\Apprise\\plugins'),
expandvars('%LOCALAPPDATA%/Apprise/plugins'), expandvars('%LOCALAPPDATA%\\Apprise\\plugins'),
#
# Global Support
#
# C:\ProgramData\Apprise\plugins
expandvars('%ALLUSERSPROFILE%\\Apprise\\plugins'),
# C:\Program Files\Apprise\plugins
expandvars('%PROGRAMFILES%\\Apprise\\plugins'),
# C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\plugins'),
) )

View File

@ -1,28 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# we mirror our base purely for the ability to reset everything; this # we mirror our base purely for the ability to reset everything; this
# is generally only used in testing and should not be used by developers # is generally only used in testing and should not be used by developers

View File

@ -1,3 +1,7 @@
import types
import typing as t
class NotifyType: class NotifyType:
INFO: NotifyType INFO: NotifyType
SUCCESS: NotifyType SUCCESS: NotifyType
@ -12,4 +16,7 @@ class NotifyFormat:
class ContentLocation: class ContentLocation:
LOCAL: ContentLocation LOCAL: ContentLocation
HOSTED: ContentLocation HOSTED: ContentLocation
INACCESSIBLE: ContentLocation INACCESSIBLE: ContentLocation
NOTIFY_MODULE_MAP: t.Dict[str, t.Dict[str, t.Union[t.Type["NotifyBase"], types.ModuleType]]]

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os import os
import re import re
@ -113,7 +120,7 @@ class ConfigBase(URLBase):
these 'include' entries to be honored, this value must be set to True. these 'include' entries to be honored, this value must be set to True.
""" """
super(ConfigBase, self).__init__(**kwargs) super().__init__(**kwargs)
# Tracks the time the content was last retrieved on. This place a role # Tracks the time the content was last retrieved on. This place a role
# for cases where we are not caching our response and are required to # for cases where we are not caching our response and are required to
@ -548,7 +555,7 @@ class ConfigBase(URLBase):
# Define what a valid line should look like # Define what a valid line should look like
valid_line_re = re.compile( valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|' r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
r'(\s*(?P<tags>[^=]+)=|=)?\s*' r'(\s*(?P<tags>[a-z0-9, \t_-]+)\s*=|=)?\s*'
r'(?P<url>[a-z0-9]{2,9}://.*)|' r'(?P<url>[a-z0-9]{2,9}://.*)|'
r'include\s+(?P<config>.+))?\s*$', re.I) r'include\s+(?P<config>.+))?\s*$', re.I)

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
import os import os
@ -53,7 +60,7 @@ class ConfigFile(ConfigBase):
additionally include as part of the server headers to post with additionally include as part of the server headers to post with
""" """
super(ConfigFile, self).__init__(**kwargs) super().__init__(**kwargs)
# Store our file path as it was set # Store our file path as it was set
self.path = os.path.abspath(os.path.expanduser(path)) self.path = os.path.abspath(os.path.expanduser(path))

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
import requests import requests
@ -75,7 +82,7 @@ class ConfigHTTP(ConfigBase):
additionally include as part of the server headers to post with additionally include as part of the server headers to post with
""" """
super(ConfigHTTP, self).__init__(**kwargs) super().__init__(**kwargs)
self.schema = 'https' if self.secure else 'http' self.schema = 'https' if self.secure else 'http'

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .ConfigBase import ConfigBase from .ConfigBase import ConfigBase
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -46,7 +53,7 @@ class ConfigMemory(ConfigBase):
Memory objects just store the raw configuration in memory. There is Memory objects just store the raw configuration in memory. There is
no external reference point. It's always considered cached. no external reference point. It's always considered cached.
""" """
super(ConfigMemory, self).__init__(**kwargs) super().__init__(**kwargs)
# Store our raw config into memory # Store our raw config into memory
self.content = content self.content = content

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
from os import listdir from os import listdir

View File

@ -1,27 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2022 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re import re
from markdown import markdown from markdown import markdown
@ -99,7 +106,7 @@ class HTMLConverter(HTMLParser, object):
BLOCK_END = {} BLOCK_END = {}
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(HTMLConverter, self).__init__(**kwargs) super().__init__(**kwargs)
# Shoudl we store the text content or not? # Shoudl we store the text content or not?
self._do_store = True self._do_store = True

View File

@ -1,27 +1,35 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2022 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from ..plugins.NotifyBase import NotifyBase from ..plugins.NotifyBase import NotifyBase
from ..utils import URL_DETAILS_RE from ..utils import URL_DETAILS_RE
from ..utils import parse_url from ..utils import parse_url
@ -134,7 +142,7 @@ class CustomNotifyPlugin(NotifyBase):
""" """
# init parent # init parent
super(CustomNotifyPluginWrapper, self).__init__(**kwargs) super().__init__(**kwargs)
self._default_args = {} self._default_args = {}

View File

@ -1,27 +1,35 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License
# #
# Copyright (C) 2022 Chris Caron <lead2gold@gmail.com> # Apprise - Push Notification Library.
# All rights reserved. # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
# #
# This code is licensed under the MIT License. # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # 1. Redistributions of source code must retain the above copyright notice,
# of this software and associated documentation files(the "Software"), to deal # this list of conditions and the following disclaimer.
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
# #
# The above copyright notice and this permission notice shall be included in # 2. Redistributions in binary form must reproduce the above copyright notice,
# all copies or substantial portions of the Software. # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 3. Neither the name of the copyright holder nor the names of its
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # contributors may be used to endorse or promote products derived from
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE # this software without specific prior written permission.
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# THE SOFTWARE. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .notify import notify from .notify import notify

Some files were not shown because too many files have changed in this diff Show More