mirror of https://github.com/morpheus65535/bazarr
Merge branch 'development'
# Conflicts: # .github/workflows/ci.yml
This commit is contained in:
commit
8282899fac
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
python3 "${ROOT_DIRECTORY}"/bazarr.py &
|
python3 "${ROOT_DIRECTORY}"/bazarr.py --no-update &
|
||||||
PID=$!
|
PID=$!
|
||||||
|
|
||||||
sleep 30
|
sleep 30
|
||||||
|
|
|
@ -23,19 +23,8 @@ jobs:
|
||||||
Frontend:
|
Frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get source branch name
|
|
||||||
uses: haya14busa/action-cond@v1
|
|
||||||
id: branch_ref
|
|
||||||
with:
|
|
||||||
cond: ${{ github.event_name == 'pull_request' }}
|
|
||||||
if_true: ${{ github.head_ref }}
|
|
||||||
if_false: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: ${{ steps.branch_ref.outputs.value }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
|
@ -83,19 +72,8 @@ jobs:
|
||||||
needs: Frontend
|
needs: Frontend
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get source branch name
|
|
||||||
uses: haya14busa/action-cond@v1
|
|
||||||
id: branch_ref
|
|
||||||
with:
|
|
||||||
cond: ${{ github.event_name == 'pull_request' }}
|
|
||||||
if_true: ${{ github.head_ref }}
|
|
||||||
if_false: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: ${{ steps.branch_ref.outputs.value }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
|
@ -113,7 +91,7 @@ jobs:
|
||||||
|
|
||||||
- name: Unit Tests
|
- name: Unit Tests
|
||||||
run: |
|
run: |
|
||||||
python3 bazarr.py &
|
python3 bazarr.py --no-update &
|
||||||
PID=$!
|
PID=$!
|
||||||
sleep 15
|
sleep 15
|
||||||
if kill -s 0 $PID
|
if kill -s 0 $PID
|
||||||
|
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: ${{ env.FETCH_DEPTH }}
|
fetch-depth: ${{ env.FETCH_DEPTH }}
|
||||||
ref: development
|
ref: development
|
||||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: development
|
ref: development
|
||||||
|
@ -64,7 +64,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Merge development -> master
|
- name: Merge development -> master
|
||||||
uses: devmasx/merge-branch@1.4.0
|
uses: devmasx/merge-branch@1.4.0
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: ${{ env.FETCH_DEPTH }}
|
fetch-depth: ${{ env.FETCH_DEPTH }}
|
||||||
ref: development
|
ref: development
|
||||||
|
|
|
@ -86,6 +86,7 @@ If you need something that is not already part of Bazarr, feel free to create a
|
||||||
- Titulky.com
|
- Titulky.com
|
||||||
- TuSubtitulo
|
- TuSubtitulo
|
||||||
- TVSubtitles
|
- TVSubtitles
|
||||||
|
- Whisper (requires [ahmetoner/whisper-asr-webservice](https://github.com/ahmetoner/whisper-asr-webservice))
|
||||||
- Wizdom
|
- Wizdom
|
||||||
- XSubs
|
- XSubs
|
||||||
- Yavka.net
|
- Yavka.net
|
||||||
|
|
|
@ -15,7 +15,7 @@ from subtitles.tools.delete import delete_subtitles
|
||||||
from sonarr.history import history_log
|
from sonarr.history import history_log
|
||||||
from app.notifier import send_notifications
|
from app.notifier import send_notifications
|
||||||
from subtitles.indexer.series import store_subtitles
|
from subtitles.indexer.series import store_subtitles
|
||||||
from app.event_handler import event_stream
|
from app.event_handler import event_stream, show_message
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
from ..utils import authenticate
|
from ..utils import authenticate
|
||||||
|
@ -69,6 +69,12 @@ class EpisodesSubtitles(Resource):
|
||||||
language = args.get('language')
|
language = args.get('language')
|
||||||
hi = args.get('hi').capitalize()
|
hi = args.get('hi').capitalize()
|
||||||
forced = args.get('forced').capitalize()
|
forced = args.get('forced').capitalize()
|
||||||
|
if hi == 'True':
|
||||||
|
language_str = f'{language}:hi'
|
||||||
|
elif forced == 'True':
|
||||||
|
language_str = f'{language}:forced'
|
||||||
|
else:
|
||||||
|
language_str = language
|
||||||
|
|
||||||
audio_language_list = get_audio_profile_languages(episodeInfo.audio_language)
|
audio_language_list = get_audio_profile_languages(episodeInfo.audio_language)
|
||||||
if len(audio_language_list) > 0:
|
if len(audio_language_list) > 0:
|
||||||
|
@ -88,7 +94,8 @@ class EpisodesSubtitles(Resource):
|
||||||
store_subtitles(result.path, episodePath)
|
store_subtitles(result.path, episodePath)
|
||||||
else:
|
else:
|
||||||
event_stream(type='episode', payload=sonarrEpisodeId)
|
event_stream(type='episode', payload=sonarrEpisodeId)
|
||||||
return 'No subtitles found', 500
|
show_message(f'No {language_str.upper()} subtitles found')
|
||||||
|
return '', 204
|
||||||
except OSError:
|
except OSError:
|
||||||
return 'Unable to save subtitles file. Permission or path mapping issue?', 409
|
return 'Unable to save subtitles file. Permission or path mapping issue?', 409
|
||||||
else:
|
else:
|
||||||
|
@ -162,7 +169,7 @@ class EpisodesSubtitles(Resource):
|
||||||
provider = "manual"
|
provider = "manual"
|
||||||
score = 360
|
score = 360
|
||||||
history_log(4, sonarrSeriesId, sonarrEpisodeId, result, fake_provider=provider, fake_score=score)
|
history_log(4, sonarrSeriesId, sonarrEpisodeId, result, fake_provider=provider, fake_score=score)
|
||||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
if not settings.general.dont_notify_manual_actions:
|
||||||
send_notifications(sonarrSeriesId, sonarrEpisodeId, result.message)
|
send_notifications(sonarrSeriesId, sonarrEpisodeId, result.message)
|
||||||
store_subtitles(result.path, episodePath)
|
store_subtitles(result.path, episodePath)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|
|
@ -150,7 +150,7 @@ class EpisodesHistory(Resource):
|
||||||
del item['external_subtitles']
|
del item['external_subtitles']
|
||||||
|
|
||||||
if item['score']:
|
if item['score']:
|
||||||
item['score'] = str(round((int(item['score']) * 100 / 360), 2)) + "%"
|
item['score'] = f"{round((int(item['score']) * 100 / 360), 2)}%"
|
||||||
|
|
||||||
# Make timestamp pretty
|
# Make timestamp pretty
|
||||||
if item['timestamp']:
|
if item['timestamp']:
|
||||||
|
|
|
@ -141,7 +141,7 @@ class MoviesHistory(Resource):
|
||||||
del item['external_subtitles']
|
del item['external_subtitles']
|
||||||
|
|
||||||
if item['score']:
|
if item['score']:
|
||||||
item['score'] = str(round((int(item['score']) * 100 / 120), 2)) + "%"
|
item['score'] = f"{round((int(item['score']) * 100 / 120), 2)}%"
|
||||||
|
|
||||||
# Make timestamp pretty
|
# Make timestamp pretty
|
||||||
if item['timestamp']:
|
if item['timestamp']:
|
||||||
|
|
|
@ -15,7 +15,7 @@ from subtitles.tools.delete import delete_subtitles
|
||||||
from radarr.history import history_log_movie
|
from radarr.history import history_log_movie
|
||||||
from app.notifier import send_notifications_movie
|
from app.notifier import send_notifications_movie
|
||||||
from subtitles.indexer.movies import store_subtitles_movie
|
from subtitles.indexer.movies import store_subtitles_movie
|
||||||
from app.event_handler import event_stream
|
from app.event_handler import event_stream, show_message
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
from ..utils import authenticate
|
from ..utils import authenticate
|
||||||
|
@ -67,6 +67,12 @@ class MoviesSubtitles(Resource):
|
||||||
language = args.get('language')
|
language = args.get('language')
|
||||||
hi = args.get('hi').capitalize()
|
hi = args.get('hi').capitalize()
|
||||||
forced = args.get('forced').capitalize()
|
forced = args.get('forced').capitalize()
|
||||||
|
if hi == 'True':
|
||||||
|
language_str = f'{language}:hi'
|
||||||
|
elif forced == 'True':
|
||||||
|
language_str = f'{language}:forced'
|
||||||
|
else:
|
||||||
|
language_str = language
|
||||||
|
|
||||||
audio_language_list = get_audio_profile_languages(movieInfo.audio_language)
|
audio_language_list = get_audio_profile_languages(movieInfo.audio_language)
|
||||||
if len(audio_language_list) > 0:
|
if len(audio_language_list) > 0:
|
||||||
|
@ -85,7 +91,8 @@ class MoviesSubtitles(Resource):
|
||||||
store_subtitles_movie(result.path, moviePath)
|
store_subtitles_movie(result.path, moviePath)
|
||||||
else:
|
else:
|
||||||
event_stream(type='movie', payload=radarrId)
|
event_stream(type='movie', payload=radarrId)
|
||||||
return 'No subtitles found', 500
|
show_message(f'No {language_str.upper()} subtitles found')
|
||||||
|
return '', 204
|
||||||
except OSError:
|
except OSError:
|
||||||
return 'Unable to save subtitles file. Permission or path mapping issue?', 409
|
return 'Unable to save subtitles file. Permission or path mapping issue?', 409
|
||||||
else:
|
else:
|
||||||
|
@ -158,7 +165,7 @@ class MoviesSubtitles(Resource):
|
||||||
provider = "manual"
|
provider = "manual"
|
||||||
score = 120
|
score = 120
|
||||||
history_log_movie(4, radarrId, result, fake_provider=provider, fake_score=score)
|
history_log_movie(4, radarrId, result, fake_provider=provider, fake_score=score)
|
||||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
if not settings.general.dont_notify_manual_actions:
|
||||||
send_notifications_movie(radarrId, result.message)
|
send_notifications_movie(radarrId, result.message)
|
||||||
store_subtitles_movie(result.path, moviePath)
|
store_subtitles_movie(result.path, moviePath)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|
|
@ -141,7 +141,7 @@ class ProviderEpisodes(Resource):
|
||||||
result = result[0]
|
result = result[0]
|
||||||
if isinstance(result, ProcessSubtitlesResult):
|
if isinstance(result, ProcessSubtitlesResult):
|
||||||
history_log(2, sonarrSeriesId, sonarrEpisodeId, result)
|
history_log(2, sonarrSeriesId, sonarrEpisodeId, result)
|
||||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
if not settings.general.dont_notify_manual_actions:
|
||||||
send_notifications(sonarrSeriesId, sonarrEpisodeId, result.message)
|
send_notifications(sonarrSeriesId, sonarrEpisodeId, result.message)
|
||||||
store_subtitles(result.path, episodePath)
|
store_subtitles(result.path, episodePath)
|
||||||
elif isinstance(result, str):
|
elif isinstance(result, str):
|
||||||
|
|
|
@ -135,7 +135,7 @@ class ProviderMovies(Resource):
|
||||||
result = result[0]
|
result = result[0]
|
||||||
if isinstance(result, ProcessSubtitlesResult):
|
if isinstance(result, ProcessSubtitlesResult):
|
||||||
history_log_movie(2, radarrId, result)
|
history_log_movie(2, radarrId, result)
|
||||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
if not settings.general.dont_notify_manual_actions:
|
||||||
send_notifications_movie(radarrId, result.message)
|
send_notifications_movie(radarrId, result.message)
|
||||||
store_subtitles_movie(result.path, moviePath)
|
store_subtitles_movie(result.path, moviePath)
|
||||||
elif isinstance(result, str):
|
elif isinstance(result, str):
|
||||||
|
|
|
@ -116,7 +116,7 @@ class Subtitles(Resource):
|
||||||
|
|
||||||
# apply chmod if required
|
# apply chmod if required
|
||||||
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
||||||
'win') and settings.general.getboolean('chmod_enabled') else None
|
'win') and settings.general.chmod_enabled else None
|
||||||
if chmod:
|
if chmod:
|
||||||
os.chmod(subtitles_path, chmod)
|
os.chmod(subtitles_path, chmod)
|
||||||
|
|
||||||
|
|
|
@ -24,12 +24,12 @@ class SystemAccount(Resource):
|
||||||
@api_ns_system_account.response(400, 'Unknown action')
|
@api_ns_system_account.response(400, 'Unknown action')
|
||||||
@api_ns_system_account.response(403, 'Authentication failed')
|
@api_ns_system_account.response(403, 'Authentication failed')
|
||||||
@api_ns_system_account.response(406, 'Browser must be closed to invalidate basic authentication')
|
@api_ns_system_account.response(406, 'Browser must be closed to invalidate basic authentication')
|
||||||
@api_ns_system_account.response(500, 'Unknown authentication type define in config.ini')
|
@api_ns_system_account.response(500, 'Unknown authentication type define in config')
|
||||||
def post(self):
|
def post(self):
|
||||||
"""Login or logout from Bazarr UI when using form login"""
|
"""Login or logout from Bazarr UI when using form login"""
|
||||||
args = self.post_request_parser.parse_args()
|
args = self.post_request_parser.parse_args()
|
||||||
if settings.auth.type != 'form':
|
if settings.auth.type != 'form':
|
||||||
return 'Unknown authentication type define in config.ini', 500
|
return 'Unknown authentication type define in config', 500
|
||||||
|
|
||||||
action = args.get('action')
|
action = args.get('action')
|
||||||
if action == 'login':
|
if action == 'login':
|
||||||
|
|
|
@ -58,5 +58,6 @@ class SystemReleases(Resource):
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(
|
logging.exception(
|
||||||
'BAZARR cannot parse releases caching file: ' + os.path.join(args.config_dir, 'config', 'releases.txt'))
|
f'BAZARR cannot parse releases caching file: '
|
||||||
|
f'{os.path.join(args.config_dir, "config", "releases.txt")}')
|
||||||
return marshal(filtered_releases, self.get_response_model, envelope='data')
|
return marshal(filtered_releases, self.get_response_model, envelope='data')
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Searches(Resource):
|
||||||
search_list = []
|
search_list = []
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
# Get matching series
|
# Get matching series
|
||||||
search_list += database.execute(
|
search_list += database.execute(
|
||||||
select(TableShows.title,
|
select(TableShows.title,
|
||||||
|
@ -36,7 +36,7 @@ class Searches(Resource):
|
||||||
.order_by(TableShows.title)) \
|
.order_by(TableShows.title)) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
# Get matching movies
|
# Get matching movies
|
||||||
search_list += database.execute(
|
search_list += database.execute(
|
||||||
select(TableMovies.title,
|
select(TableMovies.title,
|
||||||
|
|
|
@ -4,7 +4,9 @@ import json
|
||||||
|
|
||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from flask_restx import Resource, Namespace
|
from flask_restx import Resource, Namespace
|
||||||
|
from dynaconf.validator import ValidationError
|
||||||
|
|
||||||
|
from api.utils import None_Keys
|
||||||
from app.database import TableLanguagesProfiles, TableSettingsLanguages, TableSettingsNotifier, \
|
from app.database import TableLanguagesProfiles, TableSettingsLanguages, TableSettingsNotifier, \
|
||||||
update_profile_id_list, database, insert, update, delete, select
|
update_profile_id_list, database, insert, update, delete, select
|
||||||
from app.event_handler import event_stream
|
from app.event_handler import event_stream
|
||||||
|
@ -65,11 +67,12 @@ class SystemSettings(Resource):
|
||||||
update(TableLanguagesProfiles)
|
update(TableLanguagesProfiles)
|
||||||
.values(
|
.values(
|
||||||
name=item['name'],
|
name=item['name'],
|
||||||
cutoff=item['cutoff'] if item['cutoff'] != 'null' else None,
|
cutoff=item['cutoff'] if item['cutoff'] not in None_Keys else None,
|
||||||
items=json.dumps(item['items']),
|
items=json.dumps(item['items']),
|
||||||
mustContain=str(item['mustContain']),
|
mustContain=str(item['mustContain']),
|
||||||
mustNotContain=str(item['mustNotContain']),
|
mustNotContain=str(item['mustNotContain']),
|
||||||
originalFormat=item['originalFormat'] if item['originalFormat'] != 'null' else None,
|
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.where(TableLanguagesProfiles.profileId == item['profileId']))
|
.where(TableLanguagesProfiles.profileId == item['profileId']))
|
||||||
existing.remove(item['profileId'])
|
existing.remove(item['profileId'])
|
||||||
|
@ -80,11 +83,12 @@ class SystemSettings(Resource):
|
||||||
.values(
|
.values(
|
||||||
profileId=item['profileId'],
|
profileId=item['profileId'],
|
||||||
name=item['name'],
|
name=item['name'],
|
||||||
cutoff=item['cutoff'] if item['cutoff'] != 'null' else None,
|
cutoff=item['cutoff'] if item['cutoff'] not in None_Keys else None,
|
||||||
items=json.dumps(item['items']),
|
items=json.dumps(item['items']),
|
||||||
mustContain=str(item['mustContain']),
|
mustContain=str(item['mustContain']),
|
||||||
mustNotContain=str(item['mustNotContain']),
|
mustNotContain=str(item['mustNotContain']),
|
||||||
originalFormat=item['originalFormat'] if item['originalFormat'] != 'null' else None,
|
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
for profileId in existing:
|
for profileId in existing:
|
||||||
# Remove deleted profiles
|
# Remove deleted profiles
|
||||||
|
@ -97,9 +101,9 @@ class SystemSettings(Resource):
|
||||||
|
|
||||||
event_stream("languages")
|
event_stream("languages")
|
||||||
|
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
scheduler.add_job(list_missing_subtitles, kwargs={'send_event': True})
|
scheduler.add_job(list_missing_subtitles, kwargs={'send_event': True})
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
scheduler.add_job(list_missing_subtitles_movies, kwargs={'send_event': True})
|
scheduler.add_job(list_missing_subtitles_movies, kwargs={'send_event': True})
|
||||||
|
|
||||||
# Update Notification
|
# Update Notification
|
||||||
|
@ -112,6 +116,11 @@ class SystemSettings(Resource):
|
||||||
url=item['url'])
|
url=item['url'])
|
||||||
.where(TableSettingsNotifier.name == item['name']))
|
.where(TableSettingsNotifier.name == item['name']))
|
||||||
|
|
||||||
save_settings(zip(request.form.keys(), request.form.listvalues()))
|
try:
|
||||||
event_stream("settings")
|
save_settings(zip(request.form.keys(), request.form.listvalues()))
|
||||||
return '', 204
|
except ValidationError as e:
|
||||||
|
event_stream("settings")
|
||||||
|
return e.message, 406
|
||||||
|
else:
|
||||||
|
event_stream("settings")
|
||||||
|
return '', 204
|
||||||
|
|
|
@ -77,7 +77,7 @@ def postprocess(item):
|
||||||
"hi": language[1] == 'hi',
|
"hi": language[1] == 'hi',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if settings.general.getboolean('embedded_subs_show_desired') and item.get('profileId'):
|
if settings.general.embedded_subs_show_desired and item.get('profileId'):
|
||||||
desired_lang_list = get_desired_languages(item['profileId'])
|
desired_lang_list = get_desired_languages(item['profileId'])
|
||||||
item['subtitles'] = [x for x in item['subtitles'] if x['code2'] in desired_lang_list or x['path']]
|
item['subtitles'] = [x for x in item['subtitles'] if x['code2'] in desired_lang_list or x['path']]
|
||||||
item['subtitles'] = sorted(item['subtitles'], key=itemgetter('name', 'forced'))
|
item['subtitles'] = sorted(item['subtitles'], key=itemgetter('name', 'forced'))
|
||||||
|
|
|
@ -62,7 +62,7 @@ class WebHooksPlex(Resource):
|
||||||
if media_type == 'episode':
|
if media_type == 'episode':
|
||||||
try:
|
try:
|
||||||
episode_imdb_id = [x['imdb'] for x in ids if 'imdb' in x][0]
|
episode_imdb_id = [x['imdb'] for x in ids if 'imdb' in x][0]
|
||||||
r = requests.get('https://imdb.com/title/{}'.format(episode_imdb_id),
|
r = requests.get(f'https://imdb.com/title/{episode_imdb_id}',
|
||||||
headers={"User-Agent": os.environ["SZ_USER_AGENT"]})
|
headers={"User-Agent": os.environ["SZ_USER_AGENT"]})
|
||||||
soup = bso(r.content, "html.parser")
|
soup = bso(r.content, "html.parser")
|
||||||
script_tag = soup.find(id='__NEXT_DATA__')
|
script_tag = soup.find(id='__NEXT_DATA__')
|
||||||
|
|
|
@ -34,7 +34,7 @@ def create_app():
|
||||||
else:
|
else:
|
||||||
app.config["DEBUG"] = False
|
app.config["DEBUG"] = False
|
||||||
|
|
||||||
socketio.init_app(app, path=base_url.rstrip('/')+'/api/socket.io', cors_allowed_origins='*',
|
socketio.init_app(app, path=f'{base_url.rstrip("/")}/api/socket.io', cors_allowed_origins='*',
|
||||||
async_mode='threading', allow_upgrades=False, transports='polling')
|
async_mode='threading', allow_upgrades=False, transports='polling')
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
|
|
|
@ -24,7 +24,7 @@ def check_releases():
|
||||||
releases = []
|
releases = []
|
||||||
url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases?per_page=100'
|
url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases?per_page=100'
|
||||||
try:
|
try:
|
||||||
logging.debug('BAZARR getting releases from Github: {}'.format(url_releases))
|
logging.debug(f'BAZARR getting releases from Github: {url_releases}')
|
||||||
r = requests.get(url_releases, allow_redirects=True)
|
r = requests.get(url_releases, allow_redirects=True)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except requests.exceptions.HTTPError:
|
except requests.exceptions.HTTPError:
|
||||||
|
@ -50,7 +50,7 @@ def check_releases():
|
||||||
'download_link': download_link})
|
'download_link': download_link})
|
||||||
with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'w') as f:
|
with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'w') as f:
|
||||||
json.dump(releases, f)
|
json.dump(releases, f)
|
||||||
logging.debug('BAZARR saved {} releases to releases.txt'.format(len(r.json())))
|
logging.debug(f'BAZARR saved {len(r.json())} releases to releases.txt')
|
||||||
|
|
||||||
|
|
||||||
def check_if_new_update():
|
def check_if_new_update():
|
||||||
|
@ -59,9 +59,9 @@ def check_if_new_update():
|
||||||
elif settings.general.branch == 'development':
|
elif settings.general.branch == 'development':
|
||||||
use_prerelease = True
|
use_prerelease = True
|
||||||
else:
|
else:
|
||||||
logging.error('BAZARR unknown branch provided to updater: {}'.format(settings.general.branch))
|
logging.error(f'BAZARR unknown branch provided to updater: {settings.general.branch}')
|
||||||
return
|
return
|
||||||
logging.debug('BAZARR updater is using {} branch'.format(settings.general.branch))
|
logging.debug(f'BAZARR updater is using {settings.general.branch} branch')
|
||||||
|
|
||||||
check_releases()
|
check_releases()
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ def check_if_new_update():
|
||||||
release = next((item for item in data if not item["prerelease"]), None)
|
release = next((item for item in data if not item["prerelease"]), None)
|
||||||
|
|
||||||
if release and 'name' in release:
|
if release and 'name' in release:
|
||||||
logging.debug('BAZARR last release available is {}'.format(release['name']))
|
logging.debug(f'BAZARR last release available is {release["name"]}')
|
||||||
if deprecated_python_version():
|
if deprecated_python_version():
|
||||||
logging.warning('BAZARR is using a deprecated Python version, you must update Python to get latest '
|
logging.warning('BAZARR is using a deprecated Python version, you must update Python to get latest '
|
||||||
'version available.')
|
'version available.')
|
||||||
|
@ -101,12 +101,12 @@ def check_if_new_update():
|
||||||
|
|
||||||
# skip update process if latest release is v0.9.1.1 which is the latest pre-semver compatible release
|
# skip update process if latest release is v0.9.1.1 which is the latest pre-semver compatible release
|
||||||
if new_version and release['name'] != 'v0.9.1.1':
|
if new_version and release['name'] != 'v0.9.1.1':
|
||||||
logging.debug('BAZARR newer release available and will be downloaded: {}'.format(release['name']))
|
logging.debug(f'BAZARR newer release available and will be downloaded: {release["name"]}')
|
||||||
download_release(url=release['download_link'])
|
download_release(url=release['download_link'])
|
||||||
# rolling back from nightly to stable release
|
# rolling back from nightly to stable release
|
||||||
elif current_version:
|
elif current_version:
|
||||||
if current_version.prerelease and not use_prerelease:
|
if current_version.prerelease and not use_prerelease:
|
||||||
logging.debug('BAZARR previous stable version will be downloaded: {}'.format(release['name']))
|
logging.debug(f'BAZARR previous stable version will be downloaded: {release["name"]}')
|
||||||
download_release(url=release['download_link'])
|
download_release(url=release['download_link'])
|
||||||
else:
|
else:
|
||||||
logging.debug('BAZARR no newer release have been found')
|
logging.debug('BAZARR no newer release have been found')
|
||||||
|
@ -122,9 +122,9 @@ def download_release(url):
|
||||||
try:
|
try:
|
||||||
os.makedirs(update_dir, exist_ok=True)
|
os.makedirs(update_dir, exist_ok=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug('BAZARR unable to create update directory {}'.format(update_dir))
|
logging.debug(f'BAZARR unable to create update directory {update_dir}')
|
||||||
else:
|
else:
|
||||||
logging.debug('BAZARR downloading release from Github: {}'.format(url))
|
logging.debug(f'BAZARR downloading release from Github: {url}')
|
||||||
r = requests.get(url, allow_redirects=True)
|
r = requests.get(url, allow_redirects=True)
|
||||||
if r:
|
if r:
|
||||||
try:
|
try:
|
||||||
|
@ -145,7 +145,7 @@ def apply_update():
|
||||||
|
|
||||||
if os.path.isdir(update_dir):
|
if os.path.isdir(update_dir):
|
||||||
if os.path.isfile(bazarr_zip):
|
if os.path.isfile(bazarr_zip):
|
||||||
logging.debug('BAZARR is trying to unzip this release to {0}: {1}'.format(bazarr_dir, bazarr_zip))
|
logging.debug(f'BAZARR is trying to unzip this release to {bazarr_dir}: {bazarr_zip}')
|
||||||
try:
|
try:
|
||||||
with ZipFile(bazarr_zip, 'r') as archive:
|
with ZipFile(bazarr_zip, 'r') as archive:
|
||||||
zip_root_directory = ''
|
zip_root_directory = ''
|
||||||
|
@ -195,7 +195,7 @@ def apply_update():
|
||||||
def update_cleaner(zipfile, bazarr_dir, config_dir):
|
def update_cleaner(zipfile, bazarr_dir, config_dir):
|
||||||
with ZipFile(zipfile, 'r') as archive:
|
with ZipFile(zipfile, 'r') as archive:
|
||||||
file_in_zip = archive.namelist()
|
file_in_zip = archive.namelist()
|
||||||
logging.debug('BAZARR zip file contain {} directories and files'.format(len(file_in_zip)))
|
logging.debug(f'BAZARR zip file contain {len(file_in_zip)} directories and files')
|
||||||
separator = os.path.sep
|
separator = os.path.sep
|
||||||
if os.path.sep == '\\':
|
if os.path.sep == '\\':
|
||||||
logging.debug('BAZARR upgrade leftover cleaner is running on Windows. We\'ll fix the zip file separator '
|
logging.debug('BAZARR upgrade leftover cleaner is running on Windows. We\'ll fix the zip file separator '
|
||||||
|
@ -207,33 +207,33 @@ def update_cleaner(zipfile, bazarr_dir, config_dir):
|
||||||
logging.debug('BAZARR upgrade leftover cleaner is running on something else than Windows. The zip file '
|
logging.debug('BAZARR upgrade leftover cleaner is running on something else than Windows. The zip file '
|
||||||
'separator are fine.')
|
'separator are fine.')
|
||||||
|
|
||||||
dir_to_ignore = ['^.' + separator,
|
dir_to_ignore = [f'^.{separator}',
|
||||||
'^bin' + separator,
|
f'^bin{separator}',
|
||||||
'^venv' + separator,
|
f'^venv{separator}',
|
||||||
'^WinPython' + separator,
|
f'^WinPython{separator}',
|
||||||
separator + '__pycache__' + separator + '$']
|
f'{separator}__pycache__{separator}$']
|
||||||
if os.path.abspath(bazarr_dir).lower() == os.path.abspath(config_dir).lower():
|
if os.path.abspath(bazarr_dir).lower() == os.path.abspath(config_dir).lower():
|
||||||
# for users who installed Bazarr inside the config directory (ie: `%programdata%\Bazarr` on windows)
|
# for users who installed Bazarr inside the config directory (ie: `%programdata%\Bazarr` on windows)
|
||||||
dir_to_ignore.append('^backup' + separator)
|
dir_to_ignore.append(f'^backup{separator}')
|
||||||
dir_to_ignore.append('^cache' + separator)
|
dir_to_ignore.append(f'^cache{separator}')
|
||||||
dir_to_ignore.append('^config' + separator)
|
dir_to_ignore.append(f'^config{separator}')
|
||||||
dir_to_ignore.append('^db' + separator)
|
dir_to_ignore.append(f'^db{separator}')
|
||||||
dir_to_ignore.append('^log' + separator)
|
dir_to_ignore.append(f'^log{separator}')
|
||||||
dir_to_ignore.append('^restore' + separator)
|
dir_to_ignore.append(f'^restore{separator}')
|
||||||
dir_to_ignore.append('^update' + separator)
|
dir_to_ignore.append(f'^update{separator}')
|
||||||
elif os.path.abspath(bazarr_dir).lower() in os.path.abspath(config_dir).lower():
|
elif os.path.abspath(bazarr_dir).lower() in os.path.abspath(config_dir).lower():
|
||||||
# when config directory is a child of Bazarr installation directory
|
# when config directory is a child of Bazarr installation directory
|
||||||
dir_to_ignore.append('^' + os.path.relpath(config_dir, bazarr_dir) + separator)
|
dir_to_ignore.append(f'^{os.path.relpath(config_dir, bazarr_dir)}{separator}')
|
||||||
dir_to_ignore_regex_string = '(?:% s)' % '|'.join(dir_to_ignore)
|
dir_to_ignore_regex_string = '(?:% s)' % '|'.join(dir_to_ignore)
|
||||||
logging.debug(f'BAZARR upgrade leftover cleaner will ignore directories matching this '
|
logging.debug(f'BAZARR upgrade leftover cleaner will ignore directories matching this '
|
||||||
f'regex: {dir_to_ignore_regex_string}')
|
f'regex: {dir_to_ignore_regex_string}')
|
||||||
dir_to_ignore_regex = re.compile(dir_to_ignore_regex_string)
|
dir_to_ignore_regex = re.compile(dir_to_ignore_regex_string)
|
||||||
|
|
||||||
file_to_ignore = ['nssm.exe', '7za.exe', 'unins000.exe', 'unins000.dat']
|
file_to_ignore = ['nssm.exe', '7za.exe', 'unins000.exe', 'unins000.dat']
|
||||||
logging.debug('BAZARR upgrade leftover cleaner will ignore those files: {}'.format(', '.join(file_to_ignore)))
|
logging.debug(f'BAZARR upgrade leftover cleaner will ignore those files: {", ".join(file_to_ignore)}')
|
||||||
extension_to_ignore = ['.pyc']
|
extension_to_ignore = ['.pyc']
|
||||||
logging.debug('BAZARR upgrade leftover cleaner will ignore files with those extensions: '
|
logging.debug(
|
||||||
'{}'.format(', '.join(extension_to_ignore)))
|
f'BAZARR upgrade leftover cleaner will ignore files with those extensions: {", ".join(extension_to_ignore)}')
|
||||||
|
|
||||||
file_on_disk = []
|
file_on_disk = []
|
||||||
folder_list = []
|
folder_list = []
|
||||||
|
@ -256,14 +256,14 @@ def update_cleaner(zipfile, bazarr_dir, config_dir):
|
||||||
filepath = os.path.join(current_dir, file)
|
filepath = os.path.join(current_dir, file)
|
||||||
if not dir_to_ignore_regex.findall(filepath):
|
if not dir_to_ignore_regex.findall(filepath):
|
||||||
file_on_disk.append(filepath)
|
file_on_disk.append(filepath)
|
||||||
logging.debug('BAZARR directory contain {} files'.format(len(file_on_disk)))
|
logging.debug(f'BAZARR directory contain {len(file_on_disk)} files')
|
||||||
logging.debug('BAZARR directory contain {} directories'.format(len(folder_list)))
|
logging.debug(f'BAZARR directory contain {len(folder_list)} directories')
|
||||||
file_on_disk += folder_list
|
file_on_disk += folder_list
|
||||||
logging.debug('BAZARR directory contain {} directories and files'.format(len(file_on_disk)))
|
logging.debug(f'BAZARR directory contain {len(file_on_disk)} directories and files')
|
||||||
|
|
||||||
file_to_remove = list(set(file_on_disk) - set(file_in_zip))
|
file_to_remove = list(set(file_on_disk) - set(file_in_zip))
|
||||||
logging.debug('BAZARR will delete {} directories and files'.format(len(file_to_remove)))
|
logging.debug(f'BAZARR will delete {len(file_to_remove)} directories and files')
|
||||||
logging.debug('BAZARR will delete this: {}'.format(', '.join(file_to_remove)))
|
logging.debug(f'BAZARR will delete this: {", ".join(file_to_remove)}')
|
||||||
|
|
||||||
for file in file_to_remove:
|
for file in file_to_remove:
|
||||||
filepath = os.path.join(bazarr_dir, file)
|
filepath = os.path.join(bazarr_dir, file)
|
||||||
|
@ -273,4 +273,4 @@ def update_cleaner(zipfile, bazarr_dir, config_dir):
|
||||||
else:
|
else:
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug('BAZARR upgrade leftover cleaner cannot delete {}'.format(filepath))
|
logging.debug(f'BAZARR upgrade leftover cleaner cannot delete {filepath}')
|
||||||
|
|
|
@ -3,21 +3,21 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import ast
|
import ast
|
||||||
|
import logging
|
||||||
|
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
from subliminal.cache import region
|
from subliminal.cache import region
|
||||||
from simpleconfigparser import simpleconfigparser, configparser, NoOptionError
|
from dynaconf import Dynaconf, Validator as OriginalValidator
|
||||||
|
from dynaconf.loaders.yaml_loader import write
|
||||||
|
from dynaconf.validator import ValidationError
|
||||||
|
from dynaconf.utils.functional import empty
|
||||||
|
from ipaddress import ip_address
|
||||||
|
from binascii import hexlify
|
||||||
|
from types import MappingProxyType
|
||||||
|
|
||||||
from .get_args import args
|
from .get_args import args
|
||||||
|
|
||||||
|
NoneType = type(None)
|
||||||
class SimpleConfigParser(simpleconfigparser):
|
|
||||||
|
|
||||||
def get(self, section, option, raw=False, vars=None):
|
|
||||||
try:
|
|
||||||
return configparser.get(self, section, option, raw=raw, vars=vars)
|
|
||||||
except NoOptionError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def base_url_slash_cleaner(uri):
|
def base_url_slash_cleaner(uri):
|
||||||
|
@ -26,275 +26,371 @@ def base_url_slash_cleaner(uri):
|
||||||
return uri
|
return uri
|
||||||
|
|
||||||
|
|
||||||
defaults = {
|
def validate_ip_address(ip_string):
|
||||||
'general': {
|
try:
|
||||||
'ip': '0.0.0.0',
|
ip_address(ip_string)
|
||||||
'port': '6767',
|
return True
|
||||||
'base_url': '',
|
except ValueError:
|
||||||
'path_mappings': '[]',
|
return False
|
||||||
'debug': 'False',
|
|
||||||
'branch': 'master',
|
|
||||||
'auto_update': 'True',
|
class Validator(OriginalValidator):
|
||||||
'single_language': 'False',
|
# Give the ability to personalize messages sent by the original dynasync Validator class.
|
||||||
'minimum_score': '90',
|
default_messages = MappingProxyType(
|
||||||
'use_scenename': 'True',
|
{
|
||||||
'use_postprocessing': 'False',
|
"must_exist_true": "{name} is required",
|
||||||
'postprocessing_cmd': '',
|
"must_exist_false": "{name} cannot exists",
|
||||||
'postprocessing_threshold': '90',
|
"condition": "{name} invalid for {function}({value})",
|
||||||
'use_postprocessing_threshold': 'False',
|
"operations": "{name} must {operation} {op_value} but it is {value}",
|
||||||
'postprocessing_threshold_movie': '70',
|
"combined": "combined validators failed {errors}",
|
||||||
'use_postprocessing_threshold_movie': 'False',
|
}
|
||||||
'use_sonarr': 'False',
|
)
|
||||||
'use_radarr': 'False',
|
|
||||||
'path_mappings_movie': '[]',
|
|
||||||
'serie_default_enabled': 'False',
|
validators = [
|
||||||
'serie_default_profile': '',
|
# general section
|
||||||
'movie_default_enabled': 'False',
|
Validator('general.flask_secret_key', must_exist=True, default=hexlify(os.urandom(16)).decode(),
|
||||||
'movie_default_profile': '',
|
is_type_of=str),
|
||||||
'page_size': '25',
|
Validator('general.ip', must_exist=True, default='0.0.0.0', is_type_of=str, condition=validate_ip_address),
|
||||||
'theme': 'auto',
|
Validator('general.port', must_exist=True, default=6767, is_type_of=int, gte=1, lte=65535),
|
||||||
'page_size_manual_search': '10',
|
Validator('general.base_url', must_exist=True, default='', is_type_of=str),
|
||||||
'minimum_score_movie': '70',
|
Validator('general.path_mappings', must_exist=True, default=[], is_type_of=list),
|
||||||
'use_embedded_subs': 'True',
|
Validator('general.debug', must_exist=True, default=False, is_type_of=bool),
|
||||||
'embedded_subs_show_desired': 'True',
|
Validator('general.branch', must_exist=True, default='master', is_type_of=str,
|
||||||
'utf8_encode': 'True',
|
is_in=['master', 'development']),
|
||||||
'ignore_pgs_subs': 'False',
|
Validator('general.auto_update', must_exist=True, default=True, is_type_of=bool),
|
||||||
'ignore_vobsub_subs': 'False',
|
Validator('general.single_language', must_exist=True, default=False, is_type_of=bool),
|
||||||
'ignore_ass_subs': 'False',
|
Validator('general.minimum_score', must_exist=True, default=90, is_type_of=int, gte=0, lte=100),
|
||||||
'adaptive_searching': 'True',
|
Validator('general.use_scenename', must_exist=True, default=True, is_type_of=bool),
|
||||||
'adaptive_searching_delay': '3w',
|
Validator('general.use_postprocessing', must_exist=True, default=False, is_type_of=bool),
|
||||||
'adaptive_searching_delta': '1w',
|
Validator('general.postprocessing_cmd', must_exist=True, default='', is_type_of=str),
|
||||||
'enabled_providers': '[]',
|
Validator('general.postprocessing_threshold', must_exist=True, default=90, is_type_of=int, gte=0, lte=100),
|
||||||
'multithreading': 'True',
|
Validator('general.use_postprocessing_threshold', must_exist=True, default=False, is_type_of=bool),
|
||||||
'chmod_enabled': 'False',
|
Validator('general.postprocessing_threshold_movie', must_exist=True, default=70, is_type_of=int, gte=0,
|
||||||
'chmod': '0640',
|
lte=100),
|
||||||
'subfolder': 'current',
|
Validator('general.use_postprocessing_threshold_movie', must_exist=True, default=False, is_type_of=bool),
|
||||||
'subfolder_custom': '',
|
Validator('general.use_sonarr', must_exist=True, default=False, is_type_of=bool),
|
||||||
'upgrade_subs': 'True',
|
Validator('general.use_radarr', must_exist=True, default=False, is_type_of=bool),
|
||||||
'upgrade_frequency': '12',
|
Validator('general.path_mappings_movie', must_exist=True, default=[], is_type_of=list),
|
||||||
'days_to_upgrade_subs': '7',
|
Validator('general.serie_default_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
'upgrade_manual': 'True',
|
Validator('general.serie_default_profile', must_exist=True, default='', is_type_of=(int, str)),
|
||||||
'anti_captcha_provider': 'None',
|
Validator('general.movie_default_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
'wanted_search_frequency': '6',
|
Validator('general.movie_default_profile', must_exist=True, default='', is_type_of=(int, str)),
|
||||||
'wanted_search_frequency_movie': '6',
|
Validator('general.page_size', must_exist=True, default=25, is_type_of=int,
|
||||||
'subzero_mods': '[]',
|
is_in=[25, 50, 100, 250, 500, 1000]),
|
||||||
'dont_notify_manual_actions': 'False',
|
Validator('general.theme', must_exist=True, default='auto', is_type_of=str,
|
||||||
'hi_extension': 'hi',
|
is_in=['auto', 'light', 'dark']),
|
||||||
'embedded_subtitles_parser': 'ffprobe',
|
Validator('general.minimum_score_movie', must_exist=True, default=70, is_type_of=int, gte=0, lte=100),
|
||||||
'default_und_audio_lang': '',
|
Validator('general.use_embedded_subs', must_exist=True, default=True, is_type_of=bool),
|
||||||
'default_und_embedded_subtitles_lang': '',
|
Validator('general.embedded_subs_show_desired', must_exist=True, default=True, is_type_of=bool),
|
||||||
'parse_embedded_audio_track': 'False',
|
Validator('general.utf8_encode', must_exist=True, default=True, is_type_of=bool),
|
||||||
'skip_hashing': 'False',
|
Validator('general.ignore_pgs_subs', must_exist=True, default=False, is_type_of=bool),
|
||||||
'language_equals': '[]',
|
Validator('general.ignore_vobsub_subs', must_exist=True, default=False, is_type_of=bool),
|
||||||
},
|
Validator('general.ignore_ass_subs', must_exist=True, default=False, is_type_of=bool),
|
||||||
'auth': {
|
Validator('general.adaptive_searching', must_exist=True, default=True, is_type_of=bool),
|
||||||
'type': 'None',
|
Validator('general.adaptive_searching_delay', must_exist=True, default='3w', is_type_of=str,
|
||||||
'username': '',
|
is_in=['1w', '2w', '3w', '4w']),
|
||||||
'password': ''
|
Validator('general.adaptive_searching_delta', must_exist=True, default='1w', is_type_of=str,
|
||||||
},
|
is_in=['3d', '1w', '2w', '3w', '4w']),
|
||||||
'cors': {
|
Validator('general.enabled_providers', must_exist=True, default=[], is_type_of=list),
|
||||||
'enabled': 'False'
|
Validator('general.multithreading', must_exist=True, default=True, is_type_of=bool),
|
||||||
},
|
Validator('general.chmod_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
'backup': {
|
Validator('general.chmod', must_exist=True, default='0640', is_type_of=str),
|
||||||
'folder': os.path.join(args.config_dir, 'backup'),
|
Validator('general.subfolder', must_exist=True, default='current', is_type_of=str),
|
||||||
'retention': '31',
|
Validator('general.subfolder_custom', must_exist=True, default='', is_type_of=str),
|
||||||
'frequency': 'Weekly',
|
Validator('general.upgrade_subs', must_exist=True, default=True, is_type_of=bool),
|
||||||
'day': '6',
|
Validator('general.upgrade_frequency', must_exist=True, default=12, is_type_of=int, is_in=[6, 12, 24]),
|
||||||
'hour': '3'
|
Validator('general.days_to_upgrade_subs', must_exist=True, default=7, is_type_of=int, gte=0, lte=30),
|
||||||
},
|
Validator('general.upgrade_manual', must_exist=True, default=True, is_type_of=bool),
|
||||||
'sonarr': {
|
Validator('general.anti_captcha_provider', must_exist=True, default=None, is_type_of=(NoneType, str),
|
||||||
'ip': '127.0.0.1',
|
is_in=[None, 'anti-captcha', 'death-by-captcha']),
|
||||||
'port': '8989',
|
Validator('general.wanted_search_frequency', must_exist=True, default=6, is_type_of=int, is_in=[6, 12, 24]),
|
||||||
'base_url': '/',
|
Validator('general.wanted_search_frequency_movie', must_exist=True, default=6, is_type_of=int,
|
||||||
'ssl': 'False',
|
is_in=[6, 12, 24]),
|
||||||
'http_timeout': '60',
|
Validator('general.subzero_mods', must_exist=True, default='', is_type_of=str),
|
||||||
'apikey': '',
|
Validator('general.dont_notify_manual_actions', must_exist=True, default=False, is_type_of=bool),
|
||||||
'full_update': 'Daily',
|
Validator('general.hi_extension', must_exist=True, default='hi', is_type_of=str, is_in=['hi', 'cc', 'sdh']),
|
||||||
'full_update_day': '6',
|
Validator('general.embedded_subtitles_parser', must_exist=True, default='ffprobe', is_type_of=str,
|
||||||
'full_update_hour': '4',
|
is_in=['ffprobe', 'mediainfo']),
|
||||||
'only_monitored': 'False',
|
Validator('general.default_und_audio_lang', must_exist=True, default='', is_type_of=str),
|
||||||
'series_sync': '60',
|
Validator('general.default_und_embedded_subtitles_lang', must_exist=True, default='', is_type_of=str),
|
||||||
'episodes_sync': '60',
|
Validator('general.parse_embedded_audio_track', must_exist=True, default=False, is_type_of=bool),
|
||||||
'excluded_tags': '[]',
|
Validator('general.skip_hashing', must_exist=True, default=False, is_type_of=bool),
|
||||||
'excluded_series_types': '[]',
|
Validator('general.language_equals', must_exist=True, default=[], is_type_of=list),
|
||||||
'use_ffprobe_cache': 'True',
|
|
||||||
'exclude_season_zero': 'False',
|
# auth section
|
||||||
'defer_search_signalr': 'False'
|
Validator('auth.apikey', must_exist=True, default=hexlify(os.urandom(16)).decode(), is_type_of=str),
|
||||||
},
|
Validator('auth.type', must_exist=True, default=None, is_type_of=(NoneType, str),
|
||||||
'radarr': {
|
is_in=[None, 'basic', 'form']),
|
||||||
'ip': '127.0.0.1',
|
Validator('auth.username', must_exist=True, default='', is_type_of=str),
|
||||||
'port': '7878',
|
Validator('auth.password', must_exist=True, default='', is_type_of=str),
|
||||||
'base_url': '/',
|
|
||||||
'ssl': 'False',
|
# cors section
|
||||||
'http_timeout': '60',
|
Validator('cors.enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
'apikey': '',
|
|
||||||
'full_update': 'Daily',
|
# backup section
|
||||||
'full_update_day': '6',
|
Validator('backup.folder', must_exist=True, default=os.path.join(args.config_dir, 'backup'),
|
||||||
'full_update_hour': '5',
|
is_type_of=str),
|
||||||
'only_monitored': 'False',
|
Validator('backup.retention', must_exist=True, default=31, is_type_of=int, gte=0),
|
||||||
'movies_sync': '60',
|
Validator('backup.frequency', must_exist=True, default='Weekly', is_type_of=str,
|
||||||
'excluded_tags': '[]',
|
is_in=['Manually', 'Daily', 'Weekly']),
|
||||||
'use_ffprobe_cache': 'True',
|
Validator('backup.day', must_exist=True, default=6, is_type_of=int, gte=0, lte=6),
|
||||||
'defer_search_signalr': 'False'
|
Validator('backup.hour', must_exist=True, default=3, is_type_of=int, gte=0, lte=23),
|
||||||
},
|
|
||||||
'proxy': {
|
# sonarr section
|
||||||
'type': 'None',
|
Validator('sonarr.ip', must_exist=True, default='127.0.0.1', is_type_of=str),
|
||||||
'url': '',
|
Validator('sonarr.port', must_exist=True, default=8989, is_type_of=int, gte=1, lte=65535),
|
||||||
'port': '',
|
Validator('sonarr.base_url', must_exist=True, default='/', is_type_of=str),
|
||||||
'username': '',
|
Validator('sonarr.ssl', must_exist=True, default=False, is_type_of=bool),
|
||||||
'password': '',
|
Validator('sonarr.http_timeout', must_exist=True, default=60, is_type_of=int,
|
||||||
'exclude': '["localhost","127.0.0.1"]'
|
is_in=[60, 120, 180, 240, 300, 600]),
|
||||||
},
|
Validator('sonarr.apikey', must_exist=True, default='', is_type_of=str),
|
||||||
'opensubtitles': {
|
Validator('sonarr.full_update', must_exist=True, default='Daily', is_type_of=str,
|
||||||
'username': '',
|
is_in=['Manually', 'Daily', 'Weekly']),
|
||||||
'password': '',
|
Validator('sonarr.full_update_day', must_exist=True, default=6, is_type_of=int, gte=0, lte=6),
|
||||||
'use_tag_search': 'False',
|
Validator('sonarr.full_update_hour', must_exist=True, default=4, is_type_of=int, gte=0, lte=23),
|
||||||
'vip': 'False',
|
Validator('sonarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
|
||||||
'ssl': 'False',
|
Validator('sonarr.series_sync', must_exist=True, default=60, is_type_of=int,
|
||||||
'timeout': '15',
|
is_in=[15, 60, 180, 360, 720, 1440]),
|
||||||
'skip_wrong_fps': 'False'
|
Validator('sonarr.episodes_sync', must_exist=True, default=60, is_type_of=int,
|
||||||
},
|
is_in=[15, 60, 180, 360, 720, 1440]),
|
||||||
'opensubtitlescom': {
|
Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
|
||||||
'username': '',
|
Validator('sonarr.excluded_series_types', must_exist=True, default=[], is_type_of=list),
|
||||||
'password': '',
|
Validator('sonarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
|
||||||
'use_hash': 'True'
|
Validator('sonarr.exclude_season_zero', must_exist=True, default=False, is_type_of=bool),
|
||||||
},
|
Validator('sonarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool),
|
||||||
'addic7ed': {
|
|
||||||
'username': '',
|
# radarr section
|
||||||
'password': '',
|
Validator('radarr.ip', must_exist=True, default='127.0.0.1', is_type_of=str),
|
||||||
'cookies': '',
|
Validator('radarr.port', must_exist=True, default=7878, is_type_of=int, gte=1, lte=65535),
|
||||||
'user_agent': '',
|
Validator('radarr.base_url', must_exist=True, default='/', is_type_of=str),
|
||||||
'vip': 'False'
|
Validator('radarr.ssl', must_exist=True, default=False, is_type_of=bool),
|
||||||
},
|
Validator('radarr.http_timeout', must_exist=True, default=60, is_type_of=int,
|
||||||
'podnapisi': {
|
is_in=[60, 120, 180, 240, 300, 600]),
|
||||||
'verify_ssl': 'True'
|
Validator('radarr.apikey', must_exist=True, default='', is_type_of=str),
|
||||||
},
|
Validator('radarr.full_update', must_exist=True, default='Daily', is_type_of=str,
|
||||||
'subf2m': {
|
is_in=['Manually', 'Daily', 'Weekly']),
|
||||||
'verify_ssl': 'True',
|
Validator('radarr.full_update_day', must_exist=True, default=6, is_type_of=int, gte=0, lte=6),
|
||||||
'user_agent': ''
|
Validator('radarr.full_update_hour', must_exist=True, default=4, is_type_of=int, gte=0, lte=23),
|
||||||
},
|
Validator('radarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
|
||||||
'whisperai': {
|
Validator('radarr.movies_sync', must_exist=True, default=60, is_type_of=int,
|
||||||
'endpoint': 'http://127.0.0.1:9000',
|
is_in=[15, 60, 180, 360, 720, 1440]),
|
||||||
'timeout': '3600'
|
Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
|
||||||
},
|
Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
|
||||||
'legendasdivx': {
|
Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool),
|
||||||
'username': '',
|
|
||||||
'password': '',
|
# proxy section
|
||||||
'skip_wrong_fps': 'False'
|
Validator('proxy.type', must_exist=True, default=None, is_type_of=(NoneType, str),
|
||||||
},
|
is_in=[None, 'socks5', 'http']),
|
||||||
'ktuvit': {
|
Validator('proxy.url', must_exist=True, default='', is_type_of=str),
|
||||||
'email': '',
|
Validator('proxy.port', must_exist=True, default='', is_type_of=(str, int)),
|
||||||
'hashed_password': ''
|
Validator('proxy.username', must_exist=True, default='', is_type_of=str),
|
||||||
},
|
Validator('proxy.password', must_exist=True, default='', is_type_of=str),
|
||||||
'xsubs': {
|
Validator('proxy.exclude', must_exist=True, default=["localhost", "127.0.0.1"], is_type_of=list),
|
||||||
'username': '',
|
|
||||||
'password': ''
|
# opensubtitles.org section
|
||||||
},
|
Validator('opensubtitles.username', must_exist=True, default='', is_type_of=str),
|
||||||
'assrt': {
|
Validator('opensubtitles.password', must_exist=True, default='', is_type_of=str),
|
||||||
'token': ''
|
Validator('opensubtitles.use_tag_search', must_exist=True, default=False, is_type_of=bool),
|
||||||
},
|
Validator('opensubtitles.vip', must_exist=True, default=False, is_type_of=bool),
|
||||||
'anticaptcha': {
|
Validator('opensubtitles.ssl', must_exist=True, default=False, is_type_of=bool),
|
||||||
'anti_captcha_key': ''
|
Validator('opensubtitles.timeout', must_exist=True, default=15, is_type_of=int, gte=1),
|
||||||
},
|
Validator('opensubtitles.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool),
|
||||||
'deathbycaptcha': {
|
|
||||||
'username': '',
|
# opensubtitles.com section
|
||||||
'password': ''
|
Validator('opensubtitlescom.username', must_exist=True, default='', is_type_of=str),
|
||||||
},
|
Validator('opensubtitlescom.password', must_exist=True, default='', is_type_of=str),
|
||||||
'napisy24': {
|
Validator('opensubtitlescom.use_hash', must_exist=True, default=True, is_type_of=bool),
|
||||||
'username': '',
|
|
||||||
'password': ''
|
# addic7ed section
|
||||||
},
|
Validator('addic7ed.username', must_exist=True, default='', is_type_of=str),
|
||||||
'subscene': {
|
Validator('addic7ed.password', must_exist=True, default='', is_type_of=str),
|
||||||
'username': '',
|
Validator('addic7ed.cookies', must_exist=True, default='', is_type_of=str),
|
||||||
'password': ''
|
Validator('addic7ed.user_agent', must_exist=True, default='', is_type_of=str),
|
||||||
},
|
Validator('addic7ed.vip', must_exist=True, default=False, is_type_of=bool),
|
||||||
'betaseries': {
|
|
||||||
'token': ''
|
# podnapisi section
|
||||||
},
|
Validator('podnapisi.verify_ssl', must_exist=True, default=True, is_type_of=bool),
|
||||||
'analytics': {
|
|
||||||
'enabled': 'True'
|
# subf2m section
|
||||||
},
|
Validator('subf2m.verify_ssl', must_exist=True, default=True, is_type_of=bool),
|
||||||
'titlovi': {
|
Validator('subf2m.user_agent', must_exist=True, default='', is_type_of=str),
|
||||||
'username': '',
|
|
||||||
'password': ''
|
# hdbits section
|
||||||
},
|
Validator('hdbits.username', must_exist=True, default='', is_type_of=str),
|
||||||
'titulky': {
|
Validator('hdbits.passkey', must_exist=True, default='', is_type_of=str),
|
||||||
'username': '',
|
|
||||||
'password': '',
|
# whisperai section
|
||||||
'approved_only': 'False'
|
Validator('whisperai.endpoint', must_exist=True, default='http://127.0.0.1:9000', is_type_of=str),
|
||||||
},
|
Validator('whisperai.timeout', must_exist=True, default=3600, is_type_of=int, gte=1),
|
||||||
'embeddedsubtitles': {
|
|
||||||
'included_codecs': '[]',
|
# legendasdivx section
|
||||||
'hi_fallback': 'False',
|
Validator('legendasdivx.username', must_exist=True, default='', is_type_of=str),
|
||||||
'timeout': '600',
|
Validator('legendasdivx.password', must_exist=True, default='', is_type_of=str),
|
||||||
'unknown_as_english': 'False',
|
Validator('legendasdivx.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool),
|
||||||
},
|
|
||||||
'hdbits': {
|
# ktuvit section
|
||||||
'username': '',
|
Validator('ktuvit.email', must_exist=True, default='', is_type_of=str),
|
||||||
'passkey': '',
|
Validator('ktuvit.hashed_password', must_exist=True, default='', is_type_of=str),
|
||||||
},
|
|
||||||
'karagarga': {
|
# xsubs section
|
||||||
'username': '',
|
Validator('xsubs.username', must_exist=True, default='', is_type_of=str),
|
||||||
'password': '',
|
Validator('xsubs.password', must_exist=True, default='', is_type_of=str),
|
||||||
'f_username': '',
|
|
||||||
'f_password': '',
|
# assrt section
|
||||||
},
|
Validator('assrt.token', must_exist=True, default='', is_type_of=str),
|
||||||
'subsync': {
|
|
||||||
'use_subsync': 'False',
|
# anticaptcha section
|
||||||
'use_subsync_threshold': 'False',
|
Validator('anticaptcha.anti_captcha_key', must_exist=True, default='', is_type_of=str),
|
||||||
'subsync_threshold': '90',
|
|
||||||
'use_subsync_movie_threshold': 'False',
|
# deathbycaptcha section
|
||||||
'subsync_movie_threshold': '70',
|
Validator('deathbycaptcha.username', must_exist=True, default='', is_type_of=str),
|
||||||
'debug': 'False',
|
Validator('deathbycaptcha.password', must_exist=True, default='', is_type_of=str),
|
||||||
'force_audio': 'False'
|
|
||||||
},
|
# napisy24 section
|
||||||
'series_scores': {
|
Validator('napisy24.username', must_exist=True, default='', is_type_of=str),
|
||||||
"hash": 359,
|
Validator('napisy24.password', must_exist=True, default='', is_type_of=str),
|
||||||
"series": 180,
|
|
||||||
"year": 90,
|
# subscene section
|
||||||
"season": 30,
|
Validator('subscene.username', must_exist=True, default='', is_type_of=str),
|
||||||
"episode": 30,
|
Validator('subscene.password', must_exist=True, default='', is_type_of=str),
|
||||||
"release_group": 14,
|
|
||||||
"source": 7,
|
# betaseries section
|
||||||
"audio_codec": 3,
|
Validator('betaseries.token', must_exist=True, default='', is_type_of=str),
|
||||||
"resolution": 2,
|
|
||||||
"video_codec": 2,
|
# analytics section
|
||||||
"streaming_service": 1,
|
Validator('analytics.enabled', must_exist=True, default=True, is_type_of=bool),
|
||||||
"hearing_impaired": 1,
|
|
||||||
},
|
# titlovi section
|
||||||
'movie_scores': {
|
Validator('titlovi.username', must_exist=True, default='', is_type_of=str),
|
||||||
"hash": 119,
|
Validator('titlovi.password', must_exist=True, default='', is_type_of=str),
|
||||||
"title": 60,
|
|
||||||
"year": 30,
|
# titulky section
|
||||||
"release_group": 13,
|
Validator('titulky.username', must_exist=True, default='', is_type_of=str),
|
||||||
"source": 7,
|
Validator('titulky.password', must_exist=True, default='', is_type_of=str),
|
||||||
"audio_codec": 3,
|
Validator('titulky.approved_only', must_exist=True, default=False, is_type_of=bool),
|
||||||
"resolution": 2,
|
|
||||||
"video_codec": 2,
|
# embeddedsubtitles section
|
||||||
"streaming_service": 1,
|
Validator('embeddedsubtitles.included_codecs', must_exist=True, default=[], is_type_of=list),
|
||||||
"edition": 1,
|
Validator('embeddedsubtitles.hi_fallback', must_exist=True, default=False, is_type_of=bool),
|
||||||
"hearing_impaired": 1,
|
Validator('embeddedsubtitles.timeout', must_exist=True, default=600, is_type_of=int, gte=1),
|
||||||
},
|
Validator('embeddedsubtitles.unknown_as_english', must_exist=True, default=False, is_type_of=bool),
|
||||||
'postgresql': {
|
|
||||||
'enabled': 'False',
|
# karagarga section
|
||||||
'host': 'localhost',
|
Validator('karagarga.username', must_exist=True, default='', is_type_of=str),
|
||||||
'port': '5432',
|
Validator('karagarga.password', must_exist=True, default='', is_type_of=str),
|
||||||
'database': '',
|
Validator('karagarga.f_username', must_exist=True, default='', is_type_of=str),
|
||||||
'username': '',
|
Validator('karagarga.f_password', must_exist=True, default='', is_type_of=str),
|
||||||
'password': '',
|
|
||||||
},
|
# subsync section
|
||||||
}
|
Validator('subsync.use_subsync', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('subsync.use_subsync_threshold', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('subsync.subsync_threshold', must_exist=True, default=90, is_type_of=int, gte=0, lte=100),
|
||||||
|
Validator('subsync.use_subsync_movie_threshold', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('subsync.subsync_movie_threshold', must_exist=True, default=70, is_type_of=int, gte=0, lte=100),
|
||||||
|
Validator('subsync.debug', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('subsync.force_audio', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
|
||||||
|
# series_scores section
|
||||||
|
Validator('series_scores.hash', must_exist=True, default=359, is_type_of=int),
|
||||||
|
Validator('series_scores.series', must_exist=True, default=180, is_type_of=int),
|
||||||
|
Validator('series_scores.year', must_exist=True, default=90, is_type_of=int),
|
||||||
|
Validator('series_scores.season', must_exist=True, default=30, is_type_of=int),
|
||||||
|
Validator('series_scores.episode', must_exist=True, default=30, is_type_of=int),
|
||||||
|
Validator('series_scores.release_group', must_exist=True, default=14, is_type_of=int),
|
||||||
|
Validator('series_scores.source', must_exist=True, default=7, is_type_of=int),
|
||||||
|
Validator('series_scores.audio_codec', must_exist=True, default=3, is_type_of=int),
|
||||||
|
Validator('series_scores.resolution', must_exist=True, default=2, is_type_of=int),
|
||||||
|
Validator('series_scores.video_codec', must_exist=True, default=2, is_type_of=int),
|
||||||
|
Validator('series_scores.streaming_service', must_exist=True, default=1, is_type_of=int),
|
||||||
|
Validator('series_scores.hearing_impaired', must_exist=True, default=1, is_type_of=int),
|
||||||
|
|
||||||
|
# movie_scores section
|
||||||
|
Validator('movie_scores.hash', must_exist=True, default=119, is_type_of=int),
|
||||||
|
Validator('movie_scores.title', must_exist=True, default=60, is_type_of=int),
|
||||||
|
Validator('movie_scores.year', must_exist=True, default=30, is_type_of=int),
|
||||||
|
Validator('movie_scores.release_group', must_exist=True, default=13, is_type_of=int),
|
||||||
|
Validator('movie_scores.source', must_exist=True, default=7, is_type_of=int),
|
||||||
|
Validator('movie_scores.audio_codec', must_exist=True, default=3, is_type_of=int),
|
||||||
|
Validator('movie_scores.resolution', must_exist=True, default=2, is_type_of=int),
|
||||||
|
Validator('movie_scores.video_codec', must_exist=True, default=2, is_type_of=int),
|
||||||
|
Validator('movie_scores.streaming_service', must_exist=True, default=1, is_type_of=int),
|
||||||
|
Validator('movie_scores.edition', must_exist=True, default=1, is_type_of=int),
|
||||||
|
Validator('movie_scores.hearing_impaired', must_exist=True, default=1, is_type_of=int),
|
||||||
|
|
||||||
|
# postgresql section
|
||||||
|
Validator('postgresql.enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('postgresql.host', must_exist=True, default='localhost', is_type_of=str),
|
||||||
|
Validator('postgresql.port', must_exist=True, default=5432, is_type_of=int, gte=1, lte=65535),
|
||||||
|
Validator('postgresql.database', must_exist=True, default='', is_type_of=str),
|
||||||
|
Validator('postgresql.username', must_exist=True, default='', is_type_of=str),
|
||||||
|
Validator('postgresql.password', must_exist=True, default='', is_type_of=str),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def convert_ini_to_yaml(config_file):
|
||||||
|
import configparser
|
||||||
|
import yaml
|
||||||
|
config_object = configparser.RawConfigParser()
|
||||||
|
file = open(config_file, "r")
|
||||||
|
config_object.read_file(file)
|
||||||
|
output_dict = dict()
|
||||||
|
sections = config_object.sections()
|
||||||
|
for section in sections:
|
||||||
|
items = config_object.items(section)
|
||||||
|
output_dict[section] = dict()
|
||||||
|
for item in items:
|
||||||
|
try:
|
||||||
|
output_dict[section].update({item[0]: ast.literal_eval(item[1])})
|
||||||
|
except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
|
||||||
|
output_dict[section].update({item[0]: item[1]})
|
||||||
|
with open(os.path.join(os.path.dirname(config_file), 'config.yaml'), 'w') as file:
|
||||||
|
yaml.dump(output_dict, file)
|
||||||
|
os.replace(config_file, f'{config_file}.old')
|
||||||
|
|
||||||
|
|
||||||
|
config_yaml_file = os.path.join(args.config_dir, 'config', 'config.yaml')
|
||||||
|
config_ini_file = os.path.join(args.config_dir, 'config', 'config.ini')
|
||||||
|
if os.path.exists(config_ini_file) and not os.path.exists(config_yaml_file):
|
||||||
|
convert_ini_to_yaml(config_ini_file)
|
||||||
|
elif not os.path.exists(config_yaml_file):
|
||||||
|
if not os.path.isdir(os.path.dirname(config_yaml_file)):
|
||||||
|
os.makedirs(os.path.dirname(config_yaml_file))
|
||||||
|
open(config_yaml_file, mode='w').close()
|
||||||
|
|
||||||
|
settings = Dynaconf(
|
||||||
|
settings_file=config_yaml_file,
|
||||||
|
core_loaders=['YAML'],
|
||||||
|
apply_default_on_none=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
settings.validators.register(*validators)
|
||||||
|
|
||||||
|
failed_validator = True
|
||||||
|
while failed_validator:
|
||||||
|
try:
|
||||||
|
settings.validators.validate_all()
|
||||||
|
failed_validator = False
|
||||||
|
except ValidationError as e:
|
||||||
|
current_validator_details = e.details[0][0]
|
||||||
|
if hasattr(current_validator_details, 'default') and current_validator_details.default is not empty:
|
||||||
|
settings[current_validator_details.names[0]] = current_validator_details.default
|
||||||
|
else:
|
||||||
|
logging.critical(f"Value for {current_validator_details.names[0]} doesn't pass validation and there's no "
|
||||||
|
f"default value. This issue must be reported. Bazarr won't works until it's been fixed.")
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def write_config():
|
||||||
|
write(settings_path=config_yaml_file,
|
||||||
|
settings_data={k.lower(): v for k, v in settings.as_dict().items()},
|
||||||
|
merge=False)
|
||||||
|
|
||||||
settings = SimpleConfigParser(defaults=defaults, interpolation=None)
|
|
||||||
settings.read(os.path.join(args.config_dir, 'config', 'config.ini'))
|
|
||||||
|
|
||||||
settings.general.base_url = settings.general.base_url if settings.general.base_url else '/'
|
|
||||||
base_url = settings.general.base_url.rstrip('/')
|
base_url = settings.general.base_url.rstrip('/')
|
||||||
|
|
||||||
ignore_keys = ['flask_secret_key']
|
ignore_keys = ['flask_secret_key']
|
||||||
|
|
||||||
raw_keys = ['movie_default_forced', 'serie_default_forced']
|
|
||||||
|
|
||||||
array_keys = ['excluded_tags',
|
array_keys = ['excluded_tags',
|
||||||
'exclude',
|
'exclude',
|
||||||
'included_codecs',
|
'included_codecs',
|
||||||
|
@ -305,79 +401,50 @@ array_keys = ['excluded_tags',
|
||||||
'path_mappings_movie',
|
'path_mappings_movie',
|
||||||
'language_equals']
|
'language_equals']
|
||||||
|
|
||||||
str_keys = ['chmod']
|
|
||||||
|
|
||||||
empty_values = ['', 'None', 'null', 'undefined', None, []]
|
empty_values = ['', 'None', 'null', 'undefined', None, []]
|
||||||
|
|
||||||
|
str_keys = ['chmod']
|
||||||
|
|
||||||
# Increase Sonarr and Radarr sync interval since we now use SignalR feed to update in real time
|
# Increase Sonarr and Radarr sync interval since we now use SignalR feed to update in real time
|
||||||
if int(settings.sonarr.series_sync) < 15:
|
if settings.sonarr.series_sync < 15:
|
||||||
settings.sonarr.series_sync = "60"
|
settings.sonarr.series_sync = 60
|
||||||
if int(settings.sonarr.episodes_sync) < 15:
|
if settings.sonarr.episodes_sync < 15:
|
||||||
settings.sonarr.episodes_sync = "60"
|
settings.sonarr.episodes_sync = 60
|
||||||
if int(settings.radarr.movies_sync) < 15:
|
if settings.radarr.movies_sync < 15:
|
||||||
settings.radarr.movies_sync = "60"
|
settings.radarr.movies_sync = 60
|
||||||
|
|
||||||
# Make sure to get of double slashes in base_url
|
# Make sure to get of double slashes in base_url
|
||||||
settings.general.base_url = base_url_slash_cleaner(uri=settings.general.base_url)
|
settings.general.base_url = base_url_slash_cleaner(uri=settings.general.base_url)
|
||||||
settings.sonarr.base_url = base_url_slash_cleaner(uri=settings.sonarr.base_url)
|
settings.sonarr.base_url = base_url_slash_cleaner(uri=settings.sonarr.base_url)
|
||||||
settings.radarr.base_url = base_url_slash_cleaner(uri=settings.radarr.base_url)
|
settings.radarr.base_url = base_url_slash_cleaner(uri=settings.radarr.base_url)
|
||||||
|
|
||||||
# fixing issue with improper page_size value
|
|
||||||
if settings.general.page_size not in ['25', '50', '100', '250', '500', '1000']:
|
|
||||||
settings.general.page_size = defaults['general']['page_size']
|
|
||||||
|
|
||||||
# increase delay between searches to reduce impact on providers
|
# increase delay between searches to reduce impact on providers
|
||||||
if settings.general.wanted_search_frequency == '3':
|
if settings.general.wanted_search_frequency == 3:
|
||||||
settings.general.wanted_search_frequency = '6'
|
settings.general.wanted_search_frequency = 6
|
||||||
if settings.general.wanted_search_frequency_movie == '3':
|
if settings.general.wanted_search_frequency_movie == 3:
|
||||||
settings.general.wanted_search_frequency_movie = '6'
|
settings.general.wanted_search_frequency_movie = 6
|
||||||
|
|
||||||
# save updated settings to file
|
# save updated settings to file
|
||||||
if os.path.exists(os.path.join(args.config_dir, 'config', 'config.ini')):
|
write_config()
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
|
|
||||||
def get_settings():
|
def get_settings():
|
||||||
result = dict()
|
# return {k.lower(): v for k, v in settings.as_dict().items()}
|
||||||
sections = settings.sections()
|
settings_to_return = {}
|
||||||
|
for k, v in settings.as_dict().items():
|
||||||
for sec in sections:
|
if isinstance(v, dict):
|
||||||
sec_values = settings.items(sec, False)
|
k = k.lower()
|
||||||
values_dict = dict()
|
settings_to_return[k] = dict()
|
||||||
|
for subk, subv in v.items():
|
||||||
for sec_val in sec_values:
|
if subk.lower() in ignore_keys:
|
||||||
key = sec_val[0]
|
continue
|
||||||
value = sec_val[1]
|
if subv in empty_values and subk.lower() in array_keys:
|
||||||
|
settings_to_return[k].update({subk: []})
|
||||||
if key in ignore_keys:
|
elif subk == 'subzero_mods':
|
||||||
continue
|
settings_to_return[k].update({subk: get_array_from(subv)})
|
||||||
|
|
||||||
if key not in raw_keys:
|
|
||||||
# Do some postprocessings
|
|
||||||
if value in empty_values:
|
|
||||||
if key in array_keys:
|
|
||||||
value = []
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
elif key in array_keys:
|
|
||||||
value = get_array_from(value)
|
|
||||||
elif value == 'True':
|
|
||||||
value = True
|
|
||||||
elif value == 'False':
|
|
||||||
value = False
|
|
||||||
else:
|
else:
|
||||||
if key not in str_keys:
|
settings_to_return[k].update({subk: subv})
|
||||||
try:
|
return settings_to_return
|
||||||
value = int(value)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
values_dict[key] = value
|
|
||||||
|
|
||||||
result[sec] = values_dict
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def save_settings(settings_items):
|
def save_settings(settings_items):
|
||||||
|
@ -408,24 +475,31 @@ def save_settings(settings_items):
|
||||||
|
|
||||||
settings_keys = key.split('-')
|
settings_keys = key.split('-')
|
||||||
|
|
||||||
# Make sure that text based form values aren't pass as list
|
# Make sure that text based form values aren't passed as list
|
||||||
if isinstance(value, list) and len(value) == 1 and settings_keys[-1] not in array_keys:
|
if isinstance(value, list) and len(value) == 1 and settings_keys[-1] not in array_keys:
|
||||||
value = value[0]
|
value = value[0]
|
||||||
if value in empty_values and value != '':
|
if value in empty_values and value != '':
|
||||||
value = None
|
value = None
|
||||||
|
|
||||||
|
# try to cast string as integer
|
||||||
|
if isinstance(value, str) and settings_keys[-1] not in str_keys:
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Make sure empty language list are stored correctly
|
# Make sure empty language list are stored correctly
|
||||||
if settings_keys[-1] in array_keys and value[0] in empty_values:
|
if settings_keys[-1] in array_keys and value[0] in empty_values:
|
||||||
value = []
|
value = []
|
||||||
|
|
||||||
# Handle path mappings settings since they are array in array
|
# Handle path mappings settings since they are array in array
|
||||||
if settings_keys[-1] in ['path_mappings', 'path_mappings_movie']:
|
if settings_keys[-1] in ['path_mappings', 'path_mappings_movie']:
|
||||||
value = [v.split(',') for v in value]
|
value = [x.split(',') for x in value if isinstance(x, str)]
|
||||||
|
|
||||||
if value == 'true':
|
if value == 'true':
|
||||||
value = 'True'
|
value = True
|
||||||
elif value == 'false':
|
elif value == 'false':
|
||||||
value = 'False'
|
value = False
|
||||||
|
|
||||||
if key in ['settings-general-use_embedded_subs', 'settings-general-ignore_pgs_subs',
|
if key in ['settings-general-use_embedded_subs', 'settings-general-ignore_pgs_subs',
|
||||||
'settings-general-ignore_vobsub_subs', 'settings-general-ignore_ass_subs']:
|
'settings-general-ignore_vobsub_subs', 'settings-general-ignore_ass_subs']:
|
||||||
|
@ -553,14 +627,13 @@ def save_settings(settings_items):
|
||||||
reset_throttled_providers(only_auth_or_conf_error=True)
|
reset_throttled_providers(only_auth_or_conf_error=True)
|
||||||
|
|
||||||
if settings_keys[0] == 'settings':
|
if settings_keys[0] == 'settings':
|
||||||
settings[settings_keys[1]][settings_keys[2]] = str(value)
|
settings[settings_keys[1]][settings_keys[2]] = value
|
||||||
|
|
||||||
if settings_keys[0] == 'subzero':
|
if settings_keys[0] == 'subzero':
|
||||||
mod = settings_keys[1]
|
mod = settings_keys[1]
|
||||||
enabled = value == 'True'
|
if mod in subzero_mods and not value:
|
||||||
if mod in subzero_mods and not enabled:
|
|
||||||
subzero_mods.remove(mod)
|
subzero_mods.remove(mod)
|
||||||
elif enabled:
|
elif value:
|
||||||
subzero_mods.append(mod)
|
subzero_mods.append(mod)
|
||||||
|
|
||||||
# Handle color
|
# Handle color
|
||||||
|
@ -581,77 +654,82 @@ def save_settings(settings_items):
|
||||||
from .scheduler import scheduler
|
from .scheduler import scheduler
|
||||||
from subtitles.indexer.series import list_missing_subtitles
|
from subtitles.indexer.series import list_missing_subtitles
|
||||||
from subtitles.indexer.movies import list_missing_subtitles_movies
|
from subtitles.indexer.movies import list_missing_subtitles_movies
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
scheduler.add_job(list_missing_subtitles, kwargs={'send_event': True})
|
scheduler.add_job(list_missing_subtitles, kwargs={'send_event': True})
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
scheduler.add_job(list_missing_subtitles_movies, kwargs={'send_event': True})
|
scheduler.add_job(list_missing_subtitles_movies, kwargs={'send_event': True})
|
||||||
|
|
||||||
if undefined_subtitles_track_default_changed:
|
if undefined_subtitles_track_default_changed:
|
||||||
from .scheduler import scheduler
|
from .scheduler import scheduler
|
||||||
from subtitles.indexer.series import series_full_scan_subtitles
|
from subtitles.indexer.series import series_full_scan_subtitles
|
||||||
from subtitles.indexer.movies import movies_full_scan_subtitles
|
from subtitles.indexer.movies import movies_full_scan_subtitles
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
scheduler.add_job(series_full_scan_subtitles, kwargs={'use_cache': True})
|
scheduler.add_job(series_full_scan_subtitles, kwargs={'use_cache': True})
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
scheduler.add_job(movies_full_scan_subtitles, kwargs={'use_cache': True})
|
scheduler.add_job(movies_full_scan_subtitles, kwargs={'use_cache': True})
|
||||||
|
|
||||||
if audio_tracks_parsing_changed:
|
if audio_tracks_parsing_changed:
|
||||||
from .scheduler import scheduler
|
from .scheduler import scheduler
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
from sonarr.sync.series import update_series
|
from sonarr.sync.series import update_series
|
||||||
scheduler.add_job(update_series, kwargs={'send_event': True}, max_instances=1)
|
scheduler.add_job(update_series, kwargs={'send_event': True}, max_instances=1)
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
from radarr.sync.movies import update_movies
|
from radarr.sync.movies import update_movies
|
||||||
scheduler.add_job(update_movies, kwargs={'send_event': True}, max_instances=1)
|
scheduler.add_job(update_movies, kwargs={'send_event': True}, max_instances=1)
|
||||||
|
|
||||||
if update_subzero:
|
if update_subzero:
|
||||||
settings.set('general', 'subzero_mods', ','.join(subzero_mods))
|
settings.general.subzero_mods = ','.join(subzero_mods)
|
||||||
|
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
try:
|
||||||
settings.write(handle)
|
settings.validators.validate()
|
||||||
|
except ValidationError:
|
||||||
|
settings.reload()
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
write_config()
|
||||||
|
|
||||||
# Reconfigure Bazarr to reflect changes
|
# Reconfigure Bazarr to reflect changes
|
||||||
if configure_debug:
|
if configure_debug:
|
||||||
from .logger import configure_logging
|
from .logger import configure_logging
|
||||||
configure_logging(settings.general.getboolean('debug') or args.debug)
|
configure_logging(settings.general.debug or args.debug)
|
||||||
|
|
||||||
if configure_captcha:
|
if configure_captcha:
|
||||||
configure_captcha_func()
|
configure_captcha_func()
|
||||||
|
|
||||||
if update_schedule:
|
if update_schedule:
|
||||||
from .scheduler import scheduler
|
from .scheduler import scheduler
|
||||||
from .event_handler import event_stream
|
from .event_handler import event_stream
|
||||||
scheduler.update_configurable_tasks()
|
scheduler.update_configurable_tasks()
|
||||||
event_stream(type='task')
|
event_stream(type='task')
|
||||||
|
|
||||||
if sonarr_changed:
|
if sonarr_changed:
|
||||||
from .signalr_client import sonarr_signalr_client
|
from .signalr_client import sonarr_signalr_client
|
||||||
try:
|
try:
|
||||||
sonarr_signalr_client.restart()
|
sonarr_signalr_client.restart()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if radarr_changed:
|
if radarr_changed:
|
||||||
from .signalr_client import radarr_signalr_client
|
from .signalr_client import radarr_signalr_client
|
||||||
try:
|
try:
|
||||||
radarr_signalr_client.restart()
|
radarr_signalr_client.restart()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if update_path_map:
|
if update_path_map:
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
path_mappings.update()
|
path_mappings.update()
|
||||||
|
|
||||||
if configure_proxy:
|
if configure_proxy:
|
||||||
configure_proxy_func()
|
configure_proxy_func()
|
||||||
|
|
||||||
if exclusion_updated:
|
if exclusion_updated:
|
||||||
from .event_handler import event_stream
|
from .event_handler import event_stream
|
||||||
event_stream(type='badges')
|
event_stream(type='badges')
|
||||||
if sonarr_exclusion_updated:
|
if sonarr_exclusion_updated:
|
||||||
event_stream(type='reset-episode-wanted')
|
event_stream(type='reset-episode-wanted')
|
||||||
if radarr_exclusion_updated:
|
if radarr_exclusion_updated:
|
||||||
event_stream(type='reset-movie-wanted')
|
event_stream(type='reset-movie-wanted')
|
||||||
|
|
||||||
|
|
||||||
def get_array_from(property):
|
def get_array_from(property):
|
||||||
|
@ -681,15 +759,15 @@ def configure_captcha_func():
|
||||||
|
|
||||||
|
|
||||||
def configure_proxy_func():
|
def configure_proxy_func():
|
||||||
if settings.proxy.type != 'None':
|
if settings.proxy.type:
|
||||||
if settings.proxy.username != '' and settings.proxy.password != '':
|
if settings.proxy.username != '' and settings.proxy.password != '':
|
||||||
proxy = settings.proxy.type + '://' + quote_plus(settings.proxy.username) + ':' + \
|
proxy = (f'{settings.proxy.type}://{quote_plus(settings.proxy.username)}:'
|
||||||
quote_plus(settings.proxy.password) + '@' + settings.proxy.url + ':' + settings.proxy.port
|
f'{quote_plus(settings.proxy.password)}@{settings.proxy.url}:{settings.proxy.port}')
|
||||||
else:
|
else:
|
||||||
proxy = settings.proxy.type + '://' + settings.proxy.url + ':' + settings.proxy.port
|
proxy = f'{settings.proxy.type}://{settings.proxy.url}:{settings.proxy.port}'
|
||||||
os.environ['HTTP_PROXY'] = str(proxy)
|
os.environ['HTTP_PROXY'] = str(proxy)
|
||||||
os.environ['HTTPS_PROXY'] = str(proxy)
|
os.environ['HTTPS_PROXY'] = str(proxy)
|
||||||
exclude = ','.join(get_array_from(settings.proxy.exclude))
|
exclude = ','.join(settings.proxy.exclude)
|
||||||
os.environ['NO_PROXY'] = exclude
|
os.environ['NO_PROXY'] = exclude
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,16 @@ from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
from .config import settings, get_array_from
|
from .config import settings
|
||||||
from .get_args import args
|
from .get_args import args
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
postgresql = (os.getenv("POSTGRES_ENABLED", settings.postgresql.enabled).lower() == 'true')
|
POSTGRES_ENABLED_ENV = os.getenv("POSTGRES_ENABLED")
|
||||||
|
if POSTGRES_ENABLED_ENV:
|
||||||
|
postgresql = POSTGRES_ENABLED_ENV.lower() == 'true'
|
||||||
|
else:
|
||||||
|
postgresql = settings.postgresql.enabled
|
||||||
|
|
||||||
region = make_region().configure('dogpile.cache.memory')
|
region = make_region().configure('dogpile.cache.memory')
|
||||||
|
|
||||||
|
@ -324,30 +328,30 @@ def migrate_db(app):
|
||||||
def get_exclusion_clause(exclusion_type):
|
def get_exclusion_clause(exclusion_type):
|
||||||
where_clause = []
|
where_clause = []
|
||||||
if exclusion_type == 'series':
|
if exclusion_type == 'series':
|
||||||
tagsList = ast.literal_eval(settings.sonarr.excluded_tags)
|
tagsList = settings.sonarr.excluded_tags
|
||||||
for tag in tagsList:
|
for tag in tagsList:
|
||||||
where_clause.append(~(TableShows.tags.contains("\'" + tag + "\'")))
|
where_clause.append(~(TableShows.tags.contains(f"\'{tag}\'")))
|
||||||
else:
|
else:
|
||||||
tagsList = ast.literal_eval(settings.radarr.excluded_tags)
|
tagsList = settings.radarr.excluded_tags
|
||||||
for tag in tagsList:
|
for tag in tagsList:
|
||||||
where_clause.append(~(TableMovies.tags.contains("\'" + tag + "\'")))
|
where_clause.append(~(TableMovies.tags.contains(f"\'{tag}\'")))
|
||||||
|
|
||||||
if exclusion_type == 'series':
|
if exclusion_type == 'series':
|
||||||
monitoredOnly = settings.sonarr.getboolean('only_monitored')
|
monitoredOnly = settings.sonarr.only_monitored
|
||||||
if monitoredOnly:
|
if monitoredOnly:
|
||||||
where_clause.append((TableEpisodes.monitored == 'True')) # noqa E712
|
where_clause.append((TableEpisodes.monitored == 'True')) # noqa E712
|
||||||
where_clause.append((TableShows.monitored == 'True')) # noqa E712
|
where_clause.append((TableShows.monitored == 'True')) # noqa E712
|
||||||
else:
|
else:
|
||||||
monitoredOnly = settings.radarr.getboolean('only_monitored')
|
monitoredOnly = settings.radarr.only_monitored
|
||||||
if monitoredOnly:
|
if monitoredOnly:
|
||||||
where_clause.append((TableMovies.monitored == 'True')) # noqa E712
|
where_clause.append((TableMovies.monitored == 'True')) # noqa E712
|
||||||
|
|
||||||
if exclusion_type == 'series':
|
if exclusion_type == 'series':
|
||||||
typesList = get_array_from(settings.sonarr.excluded_series_types)
|
typesList = settings.sonarr.excluded_series_types
|
||||||
for item in typesList:
|
for item in typesList:
|
||||||
where_clause.append((TableShows.seriesType != item))
|
where_clause.append((TableShows.seriesType != item))
|
||||||
|
|
||||||
exclude_season_zero = settings.sonarr.getboolean('exclude_season_zero')
|
exclude_season_zero = settings.sonarr.exclude_season_zero
|
||||||
if exclude_season_zero:
|
if exclude_season_zero:
|
||||||
where_clause.append((TableEpisodes.season != 0))
|
where_clause.append((TableEpisodes.season != 0))
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
|
||||||
import ast
|
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
import pytz
|
||||||
|
@ -13,15 +12,17 @@ import requests
|
||||||
import traceback
|
import traceback
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from requests import ConnectionError
|
||||||
|
from subzero.language import Language
|
||||||
from subliminal_patch.exceptions import TooManyRequests, APIThrottled, ParseResponseError, IPAddressBlocked, \
|
from subliminal_patch.exceptions import TooManyRequests, APIThrottled, ParseResponseError, IPAddressBlocked, \
|
||||||
MustGetBlacklisted, SearchLimitReached
|
MustGetBlacklisted, SearchLimitReached
|
||||||
from subliminal.providers.opensubtitles import DownloadLimitReached
|
from subliminal.providers.opensubtitles import DownloadLimitReached, PaymentRequired, Unauthorized
|
||||||
from subliminal.exceptions import DownloadLimitExceeded, ServiceUnavailable, AuthenticationError, ConfigurationError
|
from subliminal.exceptions import DownloadLimitExceeded, ServiceUnavailable, AuthenticationError, ConfigurationError
|
||||||
from subliminal import region as subliminal_cache_region
|
from subliminal import region as subliminal_cache_region
|
||||||
from subliminal_patch.extensions import provider_registry
|
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
|
||||||
from languages.get_languages import CustomLanguage
|
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
|
||||||
|
@ -74,17 +75,21 @@ def provider_throttle_map():
|
||||||
socket.timeout: (datetime.timedelta(hours=1), "1 hour"),
|
socket.timeout: (datetime.timedelta(hours=1), "1 hour"),
|
||||||
requests.exceptions.ConnectTimeout: (datetime.timedelta(hours=1), "1 hour"),
|
requests.exceptions.ConnectTimeout: (datetime.timedelta(hours=1), "1 hour"),
|
||||||
requests.exceptions.ReadTimeout: (datetime.timedelta(hours=1), "1 hour"),
|
requests.exceptions.ReadTimeout: (datetime.timedelta(hours=1), "1 hour"),
|
||||||
|
ConfigurationError: (datetime.timedelta(hours=12), "12 hours"),
|
||||||
|
PermissionError: (datetime.timedelta(hours=12), "12 hours"),
|
||||||
|
requests.exceptions.ProxyError: (datetime.timedelta(hours=1), "1 hour"),
|
||||||
|
AuthenticationError: (datetime.timedelta(hours=12), "12 hours"),
|
||||||
},
|
},
|
||||||
"opensubtitles": {
|
"opensubtitles": {
|
||||||
TooManyRequests: (datetime.timedelta(hours=3), "3 hours"),
|
TooManyRequests: (datetime.timedelta(hours=3), "3 hours"),
|
||||||
DownloadLimitExceeded: (datetime.timedelta(hours=6), "6 hours"),
|
DownloadLimitExceeded: (datetime.timedelta(hours=6), "6 hours"),
|
||||||
DownloadLimitReached: (datetime.timedelta(hours=6), "6 hours"),
|
DownloadLimitReached: (datetime.timedelta(hours=6), "6 hours"),
|
||||||
|
PaymentRequired: (datetime.timedelta(hours=12), "12 hours"),
|
||||||
|
Unauthorized: (datetime.timedelta(hours=12), "12 hours"),
|
||||||
APIThrottled: (datetime.timedelta(seconds=15), "15 seconds"),
|
APIThrottled: (datetime.timedelta(seconds=15), "15 seconds"),
|
||||||
ServiceUnavailable: (datetime.timedelta(hours=1), "1 hour"),
|
ServiceUnavailable: (datetime.timedelta(hours=1), "1 hour"),
|
||||||
},
|
},
|
||||||
"opensubtitlescom": {
|
"opensubtitlescom": {
|
||||||
AuthenticationError: (datetime.timedelta(hours=12), "12 hours"),
|
|
||||||
ConfigurationError: (datetime.timedelta(hours=12), "12 hours"),
|
|
||||||
TooManyRequests: (datetime.timedelta(minutes=1), "1 minute"),
|
TooManyRequests: (datetime.timedelta(minutes=1), "1 minute"),
|
||||||
DownloadLimitExceeded: (datetime.timedelta(hours=24), "24 hours"),
|
DownloadLimitExceeded: (datetime.timedelta(hours=24), "24 hours"),
|
||||||
},
|
},
|
||||||
|
@ -110,9 +115,6 @@ def provider_throttle_map():
|
||||||
legendasdivx_limit_reset_timedelta(),
|
legendasdivx_limit_reset_timedelta(),
|
||||||
f"{legendasdivx_limit_reset_timedelta().seconds // 3600 + 1} hours"),
|
f"{legendasdivx_limit_reset_timedelta().seconds // 3600 + 1} hours"),
|
||||||
},
|
},
|
||||||
"subf2m": {
|
|
||||||
ConfigurationError: (datetime.timedelta(hours=24), "24 hours"),
|
|
||||||
},
|
|
||||||
"whisperai": {
|
"whisperai": {
|
||||||
ConnectionError: (datetime.timedelta(hours=24), "24 hours"),
|
ConnectionError: (datetime.timedelta(hours=24), "24 hours"),
|
||||||
},
|
},
|
||||||
|
@ -126,7 +128,7 @@ throttle_count = {}
|
||||||
|
|
||||||
|
|
||||||
def provider_pool():
|
def provider_pool():
|
||||||
if settings.general.getboolean('multithreading'):
|
if settings.general.multithreading:
|
||||||
return subliminal_patch.core.SZAsyncProviderPool
|
return subliminal_patch.core.SZAsyncProviderPool
|
||||||
return subliminal_patch.core.SZProviderPool
|
return subliminal_patch.core.SZProviderPool
|
||||||
|
|
||||||
|
@ -157,7 +159,7 @@ def _lang_from_str(content: str):
|
||||||
def get_language_equals(settings_=None):
|
def get_language_equals(settings_=None):
|
||||||
settings_ = settings_ or settings
|
settings_ = settings_ or settings
|
||||||
|
|
||||||
equals = get_array_from(settings_.general.language_equals)
|
equals = settings_.general.language_equals
|
||||||
if not equals:
|
if not equals:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -177,7 +179,7 @@ def get_language_equals(settings_=None):
|
||||||
def get_providers():
|
def get_providers():
|
||||||
providers_list = []
|
providers_list = []
|
||||||
existing_providers = provider_registry.names()
|
existing_providers = provider_registry.names()
|
||||||
providers = [x for x in get_array_from(settings.general.enabled_providers) if x in existing_providers]
|
providers = [x for x in settings.general.enabled_providers if x in existing_providers]
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
reason, until, throttle_desc = tp.get(provider, (None, None, None))
|
reason, until, throttle_desc = tp.get(provider, (None, None, None))
|
||||||
providers_list.append(provider)
|
providers_list.append(provider)
|
||||||
|
@ -205,9 +207,9 @@ def get_providers():
|
||||||
|
|
||||||
def get_enabled_providers():
|
def get_enabled_providers():
|
||||||
# return enabled provider including those who can be throttled
|
# return enabled provider including those who can be throttled
|
||||||
try:
|
if isinstance(settings.general.enabled_providers, list):
|
||||||
return ast.literal_eval(settings.general.enabled_providers)
|
return settings.general.enabled_providers
|
||||||
except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@ -222,32 +224,28 @@ def get_providers_auth():
|
||||||
'password': settings.addic7ed.password,
|
'password': settings.addic7ed.password,
|
||||||
'cookies': settings.addic7ed.cookies,
|
'cookies': settings.addic7ed.cookies,
|
||||||
'user_agent': settings.addic7ed.user_agent,
|
'user_agent': settings.addic7ed.user_agent,
|
||||||
'is_vip': settings.addic7ed.getboolean('vip'),
|
'is_vip': settings.addic7ed.vip,
|
||||||
},
|
},
|
||||||
'opensubtitles': {
|
'opensubtitles': {
|
||||||
'username': settings.opensubtitles.username,
|
'username': settings.opensubtitles.username,
|
||||||
'password': settings.opensubtitles.password,
|
'password': settings.opensubtitles.password,
|
||||||
'use_tag_search': settings.opensubtitles.getboolean(
|
'use_tag_search': settings.opensubtitles.use_tag_search,
|
||||||
'use_tag_search'
|
|
||||||
),
|
|
||||||
'only_foreign': False, # fixme
|
'only_foreign': False, # fixme
|
||||||
'also_foreign': False, # fixme
|
'also_foreign': False, # fixme
|
||||||
'is_vip': settings.opensubtitles.getboolean('vip'),
|
'is_vip': settings.opensubtitles.vip,
|
||||||
'use_ssl': settings.opensubtitles.getboolean('ssl'),
|
'use_ssl': settings.opensubtitles.ssl,
|
||||||
'timeout': int(settings.opensubtitles.timeout) or 15,
|
'timeout': int(settings.opensubtitles.timeout) or 15,
|
||||||
'skip_wrong_fps': settings.opensubtitles.getboolean(
|
'skip_wrong_fps': settings.opensubtitles.skip_wrong_fps,
|
||||||
'skip_wrong_fps'
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
'opensubtitlescom': {'username': settings.opensubtitlescom.username,
|
'opensubtitlescom': {'username': settings.opensubtitlescom.username,
|
||||||
'password': settings.opensubtitlescom.password,
|
'password': settings.opensubtitlescom.password,
|
||||||
'use_hash': settings.opensubtitlescom.getboolean('use_hash'),
|
'use_hash': settings.opensubtitlescom.use_hash,
|
||||||
'api_key': 's38zmzVlW7IlYruWi7mHwDYl2SfMQoC1'
|
'api_key': 's38zmzVlW7IlYruWi7mHwDYl2SfMQoC1'
|
||||||
},
|
},
|
||||||
'podnapisi': {
|
'podnapisi': {
|
||||||
'only_foreign': False, # fixme
|
'only_foreign': False, # fixme
|
||||||
'also_foreign': False, # fixme
|
'also_foreign': False, # fixme
|
||||||
'verify_ssl': settings.podnapisi.getboolean('verify_ssl')
|
'verify_ssl': settings.podnapisi.verify_ssl
|
||||||
},
|
},
|
||||||
'subscene': {
|
'subscene': {
|
||||||
'username': settings.subscene.username,
|
'username': settings.subscene.username,
|
||||||
|
@ -257,9 +255,7 @@ def get_providers_auth():
|
||||||
'legendasdivx': {
|
'legendasdivx': {
|
||||||
'username': settings.legendasdivx.username,
|
'username': settings.legendasdivx.username,
|
||||||
'password': settings.legendasdivx.password,
|
'password': settings.legendasdivx.password,
|
||||||
'skip_wrong_fps': settings.legendasdivx.getboolean(
|
'skip_wrong_fps': settings.legendasdivx.skip_wrong_fps,
|
||||||
'skip_wrong_fps'
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
'xsubs': {
|
'xsubs': {
|
||||||
'username': settings.xsubs.username,
|
'username': settings.xsubs.username,
|
||||||
|
@ -276,7 +272,7 @@ def get_providers_auth():
|
||||||
'titulky': {
|
'titulky': {
|
||||||
'username': settings.titulky.username,
|
'username': settings.titulky.username,
|
||||||
'password': settings.titulky.password,
|
'password': settings.titulky.password,
|
||||||
'approved_only': settings.titulky.getboolean('approved_only'),
|
'approved_only': settings.titulky.approved_only,
|
||||||
},
|
},
|
||||||
'titlovi': {
|
'titlovi': {
|
||||||
'username': settings.titlovi.username,
|
'username': settings.titlovi.username,
|
||||||
|
@ -287,13 +283,13 @@ def get_providers_auth():
|
||||||
'hashed_password': settings.ktuvit.hashed_password,
|
'hashed_password': settings.ktuvit.hashed_password,
|
||||||
},
|
},
|
||||||
'embeddedsubtitles': {
|
'embeddedsubtitles': {
|
||||||
'included_codecs': get_array_from(settings.embeddedsubtitles.included_codecs),
|
'included_codecs': settings.embeddedsubtitles.included_codecs,
|
||||||
'hi_fallback': settings.embeddedsubtitles.getboolean('hi_fallback'),
|
'hi_fallback': settings.embeddedsubtitles.hi_fallback,
|
||||||
'cache_dir': os.path.join(args.config_dir, "cache"),
|
'cache_dir': os.path.join(args.config_dir, "cache"),
|
||||||
'ffprobe_path': _FFPROBE_BINARY,
|
'ffprobe_path': _FFPROBE_BINARY,
|
||||||
'ffmpeg_path': _FFMPEG_BINARY,
|
'ffmpeg_path': _FFMPEG_BINARY,
|
||||||
'timeout': settings.embeddedsubtitles.timeout,
|
'timeout': settings.embeddedsubtitles.timeout,
|
||||||
'unknown_as_english': settings.embeddedsubtitles.getboolean('unknown_as_english'),
|
'unknown_as_english': settings.embeddedsubtitles.unknown_as_english,
|
||||||
},
|
},
|
||||||
'karagarga': {
|
'karagarga': {
|
||||||
'username': settings.karagarga.username,
|
'username': settings.karagarga.username,
|
||||||
|
@ -306,7 +302,7 @@ def get_providers_auth():
|
||||||
'passkey': settings.hdbits.passkey,
|
'passkey': settings.hdbits.passkey,
|
||||||
},
|
},
|
||||||
'subf2m': {
|
'subf2m': {
|
||||||
'verify_ssl': settings.subf2m.getboolean('verify_ssl'),
|
'verify_ssl': settings.subf2m.verify_ssl,
|
||||||
'user_agent': settings.subf2m.user_agent,
|
'user_agent': settings.subf2m.user_agent,
|
||||||
},
|
},
|
||||||
'whisperai': {
|
'whisperai': {
|
||||||
|
@ -317,18 +313,25 @@ def get_providers_auth():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _handle_mgb(name, exception):
|
def _handle_mgb(name, exception, ids, language):
|
||||||
# There's no way to get Radarr/Sonarr IDs from subliminal_patch. Blacklisted subtitles
|
if language.forced:
|
||||||
# will not appear on fronted but they will work with get_blacklist
|
language_str = f'{language.basename}:forced'
|
||||||
if exception.media_type == "series":
|
elif language.hi:
|
||||||
blacklist_log("", "", name, exception.id, "")
|
language_str = f'{language.basename}:hi'
|
||||||
else:
|
else:
|
||||||
blacklist_log_movie("", name, exception.id, "")
|
language_str = language.basename
|
||||||
|
|
||||||
|
if ids:
|
||||||
|
if exception.media_type == "series":
|
||||||
|
if 'sonarrSeriesId' in ids and 'sonarrEpsiodeId' in ids:
|
||||||
|
blacklist_log(ids['sonarrSeriesId'], ids['sonarrEpisodeId'], name, exception.id, language_str)
|
||||||
|
else:
|
||||||
|
blacklist_log_movie(ids['radarrId'], name, exception.id, language_str)
|
||||||
|
|
||||||
|
|
||||||
def provider_throttle(name, exception):
|
def provider_throttle(name, exception, ids=None, language=None):
|
||||||
if isinstance(exception, MustGetBlacklisted):
|
if isinstance(exception, MustGetBlacklisted) and isinstance(ids, dict) and isinstance(language, Language):
|
||||||
return _handle_mgb(name, exception)
|
return _handle_mgb(name, exception, ids, language)
|
||||||
|
|
||||||
cls = getattr(exception, "__class__")
|
cls = getattr(exception, "__class__")
|
||||||
cls_name = getattr(cls, "__name__")
|
cls_name = getattr(cls, "__name__")
|
||||||
|
@ -414,7 +417,7 @@ def throttled_count(name):
|
||||||
|
|
||||||
def update_throttled_provider():
|
def update_throttled_provider():
|
||||||
existing_providers = provider_registry.names()
|
existing_providers = provider_registry.names()
|
||||||
providers_list = [x for x in get_array_from(settings.general.enabled_providers) if x in existing_providers]
|
providers_list = [x for x in settings.general.enabled_providers if x in existing_providers]
|
||||||
|
|
||||||
for provider in list(tp):
|
for provider in list(tp):
|
||||||
if provider not in providers_list:
|
if provider not in providers_list:
|
||||||
|
@ -448,7 +451,7 @@ def list_throttled_providers():
|
||||||
update_throttled_provider()
|
update_throttled_provider()
|
||||||
throttled_providers = []
|
throttled_providers = []
|
||||||
existing_providers = provider_registry.names()
|
existing_providers = provider_registry.names()
|
||||||
providers = [x for x in get_array_from(settings.general.enabled_providers) if x in existing_providers]
|
providers = [x for x in settings.general.enabled_providers if x in existing_providers]
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
reason, until, throttle_desc = tp.get(provider, (None, None, None))
|
reason, until, throttle_desc = tp.get(provider, (None, None, None))
|
||||||
throttled_providers.append([provider, reason, pretty.date(until)])
|
throttled_providers.append([provider, reason, pretty.date(until)])
|
||||||
|
@ -457,13 +460,15 @@ def list_throttled_providers():
|
||||||
|
|
||||||
def reset_throttled_providers(only_auth_or_conf_error=False):
|
def reset_throttled_providers(only_auth_or_conf_error=False):
|
||||||
for provider in list(tp):
|
for provider in list(tp):
|
||||||
if only_auth_or_conf_error and tp[provider][0] not in ['AuthenticationError', 'ConfigurationError']:
|
if only_auth_or_conf_error and tp[provider][0] not in ['AuthenticationError', 'ConfigurationError',
|
||||||
|
'PaymentRequired']:
|
||||||
continue
|
continue
|
||||||
del tp[provider]
|
del tp[provider]
|
||||||
set_throttled_providers(str(tp))
|
set_throttled_providers(str(tp))
|
||||||
update_throttled_provider()
|
update_throttled_provider()
|
||||||
if only_auth_or_conf_error:
|
if only_auth_or_conf_error:
|
||||||
logging.info('BAZARR throttled providers have been reset (only AuthenticationError and ConfigurationError).')
|
logging.info('BAZARR throttled providers have been reset (only AuthenticationError, ConfigurationError and '
|
||||||
|
'PaymentRequired).')
|
||||||
else:
|
else:
|
||||||
logging.info('BAZARR throttled providers have been reset.')
|
logging.info('BAZARR throttled providers have been reset.')
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ class PatchedTimedRotatingFileHandler(TimedRotatingFileHandler):
|
||||||
result = []
|
result = []
|
||||||
# See bpo-44753: Don't use the extension when computing the prefix.
|
# See bpo-44753: Don't use the extension when computing the prefix.
|
||||||
n, e = os.path.splitext(baseName)
|
n, e = os.path.splitext(baseName)
|
||||||
prefix = n + '.'
|
prefix = f'{n}.'
|
||||||
plen = len(prefix)
|
plen = len(prefix)
|
||||||
for fileName in fileNames:
|
for fileName in fileNames:
|
||||||
if self.namer is None:
|
if self.namer is None:
|
||||||
|
|
|
@ -24,7 +24,7 @@ def update_notifier():
|
||||||
for x in results['schemas']:
|
for x in results['schemas']:
|
||||||
if x['service_name'] not in notifiers_in_db:
|
if x['service_name'] not in notifiers_in_db:
|
||||||
notifiers_added.append({'name': str(x['service_name']), 'enabled': 0})
|
notifiers_added.append({'name': str(x['service_name']), 'enabled': 0})
|
||||||
logging.debug('Adding new notifier agent: ' + str(x['service_name']))
|
logging.debug(f'Adding new notifier agent: {x["service_name"]}')
|
||||||
else:
|
else:
|
||||||
notifiers_kept.append(x['service_name'])
|
notifiers_kept.append(x['service_name'])
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ def send_notifications(sonarr_series_id, sonarr_episode_id, message):
|
||||||
series_title = series.title
|
series_title = series.title
|
||||||
series_year = series.year
|
series_year = series.year
|
||||||
if series_year not in [None, '', '0']:
|
if series_year not in [None, '', '0']:
|
||||||
series_year = ' ({})'.format(series_year)
|
series_year = f' ({series_year})'
|
||||||
else:
|
else:
|
||||||
series_year = ''
|
series_year = ''
|
||||||
episode = database.execute(
|
episode = database.execute(
|
||||||
|
@ -80,8 +80,7 @@ def send_notifications(sonarr_series_id, sonarr_episode_id, message):
|
||||||
|
|
||||||
apobj.notify(
|
apobj.notify(
|
||||||
title='Bazarr notification',
|
title='Bazarr notification',
|
||||||
body="{}{} - S{:02d}E{:02d} - {} : {}".format(series_title, series_year, episode.season, episode.episode,
|
body=f"{series_title}{series_year} - S{episode.season:02d}E{episode.episode:02d} - {episode.title} : {message}",
|
||||||
episode.title, message),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,7 +97,7 @@ def send_notifications_movie(radarr_id, message):
|
||||||
movie_title = movie.title
|
movie_title = movie.title
|
||||||
movie_year = movie.year
|
movie_year = movie.year
|
||||||
if movie_year not in [None, '', '0']:
|
if movie_year not in [None, '', '0']:
|
||||||
movie_year = ' ({})'.format(movie_year)
|
movie_year = f' ({movie_year})'
|
||||||
else:
|
else:
|
||||||
movie_year = ''
|
movie_year = ''
|
||||||
|
|
||||||
|
@ -112,5 +111,5 @@ def send_notifications_movie(radarr_id, message):
|
||||||
|
|
||||||
apobj.notify(
|
apobj.notify(
|
||||||
title='Bazarr notification',
|
title='Bazarr notification',
|
||||||
body="{}{} : {}".format(movie_title, movie_year, message),
|
body=f"{movie_title}{movie_year} : {message}",
|
||||||
)
|
)
|
||||||
|
|
|
@ -127,10 +127,10 @@ class Scheduler:
|
||||||
if day == "*":
|
if day == "*":
|
||||||
text = "everyday"
|
text = "everyday"
|
||||||
else:
|
else:
|
||||||
text = "every " + day_name[int(day)]
|
text = f"every {day_name[int(day)]}"
|
||||||
|
|
||||||
if hour != "*":
|
if hour != "*":
|
||||||
text += " at " + hour + ":00"
|
text += f" at {hour}:00"
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ class Scheduler:
|
||||||
running = False
|
running = False
|
||||||
|
|
||||||
if isinstance(job.trigger, IntervalTrigger):
|
if isinstance(job.trigger, IntervalTrigger):
|
||||||
interval = "every " + get_time_from_interval(job.trigger.__getstate__()['interval'])
|
interval = f"every {get_time_from_interval(job.trigger.__getstate__()['interval'])}"
|
||||||
task_list.append({'name': job.name, 'interval': interval, 'next_run_in': next_run,
|
task_list.append({'name': job.name, 'interval': interval, 'next_run_in': next_run,
|
||||||
'next_run_time': next_run, 'job_id': job.id, 'job_running': running})
|
'next_run_time': next_run, 'job_id': job.id, 'job_running': running})
|
||||||
elif isinstance(job.trigger, CronTrigger):
|
elif isinstance(job.trigger, CronTrigger):
|
||||||
|
@ -160,14 +160,14 @@ class Scheduler:
|
||||||
return task_list
|
return task_list
|
||||||
|
|
||||||
def __sonarr_update_task(self):
|
def __sonarr_update_task(self):
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
update_series, IntervalTrigger(minutes=int(settings.sonarr.series_sync)), max_instances=1,
|
update_series, IntervalTrigger(minutes=int(settings.sonarr.series_sync)), max_instances=1,
|
||||||
coalesce=True, misfire_grace_time=15, id='update_series', name='Sync with Sonarr',
|
coalesce=True, misfire_grace_time=15, id='update_series', name='Sync with Sonarr',
|
||||||
replace_existing=True)
|
replace_existing=True)
|
||||||
|
|
||||||
def __radarr_update_task(self):
|
def __radarr_update_task(self):
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
update_movies, IntervalTrigger(minutes=int(settings.radarr.movies_sync)), max_instances=1,
|
update_movies, IntervalTrigger(minutes=int(settings.radarr.movies_sync)), max_instances=1,
|
||||||
coalesce=True, misfire_grace_time=15, id='update_movies', name='Sync with Radarr',
|
coalesce=True, misfire_grace_time=15, id='update_movies', name='Sync with Radarr',
|
||||||
|
@ -200,7 +200,7 @@ class Scheduler:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __sonarr_full_update_task(self):
|
def __sonarr_full_update_task(self):
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
full_update = settings.sonarr.full_update
|
full_update = settings.sonarr.full_update
|
||||||
if full_update == "Daily":
|
if full_update == "Daily":
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
|
@ -220,7 +220,7 @@ class Scheduler:
|
||||||
name='Index all Episode Subtitles from disk', replace_existing=True)
|
name='Index all Episode Subtitles from disk', replace_existing=True)
|
||||||
|
|
||||||
def __radarr_full_update_task(self):
|
def __radarr_full_update_task(self):
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
full_update = settings.radarr.full_update
|
full_update = settings.radarr.full_update
|
||||||
if full_update == "Daily":
|
if full_update == "Daily":
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
|
@ -242,7 +242,7 @@ class Scheduler:
|
||||||
if not args.no_update and os.environ["BAZARR_VERSION"] != '':
|
if not args.no_update and os.environ["BAZARR_VERSION"] != '':
|
||||||
task_name = 'Update Bazarr'
|
task_name = 'Update Bazarr'
|
||||||
|
|
||||||
if settings.general.getboolean('auto_update'):
|
if settings.general.auto_update:
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
check_if_new_update, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
|
check_if_new_update, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
|
||||||
misfire_grace_time=15, id='update_bazarr', name=task_name, replace_existing=True)
|
misfire_grace_time=15, id='update_bazarr', name=task_name, replace_existing=True)
|
||||||
|
@ -264,13 +264,13 @@ class Scheduler:
|
||||||
id='update_announcements', name='Update Announcements File', replace_existing=True)
|
id='update_announcements', name='Update Announcements File', replace_existing=True)
|
||||||
|
|
||||||
def __search_wanted_subtitles_task(self):
|
def __search_wanted_subtitles_task(self):
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
wanted_search_missing_subtitles_series,
|
wanted_search_missing_subtitles_series,
|
||||||
IntervalTrigger(hours=int(settings.general.wanted_search_frequency)), max_instances=1, coalesce=True,
|
IntervalTrigger(hours=int(settings.general.wanted_search_frequency)), max_instances=1, coalesce=True,
|
||||||
misfire_grace_time=15, id='wanted_search_missing_subtitles_series', replace_existing=True,
|
misfire_grace_time=15, id='wanted_search_missing_subtitles_series', replace_existing=True,
|
||||||
name='Search for wanted Series Subtitles')
|
name='Search for wanted Series Subtitles')
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
wanted_search_missing_subtitles_movies,
|
wanted_search_missing_subtitles_movies,
|
||||||
IntervalTrigger(hours=int(settings.general.wanted_search_frequency_movie)), max_instances=1,
|
IntervalTrigger(hours=int(settings.general.wanted_search_frequency_movie)), max_instances=1,
|
||||||
|
@ -278,8 +278,8 @@ class Scheduler:
|
||||||
name='Search for wanted Movies Subtitles', replace_existing=True)
|
name='Search for wanted Movies Subtitles', replace_existing=True)
|
||||||
|
|
||||||
def __upgrade_subtitles_task(self):
|
def __upgrade_subtitles_task(self):
|
||||||
if settings.general.getboolean('upgrade_subs') and \
|
if settings.general.upgrade_subs and \
|
||||||
(settings.general.getboolean('use_sonarr') or settings.general.getboolean('use_radarr')):
|
(settings.general.use_sonarr or settings.general.use_radarr):
|
||||||
self.aps_scheduler.add_job(
|
self.aps_scheduler.add_job(
|
||||||
upgrade_subtitles, IntervalTrigger(hours=int(settings.general.upgrade_frequency)), max_instances=1,
|
upgrade_subtitles, IntervalTrigger(hours=int(settings.general.upgrade_frequency)), max_instances=1,
|
||||||
coalesce=True, misfire_grace_time=15, id='upgrade_subtitles',
|
coalesce=True, misfire_grace_time=15, id='upgrade_subtitles',
|
||||||
|
@ -303,9 +303,9 @@ scheduler = Scheduler()
|
||||||
|
|
||||||
# Force the execution of the sync process with Sonarr and Radarr after migration to v0.9.1
|
# Force the execution of the sync process with Sonarr and Radarr after migration to v0.9.1
|
||||||
if 'BAZARR_AUDIO_PROFILES_MIGRATION' in os.environ:
|
if 'BAZARR_AUDIO_PROFILES_MIGRATION' in os.environ:
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
scheduler.aps_scheduler.modify_job('update_series', next_run_time=datetime.now())
|
scheduler.aps_scheduler.modify_job('update_series', next_run_time=datetime.now())
|
||||||
scheduler.aps_scheduler.modify_job('sync_episodes', next_run_time=datetime.now())
|
scheduler.aps_scheduler.modify_job('sync_episodes', next_run_time=datetime.now())
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
scheduler.aps_scheduler.modify_job('update_movies', next_run_time=datetime.now())
|
scheduler.aps_scheduler.modify_job('update_movies', next_run_time=datetime.now())
|
||||||
del os.environ['BAZARR_AUDIO_PROFILES_MIGRATION']
|
del os.environ['BAZARR_AUDIO_PROFILES_MIGRATION']
|
||||||
|
|
|
@ -77,13 +77,13 @@ class Server:
|
||||||
try:
|
try:
|
||||||
self.server.close()
|
self.server.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('BAZARR Cannot stop Waitress: ' + repr(e))
|
logging.error(f'BAZARR Cannot stop Waitress: {repr(e)}')
|
||||||
else:
|
else:
|
||||||
database.close()
|
database.close()
|
||||||
try:
|
try:
|
||||||
stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
|
stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('BAZARR Cannot create stop file: ' + repr(e))
|
logging.error(f'BAZARR Cannot create stop file: {repr(e)}')
|
||||||
else:
|
else:
|
||||||
logging.info('Bazarr is being shutdown...')
|
logging.info('Bazarr is being shutdown...')
|
||||||
stop_file.write(str(''))
|
stop_file.write(str(''))
|
||||||
|
@ -94,13 +94,13 @@ class Server:
|
||||||
try:
|
try:
|
||||||
self.server.close()
|
self.server.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('BAZARR Cannot stop Waitress: ' + repr(e))
|
logging.error(f'BAZARR Cannot stop Waitress: {repr(e)}')
|
||||||
else:
|
else:
|
||||||
database.close()
|
database.close()
|
||||||
try:
|
try:
|
||||||
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
|
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('BAZARR Cannot create restart file: ' + repr(e))
|
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
|
||||||
else:
|
else:
|
||||||
logging.info('Bazarr is being restarted...')
|
logging.info('Bazarr is being restarted...')
|
||||||
restart_file.write(str(''))
|
restart_file.write(str(''))
|
||||||
|
|
|
@ -45,8 +45,9 @@ class SonarrSignalrClientLegacy:
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if get_sonarr_info.is_legacy():
|
if get_sonarr_info.is_legacy():
|
||||||
logging.warning('BAZARR can only sync from Sonarr v3 SignalR feed to get real-time update. You should '
|
logging.warning(
|
||||||
'consider upgrading your version({}).'.format(get_sonarr_info.version()))
|
f'BAZARR can only sync from Sonarr v3 SignalR feed to get real-time update. You should consider '
|
||||||
|
f'upgrading your version({get_sonarr_info.version()}).')
|
||||||
else:
|
else:
|
||||||
self.connected = False
|
self.connected = False
|
||||||
event_stream(type='badges')
|
event_stream(type='badges')
|
||||||
|
@ -86,7 +87,7 @@ class SonarrSignalrClientLegacy:
|
||||||
if self.connection:
|
if self.connection:
|
||||||
if self.connection.started:
|
if self.connection.started:
|
||||||
self.stop(log=False)
|
self.stop(log=False)
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def exception_handler(self):
|
def exception_handler(self):
|
||||||
|
@ -98,7 +99,7 @@ class SonarrSignalrClientLegacy:
|
||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
self.apikey_sonarr = settings.sonarr.apikey
|
self.apikey_sonarr = settings.sonarr.apikey
|
||||||
self.connection = Connection(url_sonarr() + "/signalr", self.session)
|
self.connection = Connection(f"{url_sonarr()}/signalr", self.session)
|
||||||
self.connection.qs = {'apikey': self.apikey_sonarr}
|
self.connection.qs = {'apikey': self.apikey_sonarr}
|
||||||
sonarr_hub = self.connection.register_hub('') # Sonarr doesn't use named hub
|
sonarr_hub = self.connection.register_hub('') # Sonarr doesn't use named hub
|
||||||
|
|
||||||
|
@ -133,7 +134,7 @@ class SonarrSignalrClient:
|
||||||
if self.connection:
|
if self.connection:
|
||||||
if self.connection.transport.state.value in [0, 1, 2]:
|
if self.connection.transport.state.value in [0, 1, 2]:
|
||||||
self.stop()
|
self.stop()
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def exception_handler(self):
|
def exception_handler(self):
|
||||||
|
@ -158,7 +159,7 @@ class SonarrSignalrClient:
|
||||||
def configure(self):
|
def configure(self):
|
||||||
self.apikey_sonarr = settings.sonarr.apikey
|
self.apikey_sonarr = settings.sonarr.apikey
|
||||||
self.connection = HubConnectionBuilder() \
|
self.connection = HubConnectionBuilder() \
|
||||||
.with_url(url_sonarr() + "/signalr/messages?access_token={}".format(self.apikey_sonarr),
|
.with_url(f"{url_sonarr()}/signalr/messages?access_token={self.apikey_sonarr}",
|
||||||
options={
|
options={
|
||||||
"verify_ssl": False,
|
"verify_ssl": False,
|
||||||
"headers": headers
|
"headers": headers
|
||||||
|
@ -200,7 +201,7 @@ class RadarrSignalrClient:
|
||||||
if self.connection:
|
if self.connection:
|
||||||
if self.connection.transport.state.value in [0, 1, 2]:
|
if self.connection.transport.state.value in [0, 1, 2]:
|
||||||
self.stop()
|
self.stop()
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def exception_handler(self):
|
def exception_handler(self):
|
||||||
|
@ -225,7 +226,7 @@ class RadarrSignalrClient:
|
||||||
def configure(self):
|
def configure(self):
|
||||||
self.apikey_radarr = settings.radarr.apikey
|
self.apikey_radarr = settings.radarr.apikey
|
||||||
self.connection = HubConnectionBuilder() \
|
self.connection = HubConnectionBuilder() \
|
||||||
.with_url(url_radarr() + "/signalr/messages?access_token={}".format(self.apikey_radarr),
|
.with_url(f"{url_radarr()}/signalr/messages?access_token={self.apikey_radarr}",
|
||||||
options={
|
options={
|
||||||
"verify_ssl": False,
|
"verify_ssl": False,
|
||||||
"headers": headers
|
"headers": headers
|
||||||
|
@ -300,13 +301,13 @@ def dispatcher(data):
|
||||||
elif topic == 'episode':
|
elif topic == 'episode':
|
||||||
logging.debug(f'Event received from Sonarr for episode: {series_title} ({series_year}) - '
|
logging.debug(f'Event received from Sonarr for episode: {series_title} ({series_year}) - '
|
||||||
f'S{season_number:0>2}E{episode_number:0>2} - {episode_title}')
|
f'S{season_number:0>2}E{episode_number:0>2} - {episode_title}')
|
||||||
sync_one_episode(episode_id=media_id, defer_search=settings.sonarr.getboolean('defer_search_signalr'))
|
sync_one_episode(episode_id=media_id, defer_search=settings.sonarr.defer_search_signalr)
|
||||||
elif topic == 'movie':
|
elif topic == 'movie':
|
||||||
logging.debug(f'Event received from Radarr for movie: {movie_title} ({movie_year})')
|
logging.debug(f'Event received from Radarr for movie: {movie_title} ({movie_year})')
|
||||||
update_one_movie(movie_id=media_id, action=action,
|
update_one_movie(movie_id=media_id, action=action,
|
||||||
defer_search=settings.radarr.getboolean('defer_search_signalr'))
|
defer_search=settings.radarr.defer_search_signalr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug('BAZARR an exception occurred while parsing SignalR feed: {}'.format(repr(e)))
|
logging.debug(f'BAZARR an exception occurred while parsing SignalR feed: {repr(e)}')
|
||||||
finally:
|
finally:
|
||||||
event_stream(type='badges')
|
event_stream(type='badges')
|
||||||
return
|
return
|
||||||
|
|
|
@ -9,8 +9,8 @@ from functools import wraps
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from constants import headers
|
from constants import headers
|
||||||
from sonarr.info import get_sonarr_info, url_sonarr
|
from sonarr.info import url_api_sonarr
|
||||||
from radarr.info import get_radarr_info, url_radarr
|
from radarr.info import url_api_radarr
|
||||||
from utilities.helper import check_credentials
|
from utilities.helper import check_credentials
|
||||||
|
|
||||||
from .config import settings, base_url
|
from .config import settings, base_url
|
||||||
|
@ -109,12 +109,7 @@ def series_images(url):
|
||||||
url = url.strip("/")
|
url = url.strip("/")
|
||||||
apikey = settings.sonarr.apikey
|
apikey = settings.sonarr.apikey
|
||||||
baseUrl = settings.sonarr.base_url
|
baseUrl = settings.sonarr.base_url
|
||||||
if get_sonarr_info.is_legacy():
|
url_image = f'{url_api_sonarr()}{url.lstrip(baseUrl)}?apikey={apikey}'.replace('poster-250', 'poster-500')
|
||||||
url_image = (url_sonarr() + '/api/' + url.lstrip(baseUrl) + '?apikey=' +
|
|
||||||
apikey).replace('poster-250', 'poster-500')
|
|
||||||
else:
|
|
||||||
url_image = (url_sonarr() + '/api/v3/' + url.lstrip(baseUrl) + '?apikey=' +
|
|
||||||
apikey).replace('poster-250', 'poster-500')
|
|
||||||
try:
|
try:
|
||||||
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=headers)
|
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=headers)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -128,10 +123,7 @@ def series_images(url):
|
||||||
def movies_images(url):
|
def movies_images(url):
|
||||||
apikey = settings.radarr.apikey
|
apikey = settings.radarr.apikey
|
||||||
baseUrl = settings.radarr.base_url
|
baseUrl = settings.radarr.base_url
|
||||||
if get_radarr_info.is_legacy():
|
url_image = f'{url_api_radarr()}{url.lstrip(baseUrl)}?apikey={apikey}'
|
||||||
url_image = url_radarr() + '/api/' + url.lstrip(baseUrl) + '?apikey=' + apikey
|
|
||||||
else:
|
|
||||||
url_image = url_radarr() + '/api/v3/' + url.lstrip(baseUrl) + '?apikey=' + apikey
|
|
||||||
try:
|
try:
|
||||||
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=headers)
|
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=headers)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -171,7 +163,7 @@ def configured():
|
||||||
def proxy(protocol, url):
|
def proxy(protocol, url):
|
||||||
if protocol.lower() not in ['http', 'https']:
|
if protocol.lower() not in ['http', 'https']:
|
||||||
return dict(status=False, error='Unsupported protocol')
|
return dict(status=False, error='Unsupported protocol')
|
||||||
url = protocol + '://' + unquote(url)
|
url = f'{protocol}://{unquote(url)}'
|
||||||
params = request.args
|
params = request.args
|
||||||
try:
|
try:
|
||||||
result = requests.get(url, params, allow_redirects=False, verify=False, timeout=5, headers=headers)
|
result = requests.get(url, params, allow_redirects=False, verify=False, timeout=5, headers=headers)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import rarfile
|
||||||
|
|
||||||
from dogpile.cache.region import register_backend as register_cache_backend
|
from dogpile.cache.region import register_backend as register_cache_backend
|
||||||
|
|
||||||
from app.config import settings, configure_captcha_func, get_array_from
|
from app.config import settings, configure_captcha_func, write_config
|
||||||
from app.get_args import args
|
from app.get_args import args
|
||||||
from app.logger import configure_logging
|
from app.logger import configure_logging
|
||||||
from utilities.binaries import get_binary, BinaryNotFound
|
from utilities.binaries import get_binary, BinaryNotFound
|
||||||
|
@ -28,7 +28,7 @@ startTime = time.time()
|
||||||
restore_from_backup()
|
restore_from_backup()
|
||||||
|
|
||||||
# set subliminal_patch user agent
|
# set subliminal_patch user agent
|
||||||
os.environ["SZ_USER_AGENT"] = "Bazarr/{}".format(os.environ["BAZARR_VERSION"])
|
os.environ["SZ_USER_AGENT"] = f"Bazarr/{os.environ['BAZARR_VERSION']}"
|
||||||
|
|
||||||
# Check if args.config_dir exist
|
# Check if args.config_dir exist
|
||||||
if not os.path.exists(args.config_dir):
|
if not os.path.exists(args.config_dir):
|
||||||
|
@ -62,7 +62,7 @@ configure_captcha_func()
|
||||||
from ga4mp import GtagMP # noqa E402
|
from ga4mp import GtagMP # noqa E402
|
||||||
|
|
||||||
# configure logging
|
# configure logging
|
||||||
configure_logging(settings.general.getboolean('debug') or args.debug)
|
configure_logging(settings.general.debug or args.debug)
|
||||||
import logging # noqa E402
|
import logging # noqa E402
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ if not args.no_update:
|
||||||
pip_command.insert(4, '--user')
|
pip_command.insert(4, '--user')
|
||||||
subprocess.check_output(pip_command, stderr=subprocess.STDOUT)
|
subprocess.check_output(pip_command, stderr=subprocess.STDOUT)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logging.exception('BAZARR requirements.txt installation result: {}'.format(e.stdout))
|
logging.exception(f'BAZARR requirements.txt installation result: {e.stdout}')
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
else:
|
else:
|
||||||
logging.info('BAZARR requirements installed.')
|
logging.info('BAZARR requirements installed.')
|
||||||
|
@ -104,37 +104,21 @@ if not args.no_update:
|
||||||
try:
|
try:
|
||||||
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
|
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('BAZARR Cannot create restart file: ' + repr(e))
|
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
|
||||||
else:
|
else:
|
||||||
logging.info('Bazarr is being restarted...')
|
logging.info('Bazarr is being restarted...')
|
||||||
restart_file.write(str(''))
|
restart_file.write(str(''))
|
||||||
restart_file.close()
|
restart_file.close()
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
# create random api_key if there's none in config.ini
|
|
||||||
if not settings.auth.apikey or settings.auth.apikey.startswith("b'"):
|
|
||||||
from binascii import hexlify
|
|
||||||
settings.auth.apikey = hexlify(os.urandom(16)).decode()
|
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
# create random Flask secret_key if there's none in config.ini
|
|
||||||
if not settings.general.flask_secret_key:
|
|
||||||
from binascii import hexlify
|
|
||||||
settings.general.flask_secret_key = hexlify(os.urandom(16)).decode()
|
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
# change default base_url to ''
|
# change default base_url to ''
|
||||||
settings.general.base_url = settings.general.base_url.rstrip('/')
|
settings.general.base_url = settings.general.base_url.rstrip('/')
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
write_config()
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
# migrate enabled_providers from comma separated string to list
|
# migrate enabled_providers from comma separated string to list
|
||||||
if isinstance(settings.general.enabled_providers, str) and not settings.general.enabled_providers.startswith('['):
|
if isinstance(settings.general.enabled_providers, str) and not settings.general.enabled_providers.startswith('['):
|
||||||
settings.general.enabled_providers = str(settings.general.enabled_providers.split(","))
|
settings.general.enabled_providers = str(settings.general.enabled_providers.split(","))
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
write_config()
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
# Read package_info (if exists) to override some settings by package maintainers
|
# Read package_info (if exists) to override some settings by package maintainers
|
||||||
# This file can also provide some info about the package version and author
|
# This file can also provide some info about the package version and author
|
||||||
|
@ -166,8 +150,7 @@ if os.path.isfile(package_info_file):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
write_config()
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
# Configure dogpile file caching for Subliminal request
|
# Configure dogpile file caching for Subliminal request
|
||||||
register_cache_backend("subzero.cache.file", "subzero.cache_backends.file", "SZFileBackend")
|
register_cache_backend("subzero.cache.file", "subzero.cache_backends.file", "SZFileBackend")
|
||||||
|
@ -186,30 +169,24 @@ if not os.path.exists(os.path.join(args.config_dir, 'config', 'announcements.txt
|
||||||
get_announcements_to_file()
|
get_announcements_to_file()
|
||||||
logging.debug("BAZARR Created announcements file")
|
logging.debug("BAZARR Created announcements file")
|
||||||
|
|
||||||
config_file = os.path.normpath(os.path.join(args.config_dir, 'config', 'config.ini'))
|
# Move GA visitor from config to dedicated file
|
||||||
|
if 'visitor' in settings.analytics:
|
||||||
# Move GA visitor from config.ini to dedicated file
|
|
||||||
if settings.analytics.visitor:
|
|
||||||
with open(os.path.normpath(os.path.join(args.config_dir, 'config', 'analytics.dat')), 'w+') as handle:
|
with open(os.path.normpath(os.path.join(args.config_dir, 'config', 'analytics.dat')), 'w+') as handle:
|
||||||
handle.write(settings.analytics.visitor)
|
handle.write(settings.analytics.visitor)
|
||||||
with open(os.path.normpath(os.path.join(args.config_dir, 'config', 'config.ini')), 'w+') as handle:
|
settings['analytics'].pop('visitor', None)
|
||||||
settings.remove_option('analytics', 'visitor')
|
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
# Clean unused settings from config.ini
|
# Clean unused settings from config
|
||||||
with open(os.path.normpath(os.path.join(args.config_dir, 'config', 'config.ini')), 'w+') as handle:
|
settings['general'].pop('throtteled_providers', None)
|
||||||
settings.remove_option('general', 'throtteled_providers')
|
settings['general'].pop('update_restart', None)
|
||||||
settings.remove_option('general', 'update_restart')
|
write_config()
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
|
|
||||||
# Remove deprecated providers from enabled providers in config.ini
|
# Remove deprecated providers from enabled providers in config
|
||||||
from subliminal_patch.extensions import provider_registry # noqa E401
|
from subliminal_patch.extensions import provider_registry # noqa E401
|
||||||
existing_providers = provider_registry.names()
|
existing_providers = provider_registry.names()
|
||||||
enabled_providers = get_array_from(settings.general.enabled_providers)
|
enabled_providers = settings.general.enabled_providers
|
||||||
settings.general.enabled_providers = str([x for x in enabled_providers if x in existing_providers])
|
settings.general.enabled_providers = [x for x in enabled_providers if x in existing_providers]
|
||||||
with open(os.path.join(args.config_dir, 'config', 'config.ini'), 'w+') as handle:
|
write_config()
|
||||||
settings.write(handle)
|
|
||||||
|
|
||||||
|
|
||||||
def init_binaries():
|
def init_binaries():
|
||||||
|
|
|
@ -28,7 +28,7 @@ if bazarr_version != '':
|
||||||
apply_update()
|
apply_update()
|
||||||
|
|
||||||
# Check for new update and install latest
|
# Check for new update and install latest
|
||||||
if args.no_update or not settings.general.getboolean('auto_update'):
|
if args.no_update or not settings.general.auto_update:
|
||||||
# user have explicitly requested that we do not update or is using some kind of package/docker that prevent it
|
# user have explicitly requested that we do not update or is using some kind of package/docker that prevent it
|
||||||
check_releases()
|
check_releases()
|
||||||
else:
|
else:
|
||||||
|
@ -47,7 +47,7 @@ if args.create_db_revision:
|
||||||
try:
|
try:
|
||||||
stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
|
stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('BAZARR Cannot create stop file: ' + repr(e))
|
logging.error(f'BAZARR Cannot create stop file: {repr(e)}')
|
||||||
else:
|
else:
|
||||||
create_db_revision(app)
|
create_db_revision(app)
|
||||||
logging.info('Bazarr is being shutdown...')
|
logging.info('Bazarr is being shutdown...')
|
||||||
|
@ -74,9 +74,9 @@ login_auth = settings.auth.type
|
||||||
update_notifier()
|
update_notifier()
|
||||||
|
|
||||||
if not args.no_signalr:
|
if not args.no_signalr:
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
Thread(target=sonarr_signalr_client.start).start()
|
Thread(target=sonarr_signalr_client.start).start()
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
Thread(target=radarr_signalr_client.start).start()
|
Thread(target=radarr_signalr_client.start).start()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from radarr.info import get_radarr_info, url_radarr
|
from radarr.info import url_api_radarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,16 +12,11 @@ def browse_radarr_filesystem(path='#'):
|
||||||
if path == '#':
|
if path == '#':
|
||||||
path = ''
|
path = ''
|
||||||
|
|
||||||
if get_radarr_info.is_legacy():
|
url_radarr_api_filesystem = (f"{url_api_radarr()}filesystem?path={path}&allowFoldersWithoutTrailingSlashes=true&"
|
||||||
url_radarr_api_filesystem = url_radarr() + "/api/filesystem?path=" + path + \
|
f"includeFiles=false&apikey={settings.radarr.apikey}")
|
||||||
"&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \
|
|
||||||
settings.radarr.apikey
|
|
||||||
else:
|
|
||||||
url_radarr_api_filesystem = url_radarr() + "/api/v3/filesystem?path=" + path + \
|
|
||||||
"&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \
|
|
||||||
settings.radarr.apikey
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(url_radarr_api_filesystem, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
r = requests.get(url_radarr_api_filesystem, timeout=int(settings.radarr.http_timeout), verify=False,
|
||||||
|
headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except requests.exceptions.HTTPError:
|
except requests.exceptions.HTTPError:
|
||||||
logging.exception("BAZARR Error trying to get series from Radarr. Http error.")
|
logging.exception("BAZARR Error trying to get series from Radarr. Http error.")
|
||||||
|
|
|
@ -26,25 +26,27 @@ class GetRadarrInfo:
|
||||||
return radarr_version
|
return radarr_version
|
||||||
else:
|
else:
|
||||||
radarr_version = ''
|
radarr_version = ''
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
try:
|
try:
|
||||||
rv = url_radarr() + "/api/system/status?apikey=" + settings.radarr.apikey
|
rv = f"{url_radarr()}/api/system/status?apikey={settings.radarr.apikey}"
|
||||||
radarr_json = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers).json()
|
radarr_json = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False,
|
||||||
|
headers=headers).json()
|
||||||
if 'version' in radarr_json:
|
if 'version' in radarr_json:
|
||||||
radarr_version = radarr_json['version']
|
radarr_version = radarr_json['version']
|
||||||
else:
|
else:
|
||||||
raise json.decoder.JSONDecodeError
|
raise json.decoder.JSONDecodeError
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
try:
|
try:
|
||||||
rv = url_radarr() + "/api/v3/system/status?apikey=" + settings.radarr.apikey
|
rv = f"{url_radarr()}/api/v3/system/status?apikey={settings.radarr.apikey}"
|
||||||
radarr_version = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers).json()['version']
|
radarr_version = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False,
|
||||||
|
headers=headers).json()['version']
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
logging.debug('BAZARR cannot get Radarr version')
|
logging.debug('BAZARR cannot get Radarr version')
|
||||||
radarr_version = 'unknown'
|
radarr_version = 'unknown'
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug('BAZARR cannot get Radarr version')
|
logging.debug('BAZARR cannot get Radarr version')
|
||||||
radarr_version = 'unknown'
|
radarr_version = 'unknown'
|
||||||
logging.debug('BAZARR got this Radarr version from its API: {}'.format(radarr_version))
|
logging.debug(f'BAZARR got this Radarr version from its API: {radarr_version}')
|
||||||
region.set("radarr_version", radarr_version)
|
region.set("radarr_version", radarr_version)
|
||||||
return radarr_version
|
return radarr_version
|
||||||
|
|
||||||
|
@ -75,7 +77,7 @@ get_radarr_info = GetRadarrInfo()
|
||||||
|
|
||||||
|
|
||||||
def url_radarr():
|
def url_radarr():
|
||||||
if settings.radarr.getboolean('ssl'):
|
if settings.radarr.ssl:
|
||||||
protocol_radarr = "https"
|
protocol_radarr = "https"
|
||||||
else:
|
else:
|
||||||
protocol_radarr = "http"
|
protocol_radarr = "http"
|
||||||
|
@ -83,7 +85,7 @@ def url_radarr():
|
||||||
if settings.radarr.base_url == '':
|
if settings.radarr.base_url == '':
|
||||||
settings.radarr.base_url = "/"
|
settings.radarr.base_url = "/"
|
||||||
if not settings.radarr.base_url.startswith("/"):
|
if not settings.radarr.base_url.startswith("/"):
|
||||||
settings.radarr.base_url = "/" + settings.radarr.base_url
|
settings.radarr.base_url = f"/{settings.radarr.base_url}"
|
||||||
if settings.radarr.base_url.endswith("/"):
|
if settings.radarr.base_url.endswith("/"):
|
||||||
settings.radarr.base_url = settings.radarr.base_url[:-1]
|
settings.radarr.base_url = settings.radarr.base_url[:-1]
|
||||||
|
|
||||||
|
@ -93,3 +95,7 @@ def url_radarr():
|
||||||
port = f":{settings.radarr.port}"
|
port = f":{settings.radarr.port}"
|
||||||
|
|
||||||
return f"{protocol_radarr}://{settings.radarr.ip}{port}{settings.radarr.base_url}"
|
return f"{protocol_radarr}://{settings.radarr.ip}{port}{settings.radarr.base_url}"
|
||||||
|
|
||||||
|
|
||||||
|
def url_api_radarr():
|
||||||
|
return url_radarr() + f'/api{"/v3" if not get_radarr_info.is_legacy() else ""}/'
|
||||||
|
|
|
@ -4,16 +4,13 @@ import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from radarr.info import get_radarr_info, url_radarr
|
from radarr.info import url_api_radarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
def notify_radarr(radarr_id):
|
def notify_radarr(radarr_id):
|
||||||
try:
|
try:
|
||||||
if get_radarr_info.is_legacy():
|
url = f"{url_api_radarr()}command?apikey={settings.radarr.apikey}"
|
||||||
url = url_radarr() + "/api/command?apikey=" + settings.radarr.apikey
|
|
||||||
else:
|
|
||||||
url = url_radarr() + "/api/v3/command?apikey=" + settings.radarr.apikey
|
|
||||||
data = {
|
data = {
|
||||||
'name': 'RescanMovie',
|
'name': 'RescanMovie',
|
||||||
'movieId': int(radarr_id)
|
'movieId': int(radarr_id)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import logging
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
from app.database import TableMoviesRootfolder, TableMovies, database, delete, update, insert, select
|
from app.database import TableMoviesRootfolder, TableMovies, database, delete, update, insert, select
|
||||||
from radarr.info import get_radarr_info, url_radarr
|
from radarr.info import url_api_radarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,10 +16,7 @@ def get_radarr_rootfolder():
|
||||||
radarr_rootfolder = []
|
radarr_rootfolder = []
|
||||||
|
|
||||||
# Get root folder data from Radarr
|
# Get root folder data from Radarr
|
||||||
if get_radarr_info.is_legacy():
|
url_radarr_api_rootfolder = f"{url_api_radarr()}rootfolder?apikey={apikey_radarr}"
|
||||||
url_radarr_api_rootfolder = url_radarr() + "/api/rootfolder?apikey=" + apikey_radarr
|
|
||||||
else:
|
|
||||||
url_radarr_api_rootfolder = url_radarr() + "/api/v3/rootfolder?apikey=" + apikey_radarr
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rootfolder = requests.get(url_radarr_api_rootfolder, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
rootfolder = requests.get(url_radarr_api_rootfolder, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
||||||
|
|
|
@ -6,12 +6,11 @@ import logging
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from radarr.info import url_radarr
|
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
from subtitles.indexer.movies import store_subtitles_movie, movies_full_scan_subtitles
|
from subtitles.indexer.movies import store_subtitles_movie, movies_full_scan_subtitles
|
||||||
from radarr.rootfolder import check_radarr_rootfolder
|
from radarr.rootfolder import check_radarr_rootfolder
|
||||||
from subtitles.mass_download import movies_download_subtitles
|
from subtitles.mass_download import movies_download_subtitles
|
||||||
from app.database import TableMovies, database, insert, update, delete, select
|
from app.database import TableMovies, TableLanguagesProfiles, database, insert, update, delete, select
|
||||||
from app.event_handler import event_stream, show_progress, hide_progress
|
from app.event_handler import event_stream, show_progress, hide_progress
|
||||||
|
|
||||||
from .utils import get_profile_list, get_tags, get_movies_from_radarr_api
|
from .utils import get_profile_list, get_tags, get_movies_from_radarr_api
|
||||||
|
@ -40,8 +39,7 @@ def update_movie(updated_movie, send_event):
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logging.error(f"BAZARR cannot update movie {updated_movie['path']} because of {e}")
|
logging.error(f"BAZARR cannot update movie {updated_movie['path']} because of {e}")
|
||||||
else:
|
else:
|
||||||
store_subtitles_movie(updated_movie['path'],
|
store_subtitles_movie(updated_movie['path'], path_mappings.path_replace_movie(updated_movie['path']))
|
||||||
path_mappings.path_replace_movie(updated_movie['path']))
|
|
||||||
|
|
||||||
if send_event:
|
if send_event:
|
||||||
event_stream(type='movie', action='update', payload=updated_movie['radarrId'])
|
event_stream(type='movie', action='update', payload=updated_movie['radarrId'])
|
||||||
|
@ -56,8 +54,7 @@ def add_movie(added_movie, send_event):
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logging.error(f"BAZARR cannot insert movie {added_movie['path']} because of {e}")
|
logging.error(f"BAZARR cannot insert movie {added_movie['path']} because of {e}")
|
||||||
else:
|
else:
|
||||||
store_subtitles_movie(added_movie['path'],
|
store_subtitles_movie(added_movie['path'], path_mappings.path_replace_movie(added_movie['path']))
|
||||||
path_mappings.path_replace_movie(added_movie['path']))
|
|
||||||
|
|
||||||
if send_event:
|
if send_event:
|
||||||
event_stream(type='movie', action='update', payload=int(added_movie['radarrId']))
|
event_stream(type='movie', action='update', payload=int(added_movie['radarrId']))
|
||||||
|
@ -68,7 +65,7 @@ def update_movies(send_event=True):
|
||||||
logging.debug('BAZARR Starting movie sync from Radarr.')
|
logging.debug('BAZARR Starting movie sync from Radarr.')
|
||||||
apikey_radarr = settings.radarr.apikey
|
apikey_radarr = settings.radarr.apikey
|
||||||
|
|
||||||
movie_default_enabled = settings.general.getboolean('movie_default_enabled')
|
movie_default_enabled = settings.general.movie_default_enabled
|
||||||
|
|
||||||
if movie_default_enabled is True:
|
if movie_default_enabled is True:
|
||||||
movie_default_profile = settings.general.movie_default_profile
|
movie_default_profile = settings.general.movie_default_profile
|
||||||
|
@ -77,6 +74,13 @@ def update_movies(send_event=True):
|
||||||
else:
|
else:
|
||||||
movie_default_profile = None
|
movie_default_profile = None
|
||||||
|
|
||||||
|
# Prevent trying to insert a movie with a non-existing languages profileId
|
||||||
|
if (movie_default_profile and not database.execute(
|
||||||
|
select(TableLanguagesProfiles)
|
||||||
|
.where(TableLanguagesProfiles.profileId == movie_default_profile))
|
||||||
|
.first()):
|
||||||
|
movie_default_profile = None
|
||||||
|
|
||||||
if apikey_radarr is None:
|
if apikey_radarr is None:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -84,7 +88,7 @@ def update_movies(send_event=True):
|
||||||
tagsDict = get_tags()
|
tagsDict = get_tags()
|
||||||
|
|
||||||
# Get movies data from radarr
|
# Get movies data from radarr
|
||||||
movies = get_movies_from_radarr_api(url=url_radarr(), apikey_radarr=apikey_radarr)
|
movies = get_movies_from_radarr_api(apikey_radarr=apikey_radarr)
|
||||||
if not isinstance(movies, list):
|
if not isinstance(movies, list):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
@ -102,22 +106,19 @@ def update_movies(send_event=True):
|
||||||
'movieFile' in movie and
|
'movieFile' in movie and
|
||||||
(movie['movieFile']['size'] > 20480 or
|
(movie['movieFile']['size'] > 20480 or
|
||||||
get_movie_file_size_from_db(movie['movieFile']['path']) > 20480)]
|
get_movie_file_size_from_db(movie['movieFile']['path']) > 20480)]
|
||||||
movies_to_add = []
|
|
||||||
|
|
||||||
# Remove old movies from DB
|
# Remove old movies from DB
|
||||||
movies_to_delete = list(set(current_movies_id_db) - set(current_movies_radarr))
|
movies_to_delete = list(set(current_movies_id_db) - set(current_movies_radarr))
|
||||||
|
|
||||||
if len(movies_to_delete):
|
if len(movies_to_delete):
|
||||||
try:
|
try:
|
||||||
removed_movies = database.execute(delete(TableMovies)
|
database.execute(delete(TableMovies).where(TableMovies.tmdbId.in_(movies_to_delete)))
|
||||||
.where(TableMovies.tmdbId.in_(movies_to_delete))
|
|
||||||
.returning(TableMovies.radarrId))
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logging.error(f"BAZARR cannot delete movies because of {e}")
|
logging.error(f"BAZARR cannot delete movies because of {e}")
|
||||||
else:
|
else:
|
||||||
for removed_movie in removed_movies:
|
for removed_movie in movies_to_delete:
|
||||||
if send_event:
|
if send_event:
|
||||||
event_stream(type='movie', action='delete', payload=removed_movie.radarrId)
|
event_stream(type='movie', action='delete', payload=removed_movie)
|
||||||
|
|
||||||
# Build new and updated movies
|
# Build new and updated movies
|
||||||
movies_count = len(movies)
|
movies_count = len(movies)
|
||||||
|
@ -155,7 +156,7 @@ def update_movies(send_event=True):
|
||||||
|
|
||||||
|
|
||||||
def update_one_movie(movie_id, action, defer_search=False):
|
def update_one_movie(movie_id, action, defer_search=False):
|
||||||
logging.debug('BAZARR syncing this specific movie from Radarr: {}'.format(movie_id))
|
logging.debug(f'BAZARR syncing this specific movie from Radarr: {movie_id}')
|
||||||
|
|
||||||
# Check if there's a row in database for this movie ID
|
# Check if there's a row in database for this movie ID
|
||||||
existing_movie = database.execute(
|
existing_movie = database.execute(
|
||||||
|
@ -175,11 +176,12 @@ def update_one_movie(movie_id, action, defer_search=False):
|
||||||
f"because of {e}")
|
f"because of {e}")
|
||||||
else:
|
else:
|
||||||
event_stream(type='movie', action='delete', payload=int(movie_id))
|
event_stream(type='movie', action='delete', payload=int(movie_id))
|
||||||
logging.debug('BAZARR deleted this movie from the database:{}'.format(path_mappings.path_replace_movie(
|
logging.debug(
|
||||||
existing_movie.path)))
|
f'BAZARR deleted this movie from the database: '
|
||||||
|
f'{path_mappings.path_replace_movie(existing_movie.path)}')
|
||||||
return
|
return
|
||||||
|
|
||||||
movie_default_enabled = settings.general.getboolean('movie_default_enabled')
|
movie_default_enabled = settings.general.movie_default_enabled
|
||||||
|
|
||||||
if movie_default_enabled is True:
|
if movie_default_enabled is True:
|
||||||
movie_default_profile = settings.general.movie_default_profile
|
movie_default_profile = settings.general.movie_default_profile
|
||||||
|
@ -194,8 +196,7 @@ def update_one_movie(movie_id, action, defer_search=False):
|
||||||
try:
|
try:
|
||||||
# Get movie data from radarr api
|
# Get movie data from radarr api
|
||||||
movie = None
|
movie = None
|
||||||
movie_data = get_movies_from_radarr_api(url=url_radarr(), apikey_radarr=settings.radarr.apikey,
|
movie_data = get_movies_from_radarr_api(apikey_radarr=settings.radarr.apikey, radarr_id=movie_id)
|
||||||
radarr_id=movie_id)
|
|
||||||
if not movie_data:
|
if not movie_data:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
@ -224,8 +225,8 @@ def update_one_movie(movie_id, action, defer_search=False):
|
||||||
f"of {e}")
|
f"of {e}")
|
||||||
else:
|
else:
|
||||||
event_stream(type='movie', action='delete', payload=int(movie_id))
|
event_stream(type='movie', action='delete', payload=int(movie_id))
|
||||||
logging.debug('BAZARR deleted this movie from the database:{}'.format(path_mappings.path_replace_movie(
|
logging.debug(
|
||||||
existing_movie.path)))
|
f'BAZARR deleted this movie from the database:{path_mappings.path_replace_movie(existing_movie.path)}')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update existing movie in DB
|
# Update existing movie in DB
|
||||||
|
@ -239,9 +240,10 @@ def update_one_movie(movie_id, action, defer_search=False):
|
||||||
logging.error(f"BAZARR cannot update movie {path_mappings.path_replace_movie(movie['path'])} because "
|
logging.error(f"BAZARR cannot update movie {path_mappings.path_replace_movie(movie['path'])} because "
|
||||||
f"of {e}")
|
f"of {e}")
|
||||||
else:
|
else:
|
||||||
|
store_subtitles_movie(movie['path'], path_mappings.path_replace_movie(movie['path']))
|
||||||
event_stream(type='movie', action='update', payload=int(movie_id))
|
event_stream(type='movie', action='update', payload=int(movie_id))
|
||||||
logging.debug('BAZARR updated this movie into the database:{}'.format(path_mappings.path_replace_movie(
|
logging.debug(
|
||||||
movie['path'])))
|
f'BAZARR updated this movie into the database:{path_mappings.path_replace_movie(movie["path"])}')
|
||||||
|
|
||||||
# Insert new movie in DB
|
# Insert new movie in DB
|
||||||
elif movie and not existing_movie:
|
elif movie and not existing_movie:
|
||||||
|
@ -253,20 +255,21 @@ def update_one_movie(movie_id, action, defer_search=False):
|
||||||
logging.error(f"BAZARR cannot insert movie {path_mappings.path_replace_movie(movie['path'])} because "
|
logging.error(f"BAZARR cannot insert movie {path_mappings.path_replace_movie(movie['path'])} because "
|
||||||
f"of {e}")
|
f"of {e}")
|
||||||
else:
|
else:
|
||||||
|
store_subtitles_movie(movie['path'], path_mappings.path_replace_movie(movie['path']))
|
||||||
event_stream(type='movie', action='update', payload=int(movie_id))
|
event_stream(type='movie', action='update', payload=int(movie_id))
|
||||||
logging.debug('BAZARR inserted this movie into the database:{}'.format(path_mappings.path_replace_movie(
|
logging.debug(
|
||||||
movie['path'])))
|
f'BAZARR inserted this movie into the database:{path_mappings.path_replace_movie(movie["path"])}')
|
||||||
|
|
||||||
# Storing existing subtitles
|
# Storing existing subtitles
|
||||||
logging.debug('BAZARR storing subtitles for this movie: {}'.format(path_mappings.path_replace_movie(
|
logging.debug(f'BAZARR storing subtitles for this movie: {path_mappings.path_replace_movie(movie["path"])}')
|
||||||
movie['path'])))
|
|
||||||
store_subtitles_movie(movie['path'], path_mappings.path_replace_movie(movie['path']))
|
store_subtitles_movie(movie['path'], path_mappings.path_replace_movie(movie['path']))
|
||||||
|
|
||||||
# Downloading missing subtitles
|
# Downloading missing subtitles
|
||||||
if defer_search:
|
if defer_search:
|
||||||
logging.debug('BAZARR searching for missing subtitles is deferred until scheduled task execution for this '
|
logging.debug(
|
||||||
'movie: {}'.format(path_mappings.path_replace_movie(movie['path'])))
|
f'BAZARR searching for missing subtitles is deferred until scheduled task execution for this movie: '
|
||||||
|
f'{path_mappings.path_replace_movie(movie["path"])}')
|
||||||
else:
|
else:
|
||||||
logging.debug('BAZARR downloading missing subtitles for this movie: {}'.format(path_mappings.path_replace_movie(
|
logging.debug(
|
||||||
movie['path'])))
|
f'BAZARR downloading missing subtitles for this movie: {path_mappings.path_replace_movie(movie["path"])}')
|
||||||
movies_download_subtitles(movie_id)
|
movies_download_subtitles(movie_id)
|
||||||
|
|
|
@ -25,7 +25,7 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
|
||||||
overview = ""
|
overview = ""
|
||||||
try:
|
try:
|
||||||
poster_big = movie['images'][0]['url']
|
poster_big = movie['images'][0]['url']
|
||||||
poster = os.path.splitext(poster_big)[0] + '-500' + os.path.splitext(poster_big)[1]
|
poster = f'{os.path.splitext(poster_big)[0]}-500{os.path.splitext(poster_big)[1]}'
|
||||||
except Exception:
|
except Exception:
|
||||||
poster = ""
|
poster = ""
|
||||||
try:
|
try:
|
||||||
|
@ -56,7 +56,7 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
|
||||||
except Exception:
|
except Exception:
|
||||||
format = movie['movieFile']['quality']['quality']['name']
|
format = movie['movieFile']['quality']['quality']['name']
|
||||||
try:
|
try:
|
||||||
resolution = str(movie['movieFile']['quality']['quality']['resolution']) + 'p'
|
resolution = f'{movie["movieFile"]["quality"]["quality"]["resolution"]}p'
|
||||||
except Exception:
|
except Exception:
|
||||||
resolution = None
|
resolution = None
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
|
||||||
videoCodec = None
|
videoCodec = None
|
||||||
audioCodec = None
|
audioCodec = None
|
||||||
|
|
||||||
if settings.general.getboolean('parse_embedded_audio_track'):
|
if settings.general.parse_embedded_audio_track:
|
||||||
audio_language = embedded_audio_reader(path_mappings.path_replace_movie(movie['movieFile']['path']),
|
audio_language = embedded_audio_reader(path_mappings.path_replace_movie(movie['movieFile']['path']),
|
||||||
file_size=movie['movieFile']['size'],
|
file_size=movie['movieFile']['size'],
|
||||||
movie_file_id=movie['movieFile']['id'],
|
movie_file_id=movie['movieFile']['id'],
|
||||||
|
|
|
@ -4,7 +4,7 @@ import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from radarr.info import get_radarr_info, url_radarr
|
from radarr.info import get_radarr_info, url_api_radarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,10 +12,8 @@ def get_profile_list():
|
||||||
apikey_radarr = settings.radarr.apikey
|
apikey_radarr = settings.radarr.apikey
|
||||||
profiles_list = []
|
profiles_list = []
|
||||||
# Get profiles data from radarr
|
# Get profiles data from radarr
|
||||||
if get_radarr_info.is_legacy():
|
url_radarr_api_movies = (f"{url_api_radarr()}{'quality' if url_api_radarr().endswith('v3/') else ''}profile?"
|
||||||
url_radarr_api_movies = url_radarr() + "/api/profile?apikey=" + apikey_radarr
|
f"apikey={apikey_radarr}")
|
||||||
else:
|
|
||||||
url_radarr_api_movies = url_radarr() + "/api/v3/qualityprofile?apikey=" + apikey_radarr
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profiles_json = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
profiles_json = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
||||||
|
@ -44,10 +42,7 @@ def get_tags():
|
||||||
tagsDict = []
|
tagsDict = []
|
||||||
|
|
||||||
# Get tags data from Radarr
|
# Get tags data from Radarr
|
||||||
if get_radarr_info.is_legacy():
|
url_radarr_api_series = f"{url_api_radarr()}tag?apikey={apikey_radarr}"
|
||||||
url_radarr_api_series = url_radarr() + "/api/tag?apikey=" + apikey_radarr
|
|
||||||
else:
|
|
||||||
url_radarr_api_series = url_radarr() + "/api/v3/tag?apikey=" + apikey_radarr
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tagsDict = requests.get(url_radarr_api_series, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
tagsDict = requests.get(url_radarr_api_series, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
||||||
|
@ -70,13 +65,8 @@ def get_tags():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_movies_from_radarr_api(url, apikey_radarr, radarr_id=None):
|
def get_movies_from_radarr_api(apikey_radarr, radarr_id=None):
|
||||||
if get_radarr_info.is_legacy():
|
url_radarr_api_movies = f'{url_api_radarr()}movie{f"/{radarr_id}" if radarr_id else ""}?apikey={apikey_radarr}'
|
||||||
url_radarr_api_movies = url + "/api/movie" + ("/{}".format(radarr_id) if radarr_id else "") + "?apikey=" + \
|
|
||||||
apikey_radarr
|
|
||||||
else:
|
|
||||||
url_radarr_api_movies = url + "/api/v3/movie" + ("/{}".format(radarr_id) if radarr_id else "") + "?apikey=" + \
|
|
||||||
apikey_radarr
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
r = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
|
||||||
|
@ -95,5 +85,11 @@ def get_movies_from_radarr_api(url, apikey_radarr, radarr_id=None):
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logging.exception("BAZARR Error trying to get movies from Radarr.")
|
logging.exception("BAZARR Error trying to get movies from Radarr.")
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Exception raised while getting movies from Radarr API: {e}")
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
return r.json()
|
if r.status_code == 200:
|
||||||
|
return r.json()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
|
@ -4,23 +4,18 @@ import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from sonarr.info import get_sonarr_info, url_sonarr
|
from sonarr.info import url_api_sonarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
def browse_sonarr_filesystem(path='#'):
|
def browse_sonarr_filesystem(path='#'):
|
||||||
if path == '#':
|
if path == '#':
|
||||||
path = ''
|
path = ''
|
||||||
if get_sonarr_info.is_legacy():
|
url_sonarr_api_filesystem = (f"{url_api_sonarr()}filesystem?path={path}&allowFoldersWithoutTrailingSlashes=true&"
|
||||||
url_sonarr_api_filesystem = url_sonarr() + "/api/filesystem?path=" + path + \
|
f"includeFiles=false&apikey={settings.sonarr.apikey}")
|
||||||
"&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \
|
|
||||||
settings.sonarr.apikey
|
|
||||||
else:
|
|
||||||
url_sonarr_api_filesystem = url_sonarr() + "/api/v3/filesystem?path=" + path + \
|
|
||||||
"&allowFoldersWithoutTrailingSlashes=true&includeFiles=false&apikey=" + \
|
|
||||||
settings.sonarr.apikey
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(url_sonarr_api_filesystem, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
r = requests.get(url_sonarr_api_filesystem, timeout=int(settings.sonarr.http_timeout), verify=False,
|
||||||
|
headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except requests.exceptions.HTTPError:
|
except requests.exceptions.HTTPError:
|
||||||
logging.exception("BAZARR Error trying to get series from Sonarr. Http error.")
|
logging.exception("BAZARR Error trying to get series from Sonarr. Http error.")
|
||||||
|
|
|
@ -26,25 +26,27 @@ class GetSonarrInfo:
|
||||||
return sonarr_version
|
return sonarr_version
|
||||||
else:
|
else:
|
||||||
sonarr_version = ''
|
sonarr_version = ''
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
try:
|
try:
|
||||||
sv = url_sonarr() + "/api/system/status?apikey=" + settings.sonarr.apikey
|
sv = f"{url_sonarr()}/api/system/status?apikey={settings.sonarr.apikey}"
|
||||||
sonarr_json = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers).json()
|
sonarr_json = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False,
|
||||||
|
headers=headers).json()
|
||||||
if 'version' in sonarr_json:
|
if 'version' in sonarr_json:
|
||||||
sonarr_version = sonarr_json['version']
|
sonarr_version = sonarr_json['version']
|
||||||
else:
|
else:
|
||||||
raise json.decoder.JSONDecodeError
|
raise json.decoder.JSONDecodeError
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
try:
|
try:
|
||||||
sv = url_sonarr() + "/api/v3/system/status?apikey=" + settings.sonarr.apikey
|
sv = f"{url_sonarr()}/api/v3/system/status?apikey={settings.sonarr.apikey}"
|
||||||
sonarr_version = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers).json()['version']
|
sonarr_version = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False,
|
||||||
|
headers=headers).json()['version']
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
logging.debug('BAZARR cannot get Sonarr version')
|
logging.debug('BAZARR cannot get Sonarr version')
|
||||||
sonarr_version = 'unknown'
|
sonarr_version = 'unknown'
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug('BAZARR cannot get Sonarr version')
|
logging.debug('BAZARR cannot get Sonarr version')
|
||||||
sonarr_version = 'unknown'
|
sonarr_version = 'unknown'
|
||||||
logging.debug('BAZARR got this Sonarr version from its API: {}'.format(sonarr_version))
|
logging.debug(f'BAZARR got this Sonarr version from its API: {sonarr_version}')
|
||||||
region.set("sonarr_version", sonarr_version)
|
region.set("sonarr_version", sonarr_version)
|
||||||
return sonarr_version
|
return sonarr_version
|
||||||
|
|
||||||
|
@ -75,7 +77,7 @@ get_sonarr_info = GetSonarrInfo()
|
||||||
|
|
||||||
|
|
||||||
def url_sonarr():
|
def url_sonarr():
|
||||||
if settings.sonarr.getboolean('ssl'):
|
if settings.sonarr.ssl:
|
||||||
protocol_sonarr = "https"
|
protocol_sonarr = "https"
|
||||||
else:
|
else:
|
||||||
protocol_sonarr = "http"
|
protocol_sonarr = "http"
|
||||||
|
@ -83,7 +85,7 @@ def url_sonarr():
|
||||||
if settings.sonarr.base_url == '':
|
if settings.sonarr.base_url == '':
|
||||||
settings.sonarr.base_url = "/"
|
settings.sonarr.base_url = "/"
|
||||||
if not settings.sonarr.base_url.startswith("/"):
|
if not settings.sonarr.base_url.startswith("/"):
|
||||||
settings.sonarr.base_url = "/" + settings.sonarr.base_url
|
settings.sonarr.base_url = f"/{settings.sonarr.base_url}"
|
||||||
if settings.sonarr.base_url.endswith("/"):
|
if settings.sonarr.base_url.endswith("/"):
|
||||||
settings.sonarr.base_url = settings.sonarr.base_url[:-1]
|
settings.sonarr.base_url = settings.sonarr.base_url[:-1]
|
||||||
|
|
||||||
|
@ -93,3 +95,7 @@ def url_sonarr():
|
||||||
port = f":{settings.sonarr.port}"
|
port = f":{settings.sonarr.port}"
|
||||||
|
|
||||||
return f"{protocol_sonarr}://{settings.sonarr.ip}{port}{settings.sonarr.base_url}"
|
return f"{protocol_sonarr}://{settings.sonarr.ip}{port}{settings.sonarr.base_url}"
|
||||||
|
|
||||||
|
|
||||||
|
def url_api_sonarr():
|
||||||
|
return url_sonarr() + f'/api{"/v3" if not get_sonarr_info.is_legacy() else ""}/'
|
||||||
|
|
|
@ -4,16 +4,13 @@ import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from sonarr.info import get_sonarr_info, url_sonarr
|
from sonarr.info import url_api_sonarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
def notify_sonarr(sonarr_series_id):
|
def notify_sonarr(sonarr_series_id):
|
||||||
try:
|
try:
|
||||||
if get_sonarr_info.is_legacy():
|
url = f"{url_api_sonarr()}command?apikey={settings.sonarr.apikey}"
|
||||||
url = url_sonarr() + "/api/command?apikey=" + settings.sonarr.apikey
|
|
||||||
else:
|
|
||||||
url = url_sonarr() + "/api/v3/command?apikey=" + settings.sonarr.apikey
|
|
||||||
data = {
|
data = {
|
||||||
'name': 'RescanSeries',
|
'name': 'RescanSeries',
|
||||||
'seriesId': int(sonarr_series_id)
|
'seriesId': int(sonarr_series_id)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import logging
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import TableShowsRootfolder, TableShows, database, insert, update, delete, select
|
from app.database import TableShowsRootfolder, TableShows, database, insert, update, delete, select
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
from sonarr.info import get_sonarr_info, url_sonarr
|
from sonarr.info import url_api_sonarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,10 +16,7 @@ def get_sonarr_rootfolder():
|
||||||
sonarr_rootfolder = []
|
sonarr_rootfolder = []
|
||||||
|
|
||||||
# Get root folder data from Sonarr
|
# Get root folder data from Sonarr
|
||||||
if get_sonarr_info.is_legacy():
|
url_sonarr_api_rootfolder = f"{url_api_sonarr()}rootfolder?apikey={apikey_sonarr}"
|
||||||
url_sonarr_api_rootfolder = url_sonarr() + "/api/rootfolder?apikey=" + apikey_sonarr
|
|
||||||
else:
|
|
||||||
url_sonarr_api_rootfolder = url_sonarr() + "/api/v3/rootfolder?apikey=" + apikey_sonarr
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
||||||
|
|
|
@ -47,13 +47,11 @@ def sync_episodes(series_id, send_event=True):
|
||||||
episodes_to_add = []
|
episodes_to_add = []
|
||||||
|
|
||||||
# Get episodes data for a series from Sonarr
|
# Get episodes data for a series from Sonarr
|
||||||
episodes = get_episodes_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr,
|
episodes = get_episodes_from_sonarr_api(apikey_sonarr=apikey_sonarr, series_id=series_id)
|
||||||
series_id=series_id)
|
|
||||||
if episodes:
|
if episodes:
|
||||||
# For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results
|
# For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results
|
||||||
if not get_sonarr_info.is_legacy():
|
if not get_sonarr_info.is_legacy():
|
||||||
episodeFiles = get_episodesFiles_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr,
|
episodeFiles = get_episodesFiles_from_sonarr_api(apikey_sonarr=apikey_sonarr, series_id=series_id)
|
||||||
series_id=series_id)
|
|
||||||
for episode in episodes:
|
for episode in episodes:
|
||||||
if episodeFiles and episode['hasFile']:
|
if episodeFiles and episode['hasFile']:
|
||||||
item = [x for x in episodeFiles if x['id'] == episode['episodeFileId']]
|
item = [x for x in episodeFiles if x['id'] == episode['episodeFileId']]
|
||||||
|
@ -80,31 +78,32 @@ def sync_episodes(series_id, send_event=True):
|
||||||
episodes_to_update.append(parsed_episode)
|
episodes_to_update.append(parsed_episode)
|
||||||
else:
|
else:
|
||||||
episodes_to_add.append(episodeParser(episode))
|
episodes_to_add.append(episodeParser(episode))
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
# Remove old episodes from DB
|
# Remove old episodes from DB
|
||||||
episodes_to_delete = list(set(current_episodes_id_db_list) - set(current_episodes_sonarr))
|
episodes_to_delete = list(set(current_episodes_id_db_list) - set(current_episodes_sonarr))
|
||||||
|
|
||||||
if len(episodes_to_delete):
|
if len(episodes_to_delete):
|
||||||
try:
|
try:
|
||||||
removed_episodes = database.execute(delete(TableEpisodes)
|
database.execute(delete(TableEpisodes).where(TableEpisodes.sonarrEpisodeId.in_(episodes_to_delete)))
|
||||||
.where(TableEpisodes.sonarrEpisodeId.in_(episodes_to_delete))
|
|
||||||
.returning(TableEpisodes.sonarrEpisodeId))
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logging.error(f"BAZARR cannot delete episodes because of {e}")
|
logging.error(f"BAZARR cannot delete episodes because of {e}")
|
||||||
else:
|
else:
|
||||||
for removed_episode in removed_episodes:
|
for removed_episode in episodes_to_delete:
|
||||||
if send_event:
|
if send_event:
|
||||||
event_stream(type='episode', action='delete', payload=removed_episode.sonarrEpisodeId)
|
event_stream(type='episode', action='delete', payload=removed_episode)
|
||||||
|
|
||||||
# Update existing episodes in DB
|
# Update existing episodes in DB
|
||||||
if len(episodes_to_update):
|
if len(episodes_to_update):
|
||||||
try:
|
for updated_episode in episodes_to_update:
|
||||||
database.execute(update(TableEpisodes), episodes_to_update)
|
try:
|
||||||
except IntegrityError as e:
|
database.execute(update(TableEpisodes)
|
||||||
logging.error(f"BAZARR cannot update episodes because of {e}")
|
.values(updated_episode)
|
||||||
else:
|
.where(TableEpisodes.sonarrEpisodeId == updated_episode['sonarrEpisodeId']))
|
||||||
for updated_episode in episodes_to_update:
|
except IntegrityError as e:
|
||||||
# not using .returning() because it's not supported on executemany() with SQlite
|
logging.error(f"BAZARR cannot update episodes because of {e}")
|
||||||
|
else:
|
||||||
store_subtitles(updated_episode['path'], path_mappings.path_replace(updated_episode['path']))
|
store_subtitles(updated_episode['path'], path_mappings.path_replace(updated_episode['path']))
|
||||||
|
|
||||||
if send_event:
|
if send_event:
|
||||||
|
@ -112,25 +111,22 @@ def sync_episodes(series_id, send_event=True):
|
||||||
|
|
||||||
# Insert new episodes in DB
|
# Insert new episodes in DB
|
||||||
if len(episodes_to_add):
|
if len(episodes_to_add):
|
||||||
try:
|
for added_episode in episodes_to_add:
|
||||||
added_episodes = database.execute(
|
try:
|
||||||
insert(TableEpisodes)
|
database.execute(insert(TableEpisodes).values(added_episode))
|
||||||
.values(episodes_to_add)
|
except IntegrityError as e:
|
||||||
.returning(TableEpisodes.sonarrEpisodeId, TableEpisodes.path, TableEpisodes.sonarrSeriesId))
|
logging.error(f"BAZARR cannot insert episodes because of {e}")
|
||||||
except IntegrityError as e:
|
else:
|
||||||
logging.error(f"BAZARR cannot insert episodes because of {e}")
|
store_subtitles(added_episode['path'], path_mappings.path_replace(added_episode['path']))
|
||||||
else:
|
|
||||||
for added_episode in added_episodes:
|
|
||||||
store_subtitles(added_episode.path, path_mappings.path_replace(added_episode.path))
|
|
||||||
|
|
||||||
if send_event:
|
if send_event:
|
||||||
event_stream(type='episode', payload=added_episode.sonarrEpisodeId)
|
event_stream(type='episode', payload=added_episode['sonarrEpisodeId'])
|
||||||
|
|
||||||
logging.debug(f'BAZARR All episodes from series ID {series_id} synced from Sonarr into database.')
|
logging.debug(f'BAZARR All episodes from series ID {series_id} synced from Sonarr into database.')
|
||||||
|
|
||||||
|
|
||||||
def sync_one_episode(episode_id, defer_search=False):
|
def sync_one_episode(episode_id, defer_search=False):
|
||||||
logging.debug('BAZARR syncing this specific episode from Sonarr: {}'.format(episode_id))
|
logging.debug(f'BAZARR syncing this specific episode from Sonarr: {episode_id}')
|
||||||
url = url_sonarr()
|
url = url_sonarr()
|
||||||
apikey_sonarr = settings.sonarr.apikey
|
apikey_sonarr = settings.sonarr.apikey
|
||||||
|
|
||||||
|
@ -143,8 +139,7 @@ def sync_one_episode(episode_id, defer_search=False):
|
||||||
try:
|
try:
|
||||||
# Get episode data from sonarr api
|
# Get episode data from sonarr api
|
||||||
episode = None
|
episode = None
|
||||||
episode_data = get_episodes_from_sonarr_api(url=url, apikey_sonarr=apikey_sonarr,
|
episode_data = get_episodes_from_sonarr_api(apikey_sonarr=apikey_sonarr, episode_id=episode_id)
|
||||||
episode_id=episode_id)
|
|
||||||
if not episode_data:
|
if not episode_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -152,7 +147,7 @@ def sync_one_episode(episode_id, defer_search=False):
|
||||||
# For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results
|
# For Sonarr v3, we need to update episodes to integrate the episodeFile API endpoint results
|
||||||
if not get_sonarr_info.is_legacy() and existing_episode and episode_data['hasFile']:
|
if not get_sonarr_info.is_legacy() and existing_episode and episode_data['hasFile']:
|
||||||
episode_data['episodeFile'] = \
|
episode_data['episodeFile'] = \
|
||||||
get_episodesFiles_from_sonarr_api(url=url, apikey_sonarr=apikey_sonarr,
|
get_episodesFiles_from_sonarr_api(apikey_sonarr=apikey_sonarr,
|
||||||
episode_file_id=episode_data['episodeFileId'])
|
episode_file_id=episode_data['episodeFileId'])
|
||||||
episode = episodeParser(episode_data)
|
episode = episodeParser(episode_data)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -173,8 +168,8 @@ def sync_one_episode(episode_id, defer_search=False):
|
||||||
logging.error(f"BAZARR cannot delete episode {existing_episode.path} because of {e}")
|
logging.error(f"BAZARR cannot delete episode {existing_episode.path} because of {e}")
|
||||||
else:
|
else:
|
||||||
event_stream(type='episode', action='delete', payload=int(episode_id))
|
event_stream(type='episode', action='delete', payload=int(episode_id))
|
||||||
logging.debug('BAZARR deleted this episode from the database:{}'.format(path_mappings.path_replace(
|
logging.debug(
|
||||||
existing_episode['path'])))
|
f'BAZARR deleted this episode from the database:{path_mappings.path_replace(existing_episode["path"])}')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update existing episodes in DB
|
# Update existing episodes in DB
|
||||||
|
@ -187,9 +182,10 @@ def sync_one_episode(episode_id, defer_search=False):
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logging.error(f"BAZARR cannot update episode {episode['path']} because of {e}")
|
logging.error(f"BAZARR cannot update episode {episode['path']} because of {e}")
|
||||||
else:
|
else:
|
||||||
|
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
|
||||||
event_stream(type='episode', action='update', payload=int(episode_id))
|
event_stream(type='episode', action='update', payload=int(episode_id))
|
||||||
logging.debug('BAZARR updated this episode into the database:{}'.format(path_mappings.path_replace(
|
logging.debug(
|
||||||
episode['path'])))
|
f'BAZARR updated this episode into the database:{path_mappings.path_replace(episode["path"])}')
|
||||||
|
|
||||||
# Insert new episodes in DB
|
# Insert new episodes in DB
|
||||||
elif episode and not existing_episode:
|
elif episode and not existing_episode:
|
||||||
|
@ -200,20 +196,21 @@ def sync_one_episode(episode_id, defer_search=False):
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logging.error(f"BAZARR cannot insert episode {episode['path']} because of {e}")
|
logging.error(f"BAZARR cannot insert episode {episode['path']} because of {e}")
|
||||||
else:
|
else:
|
||||||
|
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
|
||||||
event_stream(type='episode', action='update', payload=int(episode_id))
|
event_stream(type='episode', action='update', payload=int(episode_id))
|
||||||
logging.debug('BAZARR inserted this episode into the database:{}'.format(path_mappings.path_replace(
|
logging.debug(
|
||||||
episode['path'])))
|
f'BAZARR inserted this episode into the database:{path_mappings.path_replace(episode["path"])}')
|
||||||
|
|
||||||
# Storing existing subtitles
|
# Storing existing subtitles
|
||||||
logging.debug('BAZARR storing subtitles for this episode: {}'.format(path_mappings.path_replace(
|
logging.debug(f'BAZARR storing subtitles for this episode: {path_mappings.path_replace(episode["path"])}')
|
||||||
episode['path'])))
|
|
||||||
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
|
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
|
||||||
|
|
||||||
# Downloading missing subtitles
|
# Downloading missing subtitles
|
||||||
if defer_search:
|
if defer_search:
|
||||||
logging.debug('BAZARR searching for missing subtitles is deferred until scheduled task execution for this '
|
logging.debug(
|
||||||
'episode: {}'.format(path_mappings.path_replace(episode['path'])))
|
f'BAZARR searching for missing subtitles is deferred until scheduled task execution for this episode: '
|
||||||
|
f'{path_mappings.path_replace(episode["path"])}')
|
||||||
else:
|
else:
|
||||||
logging.debug('BAZARR downloading missing subtitles for this episode: {}'.format(path_mappings.path_replace(
|
logging.debug(
|
||||||
episode['path'])))
|
f'BAZARR downloading missing subtitles for this episode: {path_mappings.path_replace(episode["path"])}')
|
||||||
episode_download_subtitles(episode_id)
|
episode_download_subtitles(episode_id)
|
||||||
|
|
|
@ -18,7 +18,7 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
|
||||||
for image in show['images']:
|
for image in show['images']:
|
||||||
if image['coverType'] == 'poster':
|
if image['coverType'] == 'poster':
|
||||||
poster_big = image['url'].split('?')[0]
|
poster_big = image['url'].split('?')[0]
|
||||||
poster = os.path.splitext(poster_big)[0] + '-250' + os.path.splitext(poster_big)[1]
|
poster = f'{os.path.splitext(poster_big)[0]}-250{os.path.splitext(poster_big)[1]}'
|
||||||
|
|
||||||
if image['coverType'] == 'fanart':
|
if image['coverType'] == 'fanart':
|
||||||
fanart = image['url'].split('?')[0]
|
fanart = image['url'].split('?')[0]
|
||||||
|
@ -32,7 +32,7 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
|
||||||
imdbId = show['imdbId'] if 'imdbId' in show else None
|
imdbId = show['imdbId'] if 'imdbId' in show else None
|
||||||
|
|
||||||
audio_language = []
|
audio_language = []
|
||||||
if not settings.general.getboolean('parse_embedded_audio_track'):
|
if not settings.general.parse_embedded_audio_track:
|
||||||
if get_sonarr_info.is_legacy():
|
if get_sonarr_info.is_legacy():
|
||||||
audio_language = profile_id_to_language(show['qualityProfileId'], audio_profiles)
|
audio_language = profile_id_to_language(show['qualityProfileId'], audio_profiles)
|
||||||
else:
|
else:
|
||||||
|
@ -98,7 +98,7 @@ def episodeParser(episode):
|
||||||
else:
|
else:
|
||||||
sceneName = None
|
sceneName = None
|
||||||
|
|
||||||
if settings.general.getboolean('parse_embedded_audio_track'):
|
if settings.general.parse_embedded_audio_track:
|
||||||
audio_language = embedded_audio_reader(path_mappings.path_replace(episode['episodeFile']
|
audio_language = embedded_audio_reader(path_mappings.path_replace(episode['episodeFile']
|
||||||
['path']),
|
['path']),
|
||||||
file_size=episode['episodeFile']['size'],
|
file_size=episode['episodeFile']['size'],
|
||||||
|
@ -144,7 +144,7 @@ def episodeParser(episode):
|
||||||
except Exception:
|
except Exception:
|
||||||
video_format = episode['episodeFile']['quality']['quality']['name']
|
video_format = episode['episodeFile']['quality']['quality']['name']
|
||||||
try:
|
try:
|
||||||
video_resolution = str(episode['episodeFile']['quality']['quality']['resolution']) + 'p'
|
video_resolution = f'{episode["episodeFile"]["quality"]["quality"]["resolution"]}p'
|
||||||
except Exception:
|
except Exception:
|
||||||
video_resolution = None
|
video_resolution = None
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from app.config import settings
|
||||||
from sonarr.info import url_sonarr
|
from sonarr.info import url_sonarr
|
||||||
from subtitles.indexer.series import list_missing_subtitles
|
from subtitles.indexer.series import list_missing_subtitles
|
||||||
from sonarr.rootfolder import check_sonarr_rootfolder
|
from sonarr.rootfolder import check_sonarr_rootfolder
|
||||||
from app.database import TableShows, database, insert, update, delete, select
|
from app.database import TableShows, TableLanguagesProfiles, database, insert, update, delete, select
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
from app.event_handler import event_stream, show_progress, hide_progress
|
from app.event_handler import event_stream, show_progress, hide_progress
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ def update_series(send_event=True):
|
||||||
if apikey_sonarr is None:
|
if apikey_sonarr is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
serie_default_enabled = settings.general.getboolean('serie_default_enabled')
|
serie_default_enabled = settings.general.serie_default_enabled
|
||||||
|
|
||||||
if serie_default_enabled is True:
|
if serie_default_enabled is True:
|
||||||
serie_default_profile = settings.general.serie_default_profile
|
serie_default_profile = settings.general.serie_default_profile
|
||||||
|
@ -32,11 +32,18 @@ def update_series(send_event=True):
|
||||||
else:
|
else:
|
||||||
serie_default_profile = None
|
serie_default_profile = None
|
||||||
|
|
||||||
|
# Prevent trying to insert a series with a non-existing languages profileId
|
||||||
|
if (serie_default_profile and not database.execute(
|
||||||
|
select(TableLanguagesProfiles)
|
||||||
|
.where(TableLanguagesProfiles.profileId == serie_default_profile))
|
||||||
|
.first()):
|
||||||
|
serie_default_profile = None
|
||||||
|
|
||||||
audio_profiles = get_profile_list()
|
audio_profiles = get_profile_list()
|
||||||
tagsDict = get_tags()
|
tagsDict = get_tags()
|
||||||
|
|
||||||
# Get shows data from Sonarr
|
# Get shows data from Sonarr
|
||||||
series = get_series_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr)
|
series = get_series_from_sonarr_api(apikey_sonarr=apikey_sonarr)
|
||||||
if not isinstance(series, list):
|
if not isinstance(series, list):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
@ -117,7 +124,7 @@ def update_series(send_event=True):
|
||||||
|
|
||||||
|
|
||||||
def update_one_series(series_id, action):
|
def update_one_series(series_id, action):
|
||||||
logging.debug('BAZARR syncing this specific series from Sonarr: {}'.format(series_id))
|
logging.debug(f'BAZARR syncing this specific series from Sonarr: {series_id}')
|
||||||
|
|
||||||
# Check if there's a row in database for this series ID
|
# Check if there's a row in database for this series ID
|
||||||
existing_series = database.execute(
|
existing_series = database.execute(
|
||||||
|
@ -134,7 +141,7 @@ def update_one_series(series_id, action):
|
||||||
event_stream(type='series', action='delete', payload=int(series_id))
|
event_stream(type='series', action='delete', payload=int(series_id))
|
||||||
return
|
return
|
||||||
|
|
||||||
serie_default_enabled = settings.general.getboolean('serie_default_enabled')
|
serie_default_enabled = settings.general.serie_default_enabled
|
||||||
|
|
||||||
if serie_default_enabled is True:
|
if serie_default_enabled is True:
|
||||||
serie_default_profile = settings.general.serie_default_profile
|
serie_default_profile = settings.general.serie_default_profile
|
||||||
|
@ -150,8 +157,7 @@ def update_one_series(series_id, action):
|
||||||
# Get series data from sonarr api
|
# Get series data from sonarr api
|
||||||
series = None
|
series = None
|
||||||
|
|
||||||
series_data = get_series_from_sonarr_api(url=url_sonarr(), apikey_sonarr=settings.sonarr.apikey,
|
series_data = get_series_from_sonarr_api(apikey_sonarr=settings.sonarr.apikey, sonarr_series_id=int(series_id))
|
||||||
sonarr_series_id=int(series_id))
|
|
||||||
|
|
||||||
if not series_data:
|
if not series_data:
|
||||||
return
|
return
|
||||||
|
@ -180,8 +186,7 @@ def update_one_series(series_id, action):
|
||||||
else:
|
else:
|
||||||
sync_episodes(series_id=int(series_id), send_event=False)
|
sync_episodes(series_id=int(series_id), send_event=False)
|
||||||
event_stream(type='series', action='update', payload=int(series_id))
|
event_stream(type='series', action='update', payload=int(series_id))
|
||||||
logging.debug('BAZARR updated this series into the database:{}'.format(path_mappings.path_replace(
|
logging.debug(f'BAZARR updated this series into the database:{path_mappings.path_replace(series["path"])}')
|
||||||
series['path'])))
|
|
||||||
|
|
||||||
# Insert new series in DB
|
# Insert new series in DB
|
||||||
elif action == 'updated' and not existing_series:
|
elif action == 'updated' and not existing_series:
|
||||||
|
@ -193,5 +198,4 @@ def update_one_series(series_id, action):
|
||||||
logging.error(f"BAZARR cannot insert series {series['path']} because of {e}")
|
logging.error(f"BAZARR cannot insert series {series['path']} because of {e}")
|
||||||
else:
|
else:
|
||||||
event_stream(type='series', action='update', payload=int(series_id))
|
event_stream(type='series', action='update', payload=int(series_id))
|
||||||
logging.debug('BAZARR inserted this series into the database:{}'.format(path_mappings.path_replace(
|
logging.debug(f'BAZARR inserted this series into the database:{path_mappings.path_replace(series["path"])}')
|
||||||
series['path'])))
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from sonarr.info import get_sonarr_info, url_sonarr
|
from sonarr.info import get_sonarr_info, url_api_sonarr
|
||||||
from constants import headers
|
from constants import headers
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,15 +14,16 @@ def get_profile_list():
|
||||||
|
|
||||||
# Get profiles data from Sonarr
|
# Get profiles data from Sonarr
|
||||||
if get_sonarr_info.is_legacy():
|
if get_sonarr_info.is_legacy():
|
||||||
url_sonarr_api_series = url_sonarr() + "/api/profile?apikey=" + apikey_sonarr
|
url_sonarr_api_series = f"{url_api_sonarr()}profile?apikey={apikey_sonarr}"
|
||||||
else:
|
else:
|
||||||
if not get_sonarr_info.version().startswith('3.'):
|
if not get_sonarr_info.version().startswith('3.'):
|
||||||
# return an empty list when using Sonarr >= v4 that does not support series languages profiles anymore
|
# return an empty list when using Sonarr >= v4 that does not support series languages profiles anymore
|
||||||
return profiles_list
|
return profiles_list
|
||||||
url_sonarr_api_series = url_sonarr() + "/api/v3/languageprofile?apikey=" + apikey_sonarr
|
url_sonarr_api_series = f"{url_api_sonarr()}languageprofile?apikey={apikey_sonarr}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profiles_json = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
profiles_json = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False,
|
||||||
|
headers=headers)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.")
|
logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.")
|
||||||
return None
|
return None
|
||||||
|
@ -49,10 +50,7 @@ def get_tags():
|
||||||
tagsDict = []
|
tagsDict = []
|
||||||
|
|
||||||
# Get tags data from Sonarr
|
# Get tags data from Sonarr
|
||||||
if get_sonarr_info.is_legacy():
|
url_sonarr_api_series = f"{url_api_sonarr()}tag?apikey={apikey_sonarr}"
|
||||||
url_sonarr_api_series = url_sonarr() + "/api/tag?apikey=" + apikey_sonarr
|
|
||||||
else:
|
|
||||||
url_sonarr_api_series = url_sonarr() + "/api/v3/tag?apikey=" + apikey_sonarr
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tagsDict = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
tagsDict = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
||||||
|
@ -69,9 +67,9 @@ def get_tags():
|
||||||
return tagsDict.json()
|
return tagsDict.json()
|
||||||
|
|
||||||
|
|
||||||
def get_series_from_sonarr_api(url, apikey_sonarr, sonarr_series_id=None):
|
def get_series_from_sonarr_api(apikey_sonarr, sonarr_series_id=None):
|
||||||
url_sonarr_api_series = url + "/api/{0}series/{1}?apikey={2}".format(
|
url_sonarr_api_series = (f"{url_api_sonarr()}series/{sonarr_series_id if sonarr_series_id else ''}?"
|
||||||
'' if get_sonarr_info.is_legacy() else 'v3/', sonarr_series_id if sonarr_series_id else "", apikey_sonarr)
|
f"apikey={apikey_sonarr}")
|
||||||
try:
|
try:
|
||||||
r = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
r = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@ -89,21 +87,25 @@ def get_series_from_sonarr_api(url, apikey_sonarr, sonarr_series_id=None):
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logging.exception("BAZARR Error trying to get series from Sonarr.")
|
logging.exception("BAZARR Error trying to get series from Sonarr.")
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Exception raised while getting series from Sonarr API: {e}")
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
result = r.json()
|
if r.status_code == 200:
|
||||||
if isinstance(result, dict):
|
result = r.json()
|
||||||
return [result]
|
if isinstance(result, dict):
|
||||||
|
return [result]
|
||||||
|
else:
|
||||||
|
return r.json()
|
||||||
else:
|
else:
|
||||||
return r.json()
|
return
|
||||||
|
|
||||||
|
|
||||||
def get_episodes_from_sonarr_api(url, apikey_sonarr, series_id=None, episode_id=None):
|
def get_episodes_from_sonarr_api(apikey_sonarr, series_id=None, episode_id=None):
|
||||||
if series_id:
|
if series_id:
|
||||||
url_sonarr_api_episode = url + "/api/{0}episode?seriesId={1}&apikey={2}".format(
|
url_sonarr_api_episode = f"{url_api_sonarr()}episode?seriesId={series_id}&apikey={apikey_sonarr}"
|
||||||
'' if get_sonarr_info.is_legacy() else 'v3/', series_id, apikey_sonarr)
|
|
||||||
elif episode_id:
|
elif episode_id:
|
||||||
url_sonarr_api_episode = url + "/api/{0}episode/{1}?apikey={2}".format(
|
url_sonarr_api_episode = f"{url_api_sonarr()}episode/{episode_id}?apikey={apikey_sonarr}"
|
||||||
'' if get_sonarr_info.is_legacy() else 'v3/', episode_id, apikey_sonarr)
|
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -122,21 +124,27 @@ def get_episodes_from_sonarr_api(url, apikey_sonarr, series_id=None, episode_id=
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logging.exception("BAZARR Error trying to get episodes from Sonarr.")
|
logging.exception("BAZARR Error trying to get episodes from Sonarr.")
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Exception raised while getting episodes from Sonarr API: {e}")
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
return r.json()
|
if r.status_code == 200:
|
||||||
|
return r.json()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def get_episodesFiles_from_sonarr_api(url, apikey_sonarr, series_id=None, episode_file_id=None):
|
def get_episodesFiles_from_sonarr_api(apikey_sonarr, series_id=None, episode_file_id=None):
|
||||||
if series_id:
|
if series_id:
|
||||||
url_sonarr_api_episodeFiles = url + "/api/v3/episodeFile?seriesId={0}&apikey={1}".format(series_id,
|
url_sonarr_api_episodeFiles = f"{url_api_sonarr()}episodeFile?seriesId={series_id}&apikey={apikey_sonarr}"
|
||||||
apikey_sonarr)
|
|
||||||
elif episode_file_id:
|
elif episode_file_id:
|
||||||
url_sonarr_api_episodeFiles = url + "/api/v3/episodeFile/{0}?apikey={1}".format(episode_file_id, apikey_sonarr)
|
url_sonarr_api_episodeFiles = f"{url_api_sonarr()}episodeFile/{episode_file_id}?apikey={apikey_sonarr}"
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(url_sonarr_api_episodeFiles, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
|
r = requests.get(url_sonarr_api_episodeFiles, timeout=int(settings.sonarr.http_timeout), verify=False,
|
||||||
|
headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except requests.exceptions.HTTPError:
|
except requests.exceptions.HTTPError:
|
||||||
logging.exception("BAZARR Error trying to get episodeFiles from Sonarr. Http error.")
|
logging.exception("BAZARR Error trying to get episodeFiles from Sonarr. Http error.")
|
||||||
|
@ -150,5 +158,11 @@ def get_episodesFiles_from_sonarr_api(url, apikey_sonarr, series_id=None, episod
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logging.exception("BAZARR Error trying to get episodeFiles from Sonarr.")
|
logging.exception("BAZARR Error trying to get episodeFiles from Sonarr.")
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Exception raised while getting episodes from Sonarr API: {e}")
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
return r.json()
|
if r.status_code == 200:
|
||||||
|
return r.json()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
|
@ -23,7 +23,7 @@ def is_search_active(desired_language, attempt_string):
|
||||||
@rtype: bool
|
@rtype: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if settings.general.getboolean('adaptive_searching'):
|
if settings.general.adaptive_searching:
|
||||||
logging.debug("Adaptive searching is enable, we'll see if it's time to search again...")
|
logging.debug("Adaptive searching is enable, we'll see if it's time to search again...")
|
||||||
try:
|
try:
|
||||||
# let's try to get a list of lists from the string representation in database
|
# let's try to get a list of lists from the string representation in database
|
||||||
|
|
|
@ -12,7 +12,7 @@ from subliminal_patch.core import save_subtitles
|
||||||
from subliminal_patch.core_persistent import download_best_subtitles
|
from subliminal_patch.core_persistent import download_best_subtitles
|
||||||
from subliminal_patch.score import ComputeScore
|
from subliminal_patch.score import ComputeScore
|
||||||
|
|
||||||
from app.config import settings, get_array_from, get_scores
|
from app.config import settings, get_scores, get_array_from
|
||||||
from app.database import TableEpisodes, TableMovies, database, select
|
from app.database import TableEpisodes, TableMovies, database, select
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
from utilities.helper import get_target_folder, force_unicode
|
from utilities.helper import get_target_folder, force_unicode
|
||||||
|
@ -29,9 +29,9 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_
|
||||||
if not languages:
|
if not languages:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logging.debug('BAZARR Searching subtitles for this file: ' + path)
|
logging.debug(f'BAZARR Searching subtitles for this file: {path}')
|
||||||
|
|
||||||
if settings.general.getboolean('utf8_encode'):
|
if settings.general.utf8_encode:
|
||||||
os.environ["SZ_KEEP_ENCODING"] = ""
|
os.environ["SZ_KEEP_ENCODING"] = ""
|
||||||
else:
|
else:
|
||||||
os.environ["SZ_KEEP_ENCODING"] = "True"
|
os.environ["SZ_KEEP_ENCODING"] = "True"
|
||||||
|
@ -40,7 +40,7 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_
|
||||||
providers = pool.providers
|
providers = pool.providers
|
||||||
|
|
||||||
language_set = _get_language_obj(languages=languages)
|
language_set = _get_language_obj(languages=languages)
|
||||||
hi_required = any([x.hi for x in language_set])
|
hi_required = "force HI" if any([x.hi for x in language_set]) else False
|
||||||
also_forced = any([x.forced for x in language_set])
|
also_forced = any([x.forced for x in language_set])
|
||||||
forced_required = all([x.forced for x in language_set])
|
forced_required = all([x.forced for x in language_set])
|
||||||
_set_forced_providers(pool=pool, also_forced=also_forced, forced_required=forced_required)
|
_set_forced_providers(pool=pool, also_forced=also_forced, forced_required=forced_required)
|
||||||
|
@ -86,9 +86,9 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_
|
||||||
try:
|
try:
|
||||||
fld = get_target_folder(path)
|
fld = get_target_folder(path)
|
||||||
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
||||||
'win') and settings.general.getboolean('chmod_enabled') else None
|
'win') and settings.general.chmod_enabled else None
|
||||||
saved_subtitles = save_subtitles(video.original_path, subtitles,
|
saved_subtitles = save_subtitles(video.original_path, subtitles,
|
||||||
single=settings.general.getboolean('single_language'),
|
single=settings.general.single_language,
|
||||||
tags=None, # fixme
|
tags=None, # fixme
|
||||||
directory=fld,
|
directory=fld,
|
||||||
chmod=chmod,
|
chmod=chmod,
|
||||||
|
@ -97,7 +97,7 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(
|
logging.exception(
|
||||||
'BAZARR Error saving Subtitles file to disk for this file:' + path + ': ' + repr(e))
|
f'BAZARR Error saving Subtitles file to disk for this file {path}: {repr(e)}')
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
saved_any = True
|
saved_any = True
|
||||||
|
@ -115,12 +115,12 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not saved_any:
|
if not saved_any:
|
||||||
logging.debug('BAZARR No Subtitles were found for this file: ' + path)
|
logging.debug(f'BAZARR No Subtitles were found for this file: {path}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
subliminal.region.backend.sync()
|
subliminal.region.backend.sync()
|
||||||
|
|
||||||
logging.debug('BAZARR Ended searching Subtitles for file: ' + path)
|
logging.debug(f'BAZARR Ended searching Subtitles for file: {path}')
|
||||||
|
|
||||||
|
|
||||||
def _get_language_obj(languages):
|
def _get_language_obj(languages):
|
||||||
|
|
|
@ -22,10 +22,10 @@ gc.enable()
|
||||||
|
|
||||||
|
|
||||||
def store_subtitles_movie(original_path, reversed_path, use_cache=True):
|
def store_subtitles_movie(original_path, reversed_path, use_cache=True):
|
||||||
logging.debug('BAZARR started subtitles indexing for this file: ' + reversed_path)
|
logging.debug(f'BAZARR started subtitles indexing for this file: {reversed_path}')
|
||||||
actual_subtitles = []
|
actual_subtitles = []
|
||||||
if os.path.exists(reversed_path):
|
if os.path.exists(reversed_path):
|
||||||
if settings.general.getboolean('use_embedded_subs'):
|
if settings.general.use_embedded_subs:
|
||||||
logging.debug("BAZARR is trying to index embedded subtitles.")
|
logging.debug("BAZARR is trying to index embedded subtitles.")
|
||||||
item = database.execute(
|
item = database.execute(
|
||||||
select(TableMovies.movie_file_id, TableMovies.file_size)
|
select(TableMovies.movie_file_id, TableMovies.file_size)
|
||||||
|
@ -41,10 +41,10 @@ def store_subtitles_movie(original_path, reversed_path, use_cache=True):
|
||||||
use_cache=use_cache)
|
use_cache=use_cache)
|
||||||
for subtitle_language, subtitle_forced, subtitle_hi, subtitle_codec in subtitle_languages:
|
for subtitle_language, subtitle_forced, subtitle_hi, subtitle_codec in subtitle_languages:
|
||||||
try:
|
try:
|
||||||
if (settings.general.getboolean("ignore_pgs_subs") and subtitle_codec.lower() == "pgs") or \
|
if (settings.general.ignore_pgs_subs and subtitle_codec.lower() == "pgs") or \
|
||||||
(settings.general.getboolean("ignore_vobsub_subs") and subtitle_codec.lower() ==
|
(settings.general.ignore_vobsub_subs and subtitle_codec.lower() ==
|
||||||
"vobsub") or \
|
"vobsub") or \
|
||||||
(settings.general.getboolean("ignore_ass_subs") and subtitle_codec.lower() ==
|
(settings.general.ignore_ass_subs and subtitle_codec.lower() ==
|
||||||
"ass"):
|
"ass"):
|
||||||
logging.debug("BAZARR skipping %s sub for language: %s" % (subtitle_codec, alpha2_from_alpha3(subtitle_language)))
|
logging.debug("BAZARR skipping %s sub for language: %s" % (subtitle_codec, alpha2_from_alpha3(subtitle_language)))
|
||||||
continue
|
continue
|
||||||
|
@ -52,18 +52,18 @@ def store_subtitles_movie(original_path, reversed_path, use_cache=True):
|
||||||
if alpha2_from_alpha3(subtitle_language) is not None:
|
if alpha2_from_alpha3(subtitle_language) is not None:
|
||||||
lang = str(alpha2_from_alpha3(subtitle_language))
|
lang = str(alpha2_from_alpha3(subtitle_language))
|
||||||
if subtitle_forced:
|
if subtitle_forced:
|
||||||
lang = lang + ':forced'
|
lang = f'{lang}:forced'
|
||||||
if subtitle_hi:
|
if subtitle_hi:
|
||||||
lang = lang + ':hi'
|
lang = f'{lang}:hi'
|
||||||
logging.debug("BAZARR embedded subtitles detected: " + lang)
|
logging.debug(f"BAZARR embedded subtitles detected: {lang}")
|
||||||
actual_subtitles.append([lang, None, None])
|
actual_subtitles.append([lang, None, None])
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logging.debug("BAZARR unable to index this unrecognized language: %s (%s)",
|
logging.debug(f"BAZARR unable to index this unrecognized language: {subtitle_language} "
|
||||||
subtitle_language, error)
|
f"({error})")
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(
|
logging.exception(
|
||||||
"BAZARR error when trying to analyze this %s file: %s" % (os.path.splitext(reversed_path)[1],
|
f"BAZARR error when trying to analyze this {os.path.splitext(reversed_path)[1]} file: "
|
||||||
reversed_path))
|
f"{reversed_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dest_folder = get_subtitle_destination_folder()
|
dest_folder = get_subtitle_destination_folder()
|
||||||
|
@ -85,7 +85,7 @@ def store_subtitles_movie(original_path, reversed_path, use_cache=True):
|
||||||
os.stat(path_mappings.path_replace(x[1])).st_size == x[2]]
|
os.stat(path_mappings.path_replace(x[1])).st_size == x[2]]
|
||||||
|
|
||||||
subtitles = search_external_subtitles(reversed_path, languages=get_language_set(),
|
subtitles = search_external_subtitles(reversed_path, languages=get_language_set(),
|
||||||
only_one=settings.general.getboolean('single_language'))
|
only_one=settings.general.single_language)
|
||||||
full_dest_folder_path = os.path.dirname(reversed_path)
|
full_dest_folder_path = os.path.dirname(reversed_path)
|
||||||
if dest_folder:
|
if dest_folder:
|
||||||
if settings.general.subfolder == "absolute":
|
if settings.general.subfolder == "absolute":
|
||||||
|
@ -119,12 +119,12 @@ def store_subtitles_movie(original_path, reversed_path, use_cache=True):
|
||||||
|
|
||||||
elif str(language.basename) != 'und':
|
elif str(language.basename) != 'und':
|
||||||
if language.forced:
|
if language.forced:
|
||||||
language_str = str(language)
|
language_str = f'{language}:forced'
|
||||||
elif language.hi:
|
elif language.hi:
|
||||||
language_str = str(language) + ':hi'
|
language_str = f'{language}:hi'
|
||||||
else:
|
else:
|
||||||
language_str = str(language)
|
language_str = str(language)
|
||||||
logging.debug("BAZARR external subtitles detected: " + language_str)
|
logging.debug(f"BAZARR external subtitles detected: {language_str}")
|
||||||
actual_subtitles.append([language_str, path_mappings.path_replace_reverse_movie(subtitle_path),
|
actual_subtitles.append([language_str, path_mappings.path_replace_reverse_movie(subtitle_path),
|
||||||
os.stat(subtitle_path).st_size])
|
os.stat(subtitle_path).st_size])
|
||||||
|
|
||||||
|
@ -139,14 +139,14 @@ def store_subtitles_movie(original_path, reversed_path, use_cache=True):
|
||||||
|
|
||||||
for movie in matching_movies:
|
for movie in matching_movies:
|
||||||
if movie:
|
if movie:
|
||||||
logging.debug("BAZARR storing those languages to DB: " + str(actual_subtitles))
|
logging.debug(f"BAZARR storing those languages to DB: {actual_subtitles}")
|
||||||
list_missing_subtitles_movies(no=movie.radarrId)
|
list_missing_subtitles_movies(no=movie.radarrId)
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR haven't been able to update existing subtitles to DB : " + str(actual_subtitles))
|
logging.debug(f"BAZARR haven't been able to update existing subtitles to DB: {actual_subtitles}")
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR this file doesn't seems to exist or isn't accessible.")
|
logging.debug("BAZARR this file doesn't seems to exist or isn't accessible.")
|
||||||
|
|
||||||
logging.debug('BAZARR ended subtitles indexing for this file: ' + reversed_path)
|
logging.debug(f'BAZARR ended subtitles indexing for this file: {reversed_path}')
|
||||||
|
|
||||||
return actual_subtitles
|
return actual_subtitles
|
||||||
|
|
||||||
|
@ -168,7 +168,7 @@ def list_missing_subtitles_movies(no=None, send_event=True):
|
||||||
TableMovies.audio_language)) \
|
TableMovies.audio_language)) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
use_embedded_subs = settings.general.getboolean('use_embedded_subs')
|
use_embedded_subs = settings.general.use_embedded_subs
|
||||||
|
|
||||||
for movie_subtitles in movies_subtitles:
|
for movie_subtitles in movies_subtitles:
|
||||||
missing_subtitles_text = '[]'
|
missing_subtitles_text = '[]'
|
||||||
|
@ -264,7 +264,7 @@ def list_missing_subtitles_movies(no=None, send_event=True):
|
||||||
event_stream(type='badges')
|
event_stream(type='badges')
|
||||||
|
|
||||||
|
|
||||||
def movies_full_scan_subtitles(use_cache=settings.radarr.getboolean('use_ffprobe_cache')):
|
def movies_full_scan_subtitles(use_cache=settings.radarr.use_ffprobe_cache):
|
||||||
movies = database.execute(
|
movies = database.execute(
|
||||||
select(TableMovies.path))\
|
select(TableMovies.path))\
|
||||||
.all()
|
.all()
|
||||||
|
|
|
@ -22,10 +22,10 @@ gc.enable()
|
||||||
|
|
||||||
|
|
||||||
def store_subtitles(original_path, reversed_path, use_cache=True):
|
def store_subtitles(original_path, reversed_path, use_cache=True):
|
||||||
logging.debug('BAZARR started subtitles indexing for this file: ' + reversed_path)
|
logging.debug(f'BAZARR started subtitles indexing for this file: {reversed_path}')
|
||||||
actual_subtitles = []
|
actual_subtitles = []
|
||||||
if os.path.exists(reversed_path):
|
if os.path.exists(reversed_path):
|
||||||
if settings.general.getboolean('use_embedded_subs'):
|
if settings.general.use_embedded_subs:
|
||||||
logging.debug("BAZARR is trying to index embedded subtitles.")
|
logging.debug("BAZARR is trying to index embedded subtitles.")
|
||||||
item = database.execute(
|
item = database.execute(
|
||||||
select(TableEpisodes.episode_file_id, TableEpisodes.file_size)
|
select(TableEpisodes.episode_file_id, TableEpisodes.file_size)
|
||||||
|
@ -41,10 +41,10 @@ def store_subtitles(original_path, reversed_path, use_cache=True):
|
||||||
use_cache=use_cache)
|
use_cache=use_cache)
|
||||||
for subtitle_language, subtitle_forced, subtitle_hi, subtitle_codec in subtitle_languages:
|
for subtitle_language, subtitle_forced, subtitle_hi, subtitle_codec in subtitle_languages:
|
||||||
try:
|
try:
|
||||||
if (settings.general.getboolean("ignore_pgs_subs") and subtitle_codec.lower() == "pgs") or \
|
if (settings.general.ignore_pgs_subs and subtitle_codec.lower() == "pgs") or \
|
||||||
(settings.general.getboolean("ignore_vobsub_subs") and subtitle_codec.lower() ==
|
(settings.general.ignore_vobsub_subs and subtitle_codec.lower() ==
|
||||||
"vobsub") or \
|
"vobsub") or \
|
||||||
(settings.general.getboolean("ignore_ass_subs") and subtitle_codec.lower() ==
|
(settings.general.ignore_ass_subs and subtitle_codec.lower() ==
|
||||||
"ass"):
|
"ass"):
|
||||||
logging.debug("BAZARR skipping %s sub for language: %s" % (subtitle_codec, alpha2_from_alpha3(subtitle_language)))
|
logging.debug("BAZARR skipping %s sub for language: %s" % (subtitle_codec, alpha2_from_alpha3(subtitle_language)))
|
||||||
continue
|
continue
|
||||||
|
@ -52,10 +52,10 @@ def store_subtitles(original_path, reversed_path, use_cache=True):
|
||||||
if alpha2_from_alpha3(subtitle_language) is not None:
|
if alpha2_from_alpha3(subtitle_language) is not None:
|
||||||
lang = str(alpha2_from_alpha3(subtitle_language))
|
lang = str(alpha2_from_alpha3(subtitle_language))
|
||||||
if subtitle_forced:
|
if subtitle_forced:
|
||||||
lang = lang + ":forced"
|
lang = f"{lang}:forced"
|
||||||
if subtitle_hi:
|
if subtitle_hi:
|
||||||
lang = lang + ":hi"
|
lang = f"{lang}:hi"
|
||||||
logging.debug("BAZARR embedded subtitles detected: " + lang)
|
logging.debug(f"BAZARR embedded subtitles detected: {lang}")
|
||||||
actual_subtitles.append([lang, None, None])
|
actual_subtitles.append([lang, None, None])
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logging.debug("BAZARR unable to index this unrecognized language: %s (%s)", subtitle_language, error)
|
logging.debug("BAZARR unable to index this unrecognized language: %s (%s)", subtitle_language, error)
|
||||||
|
@ -84,7 +84,7 @@ def store_subtitles(original_path, reversed_path, use_cache=True):
|
||||||
os.stat(path_mappings.path_replace(x[1])).st_size == x[2]]
|
os.stat(path_mappings.path_replace(x[1])).st_size == x[2]]
|
||||||
|
|
||||||
subtitles = search_external_subtitles(reversed_path, languages=get_language_set(),
|
subtitles = search_external_subtitles(reversed_path, languages=get_language_set(),
|
||||||
only_one=settings.general.getboolean('single_language'))
|
only_one=settings.general.single_language)
|
||||||
full_dest_folder_path = os.path.dirname(reversed_path)
|
full_dest_folder_path = os.path.dirname(reversed_path)
|
||||||
if dest_folder:
|
if dest_folder:
|
||||||
if settings.general.subfolder == "absolute":
|
if settings.general.subfolder == "absolute":
|
||||||
|
@ -118,12 +118,12 @@ def store_subtitles(original_path, reversed_path, use_cache=True):
|
||||||
|
|
||||||
elif str(language.basename) != 'und':
|
elif str(language.basename) != 'und':
|
||||||
if language.forced:
|
if language.forced:
|
||||||
language_str = str(language)
|
language_str = f'{language}:forced'
|
||||||
elif language.hi:
|
elif language.hi:
|
||||||
language_str = str(language) + ':hi'
|
language_str = f'{language}:hi'
|
||||||
else:
|
else:
|
||||||
language_str = str(language)
|
language_str = str(language)
|
||||||
logging.debug("BAZARR external subtitles detected: " + language_str)
|
logging.debug(f"BAZARR external subtitles detected: {language_str}")
|
||||||
actual_subtitles.append([language_str, path_mappings.path_replace_reverse(subtitle_path),
|
actual_subtitles.append([language_str, path_mappings.path_replace_reverse(subtitle_path),
|
||||||
os.stat(subtitle_path).st_size])
|
os.stat(subtitle_path).st_size])
|
||||||
|
|
||||||
|
@ -138,14 +138,14 @@ def store_subtitles(original_path, reversed_path, use_cache=True):
|
||||||
|
|
||||||
for episode in matching_episodes:
|
for episode in matching_episodes:
|
||||||
if episode:
|
if episode:
|
||||||
logging.debug("BAZARR storing those languages to DB: " + str(actual_subtitles))
|
logging.debug(f"BAZARR storing those languages to DB: {actual_subtitles}")
|
||||||
list_missing_subtitles(epno=episode.sonarrEpisodeId)
|
list_missing_subtitles(epno=episode.sonarrEpisodeId)
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR haven't been able to update existing subtitles to DB : " + str(actual_subtitles))
|
logging.debug(f"BAZARR haven't been able to update existing subtitles to DB: {actual_subtitles}")
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR this file doesn't seems to exist or isn't accessible.")
|
logging.debug("BAZARR this file doesn't seems to exist or isn't accessible.")
|
||||||
|
|
||||||
logging.debug('BAZARR ended subtitles indexing for this file: ' + reversed_path)
|
logging.debug(f'BAZARR ended subtitles indexing for this file: {reversed_path}')
|
||||||
|
|
||||||
return actual_subtitles
|
return actual_subtitles
|
||||||
|
|
||||||
|
@ -168,7 +168,7 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
||||||
.where(episodes_subtitles_clause))\
|
.where(episodes_subtitles_clause))\
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
use_embedded_subs = settings.general.getboolean('use_embedded_subs')
|
use_embedded_subs = settings.general.use_embedded_subs
|
||||||
|
|
||||||
for episode_subtitles in episodes_subtitles:
|
for episode_subtitles in episodes_subtitles:
|
||||||
missing_subtitles_text = '[]'
|
missing_subtitles_text = '[]'
|
||||||
|
@ -266,7 +266,7 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
||||||
event_stream(type='badges')
|
event_stream(type='badges')
|
||||||
|
|
||||||
|
|
||||||
def series_full_scan_subtitles(use_cache=settings.sonarr.getboolean('use_ffprobe_cache')):
|
def series_full_scan_subtitles(use_cache=settings.sonarr.use_ffprobe_cache):
|
||||||
episodes = database.execute(
|
episodes = database.execute(
|
||||||
select(TableEpisodes.path))\
|
select(TableEpisodes.path))\
|
||||||
.all()
|
.all()
|
||||||
|
|
|
@ -69,8 +69,8 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
|
||||||
|
|
||||||
# to improve performance, skip detection of files larger that 1M
|
# to improve performance, skip detection of files larger that 1M
|
||||||
if os.path.getsize(subtitle_path) > 1 * 1024 * 1024:
|
if os.path.getsize(subtitle_path) > 1 * 1024 * 1024:
|
||||||
logging.debug("BAZARR subtitles file is too large to be text based. Skipping this file: " +
|
logging.debug(f"BAZARR subtitles file is too large to be text based. Skipping this file: "
|
||||||
subtitle_path)
|
f"{subtitle_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with open(subtitle_path, 'rb') as f:
|
with open(subtitle_path, 'rb') as f:
|
||||||
|
@ -80,8 +80,8 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
|
||||||
if encoding and 'encoding' in encoding and encoding['encoding']:
|
if encoding and 'encoding' in encoding and encoding['encoding']:
|
||||||
encoding = detect(text)['encoding']
|
encoding = detect(text)['encoding']
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR skipping this subtitles because we can't guess the encoding. "
|
logging.debug(f"BAZARR skipping this subtitles because we can't guess the encoding. "
|
||||||
"It's probably a binary file: " + subtitle_path)
|
f"It's probably a binary file: {subtitle_path}")
|
||||||
continue
|
continue
|
||||||
text = text.decode(encoding)
|
text = text.decode(encoding)
|
||||||
|
|
||||||
|
@ -97,8 +97,7 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
|
||||||
detected_language = 'zt'
|
detected_language = 'zt'
|
||||||
|
|
||||||
if detected_language:
|
if detected_language:
|
||||||
logging.debug("BAZARR external subtitles detected and guessed this language: " + str(
|
logging.debug(f"BAZARR external subtitles detected and guessed this language: {detected_language}")
|
||||||
detected_language))
|
|
||||||
try:
|
try:
|
||||||
subtitles[subtitle] = Language.rebuild(Language.fromietf(detected_language), forced=forced,
|
subtitles[subtitle] = Language.rebuild(Language.fromietf(detected_language), forced=forced,
|
||||||
hi=False)
|
hi=False)
|
||||||
|
@ -121,8 +120,8 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
|
||||||
if os.path.exists(subtitle_path) and os.path.splitext(subtitle_path)[1] in core.SUBTITLE_EXTENSIONS:
|
if os.path.exists(subtitle_path) and os.path.splitext(subtitle_path)[1] in core.SUBTITLE_EXTENSIONS:
|
||||||
# to improve performance, skip detection of files larger that 1M
|
# to improve performance, skip detection of files larger that 1M
|
||||||
if os.path.getsize(subtitle_path) > 1 * 1024 * 1024:
|
if os.path.getsize(subtitle_path) > 1 * 1024 * 1024:
|
||||||
logging.debug("BAZARR subtitles file is too large to be text based. Skipping this file: " +
|
logging.debug(f"BAZARR subtitles file is too large to be text based. Skipping this file: "
|
||||||
subtitle_path)
|
f"{subtitle_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with open(subtitle_path, 'rb') as f:
|
with open(subtitle_path, 'rb') as f:
|
||||||
|
@ -132,8 +131,8 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
|
||||||
if encoding and 'encoding' in encoding and encoding['encoding']:
|
if encoding and 'encoding' in encoding and encoding['encoding']:
|
||||||
encoding = detect(text)['encoding']
|
encoding = detect(text)['encoding']
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR skipping this subtitles because we can't guess the encoding. "
|
logging.debug(f"BAZARR skipping this subtitles because we can't guess the encoding. "
|
||||||
"It's probably a binary file: " + subtitle_path)
|
f"It's probably a binary file: {subtitle_path}")
|
||||||
continue
|
continue
|
||||||
text = text.decode(encoding)
|
text = text.decode(encoding)
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ from .processing import process_subtitle
|
||||||
|
|
||||||
@update_pools
|
@update_pools
|
||||||
def manual_search(path, profile_id, providers, sceneName, title, media_type):
|
def manual_search(path, profile_id, providers, sceneName, title, media_type):
|
||||||
logging.debug('BAZARR Manually searching subtitles for this file: ' + path)
|
logging.debug(f'BAZARR Manually searching subtitles for this file: {path}')
|
||||||
|
|
||||||
final_subtitles = []
|
final_subtitles = []
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type):
|
||||||
logging.info("BAZARR All providers are throttled")
|
logging.info("BAZARR All providers are throttled")
|
||||||
return 'All providers are throttled'
|
return 'All providers are throttled'
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("BAZARR Error trying to get Subtitle list from provider for this file: " + path)
|
logging.exception(f"BAZARR Error trying to get Subtitle list from provider for this file: {path}")
|
||||||
else:
|
else:
|
||||||
subtitles_list = []
|
subtitles_list = []
|
||||||
minimum_score = settings.general.minimum_score
|
minimum_score = settings.general.minimum_score
|
||||||
|
@ -145,8 +145,8 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type):
|
||||||
|
|
||||||
final_subtitles = sorted(subtitles_list, key=lambda x: (x['orig_score'], x['score_without_hash']),
|
final_subtitles = sorted(subtitles_list, key=lambda x: (x['orig_score'], x['score_without_hash']),
|
||||||
reverse=True)
|
reverse=True)
|
||||||
logging.debug('BAZARR ' + str(len(final_subtitles)) + " Subtitles have been found for this file: " + path)
|
logging.debug(f'BAZARR {len(final_subtitles)} Subtitles have been found for this file: {path}')
|
||||||
logging.debug('BAZARR Ended searching Subtitles for this file: ' + path)
|
logging.debug(f'BAZARR Ended searching Subtitles for this file: {path}')
|
||||||
|
|
||||||
subliminal.region.backend.sync()
|
subliminal.region.backend.sync()
|
||||||
|
|
||||||
|
@ -156,9 +156,9 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type):
|
||||||
@update_pools
|
@update_pools
|
||||||
def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provider, sceneName, title, media_type,
|
def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provider, sceneName, title, media_type,
|
||||||
use_original_format, profile_id):
|
use_original_format, profile_id):
|
||||||
logging.debug('BAZARR Manually downloading Subtitles for this file: ' + path)
|
logging.debug(f'BAZARR Manually downloading Subtitles for this file: {path}')
|
||||||
|
|
||||||
if settings.general.getboolean('utf8_encode'):
|
if settings.general.utf8_encode:
|
||||||
os.environ["SZ_KEEP_ENCODING"] = ""
|
os.environ["SZ_KEEP_ENCODING"] = ""
|
||||||
else:
|
else:
|
||||||
os.environ["SZ_KEEP_ENCODING"] = "True"
|
os.environ["SZ_KEEP_ENCODING"] = "True"
|
||||||
|
@ -180,29 +180,29 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide
|
||||||
try:
|
try:
|
||||||
if provider:
|
if provider:
|
||||||
download_subtitles([subtitle], _get_pool(media_type, profile_id))
|
download_subtitles([subtitle], _get_pool(media_type, profile_id))
|
||||||
logging.debug('BAZARR Subtitles file downloaded for this file:' + path)
|
logging.debug(f'BAZARR Subtitles file downloaded for this file: {path}')
|
||||||
else:
|
else:
|
||||||
logging.info("BAZARR All providers are throttled")
|
logging.info("BAZARR All providers are throttled")
|
||||||
return 'All providers are throttled'
|
return 'All providers are throttled'
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('BAZARR Error downloading Subtitles for this file ' + path)
|
logging.exception(f'BAZARR Error downloading Subtitles for this file {path}')
|
||||||
return 'Error downloading Subtitles'
|
return 'Error downloading Subtitles'
|
||||||
else:
|
else:
|
||||||
if not subtitle.is_valid():
|
if not subtitle.is_valid():
|
||||||
logging.exception('BAZARR No valid Subtitles file found for this file: ' + path)
|
logging.error(f"BAZARR Downloaded subtitles isn't valid for this file: {path}")
|
||||||
return 'No valid Subtitles file found'
|
return "Downloaded subtitles isn't valid. Check log."
|
||||||
try:
|
try:
|
||||||
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
||||||
'win') and settings.general.getboolean('chmod_enabled') else None
|
'win') and settings.general.chmod_enabled else None
|
||||||
saved_subtitles = save_subtitles(video.original_path, [subtitle],
|
saved_subtitles = save_subtitles(video.original_path, [subtitle],
|
||||||
single=settings.general.getboolean('single_language'),
|
single=settings.general.single_language,
|
||||||
tags=None, # fixme
|
tags=None, # fixme
|
||||||
directory=get_target_folder(path),
|
directory=get_target_folder(path),
|
||||||
chmod=chmod,
|
chmod=chmod,
|
||||||
formats=(subtitle.format,),
|
formats=(subtitle.format,),
|
||||||
path_decoder=force_unicode)
|
path_decoder=force_unicode)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('BAZARR Error saving Subtitles file to disk for this file:' + path)
|
logging.exception(f'BAZARR Error saving Subtitles file to disk for this file: {path}')
|
||||||
return 'Error saving Subtitles file to disk'
|
return 'Error saving Subtitles file to disk'
|
||||||
else:
|
else:
|
||||||
if saved_subtitles:
|
if saved_subtitles:
|
||||||
|
@ -218,14 +218,14 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
"BAZARR Tried to manually download a Subtitles for file: " + path
|
f"BAZARR Tried to manually download a Subtitles for file: {path} but we weren't able to do "
|
||||||
+ " but we weren't able to do (probably throttled by " + str(subtitle.provider_name)
|
f"(probably throttled by {subtitle.provider_name}. Please retry later or select a Subtitles "
|
||||||
+ ". Please retry later or select a Subtitles from another provider.")
|
f"from another provider.")
|
||||||
return 'Something went wrong, check the logs for error'
|
return 'Something went wrong, check the logs for error'
|
||||||
|
|
||||||
subliminal.region.backend.sync()
|
subliminal.region.backend.sync()
|
||||||
|
|
||||||
logging.debug('BAZARR Ended manually downloading Subtitles for file: ' + path)
|
logging.debug(f'BAZARR Ended manually downloading Subtitles for file: {path}')
|
||||||
|
|
||||||
|
|
||||||
def _get_language_obj(profile_id):
|
def _get_language_obj(profile_id):
|
||||||
|
|
|
@ -67,7 +67,7 @@ def movies_download_subtitles(no):
|
||||||
logging.info("BAZARR All providers are throttled")
|
logging.info("BAZARR All providers are throttled")
|
||||||
break
|
break
|
||||||
|
|
||||||
show_progress(id='movie_search_progress_{}'.format(no),
|
show_progress(id=f'movie_search_progress_{no}',
|
||||||
header='Searching missing subtitles...',
|
header='Searching missing subtitles...',
|
||||||
name=movie.title,
|
name=movie.title,
|
||||||
value=0,
|
value=0,
|
||||||
|
@ -88,4 +88,4 @@ def movies_download_subtitles(no):
|
||||||
history_log_movie(1, no, result)
|
history_log_movie(1, no, result)
|
||||||
send_notifications_movie(no, result.message)
|
send_notifications_movie(no, result.message)
|
||||||
|
|
||||||
hide_progress(id='movie_search_progress_{}'.format(no))
|
hide_progress(id=f'movie_search_progress_{no}')
|
||||||
|
|
|
@ -49,8 +49,8 @@ def series_download_subtitles(no):
|
||||||
.where(reduce(operator.and_, conditions))) \
|
.where(reduce(operator.and_, conditions))) \
|
||||||
.all()
|
.all()
|
||||||
if not episodes_details:
|
if not episodes_details:
|
||||||
logging.debug("BAZARR no episode for that sonarrSeriesId have been found in database or they have all been "
|
logging.debug(f"BAZARR no episode for that sonarrSeriesId have been found in database or they have all been "
|
||||||
"ignored because of monitored status, series type or series tags: {}".format(no))
|
f"ignored because of monitored status, series type or series tags: {no}")
|
||||||
return
|
return
|
||||||
|
|
||||||
count_episodes_details = len(episodes_details)
|
count_episodes_details = len(episodes_details)
|
||||||
|
@ -59,12 +59,9 @@ def series_download_subtitles(no):
|
||||||
providers_list = get_providers()
|
providers_list = get_providers()
|
||||||
|
|
||||||
if providers_list:
|
if providers_list:
|
||||||
show_progress(id='series_search_progress_{}'.format(no),
|
show_progress(id=f'series_search_progress_{no}',
|
||||||
header='Searching missing subtitles...',
|
header='Searching missing subtitles...',
|
||||||
name='{0} - S{1:02d}E{2:02d} - {3}'.format(episode.title,
|
name=f'{episode.title} - S{episode.season:02d}E{episode.episode:02d} - {episode.episodeTitle}',
|
||||||
episode.season,
|
|
||||||
episode.episode,
|
|
||||||
episode.episodeTitle),
|
|
||||||
value=i,
|
value=i,
|
||||||
count=count_episodes_details)
|
count=count_episodes_details)
|
||||||
|
|
||||||
|
@ -101,7 +98,7 @@ def series_download_subtitles(no):
|
||||||
logging.info("BAZARR All providers are throttled")
|
logging.info("BAZARR All providers are throttled")
|
||||||
break
|
break
|
||||||
|
|
||||||
hide_progress(id='series_search_progress_{}'.format(no))
|
hide_progress(id=f'series_search_progress_{no}')
|
||||||
|
|
||||||
|
|
||||||
def episode_download_subtitles(no, send_progress=False):
|
def episode_download_subtitles(no, send_progress=False):
|
||||||
|
@ -134,12 +131,9 @@ def episode_download_subtitles(no, send_progress=False):
|
||||||
|
|
||||||
if providers_list:
|
if providers_list:
|
||||||
if send_progress:
|
if send_progress:
|
||||||
show_progress(id='episode_search_progress_{}'.format(no),
|
show_progress(id=f'episode_search_progress_{no}',
|
||||||
header='Searching missing subtitles...',
|
header='Searching missing subtitles...',
|
||||||
name='{0} - S{1:02d}E{2:02d} - {3}'.format(episode.title,
|
name=f'{episode.title} - S{episode.season:02d}E{episode.episode:02d} - {episode.episodeTitle}',
|
||||||
episode.season,
|
|
||||||
episode.episode,
|
|
||||||
episode.episodeTitle),
|
|
||||||
value=0,
|
value=0,
|
||||||
count=1)
|
count=1)
|
||||||
|
|
||||||
|
@ -174,7 +168,7 @@ def episode_download_subtitles(no, send_progress=False):
|
||||||
send_notifications(episode.sonarrSeriesId, episode.sonarrEpisodeId, result.message)
|
send_notifications(episode.sonarrSeriesId, episode.sonarrEpisodeId, result.message)
|
||||||
|
|
||||||
if send_progress:
|
if send_progress:
|
||||||
hide_progress(id='episode_search_progress_{}'.format(no))
|
hide_progress(id=f'episode_search_progress_{no}')
|
||||||
else:
|
else:
|
||||||
logging.info("BAZARR All providers are throttled")
|
logging.info("BAZARR All providers are throttled")
|
||||||
break
|
break
|
||||||
|
|
|
@ -26,13 +26,13 @@ def postprocessing(command, path):
|
||||||
out = out.replace('\n', ' ').replace('\r', ' ')
|
out = out.replace('\n', ' ').replace('\r', ' ')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('BAZARR Post-processing failed for file ' + path + ' : ' + repr(e))
|
logging.error(f'BAZARR Post-processing failed for file {path}: {repr(e)}')
|
||||||
else:
|
else:
|
||||||
if err:
|
if err:
|
||||||
logging.error(
|
parsed_err = err.replace('\n', ' ').replace('\r', ' ')
|
||||||
'BAZARR Post-processing result for file ' + path + ' : ' + err.replace('\n', ' ').replace('\r', ' '))
|
logging.error(f'BAZARR Post-processing result for file {path}: {parsed_err}')
|
||||||
elif out == "":
|
elif out == "":
|
||||||
logging.info(
|
logging.info(
|
||||||
'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution')
|
f'BAZARR Post-processing result for file {path}: Nothing returned from command execution')
|
||||||
else:
|
else:
|
||||||
logging.info('BAZARR Post-processing result for file ' + path + ' : ' + out)
|
logging.info(f'BAZARR Post-processing result for file {path}: {out}')
|
||||||
|
|
|
@ -31,15 +31,15 @@ class ProcessSubtitlesResult:
|
||||||
self.not_matched = not_matched
|
self.not_matched = not_matched
|
||||||
|
|
||||||
if hearing_impaired:
|
if hearing_impaired:
|
||||||
self.language_code = downloaded_language_code2 + ":hi"
|
self.language_code = f"{downloaded_language_code2}:hi"
|
||||||
elif forced:
|
elif forced:
|
||||||
self.language_code = downloaded_language_code2 + ":forced"
|
self.language_code = f"{downloaded_language_code2}:forced"
|
||||||
else:
|
else:
|
||||||
self.language_code = downloaded_language_code2
|
self.language_code = downloaded_language_code2
|
||||||
|
|
||||||
|
|
||||||
def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_upgrade=False, is_manual=False):
|
def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_upgrade=False, is_manual=False):
|
||||||
use_postprocessing = settings.general.getboolean('use_postprocessing')
|
use_postprocessing = settings.general.use_postprocessing
|
||||||
postprocessing_cmd = settings.general.postprocessing_cmd
|
postprocessing_cmd = settings.general.postprocessing_cmd
|
||||||
|
|
||||||
downloaded_provider = subtitle.provider_name
|
downloaded_provider = subtitle.provider_name
|
||||||
|
@ -57,7 +57,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
|
||||||
modifier_string = " forced"
|
modifier_string = " forced"
|
||||||
else:
|
else:
|
||||||
modifier_string = ""
|
modifier_string = ""
|
||||||
logging.debug('BAZARR Subtitles file saved to disk: ' + downloaded_path)
|
logging.debug(f'BAZARR Subtitles file saved to disk: {downloaded_path}')
|
||||||
if is_upgrade:
|
if is_upgrade:
|
||||||
action = "upgraded"
|
action = "upgraded"
|
||||||
elif is_manual:
|
elif is_manual:
|
||||||
|
@ -66,8 +66,8 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
|
||||||
action = "downloaded"
|
action = "downloaded"
|
||||||
|
|
||||||
percent_score = round(subtitle.score * 100 / max_score, 2)
|
percent_score = round(subtitle.score * 100 / max_score, 2)
|
||||||
message = downloaded_language + modifier_string + " subtitles " + action + " from " + \
|
message = (f"{downloaded_language}{modifier_string} subtitles {action} from {downloaded_provider} with a score of "
|
||||||
downloaded_provider + " with a score of " + str(percent_score) + "%."
|
f"{percent_score}%.")
|
||||||
|
|
||||||
if media_type == 'series':
|
if media_type == 'series':
|
||||||
episode_metadata = database.execute(
|
episode_metadata = database.execute(
|
||||||
|
@ -109,19 +109,19 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
|
||||||
percent_score, subtitle_id, downloaded_provider, series_id, episode_id)
|
percent_score, subtitle_id, downloaded_provider, series_id, episode_id)
|
||||||
|
|
||||||
if media_type == 'series':
|
if media_type == 'series':
|
||||||
use_pp_threshold = settings.general.getboolean('use_postprocessing_threshold')
|
use_pp_threshold = settings.general.use_postprocessing_threshold
|
||||||
pp_threshold = int(settings.general.postprocessing_threshold)
|
pp_threshold = int(settings.general.postprocessing_threshold)
|
||||||
else:
|
else:
|
||||||
use_pp_threshold = settings.general.getboolean('use_postprocessing_threshold_movie')
|
use_pp_threshold = settings.general.use_postprocessing_threshold_movie
|
||||||
pp_threshold = int(settings.general.postprocessing_threshold_movie)
|
pp_threshold = int(settings.general.postprocessing_threshold_movie)
|
||||||
|
|
||||||
if not use_pp_threshold or (use_pp_threshold and percent_score < pp_threshold):
|
if not use_pp_threshold or (use_pp_threshold and percent_score < pp_threshold):
|
||||||
logging.debug("BAZARR Using post-processing command: {}".format(command))
|
logging.debug(f"BAZARR Using post-processing command: {command}")
|
||||||
postprocessing(command, path)
|
postprocessing(command, path)
|
||||||
set_chmod(subtitles_path=downloaded_path)
|
set_chmod(subtitles_path=downloaded_path)
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR post-processing skipped because subtitles score isn't below this "
|
logging.debug(f"BAZARR post-processing skipped because subtitles score isn't below this "
|
||||||
"threshold value: " + str(pp_threshold) + "%")
|
f"threshold value: {pp_threshold}%")
|
||||||
|
|
||||||
if media_type == 'series':
|
if media_type == 'series':
|
||||||
reversed_path = path_mappings.path_replace_reverse(path)
|
reversed_path = path_mappings.path_replace_reverse(path)
|
||||||
|
|
|
@ -30,7 +30,9 @@ def refine_from_db(path, video):
|
||||||
TableEpisodes.video_codec,
|
TableEpisodes.video_codec,
|
||||||
TableEpisodes.audio_codec,
|
TableEpisodes.audio_codec,
|
||||||
TableEpisodes.path,
|
TableEpisodes.path,
|
||||||
TableShows.imdbId)
|
TableShows.imdbId,
|
||||||
|
TableEpisodes.sonarrSeriesId,
|
||||||
|
TableEpisodes.sonarrEpisodeId)
|
||||||
.select_from(TableEpisodes)
|
.select_from(TableEpisodes)
|
||||||
.join(TableShows)
|
.join(TableShows)
|
||||||
.where((TableEpisodes.path == path_mappings.path_replace_reverse(path)))) \
|
.where((TableEpisodes.path == path_mappings.path_replace_reverse(path)))) \
|
||||||
|
@ -38,8 +40,10 @@ def refine_from_db(path, video):
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
video.series = _TITLE_RE.sub('', data.seriesTitle)
|
video.series = _TITLE_RE.sub('', data.seriesTitle)
|
||||||
video.season = int(data.season)
|
if not video.season and data.season:
|
||||||
video.episode = int(data.episode)
|
video.season = int(data.season)
|
||||||
|
if not video.episode and data.episode:
|
||||||
|
video.episode = int(data.episode)
|
||||||
video.title = data.episodeTitle
|
video.title = data.episodeTitle
|
||||||
|
|
||||||
# Only refine year as a fallback
|
# Only refine year as a fallback
|
||||||
|
@ -61,6 +65,9 @@ def refine_from_db(path, video):
|
||||||
if not video.audio_codec:
|
if not video.audio_codec:
|
||||||
if data.audio_codec:
|
if data.audio_codec:
|
||||||
video.audio_codec = convert_to_guessit('audio_codec', data.audio_codec)
|
video.audio_codec = convert_to_guessit('audio_codec', data.audio_codec)
|
||||||
|
|
||||||
|
video.sonarrSeriesId = data.sonarrSeriesId
|
||||||
|
video.sonarrEpisodeId = data.sonarrEpisodeId
|
||||||
elif isinstance(video, Movie):
|
elif isinstance(video, Movie):
|
||||||
data = database.execute(
|
data = database.execute(
|
||||||
select(TableMovies.title,
|
select(TableMovies.title,
|
||||||
|
@ -70,7 +77,8 @@ def refine_from_db(path, video):
|
||||||
TableMovies.resolution,
|
TableMovies.resolution,
|
||||||
TableMovies.video_codec,
|
TableMovies.video_codec,
|
||||||
TableMovies.audio_codec,
|
TableMovies.audio_codec,
|
||||||
TableMovies.imdbId)
|
TableMovies.imdbId,
|
||||||
|
TableMovies.radarrId)
|
||||||
.where(TableMovies.path == path_mappings.path_replace_reverse_movie(path))) \
|
.where(TableMovies.path == path_mappings.path_replace_reverse_movie(path))) \
|
||||||
.first()
|
.first()
|
||||||
|
|
||||||
|
@ -98,4 +106,6 @@ def refine_from_db(path, video):
|
||||||
if data.audio_codec:
|
if data.audio_codec:
|
||||||
video.audio_codec = convert_to_guessit('audio_codec', data.audio_codec)
|
video.audio_codec = convert_to_guessit('audio_codec', data.audio_codec)
|
||||||
|
|
||||||
|
video.radarrId = data.radarrId
|
||||||
|
|
||||||
return video
|
return video
|
||||||
|
|
|
@ -33,7 +33,7 @@ def refine_from_ffprobe(path, video):
|
||||||
episode_file_id=file_id.episode_file_id)
|
episode_file_id=file_id.episode_file_id)
|
||||||
|
|
||||||
if not data or ('ffprobe' not in data and 'mediainfo' not in data):
|
if not data or ('ffprobe' not in data and 'mediainfo' not in data):
|
||||||
logging.debug("No cache available for this file: {}".format(path))
|
logging.debug(f"No cache available for this file: {path}")
|
||||||
return video
|
return video
|
||||||
|
|
||||||
if data['ffprobe']:
|
if data['ffprobe']:
|
||||||
|
|
|
@ -12,16 +12,16 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, media_type, percent_s
|
||||||
sonarr_episode_id=None, radarr_id=None):
|
sonarr_episode_id=None, radarr_id=None):
|
||||||
if forced:
|
if forced:
|
||||||
logging.debug('BAZARR cannot sync forced subtitles. Skipping sync routine.')
|
logging.debug('BAZARR cannot sync forced subtitles. Skipping sync routine.')
|
||||||
elif not settings.subsync.getboolean('use_subsync'):
|
elif not settings.subsync.use_subsync:
|
||||||
logging.debug('BAZARR automatic syncing is disabled in settings. Skipping sync routine.')
|
logging.debug('BAZARR automatic syncing is disabled in settings. Skipping sync routine.')
|
||||||
else:
|
else:
|
||||||
logging.debug(f'BAZARR automatic syncing is enabled in settings. We\'ll try to sync this '
|
logging.debug(f'BAZARR automatic syncing is enabled in settings. We\'ll try to sync this '
|
||||||
f'subtitles: {srt_path}.')
|
f'subtitles: {srt_path}.')
|
||||||
if media_type == 'series':
|
if media_type == 'series':
|
||||||
use_subsync_threshold = settings.subsync.getboolean('use_subsync_threshold')
|
use_subsync_threshold = settings.subsync.use_subsync_threshold
|
||||||
subsync_threshold = settings.subsync.subsync_threshold
|
subsync_threshold = settings.subsync.subsync_threshold
|
||||||
else:
|
else:
|
||||||
use_subsync_threshold = settings.subsync.getboolean('use_subsync_movie_threshold')
|
use_subsync_threshold = settings.subsync.use_subsync_movie_threshold
|
||||||
subsync_threshold = settings.subsync.subsync_movie_threshold
|
subsync_threshold = settings.subsync.subsync_movie_threshold
|
||||||
|
|
||||||
if not use_subsync_threshold or (use_subsync_threshold and percent_score < float(subsync_threshold)):
|
if not use_subsync_threshold or (use_subsync_threshold and percent_score < float(subsync_threshold)):
|
||||||
|
@ -32,6 +32,6 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, media_type, percent_s
|
||||||
gc.collect()
|
gc.collect()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logging.debug("BAZARR subsync skipped because subtitles score isn't below this "
|
logging.debug(f"BAZARR subsync skipped because subtitles score isn't below this "
|
||||||
"threshold value: " + subsync_threshold + "%")
|
f"threshold value: {subsync_threshold}%")
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -36,7 +36,7 @@ def delete_subtitles(media_type, language, forced, hi, media_path, subtitles_pat
|
||||||
language_log += ':forced'
|
language_log += ':forced'
|
||||||
language_string += ' forced'
|
language_string += ' forced'
|
||||||
|
|
||||||
result = ProcessSubtitlesResult(message=language_string + " subtitles deleted from disk.",
|
result = ProcessSubtitlesResult(message=f"{language_string} subtitles deleted from disk.",
|
||||||
reversed_path=path_mappings.path_replace_reverse(media_path),
|
reversed_path=path_mappings.path_replace_reverse(media_path),
|
||||||
downloaded_language_code2=language_log,
|
downloaded_language_code2=language_log,
|
||||||
downloaded_provider=None,
|
downloaded_provider=None,
|
||||||
|
@ -50,7 +50,7 @@ def delete_subtitles(media_type, language, forced, hi, media_path, subtitles_pat
|
||||||
try:
|
try:
|
||||||
os.remove(path_mappings.path_replace(subtitles_path))
|
os.remove(path_mappings.path_replace(subtitles_path))
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.exception('BAZARR cannot delete subtitles file: ' + subtitles_path)
|
logging.exception(f'BAZARR cannot delete subtitles file: {subtitles_path}')
|
||||||
store_subtitles(path_mappings.path_replace_reverse(media_path), media_path)
|
store_subtitles(path_mappings.path_replace_reverse(media_path), media_path)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
@ -64,7 +64,7 @@ def delete_subtitles(media_type, language, forced, hi, media_path, subtitles_pat
|
||||||
try:
|
try:
|
||||||
os.remove(path_mappings.path_replace_movie(subtitles_path))
|
os.remove(path_mappings.path_replace_movie(subtitles_path))
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.exception('BAZARR cannot delete subtitles file: ' + subtitles_path)
|
logging.exception(f'BAZARR cannot delete subtitles file: {subtitles_path}')
|
||||||
store_subtitles_movie(path_mappings.path_replace_reverse_movie(media_path), media_path)
|
store_subtitles_movie(path_mappings.path_replace_reverse_movie(media_path), media_path)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -19,14 +19,14 @@ def subtitles_apply_mods(language, subtitle_path, mods, use_original_format, vid
|
||||||
lang_obj = Language(language)
|
lang_obj = Language(language)
|
||||||
else:
|
else:
|
||||||
lang_obj = custom.subzero_language()
|
lang_obj = custom.subzero_language()
|
||||||
single = settings.general.getboolean('single_language')
|
single = settings.general.single_language
|
||||||
|
|
||||||
sub = Subtitle(lang_obj, mods=mods, original_format=use_original_format)
|
sub = Subtitle(lang_obj, mods=mods, original_format=use_original_format)
|
||||||
with open(subtitle_path, 'rb') as f:
|
with open(subtitle_path, 'rb') as f:
|
||||||
sub.content = f.read()
|
sub.content = f.read()
|
||||||
|
|
||||||
if not sub.is_valid():
|
if not sub.is_valid():
|
||||||
logging.exception('BAZARR Invalid subtitle file: ' + subtitle_path)
|
logging.exception(f'BAZARR Invalid subtitle file: {subtitle_path}')
|
||||||
return
|
return
|
||||||
|
|
||||||
if use_original_format:
|
if use_original_format:
|
||||||
|
|
|
@ -34,7 +34,7 @@ class SubSyncer:
|
||||||
radarr_id=None):
|
radarr_id=None):
|
||||||
self.reference = video_path
|
self.reference = video_path
|
||||||
self.srtin = srt_path
|
self.srtin = srt_path
|
||||||
self.srtout = '{}.synced.srt'.format(os.path.splitext(self.srtin)[0])
|
self.srtout = f'{os.path.splitext(self.srtin)[0]}.synced.srt'
|
||||||
self.args = None
|
self.args = None
|
||||||
|
|
||||||
ffprobe_exe = get_binary('ffprobe')
|
ffprobe_exe = get_binary('ffprobe')
|
||||||
|
@ -54,11 +54,11 @@ class SubSyncer:
|
||||||
self.ffmpeg_path = os.path.dirname(ffmpeg_exe)
|
self.ffmpeg_path = os.path.dirname(ffmpeg_exe)
|
||||||
unparsed_args = [self.reference, '-i', self.srtin, '-o', self.srtout, '--ffmpegpath', self.ffmpeg_path, '--vad',
|
unparsed_args = [self.reference, '-i', self.srtin, '-o', self.srtout, '--ffmpegpath', self.ffmpeg_path, '--vad',
|
||||||
self.vad, '--log-dir-path', self.log_dir_path]
|
self.vad, '--log-dir-path', self.log_dir_path]
|
||||||
if settings.subsync.getboolean('force_audio'):
|
if settings.subsync.force_audio:
|
||||||
unparsed_args.append('--no-fix-framerate')
|
unparsed_args.append('--no-fix-framerate')
|
||||||
unparsed_args.append('--reference-stream')
|
unparsed_args.append('--reference-stream')
|
||||||
unparsed_args.append('a:0')
|
unparsed_args.append('a:0')
|
||||||
if settings.subsync.getboolean('debug'):
|
if settings.subsync.debug:
|
||||||
unparsed_args.append('--make-test-case')
|
unparsed_args.append('--make-test-case')
|
||||||
parser = make_parser()
|
parser = make_parser()
|
||||||
self.args = parser.parse_args(args=unparsed_args)
|
self.args = parser.parse_args(args=unparsed_args)
|
||||||
|
@ -68,22 +68,22 @@ class SubSyncer:
|
||||||
try:
|
try:
|
||||||
result = run(self.args)
|
result = run(self.args)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('BAZARR an exception occurs during the synchronization process for this subtitles: '
|
logging.exception(
|
||||||
'{0}'.format(self.srtin))
|
f'BAZARR an exception occurs during the synchronization process for this subtitles: {self.srtin}')
|
||||||
raise OSError
|
raise OSError
|
||||||
else:
|
else:
|
||||||
if settings.subsync.getboolean('debug'):
|
if settings.subsync.debug:
|
||||||
return result
|
return result
|
||||||
if os.path.isfile(self.srtout):
|
if os.path.isfile(self.srtout):
|
||||||
if not settings.subsync.getboolean('debug'):
|
if not settings.subsync.debug:
|
||||||
os.remove(self.srtin)
|
os.remove(self.srtin)
|
||||||
os.rename(self.srtout, self.srtin)
|
os.rename(self.srtout, self.srtin)
|
||||||
|
|
||||||
offset_seconds = result['offset_seconds'] or 0
|
offset_seconds = result['offset_seconds'] or 0
|
||||||
framerate_scale_factor = result['framerate_scale_factor'] or 0
|
framerate_scale_factor = result['framerate_scale_factor'] or 0
|
||||||
message = "{0} subtitles synchronization ended with an offset of {1} seconds and a framerate " \
|
message = (f"{language_from_alpha2(srt_lang)} subtitles synchronization ended with an offset of "
|
||||||
"scale factor of {2}.".format(language_from_alpha2(srt_lang), offset_seconds,
|
f"{offset_seconds} seconds and a framerate scale factor of "
|
||||||
"{:.2f}".format(framerate_scale_factor))
|
f"{f'{framerate_scale_factor:.2f}'}.")
|
||||||
|
|
||||||
result = ProcessSubtitlesResult(message=message,
|
result = ProcessSubtitlesResult(message=message,
|
||||||
reversed_path=path_mappings.path_replace_reverse(self.reference),
|
reversed_path=path_mappings.path_replace_reverse(self.reference),
|
||||||
|
@ -101,6 +101,6 @@ class SubSyncer:
|
||||||
else:
|
else:
|
||||||
history_log_movie(action=5, radarr_id=radarr_id, result=result)
|
history_log_movie(action=5, radarr_id=radarr_id, result=result)
|
||||||
else:
|
else:
|
||||||
logging.error('BAZARR unable to sync subtitles: {0}'.format(self.srtin))
|
logging.error(f'BAZARR unable to sync subtitles: {self.srtin}')
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -31,7 +31,7 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
||||||
if hi:
|
if hi:
|
||||||
lang_obj = Language.rebuild(lang_obj, hi=True)
|
lang_obj = Language.rebuild(lang_obj, hi=True)
|
||||||
|
|
||||||
logging.debug('BAZARR is translating in {0} this subtitles {1}'.format(lang_obj, source_srt_file))
|
logging.debug(f'BAZARR is translating in {lang_obj} this subtitles {source_srt_file}')
|
||||||
|
|
||||||
max_characters = 5000
|
max_characters = 5000
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
||||||
lines_list = [x.plaintext for x in subs]
|
lines_list = [x.plaintext for x in subs]
|
||||||
joined_lines_str = '\n\n\n'.join(lines_list)
|
joined_lines_str = '\n\n\n'.join(lines_list)
|
||||||
|
|
||||||
logging.debug('BAZARR splitting subtitles into {} characters blocks'.format(max_characters))
|
logging.debug(f'BAZARR splitting subtitles into {max_characters} characters blocks')
|
||||||
lines_block_list = []
|
lines_block_list = []
|
||||||
translated_lines_list = []
|
translated_lines_list = []
|
||||||
while len(joined_lines_str):
|
while len(joined_lines_str):
|
||||||
|
@ -60,7 +60,7 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
||||||
lines_block_list.append(new_partial_lines_str)
|
lines_block_list.append(new_partial_lines_str)
|
||||||
joined_lines_str = joined_lines_str.replace(new_partial_lines_str, '')
|
joined_lines_str = joined_lines_str.replace(new_partial_lines_str, '')
|
||||||
|
|
||||||
logging.debug('BAZARR is sending {} blocks to Google Translate'.format(len(lines_block_list)))
|
logging.debug(f'BAZARR is sending {len(lines_block_list)} blocks to Google Translate')
|
||||||
for block_str in lines_block_list:
|
for block_str in lines_block_list:
|
||||||
try:
|
try:
|
||||||
translated_partial_srt_text = GoogleTranslator(source='auto',
|
translated_partial_srt_text = GoogleTranslator(source='auto',
|
||||||
|
@ -74,7 +74,7 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
||||||
translated_partial_srt_list = translated_partial_srt_text.split('\n\n\n')
|
translated_partial_srt_list = translated_partial_srt_text.split('\n\n\n')
|
||||||
translated_lines_list += translated_partial_srt_list
|
translated_lines_list += translated_partial_srt_list
|
||||||
|
|
||||||
logging.debug('BAZARR saving translated subtitles to {}'.format(dest_srt_file))
|
logging.debug(f'BAZARR saving translated subtitles to {dest_srt_file}')
|
||||||
for i, line in enumerate(subs):
|
for i, line in enumerate(subs):
|
||||||
try:
|
try:
|
||||||
line.plaintext = translated_lines_list[i]
|
line.plaintext = translated_lines_list[i]
|
||||||
|
|
|
@ -24,8 +24,8 @@ from .download import generate_subtitles
|
||||||
|
|
||||||
|
|
||||||
def upgrade_subtitles():
|
def upgrade_subtitles():
|
||||||
use_sonarr = settings.general.getboolean('use_sonarr')
|
use_sonarr = settings.general.use_sonarr
|
||||||
use_radarr = settings.general.getboolean('use_radarr')
|
use_radarr = settings.general.use_radarr
|
||||||
|
|
||||||
if use_sonarr:
|
if use_sonarr:
|
||||||
episodes_to_upgrade = get_upgradable_episode_subtitles()
|
episodes_to_upgrade = get_upgradable_episode_subtitles()
|
||||||
|
@ -87,10 +87,7 @@ def upgrade_subtitles():
|
||||||
|
|
||||||
show_progress(id='upgrade_episodes_progress',
|
show_progress(id='upgrade_episodes_progress',
|
||||||
header='Upgrading episodes subtitles...',
|
header='Upgrading episodes subtitles...',
|
||||||
name='{0} - S{1:02d}E{2:02d} - {3}'.format(episode['seriesTitle'],
|
name=f'{episode["seriesTitle"]} - S{episode["season"]:02d}E{episode["episode"]:02d} - {episode["title"]}',
|
||||||
episode['season'],
|
|
||||||
episode['episode'],
|
|
||||||
episode['title']),
|
|
||||||
value=i,
|
value=i,
|
||||||
count=count_episode_to_upgrade)
|
count=count_episode_to_upgrade)
|
||||||
|
|
||||||
|
@ -218,7 +215,7 @@ def get_queries_condition_parameters():
|
||||||
days_to_upgrade_subs = settings.general.days_to_upgrade_subs
|
days_to_upgrade_subs = settings.general.days_to_upgrade_subs
|
||||||
minimum_timestamp = (datetime.now() - timedelta(days=int(days_to_upgrade_subs)))
|
minimum_timestamp = (datetime.now() - timedelta(days=int(days_to_upgrade_subs)))
|
||||||
|
|
||||||
if settings.general.getboolean('upgrade_manual'):
|
if settings.general.upgrade_manual:
|
||||||
query_actions = [1, 2, 3, 4, 6]
|
query_actions = [1, 2, 3, 4, 6]
|
||||||
else:
|
else:
|
||||||
query_actions = [1, 3]
|
query_actions = [1, 3]
|
||||||
|
@ -244,7 +241,7 @@ def parse_language_string(language_string):
|
||||||
|
|
||||||
|
|
||||||
def get_upgradable_episode_subtitles():
|
def get_upgradable_episode_subtitles():
|
||||||
if not settings.general.getboolean('upgrade_subs'):
|
if not settings.general.upgrade_subs:
|
||||||
# return an empty set of rows
|
# return an empty set of rows
|
||||||
return select(TableHistory.id) \
|
return select(TableHistory.id) \
|
||||||
.where(TableHistory.id.is_(None)) \
|
.where(TableHistory.id.is_(None)) \
|
||||||
|
@ -277,7 +274,7 @@ def get_upgradable_episode_subtitles():
|
||||||
|
|
||||||
|
|
||||||
def get_upgradable_movies_subtitles():
|
def get_upgradable_movies_subtitles():
|
||||||
if not settings.general.getboolean('upgrade_subs'):
|
if not settings.general.upgrade_subs:
|
||||||
# return an empty set of rows
|
# return an empty set of rows
|
||||||
return select(TableHistoryMovie.id) \
|
return select(TableHistoryMovie.id) \
|
||||||
.where(TableHistoryMovie.id.is_(None)) \
|
.where(TableHistoryMovie.id.is_(None)) \
|
||||||
|
@ -323,10 +320,10 @@ def _language_from_items(items):
|
||||||
results = []
|
results = []
|
||||||
for item in items:
|
for item in items:
|
||||||
if item['forced'] == 'True':
|
if item['forced'] == 'True':
|
||||||
results.append(item['language'] + ':forced')
|
results.append(f'{item["language"]}:forced')
|
||||||
elif item['hi'] == 'True':
|
elif item['hi'] == 'True':
|
||||||
results.append(item['language'] + ':hi')
|
results.append(f'{item["language"]}:hi')
|
||||||
else:
|
else:
|
||||||
results.append(item['language'])
|
results.append(item['language'])
|
||||||
results.append(item['language'] + ':hi')
|
results.append(f'{item["language"]}:hi')
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -29,13 +29,13 @@ from .post_processing import postprocessing
|
||||||
def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, audio_language):
|
def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, audio_language):
|
||||||
logging.debug(f'BAZARR Manually uploading subtitles for this file: {path}')
|
logging.debug(f'BAZARR Manually uploading subtitles for this file: {path}')
|
||||||
|
|
||||||
single = settings.general.getboolean('single_language')
|
single = settings.general.single_language
|
||||||
|
|
||||||
use_postprocessing = settings.general.getboolean('use_postprocessing')
|
use_postprocessing = settings.general.use_postprocessing
|
||||||
postprocessing_cmd = settings.general.postprocessing_cmd
|
postprocessing_cmd = settings.general.postprocessing_cmd
|
||||||
|
|
||||||
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
||||||
'win') and settings.general.getboolean('chmod_enabled') else None
|
'win') and settings.general.chmod_enabled else None
|
||||||
|
|
||||||
language = alpha3_from_alpha2(language)
|
language = alpha3_from_alpha2(language)
|
||||||
|
|
||||||
|
@ -84,10 +84,10 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud
|
||||||
|
|
||||||
sub.content = subtitle.read()
|
sub.content = subtitle.read()
|
||||||
if not sub.is_valid():
|
if not sub.is_valid():
|
||||||
logging.exception('BAZARR Invalid subtitle file: ' + subtitle.filename)
|
logging.exception(f'BAZARR Invalid subtitle file: {subtitle.filename}')
|
||||||
sub.mods = None
|
sub.mods = None
|
||||||
|
|
||||||
if settings.general.getboolean('utf8_encode'):
|
if settings.general.utf8_encode:
|
||||||
sub.set_encoding("utf-8")
|
sub.set_encoding("utf-8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -106,11 +106,11 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud
|
||||||
formats=(sub.format,) if use_original_format else ("srt",),
|
formats=(sub.format,) if use_original_format else ("srt",),
|
||||||
path_decoder=force_unicode)
|
path_decoder=force_unicode)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('BAZARR Error saving Subtitles file to disk for this file:' + path)
|
logging.exception(f'BAZARR Error saving Subtitles file to disk for this file: {path}')
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(saved_subtitles) < 1:
|
if len(saved_subtitles) < 1:
|
||||||
logging.exception('BAZARR Error saving Subtitles file to disk for this file:' + path)
|
logging.exception(f'BAZARR Error saving Subtitles file to disk for this file: {path}')
|
||||||
return
|
return
|
||||||
|
|
||||||
subtitle_path = saved_subtitles[0].storage_path
|
subtitle_path = saved_subtitles[0].storage_path
|
||||||
|
@ -168,8 +168,8 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud
|
||||||
event_stream(type='movie', action='update', payload=movie_metadata.radarrId)
|
event_stream(type='movie', action='update', payload=movie_metadata.radarrId)
|
||||||
event_stream(type='movie-wanted', action='delete', payload=movie_metadata.radarrId)
|
event_stream(type='movie-wanted', action='delete', payload=movie_metadata.radarrId)
|
||||||
|
|
||||||
result = ProcessSubtitlesResult(message=language_from_alpha3(language) + modifier_string + " Subtitles manually "
|
result = ProcessSubtitlesResult(message=f"{language_from_alpha3(language)}{modifier_string} Subtitles manually "
|
||||||
"uploaded.",
|
"uploaded.",
|
||||||
reversed_path=reversed_path,
|
reversed_path=reversed_path,
|
||||||
downloaded_language_code2=uploaded_language_code2,
|
downloaded_language_code2=uploaded_language_code2,
|
||||||
downloaded_provider=None,
|
downloaded_provider=None,
|
||||||
|
|
|
@ -37,7 +37,7 @@ def get_video(path, title, sceneName, providers=None, media_type="movie"):
|
||||||
hash_from = original_path
|
hash_from = original_path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
skip_hashing = settings.general.getboolean('skip_hashing')
|
skip_hashing = settings.general.skip_hashing
|
||||||
video = parse_video(path, hints=hints, skip_hashing=skip_hashing, dry_run=used_scene_name, providers=providers,
|
video = parse_video(path, hints=hints, skip_hashing=skip_hashing, dry_run=used_scene_name, providers=providers,
|
||||||
hash_from=hash_from)
|
hash_from=hash_from)
|
||||||
video.used_scene_name = used_scene_name
|
video.used_scene_name = used_scene_name
|
||||||
|
|
|
@ -109,10 +109,7 @@ def wanted_search_missing_subtitles_series():
|
||||||
for i, episode in enumerate(episodes):
|
for i, episode in enumerate(episodes):
|
||||||
show_progress(id='wanted_episodes_progress',
|
show_progress(id='wanted_episodes_progress',
|
||||||
header='Searching subtitles...',
|
header='Searching subtitles...',
|
||||||
name='{0} - S{1:02d}E{2:02d} - {3}'.format(episode.title,
|
name=f'{episode.title} - S{episode.season:02d}E{episode.episode:02d} - {episode.episodeTitle}',
|
||||||
episode.season,
|
|
||||||
episode.episode,
|
|
||||||
episode.episodeTitle),
|
|
||||||
value=i,
|
value=i,
|
||||||
count=count_episodes)
|
count=count_episodes)
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ class EventTracker:
|
||||||
self.tracker.store.save()
|
self.tracker.store.save()
|
||||||
|
|
||||||
def track_subtitles(self, provider, action, language):
|
def track_subtitles(self, provider, action, language):
|
||||||
if not settings.analytics.getboolean('enabled'):
|
if not settings.analytics.enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
subtitles_event = self.tracker.create_new_event(name="subtitles")
|
subtitles_event = self.tracker.create_new_event(name="subtitles")
|
||||||
|
@ -65,7 +65,7 @@ class EventTracker:
|
||||||
self.tracker.store.save()
|
self.tracker.store.save()
|
||||||
|
|
||||||
def track_throttling(self, provider, exception_name, exception_info):
|
def track_throttling(self, provider, exception_name, exception_info):
|
||||||
if not settings.analytics.getboolean('enabled'):
|
if not settings.analytics.enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
throttling_event = self.tracker.create_new_event(name="throttling")
|
throttling_event = self.tracker.create_new_event(name="throttling")
|
||||||
|
|
|
@ -7,7 +7,7 @@ import shutil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from zipfile import ZipFile, BadZipFile
|
from zipfile import ZipFile, BadZipFile, ZIP_DEFLATED
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
|
||||||
from app.get_args import args
|
from app.get_args import args
|
||||||
|
@ -52,7 +52,7 @@ def backup_to_zip():
|
||||||
backup_filename = f"bazarr_backup_v{os.environ['BAZARR_VERSION']}_{now_string}.zip"
|
backup_filename = f"bazarr_backup_v{os.environ['BAZARR_VERSION']}_{now_string}.zip"
|
||||||
logging.debug(f'Backup filename will be: {backup_filename}')
|
logging.debug(f'Backup filename will be: {backup_filename}')
|
||||||
|
|
||||||
if not settings.postgresql.getboolean('enabled'):
|
if not settings.postgresql.enabled:
|
||||||
database_src_file = os.path.join(args.config_dir, 'db', 'bazarr.db')
|
database_src_file = os.path.join(args.config_dir, 'db', 'bazarr.db')
|
||||||
logging.debug(f'Database file path to backup is: {database_src_file}')
|
logging.debug(f'Database file path to backup is: {database_src_file}')
|
||||||
|
|
||||||
|
@ -71,10 +71,11 @@ def backup_to_zip():
|
||||||
database_backup_file = None
|
database_backup_file = None
|
||||||
logging.exception('Unable to backup database file.')
|
logging.exception('Unable to backup database file.')
|
||||||
|
|
||||||
config_file = os.path.join(args.config_dir, 'config', 'config.ini')
|
config_file = os.path.join(args.config_dir, 'config', 'config.yaml')
|
||||||
logging.debug(f'Config file path to backup is: {config_file}')
|
logging.debug(f'Config file path to backup is: {config_file}')
|
||||||
|
|
||||||
with ZipFile(os.path.join(get_backup_path(), backup_filename), 'w') as backupZip:
|
with ZipFile(os.path.join(get_backup_path(), backup_filename), 'w', compression=ZIP_DEFLATED,
|
||||||
|
compresslevel=9) as backupZip:
|
||||||
if database_backup_file:
|
if database_backup_file:
|
||||||
backupZip.write(database_backup_file, 'bazarr.db')
|
backupZip.write(database_backup_file, 'bazarr.db')
|
||||||
try:
|
try:
|
||||||
|
@ -83,12 +84,19 @@ def backup_to_zip():
|
||||||
logging.exception(f'Unable to delete temporary database backup file: {database_backup_file}')
|
logging.exception(f'Unable to delete temporary database backup file: {database_backup_file}')
|
||||||
else:
|
else:
|
||||||
logging.debug('Database file is not included in backup. See previous exception')
|
logging.debug('Database file is not included in backup. See previous exception')
|
||||||
backupZip.write(config_file, 'config.ini')
|
backupZip.write(config_file, 'config.yaml')
|
||||||
|
|
||||||
|
|
||||||
def restore_from_backup():
|
def restore_from_backup():
|
||||||
restore_config_path = os.path.join(get_restore_path(), 'config.ini')
|
if os.path.isfile(os.path.join(get_restore_path(), 'config.yaml')):
|
||||||
dest_config_path = os.path.join(args.config_dir, 'config', 'config.ini')
|
restore_config_path = os.path.join(get_restore_path(), 'config.yaml')
|
||||||
|
dest_config_path = os.path.join(args.config_dir, 'config', 'config.yaml')
|
||||||
|
new_config = True
|
||||||
|
else:
|
||||||
|
restore_config_path = os.path.join(get_restore_path(), 'config.ini')
|
||||||
|
dest_config_path = os.path.join(args.config_dir, 'config', 'config.ini')
|
||||||
|
new_config = False
|
||||||
|
|
||||||
restore_database_path = os.path.join(get_restore_path(), 'bazarr.db')
|
restore_database_path = os.path.join(get_restore_path(), 'bazarr.db')
|
||||||
dest_database_path = os.path.join(args.config_dir, 'db', 'bazarr.db')
|
dest_database_path = os.path.join(args.config_dir, 'db', 'bazarr.db')
|
||||||
|
|
||||||
|
@ -97,8 +105,15 @@ def restore_from_backup():
|
||||||
shutil.copy(restore_config_path, dest_config_path)
|
shutil.copy(restore_config_path, dest_config_path)
|
||||||
os.remove(restore_config_path)
|
os.remove(restore_config_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.exception(f'Unable to restore or delete config.ini to {dest_config_path}')
|
logging.exception(f'Unable to restore or delete config file to {dest_config_path}')
|
||||||
if not settings.postgresql.getboolean('enabled'):
|
else:
|
||||||
|
if new_config:
|
||||||
|
if os.path.isfile(os.path.join(get_restore_path(), 'config.ini')):
|
||||||
|
os.remove(os.path.join(get_restore_path(), 'config.ini'))
|
||||||
|
else:
|
||||||
|
if os.path.isfile(os.path.join(get_restore_path(), 'config.yaml')):
|
||||||
|
os.remove(os.path.join(get_restore_path(), 'config.yaml'))
|
||||||
|
if not settings.postgresql.enabled:
|
||||||
try:
|
try:
|
||||||
shutil.copy(restore_database_path, dest_database_path)
|
shutil.copy(restore_database_path, dest_database_path)
|
||||||
os.remove(restore_database_path)
|
os.remove(restore_database_path)
|
||||||
|
|
|
@ -44,7 +44,7 @@ def get_binary(name):
|
||||||
installed_exe = which(name)
|
installed_exe = which(name)
|
||||||
|
|
||||||
if installed_exe and os.path.isfile(installed_exe):
|
if installed_exe and os.path.isfile(installed_exe):
|
||||||
logging.debug('BAZARR returning this binary: {}'.format(installed_exe))
|
logging.debug(f'BAZARR returning this binary: {installed_exe}')
|
||||||
return installed_exe
|
return installed_exe
|
||||||
else:
|
else:
|
||||||
logging.debug('BAZARR binary not found in path, searching for it...')
|
logging.debug('BAZARR binary not found in path, searching for it...')
|
||||||
|
@ -72,27 +72,27 @@ def get_binary(name):
|
||||||
logging.debug('BAZARR binary not found in binaries.json')
|
logging.debug('BAZARR binary not found in binaries.json')
|
||||||
raise BinaryNotFound
|
raise BinaryNotFound
|
||||||
else:
|
else:
|
||||||
logging.debug('BAZARR found this in binaries.json: {}'.format(binary))
|
logging.debug(f'BAZARR found this in binaries.json: {binary}')
|
||||||
|
|
||||||
if os.path.isfile(exe) and md5(exe) == binary['checksum']:
|
if os.path.isfile(exe) and md5(exe) == binary['checksum']:
|
||||||
logging.debug('BAZARR returning this existing and up-to-date binary: {}'.format(exe))
|
logging.debug(f'BAZARR returning this existing and up-to-date binary: {exe}')
|
||||||
return exe
|
return exe
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
logging.debug('BAZARR creating directory tree for {}'.format(exe_dir))
|
logging.debug(f'BAZARR creating directory tree for {exe_dir}')
|
||||||
os.makedirs(exe_dir, exist_ok=True)
|
os.makedirs(exe_dir, exist_ok=True)
|
||||||
logging.debug('BAZARR downloading {0} from {1}'.format(name, binary['url']))
|
logging.debug(f'BAZARR downloading {name} from {binary["url"]}')
|
||||||
r = requests.get(binary['url'])
|
r = requests.get(binary['url'])
|
||||||
logging.debug('BAZARR saving {0} to {1}'.format(name, exe_dir))
|
logging.debug(f'BAZARR saving {name} to {exe_dir}')
|
||||||
with open(exe, 'wb') as f:
|
with open(exe, 'wb') as f:
|
||||||
f.write(r.content)
|
f.write(r.content)
|
||||||
if system != 'Windows':
|
if system != 'Windows':
|
||||||
logging.debug('BAZARR adding execute permission on {}'.format(exe))
|
logging.debug(f'BAZARR adding execute permission on {exe}')
|
||||||
st = os.stat(exe)
|
st = os.stat(exe)
|
||||||
os.chmod(exe, st.st_mode | stat.S_IEXEC)
|
os.chmod(exe, st.st_mode | stat.S_IEXEC)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('BAZARR unable to download {0} to {1}'.format(name, exe_dir))
|
logging.exception(f'BAZARR unable to download {name} to {exe_dir}')
|
||||||
raise BinaryNotFound
|
raise BinaryNotFound
|
||||||
else:
|
else:
|
||||||
logging.debug('BAZARR returning this new binary: {}'.format(exe))
|
logging.debug(f'BAZARR returning this new binary: {exe}')
|
||||||
return exe
|
return exe
|
||||||
|
|
|
@ -9,7 +9,7 @@ def browse_bazarr_filesystem(path='#'):
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
dir_list = []
|
dir_list = []
|
||||||
for drive in string.ascii_uppercase:
|
for drive in string.ascii_uppercase:
|
||||||
drive_letter = drive + ':\\'
|
drive_letter = f'{drive}:\\'
|
||||||
if os.path.exists(drive_letter):
|
if os.path.exists(drive_letter):
|
||||||
dir_list.append(drive_letter)
|
dir_list.append(drive_letter)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -9,9 +9,9 @@ from radarr.rootfolder import check_radarr_rootfolder
|
||||||
|
|
||||||
|
|
||||||
def check_health():
|
def check_health():
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
check_sonarr_rootfolder()
|
check_sonarr_rootfolder()
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
check_radarr_rootfolder()
|
check_radarr_rootfolder()
|
||||||
event_stream(type='badges')
|
event_stream(type='badges')
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ def get_health_issues():
|
||||||
health_issues = []
|
health_issues = []
|
||||||
|
|
||||||
# get Sonarr rootfolder issues
|
# get Sonarr rootfolder issues
|
||||||
if settings.general.getboolean('use_sonarr'):
|
if settings.general.use_sonarr:
|
||||||
rootfolder = database.execute(
|
rootfolder = database.execute(
|
||||||
select(TableShowsRootfolder.path,
|
select(TableShowsRootfolder.path,
|
||||||
TableShowsRootfolder.accessible,
|
TableShowsRootfolder.accessible,
|
||||||
|
@ -36,7 +36,7 @@ def get_health_issues():
|
||||||
'issue': item.error})
|
'issue': item.error})
|
||||||
|
|
||||||
# get Radarr rootfolder issues
|
# get Radarr rootfolder issues
|
||||||
if settings.general.getboolean('use_radarr'):
|
if settings.general.use_radarr:
|
||||||
rootfolder = database.execute(
|
rootfolder = database.execute(
|
||||||
select(TableMoviesRootfolder.path,
|
select(TableMoviesRootfolder.path,
|
||||||
TableMoviesRootfolder.accessible,
|
TableMoviesRootfolder.accessible,
|
||||||
|
|
|
@ -52,7 +52,7 @@ def get_target_folder(file_path):
|
||||||
try:
|
try:
|
||||||
os.makedirs(fld)
|
os.makedirs(fld)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.error('BAZARR is unable to create directory to save subtitles: ' + fld)
|
logging.error(f'BAZARR is unable to create directory to save subtitles: {fld}')
|
||||||
fld = None
|
fld = None
|
||||||
else:
|
else:
|
||||||
fld = None
|
fld = None
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from app.config import settings, get_array_from
|
from app.config import settings
|
||||||
|
|
||||||
|
|
||||||
class PathMappings:
|
class PathMappings:
|
||||||
|
@ -11,8 +11,8 @@ class PathMappings:
|
||||||
self.path_mapping_movies = []
|
self.path_mapping_movies = []
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.path_mapping_series = [x for x in get_array_from(settings.general.path_mappings) if x[0] != x[1]]
|
self.path_mapping_series = [x for x in settings.general.path_mappings if x[0] != x[1]]
|
||||||
self.path_mapping_movies = [x for x in get_array_from(settings.general.path_mappings_movie) if x[0] != x[1]]
|
self.path_mapping_movies = [x for x in settings.general.path_mappings_movie if x[0] != x[1]]
|
||||||
|
|
||||||
def path_replace(self, path):
|
def path_replace(self, path):
|
||||||
if path is None:
|
if path is None:
|
||||||
|
|
|
@ -43,7 +43,7 @@ def pp_replace(pp_command, episode, subtitles, language, language_code2, languag
|
||||||
def set_chmod(subtitles_path):
|
def set_chmod(subtitles_path):
|
||||||
# apply chmod if required
|
# apply chmod if required
|
||||||
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
||||||
'win') and settings.general.getboolean('chmod_enabled') else None
|
'win') and settings.general.chmod_enabled else None
|
||||||
if chmod:
|
if chmod:
|
||||||
logging.debug(f"BAZARR setting permission to {chmod} on {subtitles_path} after custom post-processing.")
|
logging.debug(f"BAZARR setting permission to {chmod} on {subtitles_path} after custom post-processing.")
|
||||||
os.chmod(subtitles_path, chmod)
|
os.chmod(subtitles_path, chmod)
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
# Bazarr configuration path, must be absolute path
|
# Bazarr configuration path, must be absolute path
|
||||||
# Vite will use this variable to find your bazarr's configuration file
|
# Vite will use this variable to find your bazarr's configuration file
|
||||||
VITE_BAZARR_CONFIG_FILE="../data/config/config.ini"
|
VITE_BAZARR_CONFIG_FILE="../data/config/config.yaml"
|
||||||
|
|
||||||
# Display update section in settings
|
# Display update section in settings
|
||||||
VITE_CAN_UPDATE=true
|
VITE_CAN_UPDATE=true
|
||||||
|
|
|
@ -2,48 +2,34 @@
|
||||||
/// <reference types="node" />
|
/// <reference types="node" />
|
||||||
|
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
|
import { get } from "lodash";
|
||||||
|
import YAML from "yaml";
|
||||||
|
|
||||||
class ConfigReader {
|
class ConfigReader {
|
||||||
config?: string;
|
config: object;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = undefined;
|
this.config = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async open(path: string) {
|
async open(path: string) {
|
||||||
try {
|
try {
|
||||||
this.config = await readFile(path, "utf8");
|
const rawConfig = await readFile(path, "utf8");
|
||||||
|
this.config = YAML.parse(rawConfig);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// We don't want to catch the error here, handle it on getValue method
|
// We don't want to catch the error here, handle it on getValue method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getValue(sectionName: string, fieldName: string) {
|
getValue(sectionName: string, fieldName: string) {
|
||||||
if (!this.config) {
|
const path = `${sectionName}.${fieldName}`;
|
||||||
throw new Error("Cannot find config to read");
|
const result = get(this.config, path);
|
||||||
}
|
|
||||||
const targetSection = this.config
|
|
||||||
.split("\n\n")
|
|
||||||
.filter((section) => section.includes(`[${sectionName}]`));
|
|
||||||
|
|
||||||
if (targetSection.length === 0) {
|
if (result === undefined) {
|
||||||
throw new Error(`Cannot find [${sectionName}] section in config`);
|
throw new Error(`Failed to find ${path} in the local config file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const section = targetSection[0];
|
return result;
|
||||||
|
|
||||||
for (const line of section.split("\n")) {
|
|
||||||
const matched = line.startsWith(fieldName);
|
|
||||||
if (matched) {
|
|
||||||
const results = line.split("=");
|
|
||||||
if (results.length === 2) {
|
|
||||||
const key = results[1].trim();
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Cannot find ${fieldName} in config`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,8 @@
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vite": "^4.3.0",
|
"vite": "^4.3.0",
|
||||||
"vite-plugin-checker": "^0.5.5",
|
"vite-plugin-checker": "^0.5.5",
|
||||||
"vitest": "^0.30.1"
|
"vitest": "^0.30.1",
|
||||||
|
"yaml": "^2.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
|
@ -4818,6 +4819,14 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cosmiconfig/node_modules/yaml": {
|
||||||
|
"version": "1.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||||
|
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
|
@ -10439,11 +10448,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "1.10.2",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
|
||||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
"integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@types/react-table": "^7.7.0",
|
"@types/react-table": "^7.7.0",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"vitest": "^0.30.1",
|
|
||||||
"@vitest/coverage-c8": "^0.30.0",
|
"@vitest/coverage-c8": "^0.30.0",
|
||||||
"@vitest/ui": "^0.30.0",
|
"@vitest/ui": "^0.30.0",
|
||||||
"clsx": "^1.2.0",
|
"clsx": "^1.2.0",
|
||||||
|
@ -62,7 +61,9 @@
|
||||||
"sass": "^1.62.0",
|
"sass": "^1.62.0",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vite": "^4.3.0",
|
"vite": "^4.3.0",
|
||||||
"vite-plugin-checker": "^0.5.5"
|
"vite-plugin-checker": "^0.5.5",
|
||||||
|
"vitest": "^0.30.1",
|
||||||
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
|
|
|
@ -83,24 +83,32 @@ const SettingsGeneralView: FunctionComponent = () => {
|
||||||
</CollapseBox>
|
</CollapseBox>
|
||||||
<Text
|
<Text
|
||||||
label="API Key"
|
label="API Key"
|
||||||
disabled
|
// User can copy through the clipboard button
|
||||||
|
disabled={window.isSecureContext}
|
||||||
|
// Enable user to at least copy when not in secure context
|
||||||
|
readOnly={!window.isSecureContext}
|
||||||
rightSectionWidth={95}
|
rightSectionWidth={95}
|
||||||
rightSectionProps={{ style: { justifyContent: "flex-end" } }}
|
rightSectionProps={{ style: { justifyContent: "flex-end" } }}
|
||||||
rightSection={
|
rightSection={
|
||||||
<MantineGroup spacing="xs" mx="xs" position="right">
|
<MantineGroup spacing="xs" mx="xs" position="right">
|
||||||
<Action
|
{
|
||||||
label="Copy API Key"
|
// Clipboard API is only available in secure contexts See: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#interfaces
|
||||||
variant="light"
|
window.isSecureContext && (
|
||||||
settingKey={settingApiKey}
|
<Action
|
||||||
color={copied ? "green" : undefined}
|
label="Copy API Key"
|
||||||
icon={copied ? faCheck : faClipboard}
|
variant="light"
|
||||||
onClick={(update, value) => {
|
settingKey={settingApiKey}
|
||||||
if (value) {
|
color={copied ? "green" : undefined}
|
||||||
clipboard.copy(value);
|
icon={copied ? faCheck : faClipboard}
|
||||||
toggleState(setCopy, 1500);
|
onClick={(update, value) => {
|
||||||
}
|
if (value) {
|
||||||
}}
|
clipboard.copy(value);
|
||||||
></Action>
|
toggleState(setCopy, 1500);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Action
|
<Action
|
||||||
label="Regenerate"
|
label="Regenerate"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dynaconf.base import LazySettings # noqa
|
||||||
|
from dynaconf.constants import DEFAULT_SETTINGS_FILES
|
||||||
|
from dynaconf.contrib import DjangoDynaconf # noqa
|
||||||
|
from dynaconf.contrib import FlaskDynaconf # noqa
|
||||||
|
from dynaconf.validator import ValidationError # noqa
|
||||||
|
from dynaconf.validator import Validator # noqa
|
||||||
|
|
||||||
|
settings = LazySettings(
|
||||||
|
# This global `settings` is deprecated from v3.0.0+
|
||||||
|
# kept here for backwards compatibility
|
||||||
|
# To Be Removed in 4.0.x
|
||||||
|
warn_dynaconf_global_settings=True,
|
||||||
|
environments=True,
|
||||||
|
lowercase_read=False,
|
||||||
|
load_dotenv=True,
|
||||||
|
default_settings_paths=DEFAULT_SETTINGS_FILES,
|
||||||
|
)
|
||||||
|
|
||||||
|
# This is the new recommended base class alias
|
||||||
|
Dynaconf = LazySettings # noqa
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Dynaconf",
|
||||||
|
"LazySettings",
|
||||||
|
"Validator",
|
||||||
|
"FlaskDynaconf",
|
||||||
|
"ValidationError",
|
||||||
|
"DjangoDynaconf",
|
||||||
|
]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,773 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
import webbrowser
|
||||||
|
from contextlib import suppress
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dynaconf import constants
|
||||||
|
from dynaconf import default_settings
|
||||||
|
from dynaconf import LazySettings
|
||||||
|
from dynaconf import loaders
|
||||||
|
from dynaconf import settings as legacy_settings
|
||||||
|
from dynaconf.loaders.py_loader import get_module
|
||||||
|
from dynaconf.utils import upperfy
|
||||||
|
from dynaconf.utils.files import read_file
|
||||||
|
from dynaconf.utils.functional import empty
|
||||||
|
from dynaconf.utils.parse_conf import parse_conf_data
|
||||||
|
from dynaconf.utils.parse_conf import unparse_conf_data
|
||||||
|
from dynaconf.validator import ValidationError
|
||||||
|
from dynaconf.validator import Validator
|
||||||
|
from dynaconf.vendor import click
|
||||||
|
from dynaconf.vendor import toml
|
||||||
|
from dynaconf.vendor import tomllib
|
||||||
|
|
||||||
|
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
|
||||||
|
CWD = None
|
||||||
|
try:
|
||||||
|
CWD = Path.cwd()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
EXTS = ["ini", "toml", "yaml", "json", "py", "env"]
|
||||||
|
WRITERS = ["ini", "toml", "yaml", "json", "py", "redis", "vault", "env"]
|
||||||
|
|
||||||
|
ENC = default_settings.ENCODING_FOR_DYNACONF
|
||||||
|
|
||||||
|
|
||||||
|
def set_settings(ctx, instance=None):
|
||||||
|
"""Pick correct settings instance and set it to a global variable."""
|
||||||
|
|
||||||
|
global settings
|
||||||
|
|
||||||
|
settings = None
|
||||||
|
|
||||||
|
_echo_enabled = ctx.invoked_subcommand not in ["get", None]
|
||||||
|
|
||||||
|
if instance is not None:
|
||||||
|
if ctx.invoked_subcommand in ["init"]:
|
||||||
|
raise click.UsageError(
|
||||||
|
"-i/--instance option is not allowed for `init` command"
|
||||||
|
)
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
settings = import_settings(instance)
|
||||||
|
elif "FLASK_APP" in os.environ: # pragma: no cover
|
||||||
|
with suppress(ImportError, click.UsageError):
|
||||||
|
from flask.cli import ScriptInfo # noqa
|
||||||
|
from dynaconf import FlaskDynaconf
|
||||||
|
|
||||||
|
flask_app = ScriptInfo().load_app()
|
||||||
|
settings = FlaskDynaconf(flask_app, **flask_app.config).settings
|
||||||
|
_echo_enabled and click.echo(
|
||||||
|
click.style(
|
||||||
|
"Flask app detected", fg="white", bg="bright_black"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif "DJANGO_SETTINGS_MODULE" in os.environ: # pragma: no cover
|
||||||
|
sys.path.insert(0, os.path.abspath(os.getcwd()))
|
||||||
|
try:
|
||||||
|
# Django extension v2
|
||||||
|
from django.conf import settings # noqa
|
||||||
|
|
||||||
|
settings.DYNACONF.configure()
|
||||||
|
except AttributeError:
|
||||||
|
settings = LazySettings()
|
||||||
|
|
||||||
|
if settings is not None:
|
||||||
|
_echo_enabled and click.echo(
|
||||||
|
click.style(
|
||||||
|
"Django app detected", fg="white", bg="bright_black"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings is None:
|
||||||
|
|
||||||
|
if instance is None and "--help" not in click.get_os_args():
|
||||||
|
if ctx.invoked_subcommand and ctx.invoked_subcommand not in [
|
||||||
|
"init",
|
||||||
|
]:
|
||||||
|
warnings.warn(
|
||||||
|
"Starting on 3.x the param --instance/-i is now required. "
|
||||||
|
"try passing it `dynaconf -i path.to.settings <cmd>` "
|
||||||
|
"Example `dynaconf -i config.settings list` "
|
||||||
|
)
|
||||||
|
settings = legacy_settings
|
||||||
|
else:
|
||||||
|
settings = LazySettings(create_new_settings=True)
|
||||||
|
else:
|
||||||
|
settings = LazySettings()
|
||||||
|
|
||||||
|
|
||||||
|
def import_settings(dotted_path):
|
||||||
|
"""Import settings instance from python dotted path.
|
||||||
|
|
||||||
|
Last item in dotted path must be settings instance.
|
||||||
|
|
||||||
|
Example: import_settings('path.to.settings')
|
||||||
|
"""
|
||||||
|
if "." in dotted_path:
|
||||||
|
module, name = dotted_path.rsplit(".", 1)
|
||||||
|
else:
|
||||||
|
raise click.UsageError(
|
||||||
|
f"invalid path to settings instance: {dotted_path}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(module)
|
||||||
|
except ImportError as e:
|
||||||
|
raise click.UsageError(e)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
return getattr(module, name)
|
||||||
|
except AttributeError as e:
|
||||||
|
raise click.UsageError(e)
|
||||||
|
|
||||||
|
|
||||||
|
def split_vars(_vars):
|
||||||
|
"""Splits values like foo=bar=zaz in {'foo': 'bar=zaz'}"""
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
upperfy(k.strip()): parse_conf_data(
|
||||||
|
v.strip(), tomlfy=True, box_settings=settings
|
||||||
|
)
|
||||||
|
for k, _, v in [item.partition("=") for item in _vars]
|
||||||
|
}
|
||||||
|
if _vars
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_file_in_root_directory(*names, **kwargs):
|
||||||
|
"""Read a file on root dir."""
|
||||||
|
return read_file(
|
||||||
|
os.path.join(os.path.dirname(__file__), *names),
|
||||||
|
encoding=kwargs.get("encoding", "utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def print_version(ctx, param, value):
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
click.echo(read_file_in_root_directory("VERSION"))
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
|
||||||
|
def open_docs(ctx, param, value): # pragma: no cover
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
url = "https://dynaconf.com/"
|
||||||
|
webbrowser.open(url, new=2)
|
||||||
|
click.echo(f"{url} opened in browser")
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
|
||||||
|
def show_banner(ctx, param, value):
|
||||||
|
"""Shows dynaconf awesome banner"""
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
set_settings(ctx)
|
||||||
|
click.echo(settings.dynaconf_banner)
|
||||||
|
click.echo("Learn more at: http://github.com/dynaconf/dynaconf")
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option(
|
||||||
|
"--version",
|
||||||
|
is_flag=True,
|
||||||
|
callback=print_version,
|
||||||
|
expose_value=False,
|
||||||
|
is_eager=True,
|
||||||
|
help="Show dynaconf version",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--docs",
|
||||||
|
is_flag=True,
|
||||||
|
callback=open_docs,
|
||||||
|
expose_value=False,
|
||||||
|
is_eager=True,
|
||||||
|
help="Open documentation in browser",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--banner",
|
||||||
|
is_flag=True,
|
||||||
|
callback=show_banner,
|
||||||
|
expose_value=False,
|
||||||
|
is_eager=True,
|
||||||
|
help="Show awesome banner",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--instance",
|
||||||
|
"-i",
|
||||||
|
default=None,
|
||||||
|
envvar="INSTANCE_FOR_DYNACONF",
|
||||||
|
help="Custom instance of LazySettings",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def main(ctx, instance):
|
||||||
|
"""Dynaconf - Command Line Interface\n
|
||||||
|
Documentation: https://dynaconf.com/
|
||||||
|
"""
|
||||||
|
set_settings(ctx, instance)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option(
|
||||||
|
"--format", "fileformat", "-f", default="toml", type=click.Choice(EXTS)
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--path", "-p", default=CWD, help="defaults to current directory"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--env",
|
||||||
|
"-e",
|
||||||
|
default=None,
|
||||||
|
help="deprecated command (kept for compatibility but unused)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--vars",
|
||||||
|
"_vars",
|
||||||
|
"-v",
|
||||||
|
multiple=True,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"extra values to write to settings file "
|
||||||
|
"e.g: `dynaconf init -v NAME=foo -v X=2`"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--secrets",
|
||||||
|
"_secrets",
|
||||||
|
"-s",
|
||||||
|
multiple=True,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"secret key values to be written in .secrets "
|
||||||
|
"e.g: `dynaconf init -s TOKEN=kdslmflds"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option("--wg/--no-wg", default=True)
|
||||||
|
@click.option("-y", default=False, is_flag=True)
|
||||||
|
@click.option("--django", default=os.environ.get("DJANGO_SETTINGS_MODULE"))
|
||||||
|
@click.pass_context
|
||||||
|
def init(ctx, fileformat, path, env, _vars, _secrets, wg, y, django):
|
||||||
|
"""Inits a dynaconf project
|
||||||
|
By default it creates a settings.toml and a .secrets.toml
|
||||||
|
for [default|development|staging|testing|production|global] envs.
|
||||||
|
|
||||||
|
The format of the files can be changed passing
|
||||||
|
--format=yaml|json|ini|py.
|
||||||
|
|
||||||
|
This command must run on the project's root folder or you must pass
|
||||||
|
--path=/myproject/root/folder.
|
||||||
|
|
||||||
|
The --env/-e is deprecated (kept for compatibility but unused)
|
||||||
|
"""
|
||||||
|
click.echo("⚙️ Configuring your Dynaconf environment")
|
||||||
|
click.echo("-" * 42)
|
||||||
|
if "FLASK_APP" in os.environ: # pragma: no cover
|
||||||
|
click.echo(
|
||||||
|
"⚠️ Flask detected, you can't use `dynaconf init` "
|
||||||
|
"on a flask project, instead go to dynaconf.com/flask/ "
|
||||||
|
"for more information.\n"
|
||||||
|
"Or add the following to your app.py\n"
|
||||||
|
"\n"
|
||||||
|
"from dynaconf import FlaskDynaconf\n"
|
||||||
|
"app = Flask(__name__)\n"
|
||||||
|
"FlaskDynaconf(app)\n"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
path = Path(path)
|
||||||
|
|
||||||
|
if env is not None:
|
||||||
|
click.secho(
|
||||||
|
"⚠️ The --env/-e option is deprecated (kept for\n"
|
||||||
|
" compatibility but unused)\n",
|
||||||
|
fg="red",
|
||||||
|
bold=True,
|
||||||
|
# stderr=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.get("create_new_settings") is True:
|
||||||
|
filename = Path("config.py")
|
||||||
|
if not filename.exists():
|
||||||
|
with open(filename, "w") as new_settings:
|
||||||
|
new_settings.write(
|
||||||
|
constants.INSTANCE_TEMPLATE.format(
|
||||||
|
settings_files=[
|
||||||
|
f"settings.{fileformat}",
|
||||||
|
f".secrets.{fileformat}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
click.echo(
|
||||||
|
"🐍 The file `config.py` was generated.\n"
|
||||||
|
" on your code now use `from config import settings`.\n"
|
||||||
|
" (you must have `config` importable in your PYTHONPATH).\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
click.echo(
|
||||||
|
f"⁉️ You already have a {filename} so it is not going to be\n"
|
||||||
|
" generated for you, you will need to create your own \n"
|
||||||
|
" settings instance e.g: config.py \n"
|
||||||
|
" from dynaconf import Dynaconf \n"
|
||||||
|
" settings = Dynaconf(**options)\n"
|
||||||
|
)
|
||||||
|
sys.path.append(str(path))
|
||||||
|
set_settings(ctx, "config.settings")
|
||||||
|
|
||||||
|
env = settings.current_env.lower()
|
||||||
|
|
||||||
|
loader = importlib.import_module(f"dynaconf.loaders.{fileformat}_loader")
|
||||||
|
# Turn foo=bar=zaz in {'foo': 'bar=zaz'}
|
||||||
|
env_data = split_vars(_vars)
|
||||||
|
_secrets = split_vars(_secrets)
|
||||||
|
|
||||||
|
# create placeholder data for every env
|
||||||
|
settings_data = {}
|
||||||
|
secrets_data = {}
|
||||||
|
if env_data:
|
||||||
|
settings_data[env] = env_data
|
||||||
|
settings_data["default"] = {k: "a default value" for k in env_data}
|
||||||
|
if _secrets:
|
||||||
|
secrets_data[env] = _secrets
|
||||||
|
secrets_data["default"] = {k: "a default value" for k in _secrets}
|
||||||
|
|
||||||
|
if str(path).endswith(
|
||||||
|
constants.ALL_EXTENSIONS + ("py",)
|
||||||
|
): # pragma: no cover # noqa
|
||||||
|
settings_path = path
|
||||||
|
secrets_path = path.parent / f".secrets.{fileformat}"
|
||||||
|
gitignore_path = path.parent / ".gitignore"
|
||||||
|
else:
|
||||||
|
if fileformat == "env":
|
||||||
|
if str(path) in (".env", "./.env"): # pragma: no cover
|
||||||
|
settings_path = path
|
||||||
|
elif str(path).endswith("/.env"): # pragma: no cover
|
||||||
|
settings_path = path
|
||||||
|
elif str(path).endswith(".env"): # pragma: no cover
|
||||||
|
settings_path = path.parent / ".env"
|
||||||
|
else:
|
||||||
|
settings_path = path / ".env"
|
||||||
|
Path.touch(settings_path)
|
||||||
|
secrets_path = None
|
||||||
|
else:
|
||||||
|
settings_path = path / f"settings.{fileformat}"
|
||||||
|
secrets_path = path / f".secrets.{fileformat}"
|
||||||
|
gitignore_path = path / ".gitignore"
|
||||||
|
|
||||||
|
if fileformat in ["py", "env"] or env == "main":
|
||||||
|
# for Main env, Python and .env formats writes a single env
|
||||||
|
settings_data = settings_data.get(env, {})
|
||||||
|
secrets_data = secrets_data.get(env, {})
|
||||||
|
|
||||||
|
if not y and settings_path and settings_path.exists(): # pragma: no cover
|
||||||
|
click.confirm(
|
||||||
|
f"⁉ {settings_path} exists do you want to overwrite it?",
|
||||||
|
abort=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not y and secrets_path and secrets_path.exists(): # pragma: no cover
|
||||||
|
click.confirm(
|
||||||
|
f"⁉ {secrets_path} exists do you want to overwrite it?",
|
||||||
|
abort=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings_path:
|
||||||
|
loader.write(settings_path, settings_data, merge=True)
|
||||||
|
click.echo(
|
||||||
|
f"🎛️ {settings_path.name} created to hold your settings.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if secrets_path:
|
||||||
|
loader.write(secrets_path, secrets_data, merge=True)
|
||||||
|
click.echo(f"🔑 {secrets_path.name} created to hold your secrets.\n")
|
||||||
|
ignore_line = ".secrets.*"
|
||||||
|
comment = "\n# Ignore dynaconf secret files\n"
|
||||||
|
if not gitignore_path.exists():
|
||||||
|
with open(str(gitignore_path), "w", encoding=ENC) as f:
|
||||||
|
f.writelines([comment, ignore_line, "\n"])
|
||||||
|
else:
|
||||||
|
existing = (
|
||||||
|
ignore_line in open(str(gitignore_path), encoding=ENC).read()
|
||||||
|
)
|
||||||
|
if not existing: # pragma: no cover
|
||||||
|
with open(str(gitignore_path), "a+", encoding=ENC) as f:
|
||||||
|
f.writelines([comment, ignore_line, "\n"])
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
f"🙈 the {secrets_path.name} is also included in `.gitignore` \n"
|
||||||
|
" beware to not push your secrets to a public repo \n"
|
||||||
|
" or use dynaconf builtin support for Vault Servers.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if django: # pragma: no cover
|
||||||
|
dj_module, _ = get_module({}, django)
|
||||||
|
dj_filename = dj_module.__file__
|
||||||
|
if Path(dj_filename).exists():
|
||||||
|
click.confirm(
|
||||||
|
f"⁉ {dj_filename} is found do you want to add dynaconf?",
|
||||||
|
abort=True,
|
||||||
|
)
|
||||||
|
with open(dj_filename, "a") as dj_file:
|
||||||
|
dj_file.write(constants.DJANGO_PATCH)
|
||||||
|
click.echo("🎠 Now your Django settings are managed by Dynaconf")
|
||||||
|
else:
|
||||||
|
click.echo("❌ Django settings file not written.")
|
||||||
|
else:
|
||||||
|
click.echo(
|
||||||
|
"🎉 Dynaconf is configured! read more on https://dynaconf.com\n"
|
||||||
|
" Use `dynaconf -i config.settings list` to see your settings\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command(name="get")
|
||||||
|
@click.argument("key", required=True)
|
||||||
|
@click.option(
|
||||||
|
"--default",
|
||||||
|
"-d",
|
||||||
|
default=empty,
|
||||||
|
help="Default value if settings doesn't exist",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--env", "-e", default=None, help="Filters the env to get the values"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--unparse",
|
||||||
|
"-u",
|
||||||
|
default=False,
|
||||||
|
help="Unparse data by adding markers such as @none, @int etc..",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
def get(key, default, env, unparse):
|
||||||
|
"""Returns the raw value for a settings key.
|
||||||
|
|
||||||
|
If result is a dict, list or tuple it is printes as a valid json string.
|
||||||
|
"""
|
||||||
|
if env:
|
||||||
|
env = env.strip()
|
||||||
|
if key:
|
||||||
|
key = key.strip()
|
||||||
|
|
||||||
|
if env:
|
||||||
|
settings.setenv(env)
|
||||||
|
|
||||||
|
if default is not empty:
|
||||||
|
result = settings.get(key, default)
|
||||||
|
else:
|
||||||
|
result = settings[key] # let the keyerror raises
|
||||||
|
|
||||||
|
if unparse:
|
||||||
|
result = unparse_conf_data(result)
|
||||||
|
|
||||||
|
if isinstance(result, (dict, list, tuple)):
|
||||||
|
result = json.dumps(result, sort_keys=True)
|
||||||
|
|
||||||
|
click.echo(result, nl=False)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command(name="list")
|
||||||
|
@click.option(
|
||||||
|
"--env", "-e", default=None, help="Filters the env to get the values"
|
||||||
|
)
|
||||||
|
@click.option("--key", "-k", default=None, help="Filters a single key")
|
||||||
|
@click.option(
|
||||||
|
"--more",
|
||||||
|
"-m",
|
||||||
|
default=None,
|
||||||
|
help="Pagination more|less style",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--loader",
|
||||||
|
"-l",
|
||||||
|
default=None,
|
||||||
|
help="a loader identifier to filter e.g: toml|yaml",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--all",
|
||||||
|
"_all",
|
||||||
|
"-a",
|
||||||
|
default=False,
|
||||||
|
is_flag=True,
|
||||||
|
help="show dynaconf internal settings?",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--output",
|
||||||
|
"-o",
|
||||||
|
type=click.Path(writable=True, dir_okay=False),
|
||||||
|
default=None,
|
||||||
|
help="Filepath to write the listed values as json",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--output-flat",
|
||||||
|
"flat",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Output file is flat (do not include [env] name)",
|
||||||
|
)
|
||||||
|
def _list(env, key, more, loader, _all=False, output=None, flat=False):
|
||||||
|
"""Lists all user defined config values
|
||||||
|
and if `--all` is passed it also shows dynaconf internal variables.
|
||||||
|
"""
|
||||||
|
if env:
|
||||||
|
env = env.strip()
|
||||||
|
if key:
|
||||||
|
key = key.strip()
|
||||||
|
if loader:
|
||||||
|
loader = loader.strip()
|
||||||
|
|
||||||
|
if env:
|
||||||
|
settings.setenv(env)
|
||||||
|
|
||||||
|
cur_env = settings.current_env.lower()
|
||||||
|
|
||||||
|
if cur_env == "main":
|
||||||
|
flat = True
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Working in {cur_env} environment ",
|
||||||
|
bold=True,
|
||||||
|
bg="bright_blue",
|
||||||
|
fg="bright_white",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not loader:
|
||||||
|
data = settings.as_dict(env=env, internal=_all)
|
||||||
|
else:
|
||||||
|
identifier = f"{loader}_{cur_env}"
|
||||||
|
data = settings._loaded_by_loaders.get(identifier, {})
|
||||||
|
data = data or settings._loaded_by_loaders.get(loader, {})
|
||||||
|
|
||||||
|
# remove to avoid displaying twice
|
||||||
|
data.pop("SETTINGS_MODULE", None)
|
||||||
|
|
||||||
|
def color(_k):
|
||||||
|
if _k in dir(default_settings):
|
||||||
|
return "blue"
|
||||||
|
return "magenta"
|
||||||
|
|
||||||
|
def format_setting(_k, _v):
|
||||||
|
key = click.style(_k, bg=color(_k), fg="bright_white")
|
||||||
|
data_type = click.style(
|
||||||
|
f"<{type(_v).__name__}>", bg="bright_black", fg="bright_white"
|
||||||
|
)
|
||||||
|
value = pprint.pformat(_v)
|
||||||
|
return f"{key}{data_type} {value}"
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
datalines = "\n".join(
|
||||||
|
format_setting(k, v)
|
||||||
|
for k, v in data.items()
|
||||||
|
if k not in data.get("RENAMED_VARS", [])
|
||||||
|
)
|
||||||
|
(click.echo_via_pager if more else click.echo)(datalines)
|
||||||
|
if output:
|
||||||
|
loaders.write(output, data, env=not flat and cur_env)
|
||||||
|
else:
|
||||||
|
key = upperfy(key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = settings.get(key, empty)
|
||||||
|
except AttributeError:
|
||||||
|
value = empty
|
||||||
|
|
||||||
|
if value is empty:
|
||||||
|
click.echo(click.style("Key not found", bg="red", fg="white"))
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo(format_setting(key, value))
|
||||||
|
if output:
|
||||||
|
loaders.write(output, {key: value}, env=not flat and cur_env)
|
||||||
|
|
||||||
|
if env:
|
||||||
|
settings.setenv()
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.argument("to", required=True, type=click.Choice(WRITERS))
|
||||||
|
@click.option(
|
||||||
|
"--vars",
|
||||||
|
"_vars",
|
||||||
|
"-v",
|
||||||
|
multiple=True,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"key values to be written "
|
||||||
|
"e.g: `dynaconf write toml -e NAME=foo -e X=2"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--secrets",
|
||||||
|
"_secrets",
|
||||||
|
"-s",
|
||||||
|
multiple=True,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"secret key values to be written in .secrets "
|
||||||
|
"e.g: `dynaconf write toml -s TOKEN=kdslmflds -s X=2"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--path",
|
||||||
|
"-p",
|
||||||
|
default=CWD,
|
||||||
|
help="defaults to current directory/settings.{ext}",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--env",
|
||||||
|
"-e",
|
||||||
|
default="default",
|
||||||
|
help=(
|
||||||
|
"env to write to defaults to DEVELOPMENT for files "
|
||||||
|
"for external sources like Redis and Vault "
|
||||||
|
"it will be DYNACONF or the value set in "
|
||||||
|
"$ENVVAR_PREFIX_FOR_DYNACONF"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option("-y", default=False, is_flag=True)
|
||||||
|
def write(to, _vars, _secrets, path, env, y):
|
||||||
|
"""Writes data to specific source"""
|
||||||
|
_vars = split_vars(_vars)
|
||||||
|
_secrets = split_vars(_secrets)
|
||||||
|
loader = importlib.import_module(f"dynaconf.loaders.{to}_loader")
|
||||||
|
|
||||||
|
if to in EXTS:
|
||||||
|
|
||||||
|
# Lets write to a file
|
||||||
|
path = Path(path)
|
||||||
|
|
||||||
|
if str(path).endswith(constants.ALL_EXTENSIONS + ("py",)):
|
||||||
|
settings_path = path
|
||||||
|
secrets_path = path.parent / f".secrets.{to}"
|
||||||
|
else:
|
||||||
|
if to == "env":
|
||||||
|
if str(path) in (".env", "./.env"): # pragma: no cover
|
||||||
|
settings_path = path
|
||||||
|
elif str(path).endswith("/.env"):
|
||||||
|
settings_path = path
|
||||||
|
elif str(path).endswith(".env"):
|
||||||
|
settings_path = path.parent / ".env"
|
||||||
|
else:
|
||||||
|
settings_path = path / ".env"
|
||||||
|
Path.touch(settings_path)
|
||||||
|
secrets_path = None
|
||||||
|
_vars.update(_secrets)
|
||||||
|
else:
|
||||||
|
settings_path = path / f"settings.{to}"
|
||||||
|
secrets_path = path / f".secrets.{to}"
|
||||||
|
|
||||||
|
if (
|
||||||
|
_vars and not y and settings_path and settings_path.exists()
|
||||||
|
): # pragma: no cover # noqa
|
||||||
|
click.confirm(
|
||||||
|
f"{settings_path} exists do you want to overwrite it?",
|
||||||
|
abort=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
_secrets and not y and secrets_path and secrets_path.exists()
|
||||||
|
): # pragma: no cover # noqa
|
||||||
|
click.confirm(
|
||||||
|
f"{secrets_path} exists do you want to overwrite it?",
|
||||||
|
abort=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if to not in ["py", "env"]:
|
||||||
|
if _vars:
|
||||||
|
_vars = {env: _vars}
|
||||||
|
if _secrets:
|
||||||
|
_secrets = {env: _secrets}
|
||||||
|
|
||||||
|
if _vars and settings_path:
|
||||||
|
loader.write(settings_path, _vars, merge=True)
|
||||||
|
click.echo(f"Data successful written to {settings_path}")
|
||||||
|
|
||||||
|
if _secrets and secrets_path:
|
||||||
|
loader.write(secrets_path, _secrets, merge=True)
|
||||||
|
click.echo(f"Data successful written to {secrets_path}")
|
||||||
|
|
||||||
|
else: # pragma: no cover
|
||||||
|
# lets write to external source
|
||||||
|
with settings.using_env(env):
|
||||||
|
# make sure we're in the correct environment
|
||||||
|
loader.write(settings, _vars, **_secrets)
|
||||||
|
click.echo(f"Data successful written to {to}")
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option(
|
||||||
|
"--path", "-p", default=CWD, help="defaults to current directory"
|
||||||
|
)
|
||||||
|
def validate(path): # pragma: no cover
|
||||||
|
"""Validates Dynaconf settings based on rules defined in
|
||||||
|
dynaconf_validators.toml"""
|
||||||
|
# reads the 'dynaconf_validators.toml' from path
|
||||||
|
# for each section register the validator for specific env
|
||||||
|
# call validate
|
||||||
|
|
||||||
|
path = Path(path)
|
||||||
|
|
||||||
|
if not str(path).endswith(".toml"):
|
||||||
|
path = path / "dynaconf_validators.toml"
|
||||||
|
|
||||||
|
if not path.exists(): # pragma: no cover # noqa
|
||||||
|
click.echo(click.style(f"{path} not found", fg="white", bg="red"))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try: # try tomlib first
|
||||||
|
validation_data = tomllib.load(open(str(path), "rb"))
|
||||||
|
except UnicodeDecodeError: # fallback to legacy toml (TBR in 4.0.0)
|
||||||
|
warnings.warn(
|
||||||
|
"TOML files should have only UTF-8 encoded characters. "
|
||||||
|
"starting on 4.0.0 dynaconf will stop allowing invalid chars.",
|
||||||
|
)
|
||||||
|
validation_data = toml.load(
|
||||||
|
open(str(path), encoding=default_settings.ENCODING_FOR_DYNACONF),
|
||||||
|
)
|
||||||
|
|
||||||
|
success = True
|
||||||
|
for env, name_data in validation_data.items():
|
||||||
|
for name, data in name_data.items():
|
||||||
|
if not isinstance(data, dict): # pragma: no cover
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Invalid rule for parameter '{name}'",
|
||||||
|
fg="white",
|
||||||
|
bg="yellow",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
data.setdefault("env", env)
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Validating '{name}' with '{data}'",
|
||||||
|
fg="white",
|
||||||
|
bg="blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
Validator(name, **data).validate(settings)
|
||||||
|
except ValidationError as e:
|
||||||
|
click.echo(
|
||||||
|
click.style(f"Error: {e}", fg="white", bg="red")
|
||||||
|
)
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if success:
|
||||||
|
click.echo(click.style("Validation success!", fg="white", bg="green"))
|
||||||
|
else:
|
||||||
|
click.echo(click.style("Validation error!", fg="white", bg="red"))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
|
@ -0,0 +1,52 @@
|
||||||
|
# pragma: no cover
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
INI_EXTENSIONS = (".ini", ".conf", ".properties")
|
||||||
|
TOML_EXTENSIONS = (".toml", ".tml")
|
||||||
|
YAML_EXTENSIONS = (".yaml", ".yml")
|
||||||
|
JSON_EXTENSIONS = (".json",)
|
||||||
|
|
||||||
|
ALL_EXTENSIONS = (
|
||||||
|
INI_EXTENSIONS + TOML_EXTENSIONS + YAML_EXTENSIONS + JSON_EXTENSIONS
|
||||||
|
) # noqa
|
||||||
|
|
||||||
|
EXTERNAL_LOADERS = {
|
||||||
|
"ENV": "dynaconf.loaders.env_loader",
|
||||||
|
"VAULT": "dynaconf.loaders.vault_loader",
|
||||||
|
"REDIS": "dynaconf.loaders.redis_loader",
|
||||||
|
}
|
||||||
|
|
||||||
|
DJANGO_PATCH = """
|
||||||
|
# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
|
||||||
|
# Read more at https://www.dynaconf.com/django/
|
||||||
|
import dynaconf # noqa
|
||||||
|
settings = dynaconf.DjangoDynaconf(__name__) # noqa
|
||||||
|
# HERE ENDS DYNACONF EXTENSION LOAD (No more code below this line)
|
||||||
|
"""
|
||||||
|
|
||||||
|
INSTANCE_TEMPLATE = """
|
||||||
|
from dynaconf import Dynaconf
|
||||||
|
|
||||||
|
settings = Dynaconf(
|
||||||
|
envvar_prefix="DYNACONF",
|
||||||
|
settings_files={settings_files},
|
||||||
|
)
|
||||||
|
|
||||||
|
# `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`.
|
||||||
|
# `settings_files` = Load these files in the order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXTS = (
|
||||||
|
"py",
|
||||||
|
"toml",
|
||||||
|
"tml",
|
||||||
|
"yaml",
|
||||||
|
"yml",
|
||||||
|
"ini",
|
||||||
|
"conf",
|
||||||
|
"properties",
|
||||||
|
"json",
|
||||||
|
)
|
||||||
|
DEFAULT_SETTINGS_FILES = [f"settings.{ext}" for ext in EXTS] + [
|
||||||
|
f".secrets.{ext}" for ext in EXTS
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dynaconf.contrib.django_dynaconf_v2 import DjangoDynaconf # noqa
|
||||||
|
from dynaconf.contrib.flask_dynaconf import DynaconfConfig # noqa
|
||||||
|
from dynaconf.contrib.flask_dynaconf import FlaskDynaconf # noqa
|
|
@ -0,0 +1,142 @@
|
||||||
|
"""Dynaconf django extension
|
||||||
|
|
||||||
|
In the `django_project/settings.py` put at the very bottom of the file:
|
||||||
|
|
||||||
|
# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
|
||||||
|
# Read more at https://www.dynaconf.com/django/
|
||||||
|
import dynaconf # noqa
|
||||||
|
settings = dynaconf.DjangoDynaconf(__name__) # noqa
|
||||||
|
# HERE ENDS DYNACONF EXTENSION LOAD (No more code below this line)
|
||||||
|
|
||||||
|
Now in the root of your Django project
|
||||||
|
(the same folder where manage.py is located)
|
||||||
|
|
||||||
|
Put your config files `settings.{py|yaml|toml|ini|json}`
|
||||||
|
and or `.secrets.{py|yaml|toml|ini|json}`
|
||||||
|
|
||||||
|
On your projects root folder now you can start as::
|
||||||
|
|
||||||
|
DJANGO_DEBUG='false' \
|
||||||
|
DJANGO_ALLOWED_HOSTS='["localhost"]' \
|
||||||
|
python manage.py runserver
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import dynaconf
|
||||||
|
|
||||||
|
try: # pragma: no cover
|
||||||
|
from django import conf
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
django_installed = True
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
django_installed = False
|
||||||
|
|
||||||
|
|
||||||
|
def load(django_settings_module_name=None, **kwargs): # pragma: no cover
|
||||||
|
if not django_installed:
|
||||||
|
raise RuntimeError(
|
||||||
|
"To use this extension django must be installed "
|
||||||
|
"install it with: pip install django"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
django_settings_module = sys.modules[django_settings_module_name]
|
||||||
|
except KeyError:
|
||||||
|
django_settings_module = sys.modules[
|
||||||
|
os.environ["DJANGO_SETTINGS_MODULE"]
|
||||||
|
]
|
||||||
|
|
||||||
|
settings_module_name = django_settings_module.__name__
|
||||||
|
settings_file = os.path.abspath(django_settings_module.__file__)
|
||||||
|
_root_path = os.path.dirname(settings_file)
|
||||||
|
|
||||||
|
# 1) Create the lazy settings object reusing settings_module consts
|
||||||
|
options = {
|
||||||
|
k.upper(): v
|
||||||
|
for k, v in django_settings_module.__dict__.items()
|
||||||
|
if k.isupper()
|
||||||
|
}
|
||||||
|
options.update(kwargs)
|
||||||
|
options.setdefault(
|
||||||
|
"SKIP_FILES_FOR_DYNACONF", [settings_file, "dynaconf_merge"]
|
||||||
|
)
|
||||||
|
options.setdefault("ROOT_PATH_FOR_DYNACONF", _root_path)
|
||||||
|
options.setdefault("ENVVAR_PREFIX_FOR_DYNACONF", "DJANGO")
|
||||||
|
options.setdefault("ENV_SWITCHER_FOR_DYNACONF", "DJANGO_ENV")
|
||||||
|
options.setdefault("ENVIRONMENTS_FOR_DYNACONF", True)
|
||||||
|
options.setdefault("load_dotenv", True)
|
||||||
|
options.setdefault(
|
||||||
|
"default_settings_paths", dynaconf.DEFAULT_SETTINGS_FILES
|
||||||
|
)
|
||||||
|
|
||||||
|
class UserSettingsHolder(dynaconf.LazySettings):
|
||||||
|
_django_override = True
|
||||||
|
|
||||||
|
lazy_settings = dynaconf.LazySettings(**options)
|
||||||
|
dynaconf.settings = lazy_settings # rebind the settings
|
||||||
|
|
||||||
|
# 2) Set all settings back to django_settings_module for 'django check'
|
||||||
|
lazy_settings.populate_obj(django_settings_module)
|
||||||
|
|
||||||
|
# 3) Bind `settings` and `DYNACONF`
|
||||||
|
setattr(django_settings_module, "settings", lazy_settings)
|
||||||
|
setattr(django_settings_module, "DYNACONF", lazy_settings)
|
||||||
|
|
||||||
|
# 4) keep django original settings
|
||||||
|
dj = {}
|
||||||
|
for key in dir(django_settings):
|
||||||
|
if (
|
||||||
|
key.isupper()
|
||||||
|
and (key != "SETTINGS_MODULE")
|
||||||
|
and key not in lazy_settings.store
|
||||||
|
):
|
||||||
|
dj[key] = getattr(django_settings, key, None)
|
||||||
|
dj["ORIGINAL_SETTINGS_MODULE"] = django_settings.SETTINGS_MODULE
|
||||||
|
|
||||||
|
lazy_settings.update(dj)
|
||||||
|
|
||||||
|
# Allow dynaconf_hooks to be in the same folder as the django.settings
|
||||||
|
dynaconf.loaders.execute_hooks(
|
||||||
|
"post",
|
||||||
|
lazy_settings,
|
||||||
|
lazy_settings.current_env,
|
||||||
|
modules=[settings_module_name],
|
||||||
|
files=[settings_file],
|
||||||
|
)
|
||||||
|
lazy_settings._loaded_py_modules.insert(0, settings_module_name)
|
||||||
|
|
||||||
|
# 5) Patch django.conf.settings
|
||||||
|
class Wrapper:
|
||||||
|
|
||||||
|
# lazy_settings = conf.settings.lazy_settings
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
if name == "settings":
|
||||||
|
return lazy_settings
|
||||||
|
if name == "UserSettingsHolder":
|
||||||
|
return UserSettingsHolder
|
||||||
|
return getattr(conf, name)
|
||||||
|
|
||||||
|
# This implementation is recommended by Guido Van Rossum
|
||||||
|
# https://mail.python.org/pipermail/python-ideas/2012-May/014969.html
|
||||||
|
sys.modules["django.conf"] = Wrapper()
|
||||||
|
|
||||||
|
# 6) Enable standalone scripts to use Dynaconf
|
||||||
|
# This is for when `django.conf.settings` is imported directly
|
||||||
|
# on external `scripts` (out of Django's lifetime)
|
||||||
|
for stack_item in reversed(inspect.stack()):
|
||||||
|
if isinstance(
|
||||||
|
stack_item.frame.f_globals.get("settings"), conf.LazySettings
|
||||||
|
):
|
||||||
|
stack_item.frame.f_globals["settings"] = lazy_settings
|
||||||
|
|
||||||
|
return lazy_settings
|
||||||
|
|
||||||
|
|
||||||
|
# syntax sugar
|
||||||
|
DjangoDynaconf = load # noqa
|
|
@ -0,0 +1,230 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
from collections import ChainMap
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask.config import Config
|
||||||
|
|
||||||
|
flask_installed = True
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
flask_installed = False
|
||||||
|
Config = object
|
||||||
|
|
||||||
|
|
||||||
|
import dynaconf
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
|
||||||
|
class FlaskDynaconf:
|
||||||
|
"""The arguments are.
|
||||||
|
app = The created app
|
||||||
|
dynaconf_args = Extra args to be passed to Dynaconf (validator for example)
|
||||||
|
|
||||||
|
All other values are stored as config vars specially::
|
||||||
|
|
||||||
|
ENVVAR_PREFIX_FOR_DYNACONF = env prefix for your envvars to be loaded
|
||||||
|
example:
|
||||||
|
if you set to `MYSITE` then
|
||||||
|
export MYSITE_SQL_PORT='@int 5445'
|
||||||
|
|
||||||
|
with that exported to env you access using:
|
||||||
|
app.config.SQL_PORT
|
||||||
|
app.config.get('SQL_PORT')
|
||||||
|
app.config.get('sql_port')
|
||||||
|
# get is case insensitive
|
||||||
|
app.config['SQL_PORT']
|
||||||
|
|
||||||
|
Dynaconf uses `@int, @bool, @float, @json` to cast
|
||||||
|
env vars
|
||||||
|
|
||||||
|
SETTINGS_FILE_FOR_DYNACONF = The name of the module or file to use as
|
||||||
|
default to load settings. If nothing is
|
||||||
|
passed it will be `settings.*` or value
|
||||||
|
found in `ENVVAR_FOR_DYNACONF`
|
||||||
|
Dynaconf supports
|
||||||
|
.py, .yml, .toml, ini, json
|
||||||
|
|
||||||
|
ATTENTION: Take a look at `settings.yml` and `.secrets.yml` to know the
|
||||||
|
required settings format.
|
||||||
|
|
||||||
|
Settings load order in Dynaconf:
|
||||||
|
|
||||||
|
- Load all defaults and Flask defaults
|
||||||
|
- Load all passed variables when applying FlaskDynaconf
|
||||||
|
- Update with data in settings files
|
||||||
|
- Update with data in environment vars `ENVVAR_FOR_DYNACONF_`
|
||||||
|
|
||||||
|
|
||||||
|
TOML files are very useful to have `envd` settings, lets say,
|
||||||
|
`production` and `development`.
|
||||||
|
|
||||||
|
You can also achieve the same using multiple `.py` files naming as
|
||||||
|
`settings.py`, `production_settings.py` and `development_settings.py`
|
||||||
|
(see examples/validator)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
FlaskDynaconf(
|
||||||
|
app,
|
||||||
|
ENV='MYSITE',
|
||||||
|
SETTINGS_FILE='settings.yml',
|
||||||
|
EXTRA_VALUE='You can add additional config vars here'
|
||||||
|
)
|
||||||
|
|
||||||
|
Take a look at examples/flask in Dynaconf repository
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app=None,
|
||||||
|
instance_relative_config=False,
|
||||||
|
dynaconf_instance=None,
|
||||||
|
extensions_list=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""kwargs holds initial dynaconf configuration"""
|
||||||
|
if not flask_installed: # pragma: no cover
|
||||||
|
raise RuntimeError(
|
||||||
|
"To use this extension Flask must be installed "
|
||||||
|
"install it with: pip install flask"
|
||||||
|
)
|
||||||
|
self.kwargs = {k.upper(): v for k, v in kwargs.items()}
|
||||||
|
kwargs.setdefault("ENVVAR_PREFIX", "FLASK")
|
||||||
|
env_prefix = f"{kwargs['ENVVAR_PREFIX']}_ENV" # FLASK_ENV
|
||||||
|
kwargs.setdefault("ENV_SWITCHER", env_prefix)
|
||||||
|
kwargs.setdefault("ENVIRONMENTS", True)
|
||||||
|
kwargs.setdefault("load_dotenv", True)
|
||||||
|
kwargs.setdefault(
|
||||||
|
"default_settings_paths", dynaconf.DEFAULT_SETTINGS_FILES
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dynaconf_instance = dynaconf_instance
|
||||||
|
self.instance_relative_config = instance_relative_config
|
||||||
|
self.extensions_list = extensions_list
|
||||||
|
if app:
|
||||||
|
self.init_app(app, **kwargs)
|
||||||
|
|
||||||
|
def init_app(self, app, **kwargs):
|
||||||
|
"""kwargs holds initial dynaconf configuration"""
|
||||||
|
self.kwargs.update(kwargs)
|
||||||
|
self.settings = self.dynaconf_instance or dynaconf.LazySettings(
|
||||||
|
**self.kwargs
|
||||||
|
)
|
||||||
|
dynaconf.settings = self.settings # rebind customized settings
|
||||||
|
app.config = self.make_config(app)
|
||||||
|
app.dynaconf = self.settings
|
||||||
|
|
||||||
|
if self.extensions_list:
|
||||||
|
if not isinstance(self.extensions_list, str):
|
||||||
|
self.extensions_list = "EXTENSIONS"
|
||||||
|
app.config.load_extensions(self.extensions_list)
|
||||||
|
|
||||||
|
def make_config(self, app):
|
||||||
|
root_path = app.root_path
|
||||||
|
if self.instance_relative_config: # pragma: no cover
|
||||||
|
root_path = app.instance_path
|
||||||
|
if self.dynaconf_instance:
|
||||||
|
self.settings.update(self.kwargs)
|
||||||
|
return DynaconfConfig(
|
||||||
|
root_path=root_path,
|
||||||
|
defaults=app.config,
|
||||||
|
_settings=self.settings,
|
||||||
|
_app=app,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DynaconfConfig(Config):
|
||||||
|
"""
|
||||||
|
Replacement for flask.config_class that responds as a Dynaconf instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, _settings, _app, *args, **kwargs):
|
||||||
|
"""perform the initial load"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Bring Dynaconf instance value to Flask Config
|
||||||
|
Config.update(self, _settings.store)
|
||||||
|
|
||||||
|
self._settings = _settings
|
||||||
|
self._app = _app
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return hasattr(self, item)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
try:
|
||||||
|
return self._settings[key]
|
||||||
|
except KeyError:
|
||||||
|
return Config.__getitem__(self, key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""
|
||||||
|
Allows app.config['key'] = 'foo'
|
||||||
|
"""
|
||||||
|
return self._settings.__setitem__(key, value)
|
||||||
|
|
||||||
|
def _chain_map(self):
|
||||||
|
return ChainMap(self._settings, dict(dict.items(self)))
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self._chain_map().keys()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return self._chain_map().values()
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self._chain_map().items()
|
||||||
|
|
||||||
|
def setdefault(self, key, value=None):
|
||||||
|
return self._chain_map().setdefault(key, value)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._chain_map().__iter__()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
"""
|
||||||
|
First try to get value from dynaconf then from Flask Config
|
||||||
|
"""
|
||||||
|
with suppress(AttributeError):
|
||||||
|
return getattr(self._settings, name)
|
||||||
|
|
||||||
|
with suppress(KeyError):
|
||||||
|
return self[name]
|
||||||
|
|
||||||
|
raise AttributeError(
|
||||||
|
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __call__(self, name, *args, **kwargs):
|
||||||
|
return self.get(name, *args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
"""Gets config from dynaconf variables
|
||||||
|
if variables does not exists in dynaconf try getting from
|
||||||
|
`app.config` to support runtime settings."""
|
||||||
|
return self._settings.get(key, Config.get(self, key, default))
|
||||||
|
|
||||||
|
def load_extensions(self, key="EXTENSIONS", app=None):
|
||||||
|
"""Loads flask extensions dynamically."""
|
||||||
|
app = app or self._app
|
||||||
|
extensions = app.config.get(key)
|
||||||
|
if not extensions:
|
||||||
|
warnings.warn(
|
||||||
|
f"Settings is missing {key} to load Flask Extensions",
|
||||||
|
RuntimeWarning,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for object_reference in app.config[key]:
|
||||||
|
# add a placeholder `name` to create a valid entry point
|
||||||
|
entry_point_spec = f"__name = {object_reference}"
|
||||||
|
# parse the entry point specification
|
||||||
|
entry_point = pkg_resources.EntryPoint.parse(entry_point_spec)
|
||||||
|
# dynamically resolve the entry point
|
||||||
|
initializer = entry_point.resolve()
|
||||||
|
# Invoke extension initializer
|
||||||
|
initializer(app)
|
|
@ -0,0 +1,252 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from dynaconf.utils import RENAMED_VARS
|
||||||
|
from dynaconf.utils import upperfy
|
||||||
|
from dynaconf.utils import warn_deprecations
|
||||||
|
from dynaconf.utils.files import find_file
|
||||||
|
from dynaconf.utils.parse_conf import parse_conf_data
|
||||||
|
from dynaconf.vendor.dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
def try_renamed(key, value, older_key, current_key):
|
||||||
|
if value is None:
|
||||||
|
if key == current_key:
|
||||||
|
if older_key in os.environ:
|
||||||
|
warnings.warn(
|
||||||
|
f"{older_key} is deprecated please use {current_key}",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
value = os.environ[older_key]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get(key, default=None):
|
||||||
|
value = os.environ.get(upperfy(key))
|
||||||
|
|
||||||
|
# compatibility with renamed variables
|
||||||
|
for old, new in RENAMED_VARS.items():
|
||||||
|
value = try_renamed(key, value, old, new)
|
||||||
|
|
||||||
|
return (
|
||||||
|
parse_conf_data(value, tomlfy=True, box_settings={})
|
||||||
|
if value is not None
|
||||||
|
else default
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def start_dotenv(obj=None, root_path=None):
|
||||||
|
# load_from_dotenv_if_installed
|
||||||
|
obj = obj or {}
|
||||||
|
_find_file = getattr(obj, "find_file", find_file)
|
||||||
|
root_path = (
|
||||||
|
root_path
|
||||||
|
or getattr(obj, "_root_path", None)
|
||||||
|
or get("ROOT_PATH_FOR_DYNACONF")
|
||||||
|
)
|
||||||
|
|
||||||
|
dotenv_path = (
|
||||||
|
obj.get("DOTENV_PATH_FOR_DYNACONF")
|
||||||
|
or get("DOTENV_PATH_FOR_DYNACONF")
|
||||||
|
or _find_file(".env", project_root=root_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
load_dotenv(
|
||||||
|
dotenv_path,
|
||||||
|
verbose=obj.get("DOTENV_VERBOSE_FOR_DYNACONF", False),
|
||||||
|
override=obj.get("DOTENV_OVERRIDE_FOR_DYNACONF", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
warn_deprecations(os.environ)
|
||||||
|
|
||||||
|
|
||||||
|
def reload(load_dotenv=None, *args, **kwargs):
|
||||||
|
if load_dotenv:
|
||||||
|
start_dotenv(*args, **kwargs)
|
||||||
|
importlib.reload(sys.modules[__name__])
|
||||||
|
|
||||||
|
|
||||||
|
# default proj root
|
||||||
|
# pragma: no cover
|
||||||
|
ROOT_PATH_FOR_DYNACONF = get("ROOT_PATH_FOR_DYNACONF", None)
|
||||||
|
|
||||||
|
# Default settings file
|
||||||
|
SETTINGS_FILE_FOR_DYNACONF = get("SETTINGS_FILE_FOR_DYNACONF", [])
|
||||||
|
|
||||||
|
# MISPELLS `FILES` when/if it happens
|
||||||
|
mispelled_files = get("SETTINGS_FILES_FOR_DYNACONF", None)
|
||||||
|
if not SETTINGS_FILE_FOR_DYNACONF and mispelled_files is not None:
|
||||||
|
SETTINGS_FILE_FOR_DYNACONF = mispelled_files
|
||||||
|
|
||||||
|
# # ENV SETTINGS
|
||||||
|
# # In dynaconf 1.0.0 `NAMESPACE` got renamed to `ENV`
|
||||||
|
|
||||||
|
|
||||||
|
# If provided environments will be loaded separately
|
||||||
|
ENVIRONMENTS_FOR_DYNACONF = get("ENVIRONMENTS_FOR_DYNACONF", False)
|
||||||
|
MAIN_ENV_FOR_DYNACONF = get("MAIN_ENV_FOR_DYNACONF", "MAIN")
|
||||||
|
|
||||||
|
# If False dynaconf will allow access to first level settings only in upper
|
||||||
|
LOWERCASE_READ_FOR_DYNACONF = get("LOWERCASE_READ_FOR_DYNACONF", True)
|
||||||
|
|
||||||
|
# The environment variable to switch current env
|
||||||
|
ENV_SWITCHER_FOR_DYNACONF = get(
|
||||||
|
"ENV_SWITCHER_FOR_DYNACONF", "ENV_FOR_DYNACONF"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The current env by default is DEVELOPMENT
|
||||||
|
# to switch is needed to `export ENV_FOR_DYNACONF=PRODUCTION`
|
||||||
|
# or put that value in .env file
|
||||||
|
# this value is used only when reading files like .toml|yaml|ini|json
|
||||||
|
ENV_FOR_DYNACONF = get(ENV_SWITCHER_FOR_DYNACONF, "DEVELOPMENT")
|
||||||
|
|
||||||
|
# This variable exists to support `from_env` method
|
||||||
|
FORCE_ENV_FOR_DYNACONF = get("FORCE_ENV_FOR_DYNACONF", None)
|
||||||
|
|
||||||
|
# Default values is taken from DEFAULT pseudo env
|
||||||
|
# this value is used only when reading files like .toml|yaml|ini|json
|
||||||
|
DEFAULT_ENV_FOR_DYNACONF = get("DEFAULT_ENV_FOR_DYNACONF", "DEFAULT")
|
||||||
|
|
||||||
|
# Global values are taken from DYNACONF env used for exported envvars
|
||||||
|
# Values here overwrites all other envs
|
||||||
|
# This namespace is used for files and also envvars
|
||||||
|
ENVVAR_PREFIX_FOR_DYNACONF = get("ENVVAR_PREFIX_FOR_DYNACONF", "DYNACONF")
|
||||||
|
|
||||||
|
# By default all environment variables (filtered by `envvar_prefix`) will
|
||||||
|
# be pulled into settings space. In case some of them are polluting the space,
|
||||||
|
# setting this flag to `True` will change this behaviour.
|
||||||
|
# Only "known" variables will be considered -- that is variables defined before
|
||||||
|
# in settings files (or includes/preloads).
|
||||||
|
IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = get(
|
||||||
|
"IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF", False
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTO_CAST_FOR_DYNACONF = get("AUTO_CAST_FOR_DYNACONF", True)
|
||||||
|
|
||||||
|
# The default encoding to open settings files
|
||||||
|
ENCODING_FOR_DYNACONF = get("ENCODING_FOR_DYNACONF", "utf-8")
|
||||||
|
|
||||||
|
# Merge objects on load
|
||||||
|
MERGE_ENABLED_FOR_DYNACONF = get("MERGE_ENABLED_FOR_DYNACONF", False)
|
||||||
|
|
||||||
|
# Lookup keys considering dots as separators
|
||||||
|
DOTTED_LOOKUP_FOR_DYNACONF = get("DOTTED_LOOKUP_FOR_DYNACONF", True)
|
||||||
|
|
||||||
|
# BY default `__` is the separator for nested env vars
|
||||||
|
# export `DYNACONF__DATABASE__server=server.com`
|
||||||
|
# export `DYNACONF__DATABASE__PORT=6666`
|
||||||
|
# Should result in settings.DATABASE == {'server': 'server.com', 'PORT': 6666}
|
||||||
|
# To disable it one can set `NESTED_SEPARATOR_FOR_DYNACONF=false`
|
||||||
|
NESTED_SEPARATOR_FOR_DYNACONF = get("NESTED_SEPARATOR_FOR_DYNACONF", "__")
|
||||||
|
|
||||||
|
# The env var specifying settings module
|
||||||
|
ENVVAR_FOR_DYNACONF = get("ENVVAR_FOR_DYNACONF", "SETTINGS_FILE_FOR_DYNACONF")
|
||||||
|
|
||||||
|
# Default values for redis configs
|
||||||
|
default_redis = {
|
||||||
|
"host": get("REDIS_HOST_FOR_DYNACONF", "localhost"),
|
||||||
|
"port": int(get("REDIS_PORT_FOR_DYNACONF", 6379)),
|
||||||
|
"db": int(get("REDIS_DB_FOR_DYNACONF", 0)),
|
||||||
|
"decode_responses": get("REDIS_DECODE_FOR_DYNACONF", True),
|
||||||
|
"username": get("REDIS_USERNAME_FOR_DYNACONF", None),
|
||||||
|
"password": get("REDIS_PASSWORD_FOR_DYNACONF", None),
|
||||||
|
}
|
||||||
|
REDIS_FOR_DYNACONF = get("REDIS_FOR_DYNACONF", default_redis)
|
||||||
|
REDIS_ENABLED_FOR_DYNACONF = get("REDIS_ENABLED_FOR_DYNACONF", False)
|
||||||
|
|
||||||
|
# Hashicorp Vault Project
|
||||||
|
vault_scheme = get("VAULT_SCHEME_FOR_DYNACONF", "http")
|
||||||
|
vault_host = get("VAULT_HOST_FOR_DYNACONF", "localhost")
|
||||||
|
vault_port = get("VAULT_PORT_FOR_DYNACONF", "8200")
|
||||||
|
default_vault = {
|
||||||
|
"url": get(
|
||||||
|
"VAULT_URL_FOR_DYNACONF", f"{vault_scheme}://{vault_host}:{vault_port}"
|
||||||
|
),
|
||||||
|
"token": get("VAULT_TOKEN_FOR_DYNACONF", None),
|
||||||
|
"cert": get("VAULT_CERT_FOR_DYNACONF", None),
|
||||||
|
"verify": get("VAULT_VERIFY_FOR_DYNACONF", None),
|
||||||
|
"timeout": get("VAULT_TIMEOUT_FOR_DYNACONF", None),
|
||||||
|
"proxies": get("VAULT_PROXIES_FOR_DYNACONF", None),
|
||||||
|
"allow_redirects": get("VAULT_ALLOW_REDIRECTS_FOR_DYNACONF", None),
|
||||||
|
"namespace": get("VAULT_NAMESPACE_FOR_DYNACONF", None),
|
||||||
|
}
|
||||||
|
VAULT_FOR_DYNACONF = get("VAULT_FOR_DYNACONF", default_vault)
|
||||||
|
VAULT_ENABLED_FOR_DYNACONF = get("VAULT_ENABLED_FOR_DYNACONF", False)
|
||||||
|
VAULT_PATH_FOR_DYNACONF = get("VAULT_PATH_FOR_DYNACONF", "dynaconf")
|
||||||
|
VAULT_MOUNT_POINT_FOR_DYNACONF = get(
|
||||||
|
"VAULT_MOUNT_POINT_FOR_DYNACONF", "secret"
|
||||||
|
)
|
||||||
|
VAULT_ROOT_TOKEN_FOR_DYNACONF = get("VAULT_ROOT_TOKEN_FOR_DYNACONF", None)
|
||||||
|
VAULT_KV_VERSION_FOR_DYNACONF = get("VAULT_KV_VERSION_FOR_DYNACONF", 1)
|
||||||
|
VAULT_AUTH_WITH_IAM_FOR_DYNACONF = get(
|
||||||
|
"VAULT_AUTH_WITH_IAM_FOR_DYNACONF", False
|
||||||
|
)
|
||||||
|
VAULT_AUTH_ROLE_FOR_DYNACONF = get("VAULT_AUTH_ROLE_FOR_DYNACONF", None)
|
||||||
|
VAULT_ROLE_ID_FOR_DYNACONF = get("VAULT_ROLE_ID_FOR_DYNACONF", None)
|
||||||
|
VAULT_SECRET_ID_FOR_DYNACONF = get("VAULT_SECRET_ID_FOR_DYNACONF", None)
|
||||||
|
|
||||||
|
# Only core loaders defined on this list will be invoked
|
||||||
|
core_loaders = ["YAML", "TOML", "INI", "JSON", "PY"]
|
||||||
|
CORE_LOADERS_FOR_DYNACONF = get("CORE_LOADERS_FOR_DYNACONF", core_loaders)
|
||||||
|
|
||||||
|
# External Loaders to read vars from different data stores
|
||||||
|
default_loaders = [
|
||||||
|
"dynaconf.loaders.env_loader",
|
||||||
|
# 'dynaconf.loaders.redis_loader'
|
||||||
|
# 'dynaconf.loaders.vault_loader'
|
||||||
|
]
|
||||||
|
LOADERS_FOR_DYNACONF = get("LOADERS_FOR_DYNACONF", default_loaders)
|
||||||
|
|
||||||
|
# Errors in loaders should be silenced?
|
||||||
|
SILENT_ERRORS_FOR_DYNACONF = get("SILENT_ERRORS_FOR_DYNACONF", True)
|
||||||
|
|
||||||
|
# always fresh variables
|
||||||
|
FRESH_VARS_FOR_DYNACONF = get("FRESH_VARS_FOR_DYNACONF", [])
|
||||||
|
|
||||||
|
DOTENV_PATH_FOR_DYNACONF = get("DOTENV_PATH_FOR_DYNACONF", None)
|
||||||
|
DOTENV_VERBOSE_FOR_DYNACONF = get("DOTENV_VERBOSE_FOR_DYNACONF", False)
|
||||||
|
DOTENV_OVERRIDE_FOR_DYNACONF = get("DOTENV_OVERRIDE_FOR_DYNACONF", False)
|
||||||
|
|
||||||
|
# Currently this is only used by cli. INSTANCE_FOR_DYNACONF specifies python
|
||||||
|
# dotted path to custom LazySettings instance. Last dotted path item should be
|
||||||
|
# instance of LazySettings.
|
||||||
|
INSTANCE_FOR_DYNACONF = get("INSTANCE_FOR_DYNACONF", None)
|
||||||
|
|
||||||
|
# https://msg.pyyaml.org/load
|
||||||
|
YAML_LOADER_FOR_DYNACONF = get("YAML_LOADER_FOR_DYNACONF", "safe_load")
|
||||||
|
|
||||||
|
# Use commentjson? https://commentjson.readthedocs.io/en/latest/
|
||||||
|
COMMENTJSON_ENABLED_FOR_DYNACONF = get(
|
||||||
|
"COMMENTJSON_ENABLED_FOR_DYNACONF", False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extra file, or list of files where to look for secrets
|
||||||
|
# useful for CI environment like jenkins
|
||||||
|
# where you can export this variable pointing to a local
|
||||||
|
# absolute path of the secrets file.
|
||||||
|
SECRETS_FOR_DYNACONF = get("SECRETS_FOR_DYNACONF", None)
|
||||||
|
|
||||||
|
# To include extra paths based on envvar
|
||||||
|
INCLUDES_FOR_DYNACONF = get("INCLUDES_FOR_DYNACONF", [])
|
||||||
|
|
||||||
|
# To pre-load extra paths based on envvar
|
||||||
|
PRELOAD_FOR_DYNACONF = get("PRELOAD_FOR_DYNACONF", [])
|
||||||
|
|
||||||
|
# Files to skip if found on search tree
|
||||||
|
SKIP_FILES_FOR_DYNACONF = get("SKIP_FILES_FOR_DYNACONF", [])
|
||||||
|
|
||||||
|
# YAML reads empty vars as None, should dynaconf apply validator defaults?
|
||||||
|
# this is set to None, then evaluated on base.Settings.setdefault
|
||||||
|
# possible values are True/False
|
||||||
|
APPLY_DEFAULT_ON_NONE_FOR_DYNACONF = get(
|
||||||
|
"APPLY_DEFAULT_ON_NONE_FOR_DYNACONF", None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Backwards compatibility with renamed variables
|
||||||
|
for old, new in RENAMED_VARS.items():
|
||||||
|
setattr(sys.modules[__name__], old, locals()[new])
|
|
@ -0,0 +1,277 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dynaconf import constants as ct
|
||||||
|
from dynaconf import default_settings
|
||||||
|
from dynaconf.loaders import ini_loader
|
||||||
|
from dynaconf.loaders import json_loader
|
||||||
|
from dynaconf.loaders import py_loader
|
||||||
|
from dynaconf.loaders import toml_loader
|
||||||
|
from dynaconf.loaders import yaml_loader
|
||||||
|
from dynaconf.utils import deduplicate
|
||||||
|
from dynaconf.utils import ensure_a_list
|
||||||
|
from dynaconf.utils.boxing import DynaBox
|
||||||
|
from dynaconf.utils.files import get_local_filename
|
||||||
|
from dynaconf.utils.parse_conf import false_values
|
||||||
|
|
||||||
|
|
||||||
|
def default_loader(obj, defaults=None):
|
||||||
|
"""Loads default settings and check if there are overridings
|
||||||
|
exported as environment variables"""
|
||||||
|
defaults = defaults or {}
|
||||||
|
default_settings_values = {
|
||||||
|
key: value
|
||||||
|
for key, value in default_settings.__dict__.items() # noqa
|
||||||
|
if key.isupper()
|
||||||
|
}
|
||||||
|
|
||||||
|
all_keys = deduplicate(
|
||||||
|
list(defaults.keys()) + list(default_settings_values.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in all_keys:
|
||||||
|
if not obj.exists(key):
|
||||||
|
value = defaults.get(key, default_settings_values.get(key))
|
||||||
|
obj.set(key, value)
|
||||||
|
|
||||||
|
# start dotenv to get default env vars from there
|
||||||
|
# check overrides in env vars
|
||||||
|
if obj.get("load_dotenv") is True:
|
||||||
|
default_settings.start_dotenv(obj)
|
||||||
|
|
||||||
|
# Deal with cases where a custom ENV_SWITCHER_IS_PROVIDED
|
||||||
|
# Example: Flask and Django Extensions
|
||||||
|
env_switcher = defaults.get(
|
||||||
|
"ENV_SWITCHER_FOR_DYNACONF", "ENV_FOR_DYNACONF"
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in all_keys:
|
||||||
|
if key not in default_settings_values.keys():
|
||||||
|
continue
|
||||||
|
|
||||||
|
env_value = obj.get_environ(
|
||||||
|
env_switcher if key == "ENV_FOR_DYNACONF" else key,
|
||||||
|
default="_not_found",
|
||||||
|
)
|
||||||
|
|
||||||
|
if env_value != "_not_found":
|
||||||
|
obj.set(key, env_value, tomlfy=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_hook_module(hook, hook_module, obj, key=None):
|
||||||
|
"""Run the hook function from the settings obj.
|
||||||
|
|
||||||
|
given a hook name, a hook_module and a settings object
|
||||||
|
load the function and execute if found.
|
||||||
|
"""
|
||||||
|
if hook in obj._loaded_hooks.get(hook_module.__file__, {}):
|
||||||
|
# already loaded
|
||||||
|
return
|
||||||
|
|
||||||
|
if hook_module and getattr(hook_module, "_error", False):
|
||||||
|
if not isinstance(hook_module._error, FileNotFoundError):
|
||||||
|
raise hook_module._error
|
||||||
|
|
||||||
|
hook_func = getattr(hook_module, hook, None)
|
||||||
|
if hook_func:
|
||||||
|
hook_dict = hook_func(obj.dynaconf.clone())
|
||||||
|
if hook_dict:
|
||||||
|
merge = hook_dict.pop(
|
||||||
|
"dynaconf_merge", hook_dict.pop("DYNACONF_MERGE", False)
|
||||||
|
)
|
||||||
|
if key and key in hook_dict:
|
||||||
|
obj.set(key, hook_dict[key], tomlfy=False, merge=merge)
|
||||||
|
elif not key:
|
||||||
|
obj.update(hook_dict, tomlfy=False, merge=merge)
|
||||||
|
obj._loaded_hooks[hook_module.__file__][hook] = hook_dict
|
||||||
|
|
||||||
|
|
||||||
|
def execute_hooks(
|
||||||
|
hook, obj, env=None, silent=True, key=None, modules=None, files=None
|
||||||
|
):
|
||||||
|
"""Execute dynaconf_hooks from module or filepath."""
|
||||||
|
if hook not in ["post"]:
|
||||||
|
raise ValueError(f"hook {hook} not supported yet.")
|
||||||
|
|
||||||
|
# try to load hooks using python module __name__
|
||||||
|
modules = modules or obj._loaded_py_modules
|
||||||
|
for loaded_module in modules:
|
||||||
|
hook_module_name = ".".join(
|
||||||
|
loaded_module.split(".")[:-1] + ["dynaconf_hooks"]
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
hook_module = importlib.import_module(hook_module_name)
|
||||||
|
except (ImportError, TypeError):
|
||||||
|
# There was no hook on the same path as a python module
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
_run_hook_module(
|
||||||
|
hook=hook,
|
||||||
|
hook_module=hook_module,
|
||||||
|
obj=obj,
|
||||||
|
key=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to load from python filename path
|
||||||
|
files = files or obj._loaded_files
|
||||||
|
for loaded_file in files:
|
||||||
|
hook_file = os.path.join(
|
||||||
|
os.path.dirname(loaded_file), "dynaconf_hooks.py"
|
||||||
|
)
|
||||||
|
hook_module = py_loader.import_from_filename(
|
||||||
|
obj, hook_file, silent=silent
|
||||||
|
)
|
||||||
|
if not hook_module:
|
||||||
|
# There was no hook on the same path as a python file
|
||||||
|
continue
|
||||||
|
_run_hook_module(
|
||||||
|
hook=hook,
|
||||||
|
hook_module=hook_module,
|
||||||
|
obj=obj,
|
||||||
|
key=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def settings_loader(
|
||||||
|
obj, settings_module=None, env=None, silent=True, key=None, filename=None
|
||||||
|
):
|
||||||
|
"""Loads from defined settings module
|
||||||
|
|
||||||
|
:param obj: A dynaconf instance
|
||||||
|
:param settings_module: A path or a list of paths e.g settings.toml
|
||||||
|
:param env: Env to look for data defaults: development
|
||||||
|
:param silent: Boolean to raise loading errors
|
||||||
|
:param key: Load a single key if provided
|
||||||
|
:param filename: optional filename to override the settings_module
|
||||||
|
"""
|
||||||
|
if filename is None:
|
||||||
|
settings_module = settings_module or obj.settings_module
|
||||||
|
if not settings_module: # pragma: no cover
|
||||||
|
return
|
||||||
|
files = ensure_a_list(settings_module)
|
||||||
|
else:
|
||||||
|
files = ensure_a_list(filename)
|
||||||
|
|
||||||
|
files.extend(ensure_a_list(obj.get("SECRETS_FOR_DYNACONF", None)))
|
||||||
|
|
||||||
|
found_files = []
|
||||||
|
modules_names = []
|
||||||
|
for item in files:
|
||||||
|
item = str(item) # Ensure str in case of LocalPath/Path is passed.
|
||||||
|
if item.endswith(ct.ALL_EXTENSIONS + (".py",)):
|
||||||
|
p_root = obj._root_path or (
|
||||||
|
os.path.dirname(found_files[0]) if found_files else None
|
||||||
|
)
|
||||||
|
found = obj.find_file(item, project_root=p_root)
|
||||||
|
if found:
|
||||||
|
found_files.append(found)
|
||||||
|
else:
|
||||||
|
# a bare python module name w/o extension
|
||||||
|
modules_names.append(item)
|
||||||
|
|
||||||
|
enabled_core_loaders = [
|
||||||
|
item.upper() for item in obj.get("CORE_LOADERS_FOR_DYNACONF") or []
|
||||||
|
]
|
||||||
|
|
||||||
|
# add `.local.` to found_files list to search for local files.
|
||||||
|
found_files.extend(
|
||||||
|
[
|
||||||
|
get_local_filename(item)
|
||||||
|
for item in found_files
|
||||||
|
if ".local." not in str(item)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for mod_file in modules_names + found_files:
|
||||||
|
# can be set to multiple files settings.py,settings.yaml,...
|
||||||
|
|
||||||
|
# Cascade all loaders
|
||||||
|
loaders = [
|
||||||
|
{"ext": ct.YAML_EXTENSIONS, "name": "YAML", "loader": yaml_loader},
|
||||||
|
{"ext": ct.TOML_EXTENSIONS, "name": "TOML", "loader": toml_loader},
|
||||||
|
{"ext": ct.INI_EXTENSIONS, "name": "INI", "loader": ini_loader},
|
||||||
|
{"ext": ct.JSON_EXTENSIONS, "name": "JSON", "loader": json_loader},
|
||||||
|
]
|
||||||
|
|
||||||
|
for loader in loaders:
|
||||||
|
if loader["name"] not in enabled_core_loaders:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mod_file.endswith(loader["ext"]):
|
||||||
|
loader["loader"].load(
|
||||||
|
obj, filename=mod_file, env=env, silent=silent, key=key
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mod_file.endswith(ct.ALL_EXTENSIONS):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "PY" not in enabled_core_loaders:
|
||||||
|
# pyloader is disabled
|
||||||
|
continue
|
||||||
|
|
||||||
|
# must be Python file or module
|
||||||
|
# load from default defined module settings.py or .secrets.py if exists
|
||||||
|
py_loader.load(obj, mod_file, key=key)
|
||||||
|
|
||||||
|
# load from the current env e.g: development_settings.py
|
||||||
|
env = env or obj.current_env
|
||||||
|
if mod_file.endswith(".py"):
|
||||||
|
if ".secrets.py" == mod_file:
|
||||||
|
tmpl = ".{0}_{1}{2}"
|
||||||
|
mod_file = "secrets.py"
|
||||||
|
else:
|
||||||
|
tmpl = "{0}_{1}{2}"
|
||||||
|
|
||||||
|
dirname = os.path.dirname(mod_file)
|
||||||
|
filename, extension = os.path.splitext(os.path.basename(mod_file))
|
||||||
|
new_filename = tmpl.format(env.lower(), filename, extension)
|
||||||
|
env_mod_file = os.path.join(dirname, new_filename)
|
||||||
|
global_filename = tmpl.format("global", filename, extension)
|
||||||
|
global_mod_file = os.path.join(dirname, global_filename)
|
||||||
|
else:
|
||||||
|
env_mod_file = f"{env.lower()}_{mod_file}"
|
||||||
|
global_mod_file = f"global_{mod_file}"
|
||||||
|
|
||||||
|
py_loader.load(
|
||||||
|
obj,
|
||||||
|
env_mod_file,
|
||||||
|
identifier=f"py_{env.upper()}",
|
||||||
|
silent=True,
|
||||||
|
key=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
# load from global_settings.py
|
||||||
|
py_loader.load(
|
||||||
|
obj, global_mod_file, identifier="py_global", silent=True, key=key
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_external_loaders(obj):
|
||||||
|
"""Enable external service loaders like `VAULT_` and `REDIS_`
|
||||||
|
looks forenv variables like `REDIS_ENABLED_FOR_DYNACONF`
|
||||||
|
"""
|
||||||
|
for name, loader in ct.EXTERNAL_LOADERS.items():
|
||||||
|
enabled = getattr(obj, f"{name.upper()}_ENABLED_FOR_DYNACONF", False)
|
||||||
|
if (
|
||||||
|
enabled
|
||||||
|
and enabled not in false_values
|
||||||
|
and loader not in obj.LOADERS_FOR_DYNACONF
|
||||||
|
): # noqa
|
||||||
|
obj.LOADERS_FOR_DYNACONF.insert(0, loader)
|
||||||
|
|
||||||
|
|
||||||
|
def write(filename, data, env=None):
|
||||||
|
"""Writes `data` to `filename` infers format by file extension."""
|
||||||
|
loader_name = f"{filename.rpartition('.')[-1]}_loader"
|
||||||
|
loader = globals().get(loader_name)
|
||||||
|
if not loader:
|
||||||
|
raise OSError(f"{loader_name} cannot be found.")
|
||||||
|
|
||||||
|
data = DynaBox(data, box_settings={}).to_dict()
|
||||||
|
if loader is not py_loader and env and env not in data:
|
||||||
|
data = {env: data}
|
||||||
|
|
||||||
|
loader.write(filename, data, merge=False)
|
|
@ -0,0 +1,195 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from dynaconf.utils import build_env_list
|
||||||
|
from dynaconf.utils import ensure_a_list
|
||||||
|
from dynaconf.utils import upperfy
|
||||||
|
|
||||||
|
|
||||||
|
class BaseLoader:
|
||||||
|
"""Base loader for dynaconf source files.
|
||||||
|
|
||||||
|
:param obj: {[LazySettings]} -- [Dynaconf settings]
|
||||||
|
:param env: {[string]} -- [the current env to be loaded defaults to
|
||||||
|
[development]]
|
||||||
|
:param identifier: {[string]} -- [identifier ini, yaml, json, py, toml]
|
||||||
|
:param extensions: {[list]} -- [List of extensions with dots ['.a', '.b']]
|
||||||
|
:param file_reader: {[callable]} -- [reads file return dict]
|
||||||
|
:param string_reader: {[callable]} -- [reads string return dict]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
obj,
|
||||||
|
env,
|
||||||
|
identifier,
|
||||||
|
extensions,
|
||||||
|
file_reader,
|
||||||
|
string_reader,
|
||||||
|
opener_params=None,
|
||||||
|
):
|
||||||
|
"""Instantiates a loader for different sources"""
|
||||||
|
self.obj = obj
|
||||||
|
self.env = env or obj.current_env
|
||||||
|
self.identifier = identifier
|
||||||
|
self.extensions = extensions
|
||||||
|
self.file_reader = file_reader
|
||||||
|
self.string_reader = string_reader
|
||||||
|
self.opener_params = opener_params or {
|
||||||
|
"mode": "r",
|
||||||
|
"encoding": obj.get("ENCODING_FOR_DYNACONF", "utf-8"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def warn_not_installed(obj, identifier): # pragma: no cover
|
||||||
|
if identifier not in obj._not_installed_warnings:
|
||||||
|
warnings.warn(
|
||||||
|
f"{identifier} support is not installed in your environment. "
|
||||||
|
f"`pip install dynaconf[{identifier}]`"
|
||||||
|
)
|
||||||
|
obj._not_installed_warnings.append(identifier)
|
||||||
|
|
||||||
|
def load(self, filename=None, key=None, silent=True):
|
||||||
|
"""
|
||||||
|
Reads and loads in to `self.obj` a single key or all keys from source
|
||||||
|
|
||||||
|
:param filename: Optional filename to load
|
||||||
|
:param key: if provided load a single key
|
||||||
|
:param silent: if load errors should be silenced
|
||||||
|
"""
|
||||||
|
|
||||||
|
filename = filename or self.obj.get(self.identifier.upper())
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(filename, (list, tuple)):
|
||||||
|
split_files = ensure_a_list(filename)
|
||||||
|
if all([f.endswith(self.extensions) for f in split_files]): # noqa
|
||||||
|
files = split_files # it is a ['file.ext', ...]
|
||||||
|
else: # it is a single config as string
|
||||||
|
files = [filename]
|
||||||
|
else: # it is already a list/tuple
|
||||||
|
files = filename
|
||||||
|
|
||||||
|
source_data = self.get_source_data(files)
|
||||||
|
|
||||||
|
if self.obj.get("ENVIRONMENTS_FOR_DYNACONF") is False:
|
||||||
|
self._envless_load(source_data, silent, key)
|
||||||
|
else:
|
||||||
|
self._load_all_envs(source_data, silent, key)
|
||||||
|
|
||||||
|
def get_source_data(self, files):
|
||||||
|
"""Reads each file and returns source data for each file
|
||||||
|
{"path/to/file.ext": {"key": "value"}}
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
for source_file in files:
|
||||||
|
if source_file.endswith(self.extensions):
|
||||||
|
try:
|
||||||
|
with open(source_file, **self.opener_params) as open_file:
|
||||||
|
content = self.file_reader(open_file)
|
||||||
|
self.obj._loaded_files.append(source_file)
|
||||||
|
if content:
|
||||||
|
data[source_file] = content
|
||||||
|
except OSError as e:
|
||||||
|
if ".local." not in source_file:
|
||||||
|
warnings.warn(
|
||||||
|
f"{self.identifier}_loader: {source_file} "
|
||||||
|
f":{str(e)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# for tests it is possible to pass string
|
||||||
|
content = self.string_reader(source_file)
|
||||||
|
if content:
|
||||||
|
data[source_file] = content
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _envless_load(self, source_data, silent=True, key=None):
|
||||||
|
"""Load all the keys from each file without env separation"""
|
||||||
|
for file_data in source_data.values():
|
||||||
|
self._set_data_to_obj(
|
||||||
|
file_data,
|
||||||
|
self.identifier,
|
||||||
|
key=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _load_all_envs(self, source_data, silent=True, key=None):
|
||||||
|
"""Load configs from files separating by each environment"""
|
||||||
|
|
||||||
|
for file_data in source_data.values():
|
||||||
|
|
||||||
|
# env name is checked in lower
|
||||||
|
file_data = {k.lower(): value for k, value in file_data.items()}
|
||||||
|
|
||||||
|
# is there a `dynaconf_merge` on top level of file?
|
||||||
|
file_merge = file_data.get("dynaconf_merge")
|
||||||
|
|
||||||
|
# is there a flag disabling dotted lookup on file?
|
||||||
|
file_dotted_lookup = file_data.get("dynaconf_dotted_lookup")
|
||||||
|
|
||||||
|
for env in build_env_list(self.obj, self.env):
|
||||||
|
env = env.lower() # lower for better comparison
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = file_data[env] or {}
|
||||||
|
except KeyError:
|
||||||
|
if silent:
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._set_data_to_obj(
|
||||||
|
data,
|
||||||
|
f"{self.identifier}_{env}",
|
||||||
|
file_merge,
|
||||||
|
key,
|
||||||
|
file_dotted_lookup=file_dotted_lookup,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_data_to_obj(
|
||||||
|
self,
|
||||||
|
data,
|
||||||
|
identifier,
|
||||||
|
file_merge=None,
|
||||||
|
key=False,
|
||||||
|
file_dotted_lookup=None,
|
||||||
|
):
|
||||||
|
"""Calls settings.set to add the keys"""
|
||||||
|
# data 1st level keys should be transformed to upper case.
|
||||||
|
data = {upperfy(k): v for k, v in data.items()}
|
||||||
|
if key:
|
||||||
|
key = upperfy(key)
|
||||||
|
|
||||||
|
if self.obj.filter_strategy:
|
||||||
|
data = self.obj.filter_strategy(data)
|
||||||
|
|
||||||
|
# is there a `dynaconf_merge` inside an `[env]`?
|
||||||
|
file_merge = file_merge or data.pop("DYNACONF_MERGE", False)
|
||||||
|
|
||||||
|
# If not passed or passed as None,
|
||||||
|
# look for inner [env] value, or default settings.
|
||||||
|
if file_dotted_lookup is None:
|
||||||
|
file_dotted_lookup = data.pop(
|
||||||
|
"DYNACONF_DOTTED_LOOKUP",
|
||||||
|
self.obj.get("DOTTED_LOOKUP_FOR_DYNACONF"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
self.obj.update(
|
||||||
|
data,
|
||||||
|
loader_identifier=identifier,
|
||||||
|
merge=file_merge,
|
||||||
|
dotted_lookup=file_dotted_lookup,
|
||||||
|
)
|
||||||
|
elif key in data:
|
||||||
|
self.obj.set(
|
||||||
|
key,
|
||||||
|
data.get(key),
|
||||||
|
loader_identifier=identifier,
|
||||||
|
merge=file_merge,
|
||||||
|
dotted_lookup=file_dotted_lookup,
|
||||||
|
)
|
|
@ -0,0 +1,108 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
from dynaconf.utils import missing
|
||||||
|
from dynaconf.utils import upperfy
|
||||||
|
from dynaconf.utils.parse_conf import parse_conf_data
|
||||||
|
|
||||||
|
DOTENV_IMPORTED = False
|
||||||
|
try:
|
||||||
|
from dynaconf.vendor.dotenv import cli as dotenv_cli
|
||||||
|
|
||||||
|
DOTENV_IMPORTED = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
IDENTIFIER = "env"
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, env=None, silent=True, key=None):
|
||||||
|
"""Loads envvars with prefixes:
|
||||||
|
|
||||||
|
`DYNACONF_` (default global) or `$(ENVVAR_PREFIX_FOR_DYNACONF)_`
|
||||||
|
"""
|
||||||
|
global_prefix = obj.get("ENVVAR_PREFIX_FOR_DYNACONF")
|
||||||
|
if global_prefix is False or global_prefix.upper() != "DYNACONF":
|
||||||
|
load_from_env(obj, "DYNACONF", key, silent, IDENTIFIER + "_global")
|
||||||
|
|
||||||
|
# Load the global env if exists and overwrite everything
|
||||||
|
load_from_env(obj, global_prefix, key, silent, IDENTIFIER + "_global")
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_env(
|
||||||
|
obj,
|
||||||
|
prefix=False,
|
||||||
|
key=None,
|
||||||
|
silent=False,
|
||||||
|
identifier=IDENTIFIER,
|
||||||
|
env=False, # backwards compatibility bc renamed param
|
||||||
|
):
|
||||||
|
if prefix is False and env is not False:
|
||||||
|
prefix = env
|
||||||
|
|
||||||
|
env_ = ""
|
||||||
|
if prefix is not False:
|
||||||
|
if not isinstance(prefix, str):
|
||||||
|
raise TypeError("`prefix/env` must be str or False")
|
||||||
|
|
||||||
|
prefix = prefix.upper()
|
||||||
|
env_ = f"{prefix}_"
|
||||||
|
|
||||||
|
# Load a single environment variable explicitly.
|
||||||
|
if key:
|
||||||
|
key = upperfy(key)
|
||||||
|
value = environ.get(f"{env_}{key}")
|
||||||
|
if value:
|
||||||
|
try: # obj is a Settings
|
||||||
|
obj.set(key, value, loader_identifier=identifier, tomlfy=True)
|
||||||
|
except AttributeError: # obj is a dict
|
||||||
|
obj[key] = parse_conf_data(
|
||||||
|
value, tomlfy=True, box_settings=obj
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load environment variables in bulk (when matching).
|
||||||
|
else:
|
||||||
|
# Only known variables should be loaded from environment?
|
||||||
|
ignore_unknown = obj.get("IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF")
|
||||||
|
|
||||||
|
trim_len = len(env_)
|
||||||
|
data = {
|
||||||
|
key[trim_len:]: parse_conf_data(
|
||||||
|
data, tomlfy=True, box_settings=obj
|
||||||
|
)
|
||||||
|
for key, data in environ.items()
|
||||||
|
if key.startswith(env_)
|
||||||
|
and not (
|
||||||
|
# Ignore environment variables that haven't been
|
||||||
|
# pre-defined in settings space.
|
||||||
|
ignore_unknown
|
||||||
|
and obj.get(key[trim_len:], default=missing) is missing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
# Update the settings space based on gathered data from environment.
|
||||||
|
if data:
|
||||||
|
filter_strategy = obj.get("FILTER_STRATEGY")
|
||||||
|
if filter_strategy:
|
||||||
|
data = filter_strategy(data)
|
||||||
|
obj.update(data, loader_identifier=identifier)
|
||||||
|
|
||||||
|
|
||||||
|
def write(settings_path, settings_data, **kwargs):
|
||||||
|
"""Write data to .env file"""
|
||||||
|
if not DOTENV_IMPORTED:
|
||||||
|
return
|
||||||
|
for key, value in settings_data.items():
|
||||||
|
quote_mode = (
|
||||||
|
isinstance(value, str)
|
||||||
|
and (value.startswith("'") or value.startswith('"'))
|
||||||
|
) or isinstance(value, (list, dict))
|
||||||
|
dotenv_cli.set_key(
|
||||||
|
str(settings_path),
|
||||||
|
key,
|
||||||
|
str(value),
|
||||||
|
quote_mode="always" if quote_mode else "none",
|
||||||
|
)
|
|
@ -0,0 +1,62 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dynaconf import default_settings
|
||||||
|
from dynaconf.constants import INI_EXTENSIONS
|
||||||
|
from dynaconf.loaders.base import BaseLoader
|
||||||
|
from dynaconf.utils import object_merge
|
||||||
|
|
||||||
|
try:
|
||||||
|
from configobj import ConfigObj
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
ConfigObj = None
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||||
|
"""
|
||||||
|
Reads and loads in to "obj" a single key or all keys from source file.
|
||||||
|
|
||||||
|
:param obj: the settings instance
|
||||||
|
:param env: settings current env default='development'
|
||||||
|
:param silent: if errors should raise
|
||||||
|
:param key: if defined load a single key, else load all in env
|
||||||
|
:param filename: Optional custom filename to load
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if ConfigObj is None: # pragma: no cover
|
||||||
|
BaseLoader.warn_not_installed(obj, "ini")
|
||||||
|
return
|
||||||
|
|
||||||
|
loader = BaseLoader(
|
||||||
|
obj=obj,
|
||||||
|
env=env,
|
||||||
|
identifier="ini",
|
||||||
|
extensions=INI_EXTENSIONS,
|
||||||
|
file_reader=lambda fileobj: ConfigObj(fileobj).dict(),
|
||||||
|
string_reader=lambda strobj: ConfigObj(strobj.split("\n")).dict(),
|
||||||
|
)
|
||||||
|
loader.load(
|
||||||
|
filename=filename,
|
||||||
|
key=key,
|
||||||
|
silent=silent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write(settings_path, settings_data, merge=True):
|
||||||
|
"""Write data to a settings file.
|
||||||
|
|
||||||
|
:param settings_path: the filepath
|
||||||
|
:param settings_data: a dictionary with data
|
||||||
|
:param merge: boolean if existing file should be merged with new data
|
||||||
|
"""
|
||||||
|
settings_path = Path(settings_path)
|
||||||
|
if settings_path.exists() and merge: # pragma: no cover
|
||||||
|
with open(
|
||||||
|
str(settings_path), encoding=default_settings.ENCODING_FOR_DYNACONF
|
||||||
|
) as open_file:
|
||||||
|
object_merge(ConfigObj(open_file).dict(), settings_data)
|
||||||
|
new = ConfigObj()
|
||||||
|
new.update(settings_data)
|
||||||
|
new.write(open(str(settings_path), "bw"))
|
|
@ -0,0 +1,80 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dynaconf import default_settings
|
||||||
|
from dynaconf.constants import JSON_EXTENSIONS
|
||||||
|
from dynaconf.loaders.base import BaseLoader
|
||||||
|
from dynaconf.utils import object_merge
|
||||||
|
from dynaconf.utils.parse_conf import try_to_encode
|
||||||
|
|
||||||
|
try: # pragma: no cover
|
||||||
|
import commentjson
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
commentjson = None
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||||
|
"""
|
||||||
|
Reads and loads in to "obj" a single key or all keys from source file.
|
||||||
|
|
||||||
|
:param obj: the settings instance
|
||||||
|
:param env: settings current env default='development'
|
||||||
|
:param silent: if errors should raise
|
||||||
|
:param key: if defined load a single key, else load all in env
|
||||||
|
:param filename: Optional custom filename to load
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
obj.get("COMMENTJSON_ENABLED_FOR_DYNACONF") and commentjson
|
||||||
|
): # pragma: no cover # noqa
|
||||||
|
file_reader = commentjson.load
|
||||||
|
string_reader = commentjson.loads
|
||||||
|
else:
|
||||||
|
file_reader = json.load
|
||||||
|
string_reader = json.loads
|
||||||
|
|
||||||
|
loader = BaseLoader(
|
||||||
|
obj=obj,
|
||||||
|
env=env,
|
||||||
|
identifier="json",
|
||||||
|
extensions=JSON_EXTENSIONS,
|
||||||
|
file_reader=file_reader,
|
||||||
|
string_reader=string_reader,
|
||||||
|
)
|
||||||
|
loader.load(
|
||||||
|
filename=filename,
|
||||||
|
key=key,
|
||||||
|
silent=silent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write(settings_path, settings_data, merge=True):
|
||||||
|
"""Write data to a settings file.
|
||||||
|
|
||||||
|
:param settings_path: the filepath
|
||||||
|
:param settings_data: a dictionary with data
|
||||||
|
:param merge: boolean if existing file should be merged with new data
|
||||||
|
"""
|
||||||
|
settings_path = Path(settings_path)
|
||||||
|
if settings_path.exists() and merge: # pragma: no cover
|
||||||
|
with open(
|
||||||
|
str(settings_path), encoding=default_settings.ENCODING_FOR_DYNACONF
|
||||||
|
) as open_file:
|
||||||
|
object_merge(json.load(open_file), settings_data)
|
||||||
|
|
||||||
|
with open(
|
||||||
|
str(settings_path),
|
||||||
|
"w",
|
||||||
|
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||||
|
) as open_file:
|
||||||
|
json.dump(settings_data, open_file, cls=DynaconfEncoder)
|
||||||
|
|
||||||
|
|
||||||
|
class DynaconfEncoder(json.JSONEncoder):
|
||||||
|
"""Transform Dynaconf custom types instances to json representation"""
|
||||||
|
|
||||||
|
def default(self, o):
|
||||||
|
return try_to_encode(o, callback=super().default)
|
|
@ -0,0 +1,148 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import io
|
||||||
|
import types
|
||||||
|
from contextlib import suppress
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dynaconf import default_settings
|
||||||
|
from dynaconf.utils import DynaconfDict
|
||||||
|
from dynaconf.utils import object_merge
|
||||||
|
from dynaconf.utils import upperfy
|
||||||
|
from dynaconf.utils.files import find_file
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, settings_module, identifier="py", silent=False, key=None):
|
||||||
|
"""Tries to import a python module"""
|
||||||
|
mod, loaded_from = get_module(obj, settings_module, silent)
|
||||||
|
if not (mod and loaded_from):
|
||||||
|
return
|
||||||
|
load_from_python_object(obj, mod, settings_module, key, identifier)
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_python_object(
|
||||||
|
obj, mod, settings_module, key=None, identifier=None
|
||||||
|
):
|
||||||
|
file_merge = getattr(mod, "dynaconf_merge", False) or getattr(
|
||||||
|
mod, "DYNACONF_MERGE", False
|
||||||
|
)
|
||||||
|
for setting in dir(mod):
|
||||||
|
# A setting var in a Python file should start with upper case
|
||||||
|
# valid: A_value=1, ABC_value=3 A_BBB__default=1
|
||||||
|
# invalid: a_value=1, MyValue=3
|
||||||
|
# This is to avoid loading functions, classes and built-ins
|
||||||
|
if setting.split("__")[0].isupper():
|
||||||
|
if key is None or key == setting:
|
||||||
|
setting_value = getattr(mod, setting)
|
||||||
|
obj.set(
|
||||||
|
setting,
|
||||||
|
setting_value,
|
||||||
|
loader_identifier=identifier,
|
||||||
|
merge=file_merge,
|
||||||
|
)
|
||||||
|
|
||||||
|
obj._loaded_py_modules.append(mod.__name__)
|
||||||
|
obj._loaded_files.append(mod.__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def try_to_load_from_py_module_name(
|
||||||
|
obj, name, key=None, identifier="py", silent=False
|
||||||
|
):
|
||||||
|
"""Try to load module by its string name.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
obj {LAzySettings} -- Dynaconf settings instance
|
||||||
|
name {str} -- Name of the module e.g: foo.bar.zaz
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
key {str} -- Single key to be loaded (default: {None})
|
||||||
|
identifier {str} -- Name of identifier to store (default: 'py')
|
||||||
|
silent {bool} -- Weather to raise or silence exceptions.
|
||||||
|
"""
|
||||||
|
ctx = suppress(ImportError, TypeError) if silent else suppress()
|
||||||
|
|
||||||
|
with ctx:
|
||||||
|
mod = importlib.import_module(str(name))
|
||||||
|
load_from_python_object(obj, mod, name, key, identifier)
|
||||||
|
return True # loaded ok!
|
||||||
|
# if it reaches this point that means exception occurred, module not found.
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_module(obj, filename, silent=False):
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(filename)
|
||||||
|
loaded_from = "module"
|
||||||
|
mod.is_error = False
|
||||||
|
except (ImportError, TypeError):
|
||||||
|
mod = import_from_filename(obj, filename, silent=silent)
|
||||||
|
if mod and not mod._is_error:
|
||||||
|
loaded_from = "filename"
|
||||||
|
else:
|
||||||
|
# it is important to return None in case of not loaded
|
||||||
|
loaded_from = None
|
||||||
|
return mod, loaded_from
|
||||||
|
|
||||||
|
|
||||||
|
def import_from_filename(obj, filename, silent=False): # pragma: no cover
|
||||||
|
"""If settings_module is a filename path import it."""
|
||||||
|
if filename in [item.filename for item in inspect.stack()]:
|
||||||
|
raise ImportError(
|
||||||
|
"Looks like you are loading dynaconf "
|
||||||
|
f"from inside the {filename} file and then it is trying "
|
||||||
|
"to load itself entering in a circular reference "
|
||||||
|
"problem. To solve it you have to "
|
||||||
|
"invoke your program from another root folder "
|
||||||
|
"or rename your program file."
|
||||||
|
)
|
||||||
|
|
||||||
|
_find_file = getattr(obj, "find_file", find_file)
|
||||||
|
if not filename.endswith(".py"):
|
||||||
|
filename = f"{filename}.py"
|
||||||
|
|
||||||
|
if filename in default_settings.SETTINGS_FILE_FOR_DYNACONF:
|
||||||
|
silent = True
|
||||||
|
mod = types.ModuleType(filename.rstrip(".py"))
|
||||||
|
mod.__file__ = filename
|
||||||
|
mod._is_error = False
|
||||||
|
mod._error = None
|
||||||
|
try:
|
||||||
|
with open(
|
||||||
|
_find_file(filename),
|
||||||
|
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||||
|
) as config_file:
|
||||||
|
exec(compile(config_file.read(), filename, "exec"), mod.__dict__)
|
||||||
|
except OSError as e:
|
||||||
|
e.strerror = (
|
||||||
|
f"py_loader: error loading file " f"({e.strerror} {filename})\n"
|
||||||
|
)
|
||||||
|
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||||
|
return
|
||||||
|
mod._is_error = True
|
||||||
|
mod._error = e
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def write(settings_path, settings_data, merge=True):
|
||||||
|
"""Write data to a settings file.
|
||||||
|
|
||||||
|
:param settings_path: the filepath
|
||||||
|
:param settings_data: a dictionary with data
|
||||||
|
:param merge: boolean if existing file should be merged with new data
|
||||||
|
"""
|
||||||
|
settings_path = Path(settings_path)
|
||||||
|
if settings_path.exists() and merge: # pragma: no cover
|
||||||
|
existing = DynaconfDict()
|
||||||
|
load(existing, str(settings_path))
|
||||||
|
object_merge(existing, settings_data)
|
||||||
|
with open(
|
||||||
|
str(settings_path),
|
||||||
|
"w",
|
||||||
|
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||||
|
) as f:
|
||||||
|
f.writelines(
|
||||||
|
[f"{upperfy(k)} = {repr(v)}\n" for k, v in settings_data.items()]
|
||||||
|
)
|
|
@ -0,0 +1,108 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dynaconf.utils import build_env_list
|
||||||
|
from dynaconf.utils import upperfy
|
||||||
|
from dynaconf.utils.parse_conf import parse_conf_data
|
||||||
|
from dynaconf.utils.parse_conf import unparse_conf_data
|
||||||
|
|
||||||
|
try:
|
||||||
|
from redis import StrictRedis
|
||||||
|
except ImportError:
|
||||||
|
StrictRedis = None
|
||||||
|
|
||||||
|
IDENTIFIER = "redis"
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, env=None, silent=True, key=None):
|
||||||
|
"""Reads and loads in to "settings" a single key or all keys from redis
|
||||||
|
|
||||||
|
:param obj: the settings instance
|
||||||
|
:param env: settings env default='DYNACONF'
|
||||||
|
:param silent: if errors should raise
|
||||||
|
:param key: if defined load a single key, else load all in env
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if StrictRedis is None:
|
||||||
|
raise ImportError(
|
||||||
|
"redis package is not installed in your environment. "
|
||||||
|
"`pip install dynaconf[redis]` or disable the redis loader with "
|
||||||
|
"export REDIS_ENABLED_FOR_DYNACONF=false"
|
||||||
|
)
|
||||||
|
|
||||||
|
redis = StrictRedis(**obj.get("REDIS_FOR_DYNACONF"))
|
||||||
|
prefix = obj.get("ENVVAR_PREFIX_FOR_DYNACONF")
|
||||||
|
# prefix is added to env_list to keep backwards compatibility
|
||||||
|
env_list = [prefix] + build_env_list(obj, env or obj.current_env)
|
||||||
|
for env_name in env_list:
|
||||||
|
holder = f"{prefix.upper()}_{env_name.upper()}"
|
||||||
|
try:
|
||||||
|
if key:
|
||||||
|
value = redis.hget(holder.upper(), key)
|
||||||
|
if value:
|
||||||
|
parsed_value = parse_conf_data(
|
||||||
|
value, tomlfy=True, box_settings=obj
|
||||||
|
)
|
||||||
|
if parsed_value:
|
||||||
|
obj.set(key, parsed_value)
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
key: parse_conf_data(value, tomlfy=True, box_settings=obj)
|
||||||
|
for key, value in redis.hgetall(holder.upper()).items()
|
||||||
|
}
|
||||||
|
if data:
|
||||||
|
obj.update(data, loader_identifier=IDENTIFIER)
|
||||||
|
except Exception:
|
||||||
|
if silent:
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def write(obj, data=None, **kwargs):
|
||||||
|
"""Write a value in to loader source
|
||||||
|
|
||||||
|
:param obj: settings object
|
||||||
|
:param data: vars to be stored
|
||||||
|
:param kwargs: vars to be stored
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if obj.REDIS_ENABLED_FOR_DYNACONF is False:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Redis is not configured \n"
|
||||||
|
"export REDIS_ENABLED_FOR_DYNACONF=true\n"
|
||||||
|
"and configure the REDIS_*_FOR_DYNACONF variables"
|
||||||
|
)
|
||||||
|
client = StrictRedis(**obj.REDIS_FOR_DYNACONF)
|
||||||
|
holder = obj.get("ENVVAR_PREFIX_FOR_DYNACONF").upper()
|
||||||
|
# add env to holder
|
||||||
|
holder = f"{holder}_{obj.current_env.upper()}"
|
||||||
|
|
||||||
|
data = data or {}
|
||||||
|
data.update(kwargs)
|
||||||
|
if not data:
|
||||||
|
raise AttributeError("Data must be provided")
|
||||||
|
redis_data = {
|
||||||
|
upperfy(key): unparse_conf_data(value) for key, value in data.items()
|
||||||
|
}
|
||||||
|
client.hmset(holder.upper(), redis_data)
|
||||||
|
load(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(obj, key=None):
|
||||||
|
"""
|
||||||
|
Delete a single key if specified, or all env if key is none
|
||||||
|
:param obj: settings object
|
||||||
|
:param key: key to delete from store location
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
client = StrictRedis(**obj.REDIS_FOR_DYNACONF)
|
||||||
|
holder = obj.get("ENVVAR_PREFIX_FOR_DYNACONF").upper()
|
||||||
|
# add env to holder
|
||||||
|
holder = f"{holder}_{obj.current_env.upper()}"
|
||||||
|
|
||||||
|
if key:
|
||||||
|
client.hdel(holder.upper(), upperfy(key))
|
||||||
|
obj.unset(key)
|
||||||
|
else:
|
||||||
|
keys = client.hkeys(holder.upper())
|
||||||
|
client.delete(holder.upper())
|
||||||
|
obj.unset_all(keys)
|
|
@ -0,0 +1,122 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dynaconf import default_settings
|
||||||
|
from dynaconf.constants import TOML_EXTENSIONS
|
||||||
|
from dynaconf.loaders.base import BaseLoader
|
||||||
|
from dynaconf.utils import object_merge
|
||||||
|
from dynaconf.vendor import toml # Backwards compatibility with uiri/toml
|
||||||
|
from dynaconf.vendor import tomllib # New tomllib stdlib on py3.11
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||||
|
"""
|
||||||
|
Reads and loads in to "obj" a single key or all keys from source file.
|
||||||
|
|
||||||
|
:param obj: the settings instance
|
||||||
|
:param env: settings current env default='development'
|
||||||
|
:param silent: if errors should raise
|
||||||
|
:param key: if defined load a single key, else load all in env
|
||||||
|
:param filename: Optional custom filename to load
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
loader = BaseLoader(
|
||||||
|
obj=obj,
|
||||||
|
env=env,
|
||||||
|
identifier="toml",
|
||||||
|
extensions=TOML_EXTENSIONS,
|
||||||
|
file_reader=tomllib.load,
|
||||||
|
string_reader=tomllib.loads,
|
||||||
|
opener_params={"mode": "rb"},
|
||||||
|
)
|
||||||
|
loader.load(
|
||||||
|
filename=filename,
|
||||||
|
key=key,
|
||||||
|
silent=silent,
|
||||||
|
)
|
||||||
|
except UnicodeDecodeError: # pragma: no cover
|
||||||
|
"""
|
||||||
|
NOTE: Compat functions exists to keep backwards compatibility with
|
||||||
|
the new tomllib library. The old library was called `toml` and
|
||||||
|
the new one is called `tomllib`.
|
||||||
|
|
||||||
|
The old lib uiri/toml allowed unicode characters and re-added files
|
||||||
|
as string.
|
||||||
|
|
||||||
|
The new tomllib (stdlib) does not allow unicode characters, only
|
||||||
|
utf-8 encoded, and read files as binary.
|
||||||
|
|
||||||
|
NOTE: In dynaconf 4.0.0 we will drop support for the old library
|
||||||
|
removing the compat functions and calling directly the new lib.
|
||||||
|
"""
|
||||||
|
loader = BaseLoader(
|
||||||
|
obj=obj,
|
||||||
|
env=env,
|
||||||
|
identifier="toml",
|
||||||
|
extensions=TOML_EXTENSIONS,
|
||||||
|
file_reader=toml.load,
|
||||||
|
string_reader=toml.loads,
|
||||||
|
)
|
||||||
|
loader.load(
|
||||||
|
filename=filename,
|
||||||
|
key=key,
|
||||||
|
silent=silent,
|
||||||
|
)
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"TOML files should have only UTF-8 encoded characters. "
|
||||||
|
"starting on 4.0.0 dynaconf will stop allowing invalid chars.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write(settings_path, settings_data, merge=True):
|
||||||
|
"""Write data to a settings file.
|
||||||
|
|
||||||
|
:param settings_path: the filepath
|
||||||
|
:param settings_data: a dictionary with data
|
||||||
|
:param merge: boolean if existing file should be merged with new data
|
||||||
|
"""
|
||||||
|
settings_path = Path(settings_path)
|
||||||
|
if settings_path.exists() and merge: # pragma: no cover
|
||||||
|
try: # tomllib first
|
||||||
|
with open(str(settings_path), "rb") as open_file:
|
||||||
|
object_merge(tomllib.load(open_file), settings_data)
|
||||||
|
except UnicodeDecodeError: # pragma: no cover
|
||||||
|
# uiri/toml fallback (TBR on 4.0.0)
|
||||||
|
with open(
|
||||||
|
str(settings_path),
|
||||||
|
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||||
|
) as open_file:
|
||||||
|
object_merge(toml.load(open_file), settings_data)
|
||||||
|
|
||||||
|
try: # tomllib first
|
||||||
|
with open(str(settings_path), "wb") as open_file:
|
||||||
|
tomllib.dump(encode_nulls(settings_data), open_file)
|
||||||
|
except UnicodeEncodeError: # pragma: no cover
|
||||||
|
# uiri/toml fallback (TBR on 4.0.0)
|
||||||
|
with open(
|
||||||
|
str(settings_path),
|
||||||
|
"w",
|
||||||
|
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||||
|
) as open_file:
|
||||||
|
toml.dump(encode_nulls(settings_data), open_file)
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"TOML files should have only UTF-8 encoded characters. "
|
||||||
|
"starting on 4.0.0 dynaconf will stop allowing invalid chars.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_nulls(data):
|
||||||
|
"""TOML does not support `None` so this function transforms to '@none '."""
|
||||||
|
if data is None:
|
||||||
|
return "@none "
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return {key: encode_nulls(value) for key, value in data.items()}
|
||||||
|
elif isinstance(data, (list, tuple)):
|
||||||
|
return [encode_nulls(item) for item in data]
|
||||||
|
return data
|
|
@ -0,0 +1,186 @@
|
||||||
|
# docker run -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
|
||||||
|
# pip install hvac
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dynaconf.utils import build_env_list
|
||||||
|
from dynaconf.utils.parse_conf import parse_conf_data
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto3
|
||||||
|
except ImportError:
|
||||||
|
boto3 = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hvac import Client
|
||||||
|
from hvac.exceptions import InvalidPath
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"vault package is not installed in your environment. "
|
||||||
|
"`pip install dynaconf[vault]` or disable the vault loader with "
|
||||||
|
"export VAULT_ENABLED_FOR_DYNACONF=false"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
IDENTIFIER = "vault"
|
||||||
|
|
||||||
|
|
||||||
|
# backwards compatibility
|
||||||
|
_get_env_list = build_env_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_client(obj):
|
||||||
|
client = Client(
|
||||||
|
**{k: v for k, v in obj.VAULT_FOR_DYNACONF.items() if v is not None}
|
||||||
|
)
|
||||||
|
if obj.VAULT_ROLE_ID_FOR_DYNACONF is not None:
|
||||||
|
client.auth.approle.login(
|
||||||
|
role_id=obj.VAULT_ROLE_ID_FOR_DYNACONF,
|
||||||
|
secret_id=obj.get("VAULT_SECRET_ID_FOR_DYNACONF"),
|
||||||
|
)
|
||||||
|
elif obj.VAULT_ROOT_TOKEN_FOR_DYNACONF is not None:
|
||||||
|
client.token = obj.VAULT_ROOT_TOKEN_FOR_DYNACONF
|
||||||
|
elif obj.VAULT_AUTH_WITH_IAM_FOR_DYNACONF:
|
||||||
|
if boto3 is None:
|
||||||
|
raise ImportError(
|
||||||
|
"boto3 package is not installed in your environment. "
|
||||||
|
"`pip install boto3` or disable the VAULT_AUTH_WITH_IAM"
|
||||||
|
)
|
||||||
|
|
||||||
|
session = boto3.Session()
|
||||||
|
credentials = session.get_credentials()
|
||||||
|
client.auth.aws.iam_login(
|
||||||
|
credentials.access_key,
|
||||||
|
credentials.secret_key,
|
||||||
|
credentials.token,
|
||||||
|
role=obj.VAULT_AUTH_ROLE_FOR_DYNACONF,
|
||||||
|
)
|
||||||
|
assert client.is_authenticated(), (
|
||||||
|
"Vault authentication error: is VAULT_TOKEN_FOR_DYNACONF or "
|
||||||
|
"VAULT_ROLE_ID_FOR_DYNACONF defined?"
|
||||||
|
)
|
||||||
|
client.secrets.kv.default_kv_version = obj.VAULT_KV_VERSION_FOR_DYNACONF
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, env=None, silent=None, key=None):
|
||||||
|
"""Reads and loads in to "settings" a single key or all keys from vault
|
||||||
|
|
||||||
|
:param obj: the settings instance
|
||||||
|
:param env: settings env default='DYNACONF'
|
||||||
|
:param silent: if errors should raise
|
||||||
|
:param key: if defined load a single key, else load all in env
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
client = get_client(obj)
|
||||||
|
try:
|
||||||
|
if obj.VAULT_KV_VERSION_FOR_DYNACONF == 2:
|
||||||
|
dirs = client.secrets.kv.v2.list_secrets(
|
||||||
|
path=obj.VAULT_PATH_FOR_DYNACONF,
|
||||||
|
mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF,
|
||||||
|
)["data"]["keys"]
|
||||||
|
else:
|
||||||
|
dirs = client.secrets.kv.v1.list_secrets(
|
||||||
|
path=obj.VAULT_PATH_FOR_DYNACONF,
|
||||||
|
mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF,
|
||||||
|
)["data"]["keys"]
|
||||||
|
except InvalidPath:
|
||||||
|
# The given path is not a directory
|
||||||
|
dirs = []
|
||||||
|
# First look for secrets into environments less store
|
||||||
|
if not obj.ENVIRONMENTS_FOR_DYNACONF:
|
||||||
|
# By adding '', dynaconf will now read secrets from environments-less
|
||||||
|
# store which are not written by `dynaconf write` to Vault store
|
||||||
|
env_list = [obj.MAIN_ENV_FOR_DYNACONF.lower(), ""]
|
||||||
|
# Finally, look for secret into all the environments
|
||||||
|
else:
|
||||||
|
env_list = dirs + build_env_list(obj, env)
|
||||||
|
for env in env_list:
|
||||||
|
path = "/".join([obj.VAULT_PATH_FOR_DYNACONF, env])
|
||||||
|
try:
|
||||||
|
if obj.VAULT_KV_VERSION_FOR_DYNACONF == 2:
|
||||||
|
data = client.secrets.kv.v2.read_secret_version(
|
||||||
|
path, mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
data = client.secrets.kv.read_secret(
|
||||||
|
"data/" + path,
|
||||||
|
mount_point=obj.VAULT_MOUNT_POINT_FOR_DYNACONF,
|
||||||
|
)
|
||||||
|
except InvalidPath:
|
||||||
|
# If the path doesn't exist, ignore it and set data to None
|
||||||
|
data = None
|
||||||
|
if data:
|
||||||
|
# There seems to be a data dict within a data dict,
|
||||||
|
# extract the inner data
|
||||||
|
data = data.get("data", {}).get("data", {})
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
obj.VAULT_KV_VERSION_FOR_DYNACONF == 2
|
||||||
|
and obj.ENVIRONMENTS_FOR_DYNACONF
|
||||||
|
and data
|
||||||
|
):
|
||||||
|
data = data.get("data", {})
|
||||||
|
if data and key:
|
||||||
|
value = parse_conf_data(
|
||||||
|
data.get(key), tomlfy=True, box_settings=obj
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
obj.set(key, value)
|
||||||
|
elif data:
|
||||||
|
obj.update(data, loader_identifier=IDENTIFIER, tomlfy=True)
|
||||||
|
except Exception:
|
||||||
|
if silent:
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def write(obj, data=None, **kwargs):
|
||||||
|
"""Write a value in to loader source
|
||||||
|
|
||||||
|
:param obj: settings object
|
||||||
|
:param data: vars to be stored
|
||||||
|
:param kwargs: vars to be stored
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if obj.VAULT_ENABLED_FOR_DYNACONF is False:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Vault is not configured \n"
|
||||||
|
"export VAULT_ENABLED_FOR_DYNACONF=true\n"
|
||||||
|
"and configure the VAULT_FOR_DYNACONF_* variables"
|
||||||
|
)
|
||||||
|
data = data or {}
|
||||||
|
data.update(kwargs)
|
||||||
|
if not data:
|
||||||
|
raise AttributeError("Data must be provided")
|
||||||
|
data = {"data": data}
|
||||||
|
client = get_client(obj)
|
||||||
|
if obj.VAULT_KV_VERSION_FOR_DYNACONF == 1:
|
||||||
|
mount_point = obj.VAULT_MOUNT_POINT_FOR_DYNACONF + "/data"
|
||||||
|
else:
|
||||||
|
mount_point = obj.VAULT_MOUNT_POINT_FOR_DYNACONF
|
||||||
|
path = "/".join([obj.VAULT_PATH_FOR_DYNACONF, obj.current_env.lower()])
|
||||||
|
client.secrets.kv.create_or_update_secret(
|
||||||
|
path, secret=data, mount_point=mount_point
|
||||||
|
)
|
||||||
|
load(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def list_envs(obj, path=""):
|
||||||
|
"""
|
||||||
|
This function is a helper to get a list of all the existing envs in
|
||||||
|
the source of data, the use case is:
|
||||||
|
existing_envs = vault_loader.list_envs(settings)
|
||||||
|
for env in exiting_envs:
|
||||||
|
with settings.using_env(env): # switch to the env
|
||||||
|
# do something with a key of that env
|
||||||
|
|
||||||
|
:param obj: settings object
|
||||||
|
:param path: path to the vault secrets
|
||||||
|
:return: list containing all the keys at the given path
|
||||||
|
"""
|
||||||
|
client = get_client(obj)
|
||||||
|
path = path or obj.get("VAULT_PATH_FOR_DYNACONF")
|
||||||
|
try:
|
||||||
|
return client.list(f"/secret/metadata/{path}")["data"]["keys"]
|
||||||
|
except TypeError:
|
||||||
|
return []
|
|
@ -0,0 +1,87 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from dynaconf import default_settings
|
||||||
|
from dynaconf.constants import YAML_EXTENSIONS
|
||||||
|
from dynaconf.loaders.base import BaseLoader
|
||||||
|
from dynaconf.utils import object_merge
|
||||||
|
from dynaconf.utils.parse_conf import try_to_encode
|
||||||
|
from dynaconf.vendor.ruamel import yaml
|
||||||
|
|
||||||
|
# Add support for Dynaconf Lazy values to YAML dumper
|
||||||
|
yaml.SafeDumper.yaml_representers[
|
||||||
|
None
|
||||||
|
] = lambda self, data: yaml.representer.SafeRepresenter.represent_str(
|
||||||
|
self, try_to_encode(data)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load(obj, env=None, silent=True, key=None, filename=None):
|
||||||
|
"""
|
||||||
|
Reads and loads in to "obj" a single key or all keys from source file.
|
||||||
|
|
||||||
|
:param obj: the settings instance
|
||||||
|
:param env: settings current env default='development'
|
||||||
|
:param silent: if errors should raise
|
||||||
|
:param key: if defined load a single key, else load all in env
|
||||||
|
:param filename: Optional custom filename to load
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Resolve the loaders
|
||||||
|
# https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation
|
||||||
|
# Possible values are `safe_load, full_load, unsafe_load, load`
|
||||||
|
yaml_reader = getattr(
|
||||||
|
yaml, obj.get("YAML_LOADER_FOR_DYNACONF"), yaml.safe_load
|
||||||
|
)
|
||||||
|
if yaml_reader.__name__ == "unsafe_load": # pragma: no cover
|
||||||
|
warn(
|
||||||
|
"yaml.unsafe_load is deprecated."
|
||||||
|
" Please read https://msg.pyyaml.org/load for full details."
|
||||||
|
" Try to use full_load or safe_load."
|
||||||
|
)
|
||||||
|
|
||||||
|
loader = BaseLoader(
|
||||||
|
obj=obj,
|
||||||
|
env=env,
|
||||||
|
identifier="yaml",
|
||||||
|
extensions=YAML_EXTENSIONS,
|
||||||
|
file_reader=yaml_reader,
|
||||||
|
string_reader=yaml_reader,
|
||||||
|
)
|
||||||
|
loader.load(
|
||||||
|
filename=filename,
|
||||||
|
key=key,
|
||||||
|
silent=silent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write(settings_path, settings_data, merge=True):
|
||||||
|
"""Write data to a settings file.
|
||||||
|
|
||||||
|
:param settings_path: the filepath
|
||||||
|
:param settings_data: a dictionary with data
|
||||||
|
:param merge: boolean if existing file should be merged with new data
|
||||||
|
"""
|
||||||
|
settings_path = Path(settings_path)
|
||||||
|
if settings_path.exists() and merge: # pragma: no cover
|
||||||
|
with open(
|
||||||
|
str(settings_path), encoding=default_settings.ENCODING_FOR_DYNACONF
|
||||||
|
) as open_file:
|
||||||
|
object_merge(yaml.safe_load(open_file), settings_data)
|
||||||
|
|
||||||
|
with open(
|
||||||
|
str(settings_path),
|
||||||
|
"w",
|
||||||
|
encoding=default_settings.ENCODING_FOR_DYNACONF,
|
||||||
|
) as open_file:
|
||||||
|
yaml.dump(
|
||||||
|
settings_data,
|
||||||
|
open_file,
|
||||||
|
Dumper=yaml.dumper.SafeDumper,
|
||||||
|
explicit_start=True,
|
||||||
|
indent=2,
|
||||||
|
default_flow_style=False,
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue