Merge development into master
15
.github/scripts/build_test.sh
vendored
|
@ -7,9 +7,16 @@ sleep 30
|
||||||
|
|
||||||
if kill -s 0 $PID
|
if kill -s 0 $PID
|
||||||
then
|
then
|
||||||
echo "Bazarr is still running. We'll kill it..."
|
echo "Bazarr is still running. We'll test if UI is working..."
|
||||||
kill $PID
|
|
||||||
exit 0
|
|
||||||
else
|
else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
exitcode=0
|
||||||
|
curl -fsSL --retry-all-errors --retry 60 --retry-max-time 120 --max-time 10 "http://127.0.0.1:6767" --output /dev/null || exitcode=$?
|
||||||
|
[[ ${exitcode} == 0 ]] && echo "UI is responsive, good news!" || echo "Oops, UI isn't reachable, bad news..."
|
||||||
|
|
||||||
|
echo "Let's stop Bazarr before we exit..."
|
||||||
|
pkill -INT -P $PID
|
||||||
|
|
||||||
|
exit ${exitcode}
|
2
.github/scripts/create_changelog.sh
vendored
|
@ -10,5 +10,5 @@ latest_verion=$(git describe --tags --abbrev=0)
|
||||||
if [[ $RELEASE_MASTER -eq 1 ]]; then
|
if [[ $RELEASE_MASTER -eq 1 ]]; then
|
||||||
auto-changelog --stdout -t changelog-master.hbs --starting-version "$master_version" --commit-limit 3
|
auto-changelog --stdout -t changelog-master.hbs --starting-version "$master_version" --commit-limit 3
|
||||||
else
|
else
|
||||||
auto-changelog --stdout --starting-version "$latest_verion" --unreleased-only --commit-limit 0
|
auto-changelog --stdout --starting-version "$latest_verion" --unreleased-only --commit-limit false
|
||||||
fi
|
fi
|
2
.github/workflows/ci.yml
vendored
|
@ -36,7 +36,7 @@ jobs:
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
2
.github/workflows/release_beta_to_dev.yaml
vendored
|
@ -38,7 +38,7 @@ jobs:
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
|
||||||
|
|
||||||
- name: Install Global Tools
|
- name: Install Global Tools
|
||||||
run: npm install -g release-it auto-changelog
|
run: npm install -g release-it auto-changelog
|
||||||
|
|
2
.github/workflows/release_dev_to_master.yaml
vendored
|
@ -40,7 +40,7 @@ jobs:
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
|
||||||
|
|
||||||
- name: Install Global Tools
|
- name: Install Global Tools
|
||||||
run: npm install -g release-it auto-changelog
|
run: npm install -g release-it auto-changelog
|
||||||
|
|
2
.github/workflows/test_bazarr_execution.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
|
||||||
|
|
||||||
- name: Install UI Dependencies
|
- name: Install UI Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
|
@ -114,6 +114,8 @@ class Subtitles(Resource):
|
||||||
subtitles_path = args.get('path')
|
subtitles_path = args.get('path')
|
||||||
media_type = args.get('type')
|
media_type = args.get('type')
|
||||||
id = args.get('id')
|
id = args.get('id')
|
||||||
|
forced = True if args.get('forced') == 'True' else False
|
||||||
|
hi = True if args.get('hi') == 'True' else False
|
||||||
|
|
||||||
if not os.path.exists(subtitles_path):
|
if not os.path.exists(subtitles_path):
|
||||||
return 'Subtitles file not found. Path mapping issue?', 500
|
return 'Subtitles file not found. Path mapping issue?', 500
|
||||||
|
@ -144,6 +146,8 @@ class Subtitles(Resource):
|
||||||
'video_path': video_path,
|
'video_path': video_path,
|
||||||
'srt_path': subtitles_path,
|
'srt_path': subtitles_path,
|
||||||
'srt_lang': language,
|
'srt_lang': language,
|
||||||
|
'hi': hi,
|
||||||
|
'forced': forced,
|
||||||
'reference': args.get('reference') if args.get('reference') not in empty_values else video_path,
|
'reference': args.get('reference') if args.get('reference') not in empty_values else video_path,
|
||||||
'max_offset_seconds': args.get('max_offset_seconds') if args.get('max_offset_seconds') not in
|
'max_offset_seconds': args.get('max_offset_seconds') if args.get('max_offset_seconds') not in
|
||||||
empty_values else str(settings.subsync.max_offset_seconds),
|
empty_values else str(settings.subsync.max_offset_seconds),
|
||||||
|
@ -167,8 +171,6 @@ class Subtitles(Resource):
|
||||||
elif action == 'translate':
|
elif action == 'translate':
|
||||||
from_language = subtitles_lang_from_filename(subtitles_path)
|
from_language = subtitles_lang_from_filename(subtitles_path)
|
||||||
dest_language = language
|
dest_language = language
|
||||||
forced = True if args.get('forced') == 'true' else False
|
|
||||||
hi = True if args.get('hi') == 'true' else False
|
|
||||||
try:
|
try:
|
||||||
translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path,
|
translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path,
|
||||||
from_lang=from_language, to_lang=dest_language, forced=forced, hi=hi,
|
from_lang=from_language, to_lang=dest_language, forced=forced, hi=hi,
|
||||||
|
|
|
@ -8,6 +8,8 @@ from app.database import TableShows, TableMovies, database, select
|
||||||
|
|
||||||
from ..utils import authenticate
|
from ..utils import authenticate
|
||||||
|
|
||||||
|
import textdistance
|
||||||
|
|
||||||
api_ns_system_searches = Namespace('System Searches', description='Search for series or movies by name')
|
api_ns_system_searches = Namespace('System Searches', description='Search for series or movies by name')
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,4 +63,6 @@ class Searches(Resource):
|
||||||
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
|
# sort results by how closely they match the query
|
||||||
|
results = sorted(results, key=lambda x: textdistance.hamming.distance(query, x['title']))
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -73,6 +73,7 @@ class SystemSettings(Resource):
|
||||||
mustNotContain=str(item['mustNotContain']),
|
mustNotContain=str(item['mustNotContain']),
|
||||||
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
||||||
None,
|
None,
|
||||||
|
tag=item['tag'] if 'tag' in item else None,
|
||||||
)
|
)
|
||||||
.where(TableLanguagesProfiles.profileId == item['profileId']))
|
.where(TableLanguagesProfiles.profileId == item['profileId']))
|
||||||
existing.remove(item['profileId'])
|
existing.remove(item['profileId'])
|
||||||
|
@ -89,6 +90,7 @@ class SystemSettings(Resource):
|
||||||
mustNotContain=str(item['mustNotContain']),
|
mustNotContain=str(item['mustNotContain']),
|
||||||
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
||||||
None,
|
None,
|
||||||
|
tag=item['tag'] if 'tag' in item else None,
|
||||||
))
|
))
|
||||||
for profileId in existing:
|
for profileId in existing:
|
||||||
# Remove deleted profiles
|
# Remove deleted profiles
|
||||||
|
|
|
@ -31,12 +31,20 @@ def base_url_slash_cleaner(uri):
|
||||||
|
|
||||||
|
|
||||||
def validate_ip_address(ip_string):
|
def validate_ip_address(ip_string):
|
||||||
|
if ip_string == '*':
|
||||||
|
return True
|
||||||
try:
|
try:
|
||||||
ip_address(ip_string)
|
ip_address(ip_string)
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def validate_tags(tags):
|
||||||
|
if not tags:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return all(re.match( r'^[a-z0-9_-]+$', item) for item in tags)
|
||||||
|
|
||||||
|
|
||||||
ONE_HUNDRED_YEARS_IN_MINUTES = 52560000
|
ONE_HUNDRED_YEARS_IN_MINUTES = 52560000
|
||||||
ONE_HUNDRED_YEARS_IN_HOURS = 876000
|
ONE_HUNDRED_YEARS_IN_HOURS = 876000
|
||||||
|
@ -67,7 +75,7 @@ validators = [
|
||||||
# general section
|
# general section
|
||||||
Validator('general.flask_secret_key', must_exist=True, default=hexlify(os.urandom(16)).decode(),
|
Validator('general.flask_secret_key', must_exist=True, default=hexlify(os.urandom(16)).decode(),
|
||||||
is_type_of=str),
|
is_type_of=str),
|
||||||
Validator('general.ip', must_exist=True, default='0.0.0.0', is_type_of=str, condition=validate_ip_address),
|
Validator('general.ip', must_exist=True, default='*', is_type_of=str, condition=validate_ip_address),
|
||||||
Validator('general.port', must_exist=True, default=6767, is_type_of=int, gte=1, lte=65535),
|
Validator('general.port', must_exist=True, default=6767, is_type_of=int, gte=1, lte=65535),
|
||||||
Validator('general.base_url', must_exist=True, default='', is_type_of=str),
|
Validator('general.base_url', must_exist=True, default='', is_type_of=str),
|
||||||
Validator('general.path_mappings', must_exist=True, default=[], is_type_of=list),
|
Validator('general.path_mappings', must_exist=True, default=[], is_type_of=list),
|
||||||
|
@ -88,6 +96,9 @@ validators = [
|
||||||
Validator('general.use_sonarr', must_exist=True, default=False, is_type_of=bool),
|
Validator('general.use_sonarr', must_exist=True, default=False, is_type_of=bool),
|
||||||
Validator('general.use_radarr', must_exist=True, default=False, is_type_of=bool),
|
Validator('general.use_radarr', must_exist=True, default=False, is_type_of=bool),
|
||||||
Validator('general.path_mappings_movie', must_exist=True, default=[], is_type_of=list),
|
Validator('general.path_mappings_movie', must_exist=True, default=[], is_type_of=list),
|
||||||
|
Validator('general.serie_tag_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('general.movie_tag_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('general.remove_profile_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags),
|
||||||
Validator('general.serie_default_enabled', must_exist=True, default=False, is_type_of=bool),
|
Validator('general.serie_default_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
Validator('general.serie_default_profile', must_exist=True, default='', is_type_of=(int, str)),
|
Validator('general.serie_default_profile', must_exist=True, default='', is_type_of=(int, str)),
|
||||||
Validator('general.movie_default_enabled', must_exist=True, default=False, is_type_of=bool),
|
Validator('general.movie_default_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
@ -176,7 +187,7 @@ validators = [
|
||||||
Validator('sonarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
|
Validator('sonarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
|
||||||
Validator('sonarr.series_sync', must_exist=True, default=60, is_type_of=int,
|
Validator('sonarr.series_sync', must_exist=True, default=60, is_type_of=int,
|
||||||
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
|
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
|
||||||
Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
|
Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags),
|
||||||
Validator('sonarr.excluded_series_types', must_exist=True, default=[], is_type_of=list),
|
Validator('sonarr.excluded_series_types', must_exist=True, default=[], is_type_of=list),
|
||||||
Validator('sonarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
|
Validator('sonarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
|
||||||
Validator('sonarr.exclude_season_zero', must_exist=True, default=False, is_type_of=bool),
|
Validator('sonarr.exclude_season_zero', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
@ -199,7 +210,7 @@ validators = [
|
||||||
Validator('radarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
|
Validator('radarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
|
||||||
Validator('radarr.movies_sync', must_exist=True, default=60, is_type_of=int,
|
Validator('radarr.movies_sync', must_exist=True, default=60, is_type_of=int,
|
||||||
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
|
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
|
||||||
Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
|
Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags),
|
||||||
Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
|
Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
|
||||||
Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool),
|
Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool),
|
||||||
Validator('radarr.sync_only_monitored_movies', must_exist=True, default=False, is_type_of=bool),
|
Validator('radarr.sync_only_monitored_movies', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
@ -271,6 +282,10 @@ validators = [
|
||||||
Validator('legendasdivx.password', must_exist=True, default='', is_type_of=str, cast=str),
|
Validator('legendasdivx.password', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
Validator('legendasdivx.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool),
|
Validator('legendasdivx.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
|
||||||
|
# legendasnet section
|
||||||
|
Validator('legendasnet.username', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
|
Validator('legendasnet.password', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
|
|
||||||
# ktuvit section
|
# ktuvit section
|
||||||
Validator('ktuvit.email', must_exist=True, default='', is_type_of=str),
|
Validator('ktuvit.email', must_exist=True, default='', is_type_of=str),
|
||||||
Validator('ktuvit.hashed_password', must_exist=True, default='', is_type_of=str, cast=str),
|
Validator('ktuvit.hashed_password', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
|
@ -298,6 +313,12 @@ validators = [
|
||||||
|
|
||||||
# analytics section
|
# analytics section
|
||||||
Validator('analytics.enabled', must_exist=True, default=True, is_type_of=bool),
|
Validator('analytics.enabled', must_exist=True, default=True, is_type_of=bool),
|
||||||
|
|
||||||
|
# jimaku section
|
||||||
|
Validator('jimaku.api_key', must_exist=True, default='', is_type_of=str),
|
||||||
|
Validator('jimaku.enable_name_search_fallback', must_exist=True, default=True, is_type_of=bool),
|
||||||
|
Validator('jimaku.enable_archives_download', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
Validator('jimaku.enable_ai_subs', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
|
||||||
# titlovi section
|
# titlovi section
|
||||||
Validator('titlovi.username', must_exist=True, default='', is_type_of=str, cast=str),
|
Validator('titlovi.username', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
|
@ -321,6 +342,9 @@ validators = [
|
||||||
Validator('karagarga.f_username', must_exist=True, default='', is_type_of=str, cast=str),
|
Validator('karagarga.f_username', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
Validator('karagarga.f_password', must_exist=True, default='', is_type_of=str, cast=str),
|
Validator('karagarga.f_password', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
|
|
||||||
|
# subdl section
|
||||||
|
Validator('subdl.api_key', must_exist=True, default='', is_type_of=str, cast=str),
|
||||||
|
|
||||||
# subsync section
|
# subsync section
|
||||||
Validator('subsync.use_subsync', must_exist=True, default=False, is_type_of=bool),
|
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.use_subsync_threshold', must_exist=True, default=False, is_type_of=bool),
|
||||||
|
@ -451,6 +475,7 @@ array_keys = ['excluded_tags',
|
||||||
'enabled_integrations',
|
'enabled_integrations',
|
||||||
'path_mappings',
|
'path_mappings',
|
||||||
'path_mappings_movie',
|
'path_mappings_movie',
|
||||||
|
'remove_profile_tags',
|
||||||
'language_equals',
|
'language_equals',
|
||||||
'blacklisted_languages',
|
'blacklisted_languages',
|
||||||
'blacklisted_providers']
|
'blacklisted_providers']
|
||||||
|
|
|
@ -172,6 +172,7 @@ class TableHistory(Base):
|
||||||
video_path = mapped_column(Text)
|
video_path = mapped_column(Text)
|
||||||
matched = mapped_column(Text)
|
matched = mapped_column(Text)
|
||||||
not_matched = mapped_column(Text)
|
not_matched = mapped_column(Text)
|
||||||
|
upgradedFromId = mapped_column(Integer, ForeignKey('table_history.id'))
|
||||||
|
|
||||||
|
|
||||||
class TableHistoryMovie(Base):
|
class TableHistoryMovie(Base):
|
||||||
|
@ -190,6 +191,7 @@ class TableHistoryMovie(Base):
|
||||||
video_path = mapped_column(Text)
|
video_path = mapped_column(Text)
|
||||||
matched = mapped_column(Text)
|
matched = mapped_column(Text)
|
||||||
not_matched = mapped_column(Text)
|
not_matched = mapped_column(Text)
|
||||||
|
upgradedFromId = mapped_column(Integer, ForeignKey('table_history_movie.id'))
|
||||||
|
|
||||||
|
|
||||||
class TableLanguagesProfiles(Base):
|
class TableLanguagesProfiles(Base):
|
||||||
|
@ -202,6 +204,7 @@ class TableLanguagesProfiles(Base):
|
||||||
name = mapped_column(Text, nullable=False)
|
name = mapped_column(Text, nullable=False)
|
||||||
mustContain = mapped_column(Text)
|
mustContain = mapped_column(Text)
|
||||||
mustNotContain = mapped_column(Text)
|
mustNotContain = mapped_column(Text)
|
||||||
|
tag = mapped_column(Text)
|
||||||
|
|
||||||
|
|
||||||
class TableMovies(Base):
|
class TableMovies(Base):
|
||||||
|
@ -376,6 +379,7 @@ def update_profile_id_list():
|
||||||
'mustContain': ast.literal_eval(x.mustContain) if x.mustContain else [],
|
'mustContain': ast.literal_eval(x.mustContain) if x.mustContain else [],
|
||||||
'mustNotContain': ast.literal_eval(x.mustNotContain) if x.mustNotContain else [],
|
'mustNotContain': ast.literal_eval(x.mustNotContain) if x.mustNotContain else [],
|
||||||
'originalFormat': x.originalFormat,
|
'originalFormat': x.originalFormat,
|
||||||
|
'tag': x.tag,
|
||||||
} for x in database.execute(
|
} for x in database.execute(
|
||||||
select(TableLanguagesProfiles.profileId,
|
select(TableLanguagesProfiles.profileId,
|
||||||
TableLanguagesProfiles.name,
|
TableLanguagesProfiles.name,
|
||||||
|
@ -383,7 +387,8 @@ def update_profile_id_list():
|
||||||
TableLanguagesProfiles.items,
|
TableLanguagesProfiles.items,
|
||||||
TableLanguagesProfiles.mustContain,
|
TableLanguagesProfiles.mustContain,
|
||||||
TableLanguagesProfiles.mustNotContain,
|
TableLanguagesProfiles.mustNotContain,
|
||||||
TableLanguagesProfiles.originalFormat))
|
TableLanguagesProfiles.originalFormat,
|
||||||
|
TableLanguagesProfiles.tag))
|
||||||
.all()
|
.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -418,7 +423,7 @@ def get_profile_cutoff(profile_id):
|
||||||
if profile_id and profile_id != 'null':
|
if profile_id and profile_id != 'null':
|
||||||
cutoff_language = []
|
cutoff_language = []
|
||||||
for profile in profile_id_list:
|
for profile in profile_id_list:
|
||||||
profileId, name, cutoff, items, mustContain, mustNotContain, originalFormat = profile.values()
|
profileId, name, cutoff, items, mustContain, mustNotContain, originalFormat, tag = profile.values()
|
||||||
if cutoff:
|
if cutoff:
|
||||||
if profileId == int(profile_id):
|
if profileId == int(profile_id):
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -497,3 +502,29 @@ def convert_list_to_clause(arr: list):
|
||||||
return f"({','.join(str(x) for x in arr)})"
|
return f"({','.join(str(x) for x in arr)})"
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_languages_profile_hi_values():
|
||||||
|
for languages_profile in (database.execute(
|
||||||
|
select(
|
||||||
|
TableLanguagesProfiles.profileId,
|
||||||
|
TableLanguagesProfiles.name,
|
||||||
|
TableLanguagesProfiles.cutoff,
|
||||||
|
TableLanguagesProfiles.items,
|
||||||
|
TableLanguagesProfiles.mustContain,
|
||||||
|
TableLanguagesProfiles.mustNotContain,
|
||||||
|
TableLanguagesProfiles.originalFormat,
|
||||||
|
TableLanguagesProfiles.tag)
|
||||||
|
))\
|
||||||
|
.all():
|
||||||
|
items = json.loads(languages_profile.items)
|
||||||
|
for language in items:
|
||||||
|
if language['hi'] == "only":
|
||||||
|
language['hi'] = "True"
|
||||||
|
elif language['hi'] in ["also", "never"]:
|
||||||
|
language['hi'] = "False"
|
||||||
|
database.execute(
|
||||||
|
update(TableLanguagesProfiles)
|
||||||
|
.values({"items": json.dumps(items)})
|
||||||
|
.where(TableLanguagesProfiles.profileId == languages_profile.profileId)
|
||||||
|
)
|
||||||
|
|
|
@ -264,6 +264,10 @@ def get_providers_auth():
|
||||||
'password': settings.legendasdivx.password,
|
'password': settings.legendasdivx.password,
|
||||||
'skip_wrong_fps': settings.legendasdivx.skip_wrong_fps,
|
'skip_wrong_fps': settings.legendasdivx.skip_wrong_fps,
|
||||||
},
|
},
|
||||||
|
'legendasnet': {
|
||||||
|
'username': settings.legendasnet.username,
|
||||||
|
'password': settings.legendasnet.password,
|
||||||
|
},
|
||||||
'xsubs': {
|
'xsubs': {
|
||||||
'username': settings.xsubs.username,
|
'username': settings.xsubs.username,
|
||||||
'password': settings.xsubs.password,
|
'password': settings.xsubs.password,
|
||||||
|
@ -285,6 +289,12 @@ def get_providers_auth():
|
||||||
'username': settings.titlovi.username,
|
'username': settings.titlovi.username,
|
||||||
'password': settings.titlovi.password,
|
'password': settings.titlovi.password,
|
||||||
},
|
},
|
||||||
|
'jimaku': {
|
||||||
|
'api_key': settings.jimaku.api_key,
|
||||||
|
'enable_name_search_fallback': settings.jimaku.enable_name_search_fallback,
|
||||||
|
'enable_archives_download': settings.jimaku.enable_archives_download,
|
||||||
|
'enable_ai_subs': settings.jimaku.enable_ai_subs,
|
||||||
|
},
|
||||||
'ktuvit': {
|
'ktuvit': {
|
||||||
'email': settings.ktuvit.email,
|
'email': settings.ktuvit.email,
|
||||||
'hashed_password': settings.ktuvit.hashed_password,
|
'hashed_password': settings.ktuvit.hashed_password,
|
||||||
|
@ -322,6 +332,9 @@ def get_providers_auth():
|
||||||
},
|
},
|
||||||
"animetosho": {
|
"animetosho": {
|
||||||
'search_threshold': settings.animetosho.search_threshold,
|
'search_threshold': settings.animetosho.search_threshold,
|
||||||
|
},
|
||||||
|
"subdl": {
|
||||||
|
'api_key': settings.subdl.api_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,10 +58,13 @@ class NoExceptionFormatter(logging.Formatter):
|
||||||
|
|
||||||
class UnwantedWaitressMessageFilter(logging.Filter):
|
class UnwantedWaitressMessageFilter(logging.Filter):
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
if settings.general.debug:
|
if settings.general.debug or "BAZARR" in record.msg:
|
||||||
# no filtering in debug mode
|
# no filtering in debug mode or if originating from us
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if record.level != loggin.ERROR:
|
||||||
|
return False
|
||||||
|
|
||||||
unwantedMessages = [
|
unwantedMessages = [
|
||||||
"Exception while serving /api/socket.io/",
|
"Exception while serving /api/socket.io/",
|
||||||
['Session is disconnected', 'Session not found'],
|
['Session is disconnected', 'Session not found'],
|
||||||
|
@ -161,7 +164,7 @@ def configure_logging(debug=False):
|
||||||
logging.getLogger("websocket").setLevel(logging.CRITICAL)
|
logging.getLogger("websocket").setLevel(logging.CRITICAL)
|
||||||
logging.getLogger("ga4mp.ga4mp").setLevel(logging.ERROR)
|
logging.getLogger("ga4mp.ga4mp").setLevel(logging.ERROR)
|
||||||
|
|
||||||
logging.getLogger("waitress").setLevel(logging.ERROR)
|
logging.getLogger("waitress").setLevel(logging.INFO)
|
||||||
logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter())
|
logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter())
|
||||||
logging.getLogger("knowit").setLevel(logging.CRITICAL)
|
logging.getLogger("knowit").setLevel(logging.CRITICAL)
|
||||||
logging.getLogger("enzyme").setLevel(logging.CRITICAL)
|
logging.getLogger("enzyme").setLevel(logging.CRITICAL)
|
||||||
|
|
|
@ -50,7 +50,7 @@ class Server:
|
||||||
self.connected = True
|
self.connected = True
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
if error.errno == errno.EADDRNOTAVAIL:
|
if error.errno == errno.EADDRNOTAVAIL:
|
||||||
logging.exception("BAZARR cannot bind to specified IP, trying with default (0.0.0.0)")
|
logging.exception("BAZARR cannot bind to specified IP, trying with 0.0.0.0")
|
||||||
self.address = '0.0.0.0'
|
self.address = '0.0.0.0'
|
||||||
self.connected = False
|
self.connected = False
|
||||||
super(Server, self).__init__()
|
super(Server, self).__init__()
|
||||||
|
@ -76,8 +76,7 @@ class Server:
|
||||||
self.shutdown(EXIT_INTERRUPT)
|
self.shutdown(EXIT_INTERRUPT)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
logging.info(f'BAZARR is started and waiting for request on http://{self.server.effective_host}:'
|
self.server.print_listen("BAZARR is started and waiting for requests on: http://{}:{}")
|
||||||
f'{self.server.effective_port}')
|
|
||||||
signal.signal(signal.SIGINT, self.interrupt_handler)
|
signal.signal(signal.SIGINT, self.interrupt_handler)
|
||||||
try:
|
try:
|
||||||
self.server.run()
|
self.server.run()
|
||||||
|
|
|
@ -20,9 +20,10 @@ from .config import settings, base_url
|
||||||
from .database import System
|
from .database import System
|
||||||
from .get_args import args
|
from .get_args import args
|
||||||
|
|
||||||
|
frontend_build_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend', 'build')
|
||||||
|
|
||||||
ui_bp = Blueprint('ui', __name__,
|
ui_bp = Blueprint('ui', __name__,
|
||||||
template_folder=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
template_folder=frontend_build_path,
|
||||||
'frontend', 'build'),
|
|
||||||
static_folder=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend',
|
static_folder=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'frontend',
|
||||||
'build', 'assets'),
|
'build', 'assets'),
|
||||||
static_url_path='/assets')
|
static_url_path='/assets')
|
||||||
|
@ -38,13 +39,15 @@ static_bp = Blueprint('images', __name__, static_folder=static_directory, static
|
||||||
|
|
||||||
ui_bp.register_blueprint(static_bp)
|
ui_bp.register_blueprint(static_bp)
|
||||||
|
|
||||||
|
|
||||||
mimetypes.add_type('application/javascript', '.js')
|
mimetypes.add_type('application/javascript', '.js')
|
||||||
mimetypes.add_type('text/css', '.css')
|
mimetypes.add_type('text/css', '.css')
|
||||||
mimetypes.add_type('font/woff2', '.woff2')
|
mimetypes.add_type('font/woff2', '.woff2')
|
||||||
mimetypes.add_type('image/svg+xml', '.svg')
|
mimetypes.add_type('image/svg+xml', '.svg')
|
||||||
mimetypes.add_type('image/png', '.png')
|
mimetypes.add_type('image/png', '.png')
|
||||||
mimetypes.add_type('image/x-icon', '.ico')
|
mimetypes.add_type('image/x-icon', '.ico')
|
||||||
|
mimetypes.add_type('application/manifest+json', '.webmanifest')
|
||||||
|
|
||||||
|
pwa_assets = ['registerSW.js', 'manifest.webmanifest', 'sw.js']
|
||||||
|
|
||||||
|
|
||||||
def check_login(actual_method):
|
def check_login(actual_method):
|
||||||
|
@ -70,6 +73,10 @@ def catch_all(path):
|
||||||
# login page has been accessed when no authentication is enabled
|
# login page has been accessed when no authentication is enabled
|
||||||
return redirect(base_url or "/", code=302)
|
return redirect(base_url or "/", code=302)
|
||||||
|
|
||||||
|
# PWA Assets are returned from frontend root folder
|
||||||
|
if path in pwa_assets or path.startswith('workbox-'):
|
||||||
|
return send_file(os.path.join(frontend_build_path, path))
|
||||||
|
|
||||||
auth = True
|
auth = True
|
||||||
if settings.auth.type == 'basic':
|
if settings.auth.type == 'basic':
|
||||||
auth = request.authorization
|
auth = request.authorization
|
||||||
|
@ -153,8 +160,8 @@ def backup_download(filename):
|
||||||
def swaggerui_static(filename):
|
def swaggerui_static(filename):
|
||||||
basepath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
|
basepath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
|
||||||
'static')
|
'static')
|
||||||
fullpath = os.path.join(basepath, filename)
|
fullpath = os.path.realpath(os.path.join(basepath, filename))
|
||||||
if not fullpath.startswith(basepath):
|
if not basepath == os.path.commonpath((basepath, fullpath)):
|
||||||
return '', 404
|
return '', 404
|
||||||
else:
|
else:
|
||||||
return send_file(fullpath)
|
return send_file(fullpath)
|
||||||
|
@ -186,7 +193,8 @@ def proxy(protocol, url):
|
||||||
elif result.status_code == 401:
|
elif result.status_code == 401:
|
||||||
return dict(status=False, error='Access Denied. Check API key.', code=result.status_code)
|
return dict(status=False, error='Access Denied. Check API key.', code=result.status_code)
|
||||||
elif result.status_code == 404:
|
elif result.status_code == 404:
|
||||||
return dict(status=False, error='Cannot get version. Maybe unsupported legacy API call?', code=result.status_code)
|
return dict(status=False, error='Cannot get version. Maybe unsupported legacy API call?',
|
||||||
|
code=result.status_code)
|
||||||
elif 300 <= result.status_code <= 399:
|
elif 300 <= result.status_code <= 399:
|
||||||
return dict(status=False, error='Wrong URL Base.', code=result.status_code)
|
return dict(status=False, error='Wrong URL Base.', code=result.status_code)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -5,7 +5,8 @@ import os
|
||||||
|
|
||||||
from subzero.language import Language
|
from subzero.language import Language
|
||||||
|
|
||||||
from app.database import database, insert
|
from app.database import database, insert, update
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ class CustomLanguage:
|
||||||
language = "pt-BR"
|
language = "pt-BR"
|
||||||
official_alpha2 = "pt"
|
official_alpha2 = "pt"
|
||||||
official_alpha3 = "por"
|
official_alpha3 = "por"
|
||||||
name = "Brazilian Portuguese"
|
name = "Portuguese (Brazil)"
|
||||||
iso = "BR"
|
iso = "BR"
|
||||||
_scripts = []
|
_scripts = []
|
||||||
_possible_matches = ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil")
|
_possible_matches = ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil")
|
||||||
|
@ -50,13 +51,19 @@ class CustomLanguage:
|
||||||
"""Register the custom language subclasses in the database."""
|
"""Register the custom language subclasses in the database."""
|
||||||
|
|
||||||
for sub in cls.__subclasses__():
|
for sub in cls.__subclasses__():
|
||||||
database.execute(
|
try:
|
||||||
insert(table)
|
database.execute(
|
||||||
.values(code3=sub.alpha3,
|
insert(table)
|
||||||
code2=sub.alpha2,
|
.values(code3=sub.alpha3,
|
||||||
name=sub.name,
|
code2=sub.alpha2,
|
||||||
enabled=0)
|
name=sub.name,
|
||||||
.on_conflict_do_nothing())
|
enabled=0))
|
||||||
|
except IntegrityError:
|
||||||
|
database.execute(
|
||||||
|
update(table)
|
||||||
|
.values(code2=sub.alpha2,
|
||||||
|
name=sub.name)
|
||||||
|
.where(table.code3 == sub.alpha3))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def found_external(cls, subtitle, subtitle_path):
|
def found_external(cls, subtitle, subtitle_path):
|
||||||
|
@ -212,7 +219,7 @@ class LatinAmericanSpanish(CustomLanguage):
|
||||||
language = "es-MX"
|
language = "es-MX"
|
||||||
official_alpha2 = "es"
|
official_alpha2 = "es"
|
||||||
official_alpha3 = "spa"
|
official_alpha3 = "spa"
|
||||||
name = "Latin American Spanish"
|
name = "Spanish (Latino)"
|
||||||
iso = "MX" # Not fair, but ok
|
iso = "MX" # Not fair, but ok
|
||||||
_scripts = ("419",)
|
_scripts = ("419",)
|
||||||
_possible_matches = (
|
_possible_matches = (
|
||||||
|
|
|
@ -44,6 +44,12 @@ def create_languages_dict():
|
||||||
.values(name='Chinese Simplified')
|
.values(name='Chinese Simplified')
|
||||||
.where(TableSettingsLanguages.code3 == 'zho'))
|
.where(TableSettingsLanguages.code3 == 'zho'))
|
||||||
|
|
||||||
|
# replace Modern Greek by Greek to match Sonarr and Radarr languages
|
||||||
|
database.execute(
|
||||||
|
update(TableSettingsLanguages)
|
||||||
|
.values(name='Greek')
|
||||||
|
.where(TableSettingsLanguages.code3 == 'ell'))
|
||||||
|
|
||||||
languages_dict = [{
|
languages_dict = [{
|
||||||
'code3': x.code3,
|
'code3': x.code3,
|
||||||
'code2': x.code2,
|
'code2': x.code2,
|
||||||
|
@ -55,6 +61,19 @@ def create_languages_dict():
|
||||||
.all()]
|
.all()]
|
||||||
|
|
||||||
|
|
||||||
|
def audio_language_from_name(lang):
|
||||||
|
lang_map = {
|
||||||
|
'Chinese': 'zh',
|
||||||
|
}
|
||||||
|
|
||||||
|
alpha2_code = lang_map.get(lang, None)
|
||||||
|
|
||||||
|
if alpha2_code is None:
|
||||||
|
return lang
|
||||||
|
|
||||||
|
return language_from_alpha2(alpha2_code)
|
||||||
|
|
||||||
|
|
||||||
def language_from_alpha2(lang):
|
def language_from_alpha2(lang):
|
||||||
return next((item['name'] for item in languages_dict if item['code2'] == lang[:2]), None)
|
return next((item['name'] for item in languages_dict if item['code2'] == lang[:2]), None)
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ else:
|
||||||
# there's missing embedded packages after a commit
|
# there's missing embedded packages after a commit
|
||||||
check_if_new_update()
|
check_if_new_update()
|
||||||
|
|
||||||
from app.database import System, database, update, migrate_db, create_db_revision # noqa E402
|
from app.database import System, database, update, migrate_db, create_db_revision, upgrade_languages_profile_hi_values # noqa E402
|
||||||
from app.notifier import update_notifier # noqa E402
|
from app.notifier import update_notifier # noqa E402
|
||||||
from languages.get_languages import load_language_in_db # noqa E402
|
from languages.get_languages import load_language_in_db # noqa E402
|
||||||
from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402
|
from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402
|
||||||
|
@ -49,6 +49,7 @@ if args.create_db_revision:
|
||||||
stop_bazarr(EXIT_NORMAL)
|
stop_bazarr(EXIT_NORMAL)
|
||||||
else:
|
else:
|
||||||
migrate_db(app)
|
migrate_db(app)
|
||||||
|
upgrade_languages_profile_hi_values()
|
||||||
|
|
||||||
configure_proxy_func()
|
configure_proxy_func()
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,11 @@ def trace(message):
|
||||||
logging.debug(FEATURE_PREFIX + message)
|
logging.debug(FEATURE_PREFIX + message)
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_profiles():
|
||||||
|
return database.execute(
|
||||||
|
select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.name, TableLanguagesProfiles.tag)).all()
|
||||||
|
|
||||||
|
|
||||||
def update_all_movies():
|
def update_all_movies():
|
||||||
movies_full_scan_subtitles()
|
movies_full_scan_subtitles()
|
||||||
logging.info('BAZARR All existing movie subtitles indexed from disk.')
|
logging.info('BAZARR All existing movie subtitles indexed from disk.')
|
||||||
|
@ -59,7 +64,7 @@ def update_movie(updated_movie, send_event):
|
||||||
def get_movie_monitored_status(movie_id):
|
def get_movie_monitored_status(movie_id):
|
||||||
existing_movie_monitored = database.execute(
|
existing_movie_monitored = database.execute(
|
||||||
select(TableMovies.monitored)
|
select(TableMovies.monitored)
|
||||||
.where(TableMovies.tmdbId == movie_id))\
|
.where(TableMovies.tmdbId == str(movie_id)))\
|
||||||
.first()
|
.first()
|
||||||
if existing_movie_monitored is None:
|
if existing_movie_monitored is None:
|
||||||
return True
|
return True
|
||||||
|
@ -108,6 +113,7 @@ def update_movies(send_event=True):
|
||||||
else:
|
else:
|
||||||
audio_profiles = get_profile_list()
|
audio_profiles = get_profile_list()
|
||||||
tagsDict = get_tags()
|
tagsDict = get_tags()
|
||||||
|
language_profiles = get_language_profiles()
|
||||||
|
|
||||||
# Get movies data from radarr
|
# Get movies data from radarr
|
||||||
movies = get_movies_from_radarr_api(apikey_radarr=apikey_radarr)
|
movies = get_movies_from_radarr_api(apikey_radarr=apikey_radarr)
|
||||||
|
@ -178,6 +184,7 @@ def update_movies(send_event=True):
|
||||||
if str(movie['tmdbId']) in current_movies_id_db:
|
if str(movie['tmdbId']) in current_movies_id_db:
|
||||||
parsed_movie = movieParser(movie, action='update',
|
parsed_movie = movieParser(movie, action='update',
|
||||||
tags_dict=tagsDict,
|
tags_dict=tagsDict,
|
||||||
|
language_profiles=language_profiles,
|
||||||
movie_default_profile=movie_default_profile,
|
movie_default_profile=movie_default_profile,
|
||||||
audio_profiles=audio_profiles)
|
audio_profiles=audio_profiles)
|
||||||
if not any([parsed_movie.items() <= x for x in current_movies_db_kv]):
|
if not any([parsed_movie.items() <= x for x in current_movies_db_kv]):
|
||||||
|
@ -186,6 +193,7 @@ def update_movies(send_event=True):
|
||||||
else:
|
else:
|
||||||
parsed_movie = movieParser(movie, action='insert',
|
parsed_movie = movieParser(movie, action='insert',
|
||||||
tags_dict=tagsDict,
|
tags_dict=tagsDict,
|
||||||
|
language_profiles=language_profiles,
|
||||||
movie_default_profile=movie_default_profile,
|
movie_default_profile=movie_default_profile,
|
||||||
audio_profiles=audio_profiles)
|
audio_profiles=audio_profiles)
|
||||||
add_movie(parsed_movie, send_event)
|
add_movie(parsed_movie, send_event)
|
||||||
|
@ -247,6 +255,7 @@ def update_one_movie(movie_id, action, defer_search=False):
|
||||||
|
|
||||||
audio_profiles = get_profile_list()
|
audio_profiles = get_profile_list()
|
||||||
tagsDict = get_tags()
|
tagsDict = get_tags()
|
||||||
|
language_profiles = get_language_profiles()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get movie data from radarr api
|
# Get movie data from radarr api
|
||||||
|
@ -256,10 +265,10 @@ def update_one_movie(movie_id, action, defer_search=False):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if action == 'updated' and existing_movie:
|
if action == 'updated' and existing_movie:
|
||||||
movie = movieParser(movie_data, action='update', tags_dict=tagsDict,
|
movie = movieParser(movie_data, action='update', tags_dict=tagsDict, language_profiles=language_profiles,
|
||||||
movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)
|
movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)
|
||||||
elif action == 'updated' and not existing_movie:
|
elif action == 'updated' and not existing_movie:
|
||||||
movie = movieParser(movie_data, action='insert', tags_dict=tagsDict,
|
movie = movieParser(movie_data, action='insert', tags_dict=tagsDict, language_profiles=language_profiles,
|
||||||
movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)
|
movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('BAZARR cannot get movie returned by SignalR feed from Radarr API.')
|
logging.exception('BAZARR cannot get movie returned by SignalR feed from Radarr API.')
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from languages.get_languages import language_from_alpha2
|
from languages.get_languages import audio_language_from_name
|
||||||
from radarr.info import get_radarr_info
|
from radarr.info import get_radarr_info
|
||||||
from utilities.video_analyzer import embedded_audio_reader
|
from utilities.video_analyzer import embedded_audio_reader
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
|
@ -11,7 +11,17 @@ from utilities.path_mappings import path_mappings
|
||||||
from .converter import RadarrFormatAudioCodec, RadarrFormatVideoCodec
|
from .converter import RadarrFormatAudioCodec, RadarrFormatVideoCodec
|
||||||
|
|
||||||
|
|
||||||
def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles):
|
def get_matching_profile(tags, language_profiles):
|
||||||
|
matching_profile = None
|
||||||
|
if len(tags) > 0:
|
||||||
|
for profileId, name, tag in language_profiles:
|
||||||
|
if tag in tags:
|
||||||
|
matching_profile = profileId
|
||||||
|
break
|
||||||
|
return matching_profile
|
||||||
|
|
||||||
|
|
||||||
|
def movieParser(movie, action, tags_dict, language_profiles, movie_default_profile, audio_profiles):
|
||||||
if 'movieFile' in movie:
|
if 'movieFile' in movie:
|
||||||
try:
|
try:
|
||||||
overview = str(movie['overview'])
|
overview = str(movie['overview'])
|
||||||
|
@ -107,9 +117,7 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
|
||||||
for item in movie['movieFile']['languages']:
|
for item in movie['movieFile']['languages']:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
if 'name' in item:
|
if 'name' in item:
|
||||||
language = item['name']
|
language = audio_language_from_name(item['name'])
|
||||||
if item['name'] == 'Portuguese (Brazil)':
|
|
||||||
language = language_from_alpha2('pb')
|
|
||||||
audio_language.append(language)
|
audio_language.append(language)
|
||||||
|
|
||||||
tags = [d['label'] for d in tags_dict if d['id'] in movie['tags']]
|
tags = [d['label'] for d in tags_dict if d['id'] in movie['tags']]
|
||||||
|
@ -140,6 +148,15 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
|
||||||
parsed_movie['subtitles'] = '[]'
|
parsed_movie['subtitles'] = '[]'
|
||||||
parsed_movie['profileId'] = movie_default_profile
|
parsed_movie['profileId'] = movie_default_profile
|
||||||
|
|
||||||
|
if settings.general.movie_tag_enabled:
|
||||||
|
tag_profile = get_matching_profile(tags, language_profiles)
|
||||||
|
if tag_profile:
|
||||||
|
parsed_movie['profileId'] = tag_profile
|
||||||
|
remove_profile_tags_list = settings.general.remove_profile_tags
|
||||||
|
if len(remove_profile_tags_list) > 0:
|
||||||
|
if set(tags) & set(remove_profile_tags_list):
|
||||||
|
parsed_movie['profileId'] = None
|
||||||
|
|
||||||
return parsed_movie
|
return parsed_movie
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import os
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import TableShows, database, select
|
from app.database import TableShows, database, select
|
||||||
from constants import MINIMUM_VIDEO_SIZE
|
from constants import MINIMUM_VIDEO_SIZE
|
||||||
|
from languages.get_languages import audio_language_from_name
|
||||||
from utilities.path_mappings import path_mappings
|
from utilities.path_mappings import path_mappings
|
||||||
from utilities.video_analyzer import embedded_audio_reader
|
from utilities.video_analyzer import embedded_audio_reader
|
||||||
from sonarr.info import get_sonarr_info
|
from sonarr.info import get_sonarr_info
|
||||||
|
@ -12,7 +13,17 @@ from sonarr.info import get_sonarr_info
|
||||||
from .converter import SonarrFormatVideoCodec, SonarrFormatAudioCodec
|
from .converter import SonarrFormatVideoCodec, SonarrFormatAudioCodec
|
||||||
|
|
||||||
|
|
||||||
def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles):
|
def get_matching_profile(tags, language_profiles):
|
||||||
|
matching_profile = None
|
||||||
|
if len(tags) > 0:
|
||||||
|
for profileId, name, tag in language_profiles:
|
||||||
|
if tag in tags:
|
||||||
|
matching_profile = profileId
|
||||||
|
break
|
||||||
|
return matching_profile
|
||||||
|
|
||||||
|
|
||||||
|
def seriesParser(show, action, tags_dict, language_profiles, serie_default_profile, audio_profiles):
|
||||||
overview = show['overview'] if 'overview' in show else ''
|
overview = show['overview'] if 'overview' in show else ''
|
||||||
poster = ''
|
poster = ''
|
||||||
fanart = ''
|
fanart = ''
|
||||||
|
@ -24,9 +35,11 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
|
||||||
if image['coverType'] == 'fanart':
|
if image['coverType'] == 'fanart':
|
||||||
fanart = image['url'].split('?')[0]
|
fanart = image['url'].split('?')[0]
|
||||||
|
|
||||||
alternate_titles = None
|
|
||||||
if show['alternateTitles'] is not None:
|
if show['alternateTitles'] is not None:
|
||||||
alternate_titles = str([item['title'] for item in show['alternateTitles']])
|
alternate_titles = [item['title'] for item in show['alternateTitles'] if 'title' in item and item['title'] not
|
||||||
|
in [None, ''] and item["title"] != show["title"]]
|
||||||
|
else:
|
||||||
|
alternate_titles = []
|
||||||
|
|
||||||
tags = [d['label'] for d in tags_dict if d['id'] in show['tags']]
|
tags = [d['label'] for d in tags_dict if d['id'] in show['tags']]
|
||||||
|
|
||||||
|
@ -42,39 +55,37 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
|
||||||
else:
|
else:
|
||||||
audio_language = []
|
audio_language = []
|
||||||
|
|
||||||
if action == 'update':
|
parsed_series = {
|
||||||
return {'title': show["title"],
|
'title': show["title"],
|
||||||
'path': show["path"],
|
'path': show["path"],
|
||||||
'tvdbId': int(show["tvdbId"]),
|
'tvdbId': int(show["tvdbId"]),
|
||||||
'sonarrSeriesId': int(show["id"]),
|
'sonarrSeriesId': int(show["id"]),
|
||||||
'overview': overview,
|
'overview': overview,
|
||||||
'poster': poster,
|
'poster': poster,
|
||||||
'fanart': fanart,
|
'fanart': fanart,
|
||||||
'audio_language': str(audio_language),
|
'audio_language': str(audio_language),
|
||||||
'sortTitle': show['sortTitle'],
|
'sortTitle': show['sortTitle'],
|
||||||
'year': str(show['year']),
|
'year': str(show['year']),
|
||||||
'alternativeTitles': alternate_titles,
|
'alternativeTitles': str(alternate_titles),
|
||||||
'tags': str(tags),
|
'tags': str(tags),
|
||||||
'seriesType': show['seriesType'],
|
'seriesType': show['seriesType'],
|
||||||
'imdbId': imdbId,
|
'imdbId': imdbId,
|
||||||
'monitored': str(bool(show['monitored']))}
|
'monitored': str(bool(show['monitored']))
|
||||||
else:
|
}
|
||||||
return {'title': show["title"],
|
|
||||||
'path': show["path"],
|
if action == 'insert':
|
||||||
'tvdbId': show["tvdbId"],
|
parsed_series['profileId'] = serie_default_profile
|
||||||
'sonarrSeriesId': show["id"],
|
|
||||||
'overview': overview,
|
if settings.general.serie_tag_enabled:
|
||||||
'poster': poster,
|
tag_profile = get_matching_profile(tags, language_profiles)
|
||||||
'fanart': fanart,
|
if tag_profile:
|
||||||
'audio_language': str(audio_language),
|
parsed_series['profileId'] = tag_profile
|
||||||
'sortTitle': show['sortTitle'],
|
remove_profile_tags_list = settings.general.remove_profile_tags
|
||||||
'year': str(show['year']),
|
if len(remove_profile_tags_list) > 0:
|
||||||
'alternativeTitles': alternate_titles,
|
if set(tags) & set(remove_profile_tags_list):
|
||||||
'tags': str(tags),
|
parsed_series['profileId'] = None
|
||||||
'seriesType': show['seriesType'],
|
|
||||||
'imdbId': imdbId,
|
return parsed_series
|
||||||
'profileId': serie_default_profile,
|
|
||||||
'monitored': str(bool(show['monitored']))}
|
|
||||||
|
|
||||||
|
|
||||||
def profile_id_to_language(id_, profiles):
|
def profile_id_to_language(id_, profiles):
|
||||||
|
@ -111,13 +122,13 @@ def episodeParser(episode):
|
||||||
item = episode['episodeFile']['language']
|
item = episode['episodeFile']['language']
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
if 'name' in item:
|
if 'name' in item:
|
||||||
audio_language.append(item['name'])
|
audio_language.append(audio_language_from_name(item['name']))
|
||||||
elif 'languages' in episode['episodeFile'] and len(episode['episodeFile']['languages']):
|
elif 'languages' in episode['episodeFile'] and len(episode['episodeFile']['languages']):
|
||||||
items = episode['episodeFile']['languages']
|
items = episode['episodeFile']['languages']
|
||||||
if isinstance(items, list):
|
if isinstance(items, list):
|
||||||
for item in items:
|
for item in items:
|
||||||
if 'name' in item:
|
if 'name' in item:
|
||||||
audio_language.append(item['name'])
|
audio_language.append(audio_language_from_name(item['name']))
|
||||||
else:
|
else:
|
||||||
audio_language = database.execute(
|
audio_language = database.execute(
|
||||||
select(TableShows.audio_language)
|
select(TableShows.audio_language)
|
||||||
|
|
|
@ -26,6 +26,11 @@ def trace(message):
|
||||||
logging.debug(FEATURE_PREFIX + message)
|
logging.debug(FEATURE_PREFIX + message)
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_profiles():
|
||||||
|
return database.execute(
|
||||||
|
select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.name, TableLanguagesProfiles.tag)).all()
|
||||||
|
|
||||||
|
|
||||||
def get_series_monitored_table():
|
def get_series_monitored_table():
|
||||||
series_monitored = database.execute(
|
series_monitored = database.execute(
|
||||||
select(TableShows.tvdbId, TableShows.monitored))\
|
select(TableShows.tvdbId, TableShows.monitored))\
|
||||||
|
@ -58,6 +63,7 @@ def update_series(send_event=True):
|
||||||
|
|
||||||
audio_profiles = get_profile_list()
|
audio_profiles = get_profile_list()
|
||||||
tagsDict = get_tags()
|
tagsDict = get_tags()
|
||||||
|
language_profiles = get_language_profiles()
|
||||||
|
|
||||||
# Get shows data from Sonarr
|
# Get shows data from Sonarr
|
||||||
series = get_series_from_sonarr_api(apikey_sonarr=apikey_sonarr)
|
series = get_series_from_sonarr_api(apikey_sonarr=apikey_sonarr)
|
||||||
|
@ -111,6 +117,7 @@ def update_series(send_event=True):
|
||||||
|
|
||||||
if show['id'] in current_shows_db:
|
if show['id'] in current_shows_db:
|
||||||
updated_series = seriesParser(show, action='update', tags_dict=tagsDict,
|
updated_series = seriesParser(show, action='update', tags_dict=tagsDict,
|
||||||
|
language_profiles=language_profiles,
|
||||||
serie_default_profile=serie_default_profile,
|
serie_default_profile=serie_default_profile,
|
||||||
audio_profiles=audio_profiles)
|
audio_profiles=audio_profiles)
|
||||||
|
|
||||||
|
@ -132,6 +139,7 @@ def update_series(send_event=True):
|
||||||
event_stream(type='series', payload=show['id'])
|
event_stream(type='series', payload=show['id'])
|
||||||
else:
|
else:
|
||||||
added_series = seriesParser(show, action='insert', tags_dict=tagsDict,
|
added_series = seriesParser(show, action='insert', tags_dict=tagsDict,
|
||||||
|
language_profiles=language_profiles,
|
||||||
serie_default_profile=serie_default_profile,
|
serie_default_profile=serie_default_profile,
|
||||||
audio_profiles=audio_profiles)
|
audio_profiles=audio_profiles)
|
||||||
|
|
||||||
|
@ -203,7 +211,7 @@ def update_one_series(series_id, action):
|
||||||
|
|
||||||
audio_profiles = get_profile_list()
|
audio_profiles = get_profile_list()
|
||||||
tagsDict = get_tags()
|
tagsDict = get_tags()
|
||||||
|
language_profiles = get_language_profiles()
|
||||||
try:
|
try:
|
||||||
# Get series data from sonarr api
|
# Get series data from sonarr api
|
||||||
series = None
|
series = None
|
||||||
|
@ -215,10 +223,12 @@ def update_one_series(series_id, action):
|
||||||
else:
|
else:
|
||||||
if action == 'updated' and existing_series:
|
if action == 'updated' and existing_series:
|
||||||
series = seriesParser(series_data[0], action='update', tags_dict=tagsDict,
|
series = seriesParser(series_data[0], action='update', tags_dict=tagsDict,
|
||||||
|
language_profiles=language_profiles,
|
||||||
serie_default_profile=serie_default_profile,
|
serie_default_profile=serie_default_profile,
|
||||||
audio_profiles=audio_profiles)
|
audio_profiles=audio_profiles)
|
||||||
elif action == 'updated' and not existing_series:
|
elif action == 'updated' and not existing_series:
|
||||||
series = seriesParser(series_data[0], action='insert', tags_dict=tagsDict,
|
series = seriesParser(series_data[0], action='insert', tags_dict=tagsDict,
|
||||||
|
language_profiles=language_profiles,
|
||||||
serie_default_profile=serie_default_profile,
|
serie_default_profile=serie_default_profile,
|
||||||
audio_profiles=audio_profiles)
|
audio_profiles=audio_profiles)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -182,7 +182,9 @@ def list_missing_subtitles_movies(no=None, send_event=True):
|
||||||
if any(x['code2'] == language['language'] for x in get_audio_profile_languages(
|
if any(x['code2'] == language['language'] for x in get_audio_profile_languages(
|
||||||
movie_subtitles.audio_language)):
|
movie_subtitles.audio_language)):
|
||||||
continue
|
continue
|
||||||
desired_subtitles_list.append([language['language'], language['forced'], language['hi']])
|
desired_subtitles_list.append({'language': language['language'],
|
||||||
|
'forced': language['forced'],
|
||||||
|
'hi': language['hi']})
|
||||||
|
|
||||||
# get existing subtitles
|
# get existing subtitles
|
||||||
actual_subtitles_list = []
|
actual_subtitles_list = []
|
||||||
|
@ -204,7 +206,9 @@ def list_missing_subtitles_movies(no=None, send_event=True):
|
||||||
elif subtitles[1] == 'hi':
|
elif subtitles[1] == 'hi':
|
||||||
forced = False
|
forced = False
|
||||||
hi = True
|
hi = True
|
||||||
actual_subtitles_list.append([lang, str(forced), str(hi)])
|
actual_subtitles_list.append({'language': lang,
|
||||||
|
'forced': str(forced),
|
||||||
|
'hi': str(hi)})
|
||||||
|
|
||||||
# check if cutoff is reached and skip any further check
|
# check if cutoff is reached and skip any further check
|
||||||
cutoff_met = False
|
cutoff_met = False
|
||||||
|
@ -212,7 +216,9 @@ def list_missing_subtitles_movies(no=None, send_event=True):
|
||||||
|
|
||||||
if cutoff_temp_list:
|
if cutoff_temp_list:
|
||||||
for cutoff_temp in cutoff_temp_list:
|
for cutoff_temp in cutoff_temp_list:
|
||||||
cutoff_language = [cutoff_temp['language'], cutoff_temp['forced'], cutoff_temp['hi']]
|
cutoff_language = {'language': cutoff_temp['language'],
|
||||||
|
'forced': cutoff_temp['forced'],
|
||||||
|
'hi': cutoff_temp['hi']}
|
||||||
if cutoff_temp['audio_exclude'] == 'True' and \
|
if cutoff_temp['audio_exclude'] == 'True' and \
|
||||||
any(x['code2'] == cutoff_temp['language'] for x in
|
any(x['code2'] == cutoff_temp['language'] for x in
|
||||||
get_audio_profile_languages(movie_subtitles.audio_language)):
|
get_audio_profile_languages(movie_subtitles.audio_language)):
|
||||||
|
@ -220,7 +226,10 @@ def list_missing_subtitles_movies(no=None, send_event=True):
|
||||||
elif cutoff_language in actual_subtitles_list:
|
elif cutoff_language in actual_subtitles_list:
|
||||||
cutoff_met = True
|
cutoff_met = True
|
||||||
# HI is considered as good as normal
|
# HI is considered as good as normal
|
||||||
elif cutoff_language and [cutoff_language[0], 'False', 'True'] in actual_subtitles_list:
|
elif (cutoff_language and
|
||||||
|
{'language': cutoff_language['language'],
|
||||||
|
'forced': 'False',
|
||||||
|
'hi': 'True'} in actual_subtitles_list):
|
||||||
cutoff_met = True
|
cutoff_met = True
|
||||||
|
|
||||||
if cutoff_met:
|
if cutoff_met:
|
||||||
|
@ -232,21 +241,23 @@ def list_missing_subtitles_movies(no=None, send_event=True):
|
||||||
if item not in actual_subtitles_list:
|
if item not in actual_subtitles_list:
|
||||||
missing_subtitles_list.append(item)
|
missing_subtitles_list.append(item)
|
||||||
|
|
||||||
# remove missing that have forced or hi subtitles for this language in existing
|
# remove missing that have forced or hi subtitles for this language in existing
|
||||||
for item in actual_subtitles_list:
|
for item in actual_subtitles_list:
|
||||||
if item[2] == 'True':
|
if item['hi'] == 'True':
|
||||||
try:
|
try:
|
||||||
missing_subtitles_list.remove([item[0], 'False', 'False'])
|
missing_subtitles_list.remove({'language': item['language'],
|
||||||
|
'forced': 'False',
|
||||||
|
'hi': 'False'})
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# make the missing languages list looks like expected
|
# make the missing languages list looks like expected
|
||||||
missing_subtitles_output_list = []
|
missing_subtitles_output_list = []
|
||||||
for item in missing_subtitles_list:
|
for item in missing_subtitles_list:
|
||||||
lang = item[0]
|
lang = item['language']
|
||||||
if item[1] == 'True':
|
if item['forced'] == 'True':
|
||||||
lang += ':forced'
|
lang += ':forced'
|
||||||
elif item[2] == 'True':
|
elif item['hi'] == 'True':
|
||||||
lang += ':hi'
|
lang += ':hi'
|
||||||
missing_subtitles_output_list.append(lang)
|
missing_subtitles_output_list.append(lang)
|
||||||
|
|
||||||
|
|
|
@ -182,7 +182,9 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
||||||
if any(x['code2'] == language['language'] for x in get_audio_profile_languages(
|
if any(x['code2'] == language['language'] for x in get_audio_profile_languages(
|
||||||
episode_subtitles.audio_language)):
|
episode_subtitles.audio_language)):
|
||||||
continue
|
continue
|
||||||
desired_subtitles_list.append([language['language'], language['forced'], language['hi']])
|
desired_subtitles_list.append({'language': language['language'],
|
||||||
|
'forced': language['forced'],
|
||||||
|
'hi': language['hi']})
|
||||||
|
|
||||||
# get existing subtitles
|
# get existing subtitles
|
||||||
actual_subtitles_list = []
|
actual_subtitles_list = []
|
||||||
|
@ -204,7 +206,9 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
||||||
elif subtitles[1] == 'hi':
|
elif subtitles[1] == 'hi':
|
||||||
forced = False
|
forced = False
|
||||||
hi = True
|
hi = True
|
||||||
actual_subtitles_list.append([lang, str(forced), str(hi)])
|
actual_subtitles_list.append({'language': lang,
|
||||||
|
'forced': str(forced),
|
||||||
|
'hi': str(hi)})
|
||||||
|
|
||||||
# check if cutoff is reached and skip any further check
|
# check if cutoff is reached and skip any further check
|
||||||
cutoff_met = False
|
cutoff_met = False
|
||||||
|
@ -212,7 +216,9 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
||||||
|
|
||||||
if cutoff_temp_list:
|
if cutoff_temp_list:
|
||||||
for cutoff_temp in cutoff_temp_list:
|
for cutoff_temp in cutoff_temp_list:
|
||||||
cutoff_language = [cutoff_temp['language'], cutoff_temp['forced'], cutoff_temp['hi']]
|
cutoff_language = {'language': cutoff_temp['language'],
|
||||||
|
'forced': cutoff_temp['forced'],
|
||||||
|
'hi': cutoff_temp['hi']}
|
||||||
if cutoff_temp['audio_exclude'] == 'True' and \
|
if cutoff_temp['audio_exclude'] == 'True' and \
|
||||||
any(x['code2'] == cutoff_temp['language'] for x in
|
any(x['code2'] == cutoff_temp['language'] for x in
|
||||||
get_audio_profile_languages(episode_subtitles.audio_language)):
|
get_audio_profile_languages(episode_subtitles.audio_language)):
|
||||||
|
@ -220,7 +226,10 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
||||||
elif cutoff_language in actual_subtitles_list:
|
elif cutoff_language in actual_subtitles_list:
|
||||||
cutoff_met = True
|
cutoff_met = True
|
||||||
# HI is considered as good as normal
|
# HI is considered as good as normal
|
||||||
elif [cutoff_language[0], 'False', 'True'] in actual_subtitles_list:
|
elif (cutoff_language and
|
||||||
|
{'language': cutoff_language['language'],
|
||||||
|
'forced': 'False',
|
||||||
|
'hi': 'True'} in actual_subtitles_list):
|
||||||
cutoff_met = True
|
cutoff_met = True
|
||||||
|
|
||||||
if cutoff_met:
|
if cutoff_met:
|
||||||
|
@ -234,21 +243,23 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
|
||||||
if item not in actual_subtitles_list:
|
if item not in actual_subtitles_list:
|
||||||
missing_subtitles_list.append(item)
|
missing_subtitles_list.append(item)
|
||||||
|
|
||||||
# remove missing that have hi subtitles for this language in existing
|
# remove missing that have hi subtitles for this language in existing
|
||||||
for item in actual_subtitles_list:
|
for item in actual_subtitles_list:
|
||||||
if item[2] == 'True':
|
if item['hi'] == 'True':
|
||||||
try:
|
try:
|
||||||
missing_subtitles_list.remove([item[0], 'False', 'False'])
|
missing_subtitles_list.remove({'language': item['language'],
|
||||||
|
'forced': 'False',
|
||||||
|
'hi': 'False'})
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# make the missing languages list looks like expected
|
# make the missing languages list looks like expected
|
||||||
missing_subtitles_output_list = []
|
missing_subtitles_output_list = []
|
||||||
for item in missing_subtitles_list:
|
for item in missing_subtitles_list:
|
||||||
lang = item[0]
|
lang = item['language']
|
||||||
if item[1] == 'True':
|
if item['forced'] == 'True':
|
||||||
lang += ':forced'
|
lang += ':forced'
|
||||||
elif item[2] == 'True':
|
elif item['hi'] == 'True':
|
||||||
lang += ':hi'
|
lang += ':hi'
|
||||||
missing_subtitles_output_list.append(lang)
|
missing_subtitles_output_list.append(lang)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
|
|
||||||
from guess_language import guess_language
|
from guess_language import guess_language
|
||||||
from subliminal_patch import core
|
from subliminal_patch import core
|
||||||
|
@ -136,6 +135,7 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
|
||||||
continue
|
continue
|
||||||
text = text.decode(encoding)
|
text = text.decode(encoding)
|
||||||
|
|
||||||
if bool(re.search(core.HI_REGEX, text)):
|
if core.parse_for_hi_regex(subtitle_text=text,
|
||||||
|
alpha3_language=language.alpha3 if hasattr(language, 'alpha3') else None):
|
||||||
subtitles[subtitle] = Language.rebuild(subtitles[subtitle], forced=False, hi=True)
|
subtitles[subtitle] = Language.rebuild(subtitles[subtitle], forced=False, hi=True)
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
|
@ -158,8 +158,9 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide
|
||||||
subtitle.language.forced = True
|
subtitle.language.forced = True
|
||||||
else:
|
else:
|
||||||
subtitle.language.forced = False
|
subtitle.language.forced = False
|
||||||
if use_original_format == 'True':
|
if use_original_format in ("1", "True"):
|
||||||
subtitle.use_original_format = use_original_format
|
subtitle.use_original_format = True
|
||||||
|
|
||||||
subtitle.mods = get_array_from(settings.general.subzero_mods)
|
subtitle.mods = get_array_from(settings.general.subzero_mods)
|
||||||
video = get_video(force_unicode(path), title, sceneName, providers={provider}, media_type=media_type)
|
video = get_video(force_unicode(path), title, sceneName, providers={provider}, media_type=media_type)
|
||||||
if video:
|
if video:
|
||||||
|
|
|
@ -88,6 +88,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
|
||||||
from .sync import sync_subtitles
|
from .sync import sync_subtitles
|
||||||
sync_subtitles(video_path=path, srt_path=downloaded_path,
|
sync_subtitles(video_path=path, srt_path=downloaded_path,
|
||||||
forced=subtitle.language.forced,
|
forced=subtitle.language.forced,
|
||||||
|
hi=subtitle.language.hi,
|
||||||
srt_lang=downloaded_language_code2,
|
srt_lang=downloaded_language_code2,
|
||||||
percent_score=percent_score,
|
percent_score=percent_score,
|
||||||
sonarr_series_id=episode_metadata.sonarrSeriesId,
|
sonarr_series_id=episode_metadata.sonarrSeriesId,
|
||||||
|
@ -106,6 +107,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u
|
||||||
from .sync import sync_subtitles
|
from .sync import sync_subtitles
|
||||||
sync_subtitles(video_path=path, srt_path=downloaded_path,
|
sync_subtitles(video_path=path, srt_path=downloaded_path,
|
||||||
forced=subtitle.language.forced,
|
forced=subtitle.language.forced,
|
||||||
|
hi=subtitle.language.hi,
|
||||||
srt_lang=downloaded_language_code2,
|
srt_lang=downloaded_language_code2,
|
||||||
percent_score=percent_score,
|
percent_score=percent_score,
|
||||||
radarr_id=movie_metadata.radarrId)
|
radarr_id=movie_metadata.radarrId)
|
||||||
|
|
|
@ -4,10 +4,12 @@ from .ffprobe import refine_from_ffprobe
|
||||||
from .database import refine_from_db
|
from .database import refine_from_db
|
||||||
from .arr_history import refine_from_arr_history
|
from .arr_history import refine_from_arr_history
|
||||||
from .anidb import refine_from_anidb
|
from .anidb import refine_from_anidb
|
||||||
|
from .anilist import refine_from_anilist
|
||||||
|
|
||||||
registered = {
|
registered = {
|
||||||
"database": refine_from_db,
|
"database": refine_from_db,
|
||||||
"ffprobe": refine_from_ffprobe,
|
"ffprobe": refine_from_ffprobe,
|
||||||
"arr_history": refine_from_arr_history,
|
"arr_history": refine_from_arr_history,
|
||||||
"anidb": refine_from_anidb,
|
"anidb": refine_from_anidb,
|
||||||
|
"anilist": refine_from_anilist, # Must run AFTER AniDB
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from subliminal import Episode, region
|
from subliminal import Episode, region
|
||||||
|
from subliminal.cache import REFINER_EXPIRATION_TIME
|
||||||
|
from subliminal_patch.exceptions import TooManyRequests
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
@ -18,16 +20,40 @@ except ImportError:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
refined_providers = {'animetosho'}
|
refined_providers = {'animetosho', 'jimaku'}
|
||||||
|
providers_requiring_anidb_api = {'animetosho'}
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
api_url = 'http://api.anidb.net:9001/httpapi'
|
api_url = 'http://api.anidb.net:9001/httpapi'
|
||||||
|
|
||||||
|
cache_key_refiner = "anidb_refiner"
|
||||||
|
|
||||||
|
# Soft Limit for amount of requests per day
|
||||||
|
daily_limit_request_count = 200
|
||||||
|
|
||||||
|
|
||||||
class AniDBClient(object):
|
class AniDBClient(object):
|
||||||
def __init__(self, api_client_key=None, api_client_ver=1, session=None):
|
def __init__(self, api_client_key=None, api_client_ver=1, session=None):
|
||||||
self.session = session or requests.Session()
|
self.session = session or requests.Session()
|
||||||
self.api_client_key = api_client_key
|
self.api_client_key = api_client_key
|
||||||
self.api_client_ver = api_client_ver
|
self.api_client_ver = api_client_ver
|
||||||
|
self.cache = region.get(cache_key_refiner, expiration_time=timedelta(days=1).total_seconds())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_throttled(self):
|
||||||
|
return self.cache and self.cache.get('is_throttled')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_api_credentials(self):
|
||||||
|
return self.api_client_key != '' and self.api_client_key is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def daily_api_request_count(self):
|
||||||
|
if not self.cache:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return self.cache.get('daily_api_request_count', 0)
|
||||||
|
|
||||||
AnimeInfo = namedtuple('AnimeInfo', ['anime', 'episode_offset'])
|
AnimeInfo = namedtuple('AnimeInfo', ['anime', 'episode_offset'])
|
||||||
|
|
||||||
|
@ -43,7 +69,9 @@ class AniDBClient(object):
|
||||||
return r.content
|
return r.content
|
||||||
|
|
||||||
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
|
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
|
||||||
def get_series_id(self, mappings, tvdb_series_season, tvdb_series_id, episode):
|
def get_show_information(self, tvdb_series_id, tvdb_series_season, episode):
|
||||||
|
mappings = etree.fromstring(self.get_series_mappings())
|
||||||
|
|
||||||
# Enrich the collection of anime with the episode offset
|
# Enrich the collection of anime with the episode offset
|
||||||
animes = [
|
animes = [
|
||||||
self.AnimeInfo(anime, int(anime.attrib.get('episodeoffset', 0)))
|
self.AnimeInfo(anime, int(anime.attrib.get('episodeoffset', 0)))
|
||||||
|
@ -52,40 +80,78 @@ class AniDBClient(object):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
is_special_entry = False
|
||||||
if not animes:
|
if not animes:
|
||||||
return None, None
|
# Some entries will store TVDB seasons in a nested mapping list, identifiable by the value 'a' as the season
|
||||||
|
special_entries = mappings.findall(
|
||||||
|
f".//anime[@tvdbid='{tvdb_series_id}'][@defaulttvdbseason='a']"
|
||||||
|
)
|
||||||
|
|
||||||
# Sort the anime by offset in ascending order
|
if not special_entries:
|
||||||
animes.sort(key=lambda a: a.episode_offset)
|
return None, None, None
|
||||||
|
|
||||||
# Different from Tvdb, Anidb have different ids for the Parts of a season
|
is_special_entry = True
|
||||||
anidb_id = None
|
for special_entry in special_entries:
|
||||||
offset = 0
|
mapping_list = special_entry.findall(f".//mapping[@tvdbseason='{tvdb_series_season}']")
|
||||||
|
if len(mapping_list) > 0:
|
||||||
|
anidb_id = int(special_entry.attrib.get('anidbid'))
|
||||||
|
offset = int(mapping_list[0].attrib.get('offset', 0))
|
||||||
|
|
||||||
for index, anime_info in enumerate(animes):
|
if not is_special_entry:
|
||||||
anime, episode_offset = anime_info
|
# Sort the anime by offset in ascending order
|
||||||
anidb_id = int(anime.attrib.get('anidbid'))
|
animes.sort(key=lambda a: a.episode_offset)
|
||||||
if episode > episode_offset:
|
|
||||||
anidb_id = anidb_id
|
|
||||||
offset = episode_offset
|
|
||||||
|
|
||||||
return anidb_id, episode - offset
|
# Different from Tvdb, Anidb have different ids for the Parts of a season
|
||||||
|
anidb_id = None
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
for index, anime_info in enumerate(animes):
|
||||||
|
anime, episode_offset = anime_info
|
||||||
|
|
||||||
|
mapping_list = anime.find('mapping-list')
|
||||||
|
|
||||||
|
# Handle mapping list for Specials
|
||||||
|
if mapping_list:
|
||||||
|
for mapping in mapping_list.findall("mapping"):
|
||||||
|
if mapping.text is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Mapping values are usually like ;1-1;2-1;3-1;
|
||||||
|
for episode_ref in mapping.text.split(';'):
|
||||||
|
if not episode_ref:
|
||||||
|
continue
|
||||||
|
|
||||||
|
anidb_episode, tvdb_episode = map(int, episode_ref.split('-'))
|
||||||
|
if tvdb_episode == episode:
|
||||||
|
anidb_id = int(anime.attrib.get('anidbid'))
|
||||||
|
|
||||||
|
return anidb_id, anidb_episode, 0
|
||||||
|
|
||||||
|
if episode > episode_offset:
|
||||||
|
anidb_id = int(anime.attrib.get('anidbid'))
|
||||||
|
offset = episode_offset
|
||||||
|
|
||||||
|
return anidb_id, episode - offset, offset
|
||||||
|
|
||||||
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
|
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
|
||||||
def get_series_episodes_ids(self, tvdb_series_id, season, episode):
|
def get_episode_ids(self, series_id, episode_no):
|
||||||
mappings = etree.fromstring(self.get_series_mappings())
|
|
||||||
|
|
||||||
series_id, episode_no = self.get_series_id(mappings, season, tvdb_series_id, episode)
|
|
||||||
|
|
||||||
if not series_id:
|
if not series_id:
|
||||||
return None, None
|
return None
|
||||||
|
|
||||||
episodes = etree.fromstring(self.get_episodes(series_id))
|
episodes = etree.fromstring(self.get_episodes(series_id))
|
||||||
|
|
||||||
return series_id, int(episodes.find(f".//episode[epno='{episode_no}']").attrib.get('id'))
|
episode = episodes.find(f".//episode[epno='{episode_no}']")
|
||||||
|
|
||||||
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
|
if not episode:
|
||||||
|
return series_id, None
|
||||||
|
|
||||||
|
return series_id, int(episode.attrib.get('id'))
|
||||||
|
|
||||||
|
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
|
||||||
def get_episodes(self, series_id):
|
def get_episodes(self, series_id):
|
||||||
|
if self.daily_api_request_count >= 200:
|
||||||
|
raise TooManyRequests('Daily API request limit exceeded')
|
||||||
|
|
||||||
r = self.session.get(
|
r = self.session.get(
|
||||||
api_url,
|
api_url,
|
||||||
params={
|
params={
|
||||||
|
@ -102,10 +168,12 @@ class AniDBClient(object):
|
||||||
|
|
||||||
response_code = xml_root.attrib.get('code')
|
response_code = xml_root.attrib.get('code')
|
||||||
if response_code == '500':
|
if response_code == '500':
|
||||||
raise HTTPError('AniDB API Abuse detected. Banned status.')
|
raise TooManyRequests('AniDB API Abuse detected. Banned status.')
|
||||||
elif response_code == '302':
|
elif response_code == '302':
|
||||||
raise HTTPError('AniDB API Client error. Client is disabled or does not exists.')
|
raise HTTPError('AniDB API Client error. Client is disabled or does not exists.')
|
||||||
|
|
||||||
|
self.increment_daily_quota()
|
||||||
|
|
||||||
episode_elements = xml_root.find('episodes')
|
episode_elements = xml_root.find('episodes')
|
||||||
|
|
||||||
if not episode_elements:
|
if not episode_elements:
|
||||||
|
@ -113,11 +181,25 @@ class AniDBClient(object):
|
||||||
|
|
||||||
return etree.tostring(episode_elements, encoding='utf8', method='xml')
|
return etree.tostring(episode_elements, encoding='utf8', method='xml')
|
||||||
|
|
||||||
|
def increment_daily_quota(self):
|
||||||
|
daily_quota = self.daily_api_request_count + 1
|
||||||
|
|
||||||
|
if not self.cache:
|
||||||
|
region.set(cache_key_refiner, {'daily_api_request_count': daily_quota})
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cache['daily_api_request_count'] = daily_quota
|
||||||
|
|
||||||
|
region.set(cache_key_refiner, self.cache)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mark_as_throttled():
|
||||||
|
region.set(cache_key_refiner, {'is_throttled': True})
|
||||||
|
|
||||||
|
|
||||||
def refine_from_anidb(path, video):
|
def refine_from_anidb(path, video):
|
||||||
if not isinstance(video, Episode) or not video.series_tvdb_id:
|
if not isinstance(video, Episode) or not video.series_tvdb_id:
|
||||||
logging.debug(f'Video is not an Anime TV series, skipping refinement for {video}')
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if refined_providers.intersection(settings.general.enabled_providers) and video.series_anidb_id is None:
|
if refined_providers.intersection(settings.general.enabled_providers) and video.series_anidb_id is None:
|
||||||
|
@ -129,12 +211,35 @@ def refine_anidb_ids(video):
|
||||||
|
|
||||||
season = video.season if video.season else 0
|
season = video.season if video.season else 0
|
||||||
|
|
||||||
anidb_series_id, anidb_episode_id = anidb_client.get_series_episodes_ids(video.series_tvdb_id, season, video.episode)
|
anidb_series_id, anidb_episode_no, anidb_season_episode_offset = anidb_client.get_show_information(
|
||||||
|
video.series_tvdb_id,
|
||||||
if not anidb_episode_id:
|
season,
|
||||||
logging.error(f'Could not find anime series {video.series}')
|
video.episode,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not anidb_series_id:
|
||||||
|
logger.error(f'Could not find anime series {video.series}')
|
||||||
return video
|
return video
|
||||||
|
|
||||||
|
anidb_episode_id = None
|
||||||
|
if anidb_client.has_api_credentials:
|
||||||
|
if anidb_client.is_throttled:
|
||||||
|
logger.warning(f'API daily limit reached. Skipping episode ID refinement for {video.series}')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
anidb_episode_id = anidb_client.get_episode_ids(
|
||||||
|
anidb_series_id,
|
||||||
|
anidb_episode_no
|
||||||
|
)
|
||||||
|
except TooManyRequests:
|
||||||
|
logger.error(f'API daily limit reached while refining {video.series}')
|
||||||
|
anidb_client.mark_as_throttled()
|
||||||
|
else:
|
||||||
|
intersect = providers_requiring_anidb_api.intersection(settings.general.enabled_providers)
|
||||||
|
if len(intersect) >= 1:
|
||||||
|
logger.warn(f'AniDB API credentials are not fully set up, the following providers may not work: {intersect}')
|
||||||
|
|
||||||
video.series_anidb_id = anidb_series_id
|
video.series_anidb_id = anidb_series_id
|
||||||
video.series_anidb_episode_id = anidb_episode_id
|
video.series_anidb_episode_id = anidb_episode_id
|
||||||
|
video.series_anidb_episode_no = anidb_episode_no
|
||||||
|
video.series_anidb_season_episode_offset = anidb_season_episode_offset
|
||||||
|
|
79
bazarr/subtitles/refiners/anilist.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
from collections import namedtuple
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from subliminal import Episode, region, __short_version__
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
refined_providers = {'jimaku'}
|
||||||
|
|
||||||
|
|
||||||
|
class AniListClient(object):
|
||||||
|
def __init__(self, session=None, timeout=10):
|
||||||
|
self.session = session or requests.Session()
|
||||||
|
self.session.timeout = timeout
|
||||||
|
self.session.headers['Content-Type'] = 'application/json'
|
||||||
|
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
|
||||||
|
|
||||||
|
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
|
||||||
|
def get_series_mappings(self):
|
||||||
|
r = self.session.get(
|
||||||
|
'https://raw.githubusercontent.com/Fribb/anime-lists/master/anime-list-mini.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_series_id(self, candidate_id_name, candidate_id_value):
|
||||||
|
anime_list = self.get_series_mappings()
|
||||||
|
|
||||||
|
tag_map = {
|
||||||
|
"series_anidb_id": "anidb_id",
|
||||||
|
"imdb_id": "imdb_id"
|
||||||
|
}
|
||||||
|
mapped_tag = tag_map.get(candidate_id_name, candidate_id_name)
|
||||||
|
|
||||||
|
obj = [obj for obj in anime_list if mapped_tag in obj and str(obj[mapped_tag]) == str(candidate_id_value)]
|
||||||
|
logger.debug(f"Based on '{mapped_tag}': '{candidate_id_value}', anime-list matched: {obj}")
|
||||||
|
|
||||||
|
if len(obj) > 0:
|
||||||
|
return obj[0]["anilist_id"]
|
||||||
|
else:
|
||||||
|
logger.debug(f"Could not find corresponding AniList ID with '{mapped_tag}': {candidate_id_value}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def refine_from_anilist(path, video):
|
||||||
|
# Safety checks
|
||||||
|
if isinstance(video, Episode):
|
||||||
|
if not video.series_anidb_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
if refined_providers.intersection(settings.general.enabled_providers) and video.anilist_id is None:
|
||||||
|
refine_anilist_ids(video)
|
||||||
|
|
||||||
|
|
||||||
|
def refine_anilist_ids(video):
|
||||||
|
anilist_client = AniListClient()
|
||||||
|
|
||||||
|
if isinstance(video, Episode):
|
||||||
|
candidate_id_name = "series_anidb_id"
|
||||||
|
else:
|
||||||
|
candidate_id_name = "imdb_id"
|
||||||
|
|
||||||
|
candidate_id_value = getattr(video, candidate_id_name, None)
|
||||||
|
if not candidate_id_value:
|
||||||
|
logger.error(f"Found no value for property {candidate_id_name} of video.")
|
||||||
|
return video
|
||||||
|
|
||||||
|
anilist_id = anilist_client.get_series_id(candidate_id_name, candidate_id_value)
|
||||||
|
if not anilist_id:
|
||||||
|
return video
|
||||||
|
|
||||||
|
video.anilist_id = anilist_id
|
|
@ -8,7 +8,7 @@ from app.config import settings
|
||||||
from subtitles.tools.subsyncer import SubSyncer
|
from subtitles.tools.subsyncer import SubSyncer
|
||||||
|
|
||||||
|
|
||||||
def sync_subtitles(video_path, srt_path, srt_lang, forced, percent_score, sonarr_series_id=None,
|
def sync_subtitles(video_path, srt_path, srt_lang, forced, hi, percent_score, sonarr_series_id=None,
|
||||||
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.')
|
||||||
|
@ -30,6 +30,8 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, percent_score, sonarr
|
||||||
'video_path': video_path,
|
'video_path': video_path,
|
||||||
'srt_path': srt_path,
|
'srt_path': srt_path,
|
||||||
'srt_lang': srt_lang,
|
'srt_lang': srt_lang,
|
||||||
|
'forced': forced,
|
||||||
|
'hi': hi,
|
||||||
'max_offset_seconds': str(settings.subsync.max_offset_seconds),
|
'max_offset_seconds': str(settings.subsync.max_offset_seconds),
|
||||||
'no_fix_framerate': settings.subsync.no_fix_framerate,
|
'no_fix_framerate': settings.subsync.no_fix_framerate,
|
||||||
'gss': settings.subsync.gss,
|
'gss': settings.subsync.gss,
|
||||||
|
|
|
@ -30,7 +30,7 @@ class SubSyncer:
|
||||||
self.vad = 'subs_then_webrtc'
|
self.vad = 'subs_then_webrtc'
|
||||||
self.log_dir_path = os.path.join(args.config_dir, 'log')
|
self.log_dir_path = os.path.join(args.config_dir, 'log')
|
||||||
|
|
||||||
def sync(self, video_path, srt_path, srt_lang,
|
def sync(self, video_path, srt_path, srt_lang, hi, forced,
|
||||||
max_offset_seconds, no_fix_framerate, gss, reference=None,
|
max_offset_seconds, no_fix_framerate, gss, reference=None,
|
||||||
sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None):
|
sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None):
|
||||||
self.reference = video_path
|
self.reference = video_path
|
||||||
|
@ -97,8 +97,7 @@ class SubSyncer:
|
||||||
result = run(self.args)
|
result = run(self.args)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(
|
logging.exception(
|
||||||
f'BAZARR an exception occurs during the synchronization process for this subtitles: {self.srtin}')
|
f'BAZARR an exception occurs during the synchronization process for this subtitle file: {self.srtin}')
|
||||||
raise OSError
|
|
||||||
else:
|
else:
|
||||||
if settings.subsync.debug:
|
if settings.subsync.debug:
|
||||||
return result
|
return result
|
||||||
|
@ -118,10 +117,10 @@ class SubSyncer:
|
||||||
downloaded_language_code2=srt_lang,
|
downloaded_language_code2=srt_lang,
|
||||||
downloaded_provider=None,
|
downloaded_provider=None,
|
||||||
score=None,
|
score=None,
|
||||||
forced=None,
|
forced=forced,
|
||||||
subtitle_id=None,
|
subtitle_id=None,
|
||||||
reversed_subtitles_path=srt_path,
|
reversed_subtitles_path=srt_path,
|
||||||
hearing_impaired=None)
|
hearing_impaired=hi)
|
||||||
|
|
||||||
if sonarr_episode_id:
|
if sonarr_episode_id:
|
||||||
history_log(action=5, sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id,
|
history_log(action=5, sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id,
|
||||||
|
|
|
@ -6,12 +6,16 @@ import pysubs2
|
||||||
from subliminal_patch.core import get_subtitle_path
|
from subliminal_patch.core import get_subtitle_path
|
||||||
from subzero.language import Language
|
from subzero.language import Language
|
||||||
from deep_translator import GoogleTranslator
|
from deep_translator import GoogleTranslator
|
||||||
|
from deep_translator.exceptions import TooManyRequests, RequestError, TranslationNotFound
|
||||||
|
from time import sleep
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from languages.custom_lang import CustomLanguage
|
from languages.custom_lang import CustomLanguage
|
||||||
from languages.get_languages import alpha3_from_alpha2, language_from_alpha2, language_from_alpha3
|
from languages.get_languages import alpha3_from_alpha2, language_from_alpha2, language_from_alpha3
|
||||||
from radarr.history import history_log_movie
|
from radarr.history import history_log_movie
|
||||||
from sonarr.history import history_log
|
from sonarr.history import history_log
|
||||||
from subtitles.processing import ProcessSubtitlesResult
|
from subtitles.processing import ProcessSubtitlesResult
|
||||||
|
from app.event_handler import show_progress, hide_progress
|
||||||
|
|
||||||
|
|
||||||
def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, forced, hi, media_type, sonarr_series_id,
|
def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, forced, hi, media_type, sonarr_series_id,
|
||||||
|
@ -33,8 +37,6 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
||||||
|
|
||||||
logging.debug(f'BAZARR is translating in {lang_obj} this subtitles {source_srt_file}')
|
logging.debug(f'BAZARR is translating in {lang_obj} this subtitles {source_srt_file}')
|
||||||
|
|
||||||
max_characters = 5000
|
|
||||||
|
|
||||||
dest_srt_file = get_subtitle_path(video_path,
|
dest_srt_file = get_subtitle_path(video_path,
|
||||||
language=lang_obj if isinstance(lang_obj, Language) else lang_obj.subzero_language(),
|
language=lang_obj if isinstance(lang_obj, Language) else lang_obj.subzero_language(),
|
||||||
extension='.srt',
|
extension='.srt',
|
||||||
|
@ -44,40 +46,53 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
||||||
subs = pysubs2.load(source_srt_file, encoding='utf-8')
|
subs = pysubs2.load(source_srt_file, encoding='utf-8')
|
||||||
subs.remove_miscellaneous_events()
|
subs.remove_miscellaneous_events()
|
||||||
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)
|
lines_list_len = len(lines_list)
|
||||||
|
|
||||||
logging.debug(f'BAZARR splitting subtitles into {max_characters} characters blocks')
|
def translate_line(id, line, attempt):
|
||||||
lines_block_list = []
|
|
||||||
translated_lines_list = []
|
|
||||||
while len(joined_lines_str):
|
|
||||||
partial_lines_str = joined_lines_str[:max_characters]
|
|
||||||
|
|
||||||
if len(joined_lines_str) > max_characters:
|
|
||||||
new_partial_lines_str = partial_lines_str.rsplit('\n\n\n', 1)[0]
|
|
||||||
else:
|
|
||||||
new_partial_lines_str = partial_lines_str
|
|
||||||
|
|
||||||
lines_block_list.append(new_partial_lines_str)
|
|
||||||
joined_lines_str = joined_lines_str.replace(new_partial_lines_str, '')
|
|
||||||
|
|
||||||
logging.debug(f'BAZARR is sending {len(lines_block_list)} blocks to Google Translate')
|
|
||||||
for block_str in lines_block_list:
|
|
||||||
try:
|
try:
|
||||||
translated_partial_srt_text = GoogleTranslator(source='auto',
|
translated_text = GoogleTranslator(
|
||||||
target=language_code_convert_dict.get(lang_obj.alpha2,
|
source='auto',
|
||||||
lang_obj.alpha2)
|
target=language_code_convert_dict.get(lang_obj.alpha2, lang_obj.alpha2)
|
||||||
).translate(text=block_str)
|
).translate(text=line)
|
||||||
except Exception:
|
except TooManyRequests:
|
||||||
logging.exception(f'BAZARR Unable to translate subtitles {source_srt_file}')
|
if attempt <= 5:
|
||||||
return False
|
sleep(1)
|
||||||
|
super(translate_line(id, line, attempt+1))
|
||||||
|
else:
|
||||||
|
logging.debug(f'Too many requests while translating {line}')
|
||||||
|
translated_lines.append({'id': id, 'line': line})
|
||||||
|
except (RequestError, TranslationNotFound):
|
||||||
|
logging.debug(f'Unable to translate line {line}')
|
||||||
|
translated_lines.append({'id': id, 'line': line})
|
||||||
else:
|
else:
|
||||||
translated_partial_srt_list = translated_partial_srt_text.split('\n\n\n')
|
translated_lines.append({'id': id, 'line': translated_text})
|
||||||
translated_lines_list += translated_partial_srt_list
|
finally:
|
||||||
|
show_progress(id=f'translate_progress_{dest_srt_file}',
|
||||||
|
header=f'Translating subtitles lines to {language_from_alpha3(to_lang)}...',
|
||||||
|
name='',
|
||||||
|
value=len(translated_lines),
|
||||||
|
count=lines_list_len)
|
||||||
|
|
||||||
|
logging.debug(f'BAZARR is sending {lines_list_len} blocks to Google Translate')
|
||||||
|
|
||||||
|
pool = ThreadPoolExecutor(max_workers=10)
|
||||||
|
|
||||||
|
translated_lines = []
|
||||||
|
|
||||||
|
for i, line in enumerate(lines_list):
|
||||||
|
pool.submit(translate_line, i, line, 1)
|
||||||
|
|
||||||
|
pool.shutdown(wait=True)
|
||||||
|
|
||||||
|
for i, line in enumerate(translated_lines):
|
||||||
|
lines_list[line['id']] = line['line']
|
||||||
|
|
||||||
|
hide_progress(id=f'translate_progress_{dest_srt_file}')
|
||||||
|
|
||||||
logging.debug(f'BAZARR saving translated subtitles to {dest_srt_file}')
|
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 = lines_list[i]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.error(f'BAZARR is unable to translate malformed subtitles: {source_srt_file}')
|
logging.error(f'BAZARR is unable to translate malformed subtitles: {source_srt_file}')
|
||||||
return False
|
return False
|
||||||
|
@ -94,10 +109,10 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
||||||
downloaded_language_code2=to_lang,
|
downloaded_language_code2=to_lang,
|
||||||
downloaded_provider=None,
|
downloaded_provider=None,
|
||||||
score=None,
|
score=None,
|
||||||
forced=None,
|
forced=forced,
|
||||||
subtitle_id=None,
|
subtitle_id=None,
|
||||||
reversed_subtitles_path=dest_srt_file,
|
reversed_subtitles_path=dest_srt_file,
|
||||||
hearing_impaired=None)
|
hearing_impaired=hi)
|
||||||
|
|
||||||
if media_type == 'series':
|
if media_type == 'series':
|
||||||
history_log(action=6, sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id, result=result)
|
history_log(action=6, sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id, result=result)
|
||||||
|
|
|
@ -69,14 +69,12 @@ def upgrade_subtitles():
|
||||||
.join(TableEpisodes, onclause=TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId)
|
.join(TableEpisodes, onclause=TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId)
|
||||||
.join(episodes_to_upgrade, onclause=TableHistory.id == episodes_to_upgrade.c.id, isouter=True)
|
.join(episodes_to_upgrade, onclause=TableHistory.id == episodes_to_upgrade.c.id, isouter=True)
|
||||||
.where(episodes_to_upgrade.c.id.is_not(None)))
|
.where(episodes_to_upgrade.c.id.is_not(None)))
|
||||||
.all() if _language_still_desired(x.language, x.profileId)]
|
.all() if _language_still_desired(x.language, x.profileId) and
|
||||||
|
x.subtitles_path in x.external_subtitles and
|
||||||
|
x.video_path == x.path
|
||||||
|
]
|
||||||
|
|
||||||
for item in episodes_data:
|
for item in episodes_data:
|
||||||
if item['upgradable']:
|
|
||||||
if item['subtitles_path'] not in item['external_subtitles'] or \
|
|
||||||
not item['video_path'] == item['path']:
|
|
||||||
item.update({"upgradable": False})
|
|
||||||
|
|
||||||
del item['path']
|
del item['path']
|
||||||
del item['external_subtitles']
|
del item['external_subtitles']
|
||||||
|
|
||||||
|
@ -156,14 +154,12 @@ def upgrade_subtitles():
|
||||||
.join(TableMovies, onclause=TableHistoryMovie.radarrId == TableMovies.radarrId)
|
.join(TableMovies, onclause=TableHistoryMovie.radarrId == TableMovies.radarrId)
|
||||||
.join(movies_to_upgrade, onclause=TableHistoryMovie.id == movies_to_upgrade.c.id, isouter=True)
|
.join(movies_to_upgrade, onclause=TableHistoryMovie.id == movies_to_upgrade.c.id, isouter=True)
|
||||||
.where(movies_to_upgrade.c.id.is_not(None)))
|
.where(movies_to_upgrade.c.id.is_not(None)))
|
||||||
.all() if _language_still_desired(x.language, x.profileId)]
|
.all() if _language_still_desired(x.language, x.profileId) and
|
||||||
|
x.subtitles_path in x.external_subtitles and
|
||||||
|
x.video_path == x.path
|
||||||
|
]
|
||||||
|
|
||||||
for item in movies_data:
|
for item in movies_data:
|
||||||
if item['upgradable']:
|
|
||||||
if item['subtitles_path'] not in item['external_subtitles'] or \
|
|
||||||
not item['video_path'] == item['path']:
|
|
||||||
item.update({"upgradable": False})
|
|
||||||
|
|
||||||
del item['path']
|
del item['path']
|
||||||
del item['external_subtitles']
|
del item['external_subtitles']
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,7 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud
|
||||||
series_id = episode_metadata.sonarrSeriesId
|
series_id = episode_metadata.sonarrSeriesId
|
||||||
episode_id = episode_metadata.sonarrEpisodeId
|
episode_id = episode_metadata.sonarrEpisodeId
|
||||||
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100,
|
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100,
|
||||||
sonarr_series_id=episode_metadata.sonarrSeriesId, forced=forced,
|
sonarr_series_id=episode_metadata.sonarrSeriesId, forced=forced, hi=hi,
|
||||||
sonarr_episode_id=episode_metadata.sonarrEpisodeId)
|
sonarr_episode_id=episode_metadata.sonarrEpisodeId)
|
||||||
else:
|
else:
|
||||||
if not movie_metadata:
|
if not movie_metadata:
|
||||||
|
@ -146,7 +146,7 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, aud
|
||||||
series_id = ""
|
series_id = ""
|
||||||
episode_id = movie_metadata.radarrId
|
episode_id = movie_metadata.radarrId
|
||||||
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100,
|
sync_subtitles(video_path=path, srt_path=subtitle_path, srt_lang=uploaded_language_code2, percent_score=100,
|
||||||
radarr_id=movie_metadata.radarrId, forced=forced)
|
radarr_id=movie_metadata.radarrId, forced=forced, hi=hi)
|
||||||
|
|
||||||
if use_postprocessing:
|
if use_postprocessing:
|
||||||
command = pp_replace(postprocessing_cmd, path, subtitle_path, uploaded_language, uploaded_language_code2,
|
command = pp_replace(postprocessing_cmd, path, subtitle_path, uploaded_language, uploaded_language_code2,
|
||||||
|
|
|
@ -121,7 +121,9 @@ def subtitles_sync_references(subtitles_path, sonarr_episode_id=None, radarr_mov
|
||||||
if not media_data:
|
if not media_data:
|
||||||
return references_dict
|
return references_dict
|
||||||
|
|
||||||
data = parse_video_metadata(media_data.path, media_data.file_size, media_data.episode_file_id, None,
|
mapped_path = path_mappings.path_replace(media_data.path)
|
||||||
|
|
||||||
|
data = parse_video_metadata(mapped_path, media_data.file_size, media_data.episode_file_id, None,
|
||||||
use_cache=True)
|
use_cache=True)
|
||||||
elif radarr_movie_id:
|
elif radarr_movie_id:
|
||||||
media_data = database.execute(
|
media_data = database.execute(
|
||||||
|
@ -132,7 +134,9 @@ def subtitles_sync_references(subtitles_path, sonarr_episode_id=None, radarr_mov
|
||||||
if not media_data:
|
if not media_data:
|
||||||
return references_dict
|
return references_dict
|
||||||
|
|
||||||
data = parse_video_metadata(media_data.path, media_data.file_size, None, media_data.movie_file_id,
|
mapped_path = path_mappings.path_replace_movie(media_data.path)
|
||||||
|
|
||||||
|
data = parse_video_metadata(mapped_path, media_data.file_size, None, media_data.movie_file_id,
|
||||||
use_cache=True)
|
use_cache=True)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
@ -213,6 +217,25 @@ def subtitles_sync_references(subtitles_path, sonarr_episode_id=None, radarr_mov
|
||||||
|
|
||||||
|
|
||||||
def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=None, use_cache=True):
|
def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=None, use_cache=True):
|
||||||
|
"""
|
||||||
|
This function return the video file properties as parsed by knowit using ffprobe or mediainfo using the cached
|
||||||
|
value by default.
|
||||||
|
|
||||||
|
@type file: string
|
||||||
|
@param file: Properly mapped path of a video file
|
||||||
|
@type file_size: int
|
||||||
|
@param file_size: File size in bytes of the video file
|
||||||
|
@type episode_file_id: int or None
|
||||||
|
@param episode_file_id: episode ID of the video file from Sonarr (or None if it's a movie)
|
||||||
|
@type movie_file_id: int or None
|
||||||
|
@param movie_file_id: movie ID of the video file from Radarr (or None if it's an episode)
|
||||||
|
@type use_cache: bool
|
||||||
|
@param use_cache:
|
||||||
|
|
||||||
|
@rtype: dict or None
|
||||||
|
@return: return a dictionary including the video file properties as parsed by ffprobe or mediainfo
|
||||||
|
"""
|
||||||
|
|
||||||
# Define default data keys value
|
# Define default data keys value
|
||||||
data = {
|
data = {
|
||||||
"ffprobe": {},
|
"ffprobe": {},
|
||||||
|
@ -228,12 +251,12 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No
|
||||||
if episode_file_id:
|
if episode_file_id:
|
||||||
cache_key = database.execute(
|
cache_key = database.execute(
|
||||||
select(TableEpisodes.ffprobe_cache)
|
select(TableEpisodes.ffprobe_cache)
|
||||||
.where(TableEpisodes.path == path_mappings.path_replace_reverse(file))) \
|
.where(TableEpisodes.episode_file_id == episode_file_id)) \
|
||||||
.first()
|
.first()
|
||||||
elif movie_file_id:
|
elif movie_file_id:
|
||||||
cache_key = database.execute(
|
cache_key = database.execute(
|
||||||
select(TableMovies.ffprobe_cache)
|
select(TableMovies.ffprobe_cache)
|
||||||
.where(TableMovies.path == path_mappings.path_replace_reverse_movie(file))) \
|
.where(TableMovies.movie_file_id == movie_file_id)) \
|
||||||
.first()
|
.first()
|
||||||
else:
|
else:
|
||||||
cache_key = None
|
cache_key = None
|
||||||
|
@ -243,6 +266,7 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No
|
||||||
# Unpickle ffprobe cache
|
# Unpickle ffprobe cache
|
||||||
cached_value = pickle.loads(cache_key.ffprobe_cache)
|
cached_value = pickle.loads(cache_key.ffprobe_cache)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# No cached value available, we'll parse the file
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Check if file size and file id matches and if so, we return the cached value if available for the
|
# Check if file size and file id matches and if so, we return the cached value if available for the
|
||||||
|
@ -281,9 +305,7 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No
|
||||||
# or if we have mediainfo available
|
# or if we have mediainfo available
|
||||||
elif mediainfo_path:
|
elif mediainfo_path:
|
||||||
try:
|
try:
|
||||||
# disabling mediainfo path temporarily until issue with knowit is fixed.
|
data["mediainfo"] = know(video_path=file, context={"provider": "mediainfo", "mediainfo": mediainfo_path})
|
||||||
# data["mediainfo"] = know(video_path=file, context={"provider": "mediainfo", "mediainfo": mediainfo_path})
|
|
||||||
data["mediainfo"] = know(video_path=file, context={"provider": "mediainfo"})
|
|
||||||
except KnowitException as e:
|
except KnowitException as e:
|
||||||
logging.error(f"BAZARR mediainfo cannot analyze this video file {file}. Could it be corrupted? {e}")
|
logging.error(f"BAZARR mediainfo cannot analyze this video file {file}. Could it be corrupted? {e}")
|
||||||
return None
|
return None
|
||||||
|
@ -291,19 +313,19 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No
|
||||||
else:
|
else:
|
||||||
logging.error("BAZARR require ffmpeg/ffprobe or mediainfo, please install it and make sure to choose it in "
|
logging.error("BAZARR require ffmpeg/ffprobe or mediainfo, please install it and make sure to choose it in "
|
||||||
"Settings-->Subtitles.")
|
"Settings-->Subtitles.")
|
||||||
return
|
return None
|
||||||
|
|
||||||
# we write to db the result and return the newly cached ffprobe dict
|
# we write to db the result and return the newly cached ffprobe dict
|
||||||
if episode_file_id:
|
if episode_file_id:
|
||||||
database.execute(
|
database.execute(
|
||||||
update(TableEpisodes)
|
update(TableEpisodes)
|
||||||
.values(ffprobe_cache=pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
|
.values(ffprobe_cache=pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
|
||||||
.where(TableEpisodes.path == path_mappings.path_replace_reverse(file)))
|
.where(TableEpisodes.episode_file_id == episode_file_id))
|
||||||
elif movie_file_id:
|
elif movie_file_id:
|
||||||
database.execute(
|
database.execute(
|
||||||
update(TableMovies)
|
update(TableMovies)
|
||||||
.values(ffprobe_cache=pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
|
.values(ffprobe_cache=pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
|
||||||
.where(TableMovies.path == path_mappings.path_replace_reverse_movie(file)))
|
.where(TableMovies.movie_file_id == movie_file_id))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,8 @@ class Episode(Video):
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, series, season, episode, title=None, year=None, original_series=True, tvdb_id=None,
|
def __init__(self, name, series, season, episode, title=None, year=None, original_series=True, tvdb_id=None,
|
||||||
series_tvdb_id=None, series_imdb_id=None, alternative_series=None, series_anidb_id=None,
|
series_tvdb_id=None, series_imdb_id=None, alternative_series=None, series_anidb_id=None,
|
||||||
series_anidb_episode_id=None, **kwargs):
|
series_anidb_episode_id=None, series_anidb_season_episode_offset=None,
|
||||||
|
anilist_id=None, **kwargs):
|
||||||
super(Episode, self).__init__(name, **kwargs)
|
super(Episode, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
#: Series of the episode
|
#: Series of the episode
|
||||||
|
@ -163,8 +164,11 @@ class Episode(Video):
|
||||||
#: Alternative names of the series
|
#: Alternative names of the series
|
||||||
self.alternative_series = alternative_series or []
|
self.alternative_series = alternative_series or []
|
||||||
|
|
||||||
|
#: Anime specific information
|
||||||
self.series_anidb_episode_id = series_anidb_episode_id
|
self.series_anidb_episode_id = series_anidb_episode_id
|
||||||
self.series_anidb_id = series_anidb_id
|
self.series_anidb_id = series_anidb_id
|
||||||
|
self.series_anidb_season_episode_offset = series_anidb_season_episode_offset
|
||||||
|
self.anilist_id = anilist_id
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromguess(cls, name, guess):
|
def fromguess(cls, name, guess):
|
||||||
|
@ -207,10 +211,11 @@ class Movie(Video):
|
||||||
:param str title: title of the movie.
|
:param str title: title of the movie.
|
||||||
:param int year: year of the movie.
|
:param int year: year of the movie.
|
||||||
:param list alternative_titles: alternative titles of the movie
|
:param list alternative_titles: alternative titles of the movie
|
||||||
|
:param int anilist_id: AniList ID of movie (if Anime)
|
||||||
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, title, year=None, alternative_titles=None, **kwargs):
|
def __init__(self, name, title, year=None, alternative_titles=None, anilist_id=None, **kwargs):
|
||||||
super(Movie, self).__init__(name, **kwargs)
|
super(Movie, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
#: Title of the movie
|
#: Title of the movie
|
||||||
|
@ -221,6 +226,9 @@ class Movie(Video):
|
||||||
|
|
||||||
#: Alternative titles of the movie
|
#: Alternative titles of the movie
|
||||||
self.alternative_titles = alternative_titles or []
|
self.alternative_titles = alternative_titles or []
|
||||||
|
|
||||||
|
#: AniList ID of the movie
|
||||||
|
self.anilist_id = anilist_id
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromguess(cls, name, guess):
|
def fromguess(cls, name, guess):
|
||||||
|
|
90
custom_libs/subliminal_patch/converters/subdl.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from babelfish import LanguageReverseConverter
|
||||||
|
from subliminal.exceptions import ConfigurationError
|
||||||
|
|
||||||
|
|
||||||
|
class SubdlConverter(LanguageReverseConverter):
|
||||||
|
def __init__(self):
|
||||||
|
self.from_subdl = {
|
||||||
|
"AR": ("ara", None, None), # Arabic
|
||||||
|
"DA": ("dan", None, None), # Danish
|
||||||
|
"NL": ("nld", None, None), # Dutch
|
||||||
|
"EN": ("eng", None, None), # English
|
||||||
|
"FA": ("fas", None, None), # Farsi_Persian
|
||||||
|
"FI": ("fin", None, None), # Finnish
|
||||||
|
"FR": ("fra", None, None), # French
|
||||||
|
"ID": ("ind", None, None), # Indonesian
|
||||||
|
"IT": ("ita", None, None), # Italian
|
||||||
|
"NO": ("nor", None, None), # Norwegian
|
||||||
|
"RO": ("ron", None, None), # Romanian
|
||||||
|
"ES": ("spa", None, None), # Spanish
|
||||||
|
"SV": ("swe", None, None), # Swedish
|
||||||
|
"VI": ("vie", None, None), # Vietnamese
|
||||||
|
"SQ": ("sqi", None, None), # Albanian
|
||||||
|
"AZ": ("aze", None, None), # Azerbaijani
|
||||||
|
"BE": ("bel", None, None), # Belarusian
|
||||||
|
"BN": ("ben", None, None), # Bengali
|
||||||
|
"BS": ("bos", None, None), # Bosnian
|
||||||
|
"BG": ("bul", None, None), # Bulgarian
|
||||||
|
"MY": ("mya", None, None), # Burmese
|
||||||
|
"CA": ("cat", None, None), # Catalan
|
||||||
|
"ZH": ("zho", None, None), # Chinese BG code
|
||||||
|
"HR": ("hrv", None, None), # Croatian
|
||||||
|
"CS": ("ces", None, None), # Czech
|
||||||
|
"EO": ("epo", None, None), # Esperanto
|
||||||
|
"ET": ("est", None, None), # Estonian
|
||||||
|
"KA": ("kat", None, None), # Georgian
|
||||||
|
"DE": ("deu", None, None), # German
|
||||||
|
"EL": ("ell", None, None), # Greek
|
||||||
|
"KL": ("kal", None, None), # Greenlandic
|
||||||
|
"HE": ("heb", None, None), # Hebrew
|
||||||
|
"HI": ("hin", None, None), # Hindi
|
||||||
|
"HU": ("hun", None, None), # Hungarian
|
||||||
|
"IS": ("isl", None, None), # Icelandic
|
||||||
|
"JA": ("jpn", None, None), # Japanese
|
||||||
|
"KO": ("kor", None, None), # Korean
|
||||||
|
"KU": ("kur", None, None), # Kurdish
|
||||||
|
"LV": ("lav", None, None), # Latvian
|
||||||
|
"LT": ("lit", None, None), # Lithuanian
|
||||||
|
"MK": ("mkd", None, None), # Macedonian
|
||||||
|
"MS": ("msa", None, None), # Malay
|
||||||
|
"ML": ("mal", None, None), # Malayalam
|
||||||
|
"PL": ("pol", None, None), # Polish
|
||||||
|
"PT": ("por", None, None), # Portuguese
|
||||||
|
"RU": ("rus", None, None), # Russian
|
||||||
|
"SR": ("srp", None, None), # Serbian
|
||||||
|
"SI": ("sin", None, None), # Sinhala
|
||||||
|
"SK": ("slk", None, None), # Slovak
|
||||||
|
"SL": ("slv", None, None), # Slovenian
|
||||||
|
"TL": ("tgl", None, None), # Tagalog
|
||||||
|
"TA": ("tam", None, None), # Tamil
|
||||||
|
"TE": ("tel", None, None), # Telugu
|
||||||
|
"TH": ("tha", None, None), # Thai
|
||||||
|
"TR": ("tur", None, None), # Turkish
|
||||||
|
"UK": ("ukr", None, None), # Ukrainian
|
||||||
|
"UR": ("urd", None, None), # Urdu
|
||||||
|
# custom languages
|
||||||
|
"BR_PT": ("por", "BR", None), # Brazilian Portuguese
|
||||||
|
"ZH_BG": ("zho", None, "Hant"), # Big 5 code
|
||||||
|
# unsupported language in Bazarr
|
||||||
|
# "BG_EN": "Bulgarian_English",
|
||||||
|
# "NL_EN": "Dutch_English",
|
||||||
|
# "EN_DE": "English_German",
|
||||||
|
# "HU_EN": "Hungarian_English",
|
||||||
|
# "MNI": "Manipuri",
|
||||||
|
}
|
||||||
|
self.to_subdl = {v: k for k, v in self.from_subdl.items()}
|
||||||
|
self.codes = set(self.from_subdl.keys())
|
||||||
|
|
||||||
|
def convert(self, alpha3, country=None, script=None):
|
||||||
|
if (alpha3, country, script) in self.to_subdl:
|
||||||
|
return self.to_subdl[(alpha3, country, script)]
|
||||||
|
|
||||||
|
raise ConfigurationError('Unsupported language for subdl: %s, %s, %s' % (alpha3, country, script))
|
||||||
|
|
||||||
|
def reverse(self, subdl):
|
||||||
|
if subdl in self.from_subdl:
|
||||||
|
return self.from_subdl[subdl]
|
||||||
|
|
||||||
|
raise ConfigurationError('Unsupported language code for subdl: %s' % subdl)
|
|
@ -49,7 +49,17 @@ SUBTITLE_EXTENSIONS = ('.srt', '.sub', '.smi', '.txt', '.ssa', '.ass', '.mpl', '
|
||||||
|
|
||||||
_POOL_LIFETIME = datetime.timedelta(hours=12)
|
_POOL_LIFETIME = datetime.timedelta(hours=12)
|
||||||
|
|
||||||
HI_REGEX = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\(\{].{3,}[\]\)\}](?<!{\\an\d})')
|
HI_REGEX_WITHOUT_PARENTHESIS = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\{].{3,}[\]\}](?<!{\\an\d})')
|
||||||
|
HI_REGEX_WITH_PARENTHESIS = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\(\{].{3,}[\]\)\}](?<!{\\an\d})')
|
||||||
|
|
||||||
|
HI_REGEX_PARENTHESIS_EXCLUDED_LANGUAGES = ['ara']
|
||||||
|
|
||||||
|
|
||||||
|
def parse_for_hi_regex(subtitle_text, alpha3_language):
|
||||||
|
if alpha3_language in HI_REGEX_PARENTHESIS_EXCLUDED_LANGUAGES:
|
||||||
|
return bool(re.search(HI_REGEX_WITHOUT_PARENTHESIS, subtitle_text))
|
||||||
|
else:
|
||||||
|
return bool(re.search(HI_REGEX_WITH_PARENTHESIS, subtitle_text))
|
||||||
|
|
||||||
|
|
||||||
def remove_crap_from_fn(fn):
|
def remove_crap_from_fn(fn):
|
||||||
|
@ -946,8 +956,8 @@ def _search_external_subtitles(path, languages=None, only_one=False, match_stric
|
||||||
lambda m: "" if str(m.group(1)).lower() in FULL_LANGUAGE_LIST else m.group(0), p_root)
|
lambda m: "" if str(m.group(1)).lower() in FULL_LANGUAGE_LIST else m.group(0), p_root)
|
||||||
|
|
||||||
p_root_lower = p_root_bare.lower()
|
p_root_lower = p_root_bare.lower()
|
||||||
|
# comparing to both unicode normalization forms to prevent broking stuff and improve indexing on some platforms.
|
||||||
filename_matches = p_root_lower == fn_no_ext_lower
|
filename_matches = fn_no_ext_lower in [p_root_lower, unicodedata.normalize('NFC', p_root_lower)]
|
||||||
filename_contains = p_root_lower in fn_no_ext_lower
|
filename_contains = p_root_lower in fn_no_ext_lower
|
||||||
|
|
||||||
if not filename_matches:
|
if not filename_matches:
|
||||||
|
@ -1203,7 +1213,10 @@ def save_subtitles(file_path, subtitles, single=False, directory=None, chmod=Non
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# create subtitle path
|
# create subtitle path
|
||||||
if subtitle.text and bool(re.search(HI_REGEX, subtitle.text)):
|
if subtitle.text and parse_for_hi_regex(subtitle_text=subtitle.text,
|
||||||
|
alpha3_language=subtitle.language.alpha3 if
|
||||||
|
(hasattr(subtitle, 'language') and hasattr(subtitle.language, 'alpha3'))
|
||||||
|
else None):
|
||||||
subtitle.language.hi = True
|
subtitle.language.hi = True
|
||||||
subtitle_path = get_subtitle_path(file_path, None if single else subtitle.language,
|
subtitle_path = get_subtitle_path(file_path, None if single else subtitle.language,
|
||||||
forced_tag=subtitle.language.forced,
|
forced_tag=subtitle.language.forced,
|
||||||
|
|
|
@ -141,7 +141,8 @@ class AnimeToshoProvider(Provider, ProviderSubtitleArchiveMixin):
|
||||||
for subtitle_file in subtitle_files:
|
for subtitle_file in subtitle_files:
|
||||||
hex_id = format(subtitle_file['id'], '08x')
|
hex_id = format(subtitle_file['id'], '08x')
|
||||||
|
|
||||||
lang = Language.fromalpha3b(subtitle_file['info']['lang'])
|
# Animetosho assumes missing languages as english as fallback when not specified.
|
||||||
|
lang = Language.fromalpha3b(subtitle_file['info'].get('lang', 'eng'))
|
||||||
|
|
||||||
# For Portuguese and Portuguese Brazilian they both share the same code, the name is the only
|
# For Portuguese and Portuguese Brazilian they both share the same code, the name is the only
|
||||||
# identifier AnimeTosho provides. Also, some subtitles does not have name, in this case it could
|
# identifier AnimeTosho provides. Also, some subtitles does not have name, in this case it could
|
||||||
|
|
|
@ -5,7 +5,7 @@ from random import randint
|
||||||
|
|
||||||
import pycountry
|
import pycountry
|
||||||
from requests.cookies import RequestsCookieJar
|
from requests.cookies import RequestsCookieJar
|
||||||
from subliminal.exceptions import AuthenticationError
|
from subliminal.exceptions import AuthenticationError, ProviderError
|
||||||
from subliminal.providers import ParserBeautifulSoup
|
from subliminal.providers import ParserBeautifulSoup
|
||||||
from subliminal_patch.http import RetryingCFSession
|
from subliminal_patch.http import RetryingCFSession
|
||||||
from subliminal_patch.pitcher import store_verification
|
from subliminal_patch.pitcher import store_verification
|
||||||
|
@ -318,7 +318,7 @@ class AvistazNetworkProviderBase(Provider):
|
||||||
release_name = release['Title'].get_text().strip()
|
release_name = release['Title'].get_text().strip()
|
||||||
lang = lookup_lang(subtitle_cols['Language'].get_text().strip())
|
lang = lookup_lang(subtitle_cols['Language'].get_text().strip())
|
||||||
download_link = subtitle_cols['Download'].a['href']
|
download_link = subtitle_cols['Download'].a['href']
|
||||||
uploader_name = subtitle_cols['Uploader'].get_text().strip()
|
uploader_name = subtitle_cols['Uploader'].get_text().strip() if 'Uploader' in subtitle_cols else None
|
||||||
|
|
||||||
if lang not in languages:
|
if lang not in languages:
|
||||||
continue
|
continue
|
||||||
|
@ -354,7 +354,10 @@ class AvistazNetworkProviderBase(Provider):
|
||||||
|
|
||||||
def _parse_release_table(self, html):
|
def _parse_release_table(self, html):
|
||||||
release_data_table = (ParserBeautifulSoup(html, ['html.parser'])
|
release_data_table = (ParserBeautifulSoup(html, ['html.parser'])
|
||||||
.select_one('#content-area > div:nth-child(4) > div.table-responsive > table > tbody'))
|
.select_one('#content-area > div.block > div.table-responsive > table > tbody'))
|
||||||
|
|
||||||
|
if release_data_table is None:
|
||||||
|
raise ProviderError('Unexpected HTML page layout - no release data table found')
|
||||||
|
|
||||||
rows = {}
|
rows = {}
|
||||||
for tr in release_data_table.find_all('tr', recursive=False):
|
for tr in release_data_table.find_all('tr', recursive=False):
|
||||||
|
|
|
@ -112,7 +112,11 @@ class EmbeddedSubtitlesProvider(Provider):
|
||||||
# Default is True
|
# Default is True
|
||||||
container.FFMPEG_STATS = False
|
container.FFMPEG_STATS = False
|
||||||
|
|
||||||
tags.LANGUAGE_FALLBACK = self._fallback_lang if self._unknown_as_fallback and self._fallback_lang else None
|
tags.LANGUAGE_FALLBACK = (
|
||||||
|
self._fallback_lang
|
||||||
|
if self._unknown_as_fallback and self._fallback_lang
|
||||||
|
else None
|
||||||
|
)
|
||||||
logger.debug("Language fallback set: %s", tags.LANGUAGE_FALLBACK)
|
logger.debug("Language fallback set: %s", tags.LANGUAGE_FALLBACK)
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
|
@ -194,7 +198,7 @@ class EmbeddedSubtitlesProvider(Provider):
|
||||||
def download_subtitle(self, subtitle: EmbeddedSubtitle):
|
def download_subtitle(self, subtitle: EmbeddedSubtitle):
|
||||||
try:
|
try:
|
||||||
path = self._get_subtitle_path(subtitle)
|
path = self._get_subtitle_path(subtitle)
|
||||||
except KeyError: # TODO: add MustGetBlacklisted support
|
except KeyError: # TODO: add MustGetBlacklisted support
|
||||||
logger.error("Couldn't get subtitle path")
|
logger.error("Couldn't get subtitle path")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -229,6 +233,7 @@ class EmbeddedSubtitlesProvider(Provider):
|
||||||
timeout=self._timeout,
|
timeout=self._timeout,
|
||||||
fallback_to_convert=True,
|
fallback_to_convert=True,
|
||||||
basename_callback=_basename_callback,
|
basename_callback=_basename_callback,
|
||||||
|
progress_callback=lambda d: logger.debug("Progress: %s", d),
|
||||||
)
|
)
|
||||||
# Add the extracted paths to the containter path key
|
# Add the extracted paths to the containter path key
|
||||||
self._cached_paths[container.path] = extracted
|
self._cached_paths[container.path] = extracted
|
||||||
|
|
|
@ -96,7 +96,12 @@ class HDBitsProvider(Provider):
|
||||||
"https://hdbits.org/api/torrents", json={**self._def_params, **lookup}
|
"https://hdbits.org/api/torrents", json={**self._def_params, **lookup}
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
ids = [item["id"] for item in response.json()["data"]]
|
|
||||||
|
try:
|
||||||
|
ids = [item["id"] for item in response.json()["data"]]
|
||||||
|
except KeyError:
|
||||||
|
logger.debug("No data found")
|
||||||
|
return []
|
||||||
|
|
||||||
subtitles = []
|
subtitles = []
|
||||||
for torrent_id in ids:
|
for torrent_id in ids:
|
||||||
|
|
419
custom_libs/subliminal_patch/providers/jimaku.py
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
from requests import Session
|
||||||
|
from subliminal import region, __short_version__
|
||||||
|
from subliminal.cache import REFINER_EXPIRATION_TIME
|
||||||
|
from subliminal.exceptions import ConfigurationError, AuthenticationError, ServiceUnavailable
|
||||||
|
from subliminal.utils import sanitize
|
||||||
|
from subliminal.video import Episode, Movie
|
||||||
|
from subliminal_patch.providers import Provider
|
||||||
|
from subliminal_patch.subtitle import Subtitle
|
||||||
|
from subliminal_patch.exceptions import APIThrottled
|
||||||
|
from subliminal_patch.providers.utils import get_subtitle_from_archive, get_archive_from_bytes
|
||||||
|
from urllib.parse import urlencode, urljoin
|
||||||
|
from guessit import guessit
|
||||||
|
from subzero.language import Language, FULL_LANGUAGE_LIST
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Unhandled formats, such files will always get filtered out
|
||||||
|
unhandled_archive_formats = (".7z",)
|
||||||
|
accepted_archive_formats = (".zip", ".rar")
|
||||||
|
|
||||||
|
class JimakuSubtitle(Subtitle):
|
||||||
|
'''Jimaku Subtitle.'''
|
||||||
|
provider_name = 'jimaku'
|
||||||
|
|
||||||
|
hash_verifiable = False
|
||||||
|
|
||||||
|
def __init__(self, language, video, download_url, filename):
|
||||||
|
super(JimakuSubtitle, self).__init__(language, page_link=download_url)
|
||||||
|
|
||||||
|
self.video = video
|
||||||
|
self.download_url = download_url
|
||||||
|
self.filename = filename
|
||||||
|
self.release_info = filename
|
||||||
|
self.is_archive = filename.endswith(accepted_archive_formats)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self.download_url
|
||||||
|
|
||||||
|
def get_matches(self, video):
|
||||||
|
matches = set()
|
||||||
|
|
||||||
|
# Episode/Movie specific matches
|
||||||
|
if isinstance(video, Episode):
|
||||||
|
if sanitize(video.series) and sanitize(self.video.series) in (
|
||||||
|
sanitize(name) for name in [video.series] + video.alternative_series):
|
||||||
|
matches.add('series')
|
||||||
|
|
||||||
|
if video.season and self.video.season is None or video.season and video.season == self.video.season:
|
||||||
|
matches.add('season')
|
||||||
|
elif isinstance(video, Movie):
|
||||||
|
if sanitize(video.title) and sanitize(self.video.title) in (
|
||||||
|
sanitize(name) for name in [video.title] + video.alternative_titles):
|
||||||
|
matches.add('title')
|
||||||
|
|
||||||
|
# General matches
|
||||||
|
if video.year and video.year == self.video.year:
|
||||||
|
matches.add('year')
|
||||||
|
|
||||||
|
video_type = 'movie' if isinstance(video, Movie) else 'episode'
|
||||||
|
matches.add(video_type)
|
||||||
|
|
||||||
|
guess = guessit(self.filename, {'type': video_type})
|
||||||
|
for g in guess:
|
||||||
|
if g[0] == "release_group" or "source":
|
||||||
|
if video.release_group == g[1]:
|
||||||
|
matches.add('release_group')
|
||||||
|
break
|
||||||
|
|
||||||
|
# Prioritize .srt by repurposing the audio_codec match
|
||||||
|
if self.filename.endswith(".srt"):
|
||||||
|
matches.add('audio_codec')
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
class JimakuProvider(Provider):
|
||||||
|
'''Jimaku Provider.'''
|
||||||
|
video_types = (Episode, Movie)
|
||||||
|
|
||||||
|
api_url = 'https://jimaku.cc/api'
|
||||||
|
api_ratelimit_max_delay_seconds = 5
|
||||||
|
api_ratelimit_backoff_limit = 3
|
||||||
|
|
||||||
|
corrupted_file_size_threshold = 500
|
||||||
|
|
||||||
|
languages = {Language.fromietf("ja")}
|
||||||
|
|
||||||
|
def __init__(self, enable_name_search_fallback, enable_archives_download, enable_ai_subs, api_key):
|
||||||
|
if api_key:
|
||||||
|
self.api_key = api_key
|
||||||
|
else:
|
||||||
|
raise ConfigurationError('Missing api_key.')
|
||||||
|
|
||||||
|
self.enable_name_search_fallback = enable_name_search_fallback
|
||||||
|
self.download_archives = enable_archives_download
|
||||||
|
self.enable_ai_subs = enable_ai_subs
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.session = Session()
|
||||||
|
self.session.headers['Content-Type'] = 'application/json'
|
||||||
|
self.session.headers['Authorization'] = self.api_key
|
||||||
|
self.session.headers['User-Agent'] = os.environ.get("SZ_USER_AGENT")
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
self.session.close()
|
||||||
|
|
||||||
|
def _query(self, video):
|
||||||
|
if isinstance(video, Movie):
|
||||||
|
media_name = video.title.lower()
|
||||||
|
elif isinstance(video, Episode):
|
||||||
|
media_name = video.series.lower()
|
||||||
|
|
||||||
|
# With entries that have a season larger than 1, Jimaku appends the corresponding season number to the name.
|
||||||
|
# We'll reassemble media_name here to account for cases where we can only search by name alone.
|
||||||
|
season_addendum = str(video.season) if video.season > 1 else None
|
||||||
|
media_name = f"{media_name} {season_addendum}" if season_addendum else media_name
|
||||||
|
|
||||||
|
# Search for entry
|
||||||
|
searching_for_entry_attempts = 0
|
||||||
|
additional_url_params = {}
|
||||||
|
while searching_for_entry_attempts < 2:
|
||||||
|
searching_for_entry_attempts += 1
|
||||||
|
url = self._assemble_jimaku_search_url(video, media_name, additional_url_params)
|
||||||
|
if not url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
searching_for_entry = "query" in url
|
||||||
|
data = self._search_for_entry(url)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
if searching_for_entry and searching_for_entry_attempts < 2:
|
||||||
|
logger.info("Maybe this is live action media? Will retry search without anime parameter...")
|
||||||
|
additional_url_params = {'anime': "false"}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# We only go for the first entry
|
||||||
|
entry = data[0]
|
||||||
|
|
||||||
|
entry_id = entry.get('id')
|
||||||
|
anilist_id = entry.get('anilist_id', None)
|
||||||
|
entry_name = entry.get('name')
|
||||||
|
is_movie = entry.get('flags', {}).get('movie', False)
|
||||||
|
|
||||||
|
if isinstance(video, Episode) and is_movie:
|
||||||
|
logger.warn("Bazarr thinks this is a series, but Jimaku says this is a movie! May not be able to match subtitles...")
|
||||||
|
|
||||||
|
logger.info(f"Matched entry: ID: '{entry_id}', anilist_id: '{anilist_id}', name: '{entry_name}', english_name: '{entry.get('english_name')}', movie: {is_movie}")
|
||||||
|
if entry.get("flags").get("unverified"):
|
||||||
|
logger.warning(f"This entry '{entry_id}' is unverified, subtitles might be incomplete or have quality issues!")
|
||||||
|
|
||||||
|
# Get a list of subtitles for entry
|
||||||
|
episode_number = video.episode if "episode" in dir(video) else None
|
||||||
|
url_params = {'episode': episode_number} if isinstance(video, Episode) and not is_movie else {}
|
||||||
|
only_look_for_archives = False
|
||||||
|
|
||||||
|
has_offset = isinstance(video, Episode) and video.series_anidb_season_episode_offset is not None
|
||||||
|
|
||||||
|
retry_count = 0
|
||||||
|
adjusted_ep_num = None
|
||||||
|
while retry_count <= 1:
|
||||||
|
# Account for positive episode offset first
|
||||||
|
if isinstance(video, Episode) and not is_movie and retry_count < 1:
|
||||||
|
if video.season > 1 and has_offset:
|
||||||
|
offset_value = video.series_anidb_season_episode_offset
|
||||||
|
offset_value = offset_value if offset_value > 0 else -offset_value
|
||||||
|
|
||||||
|
if episode_number < offset_value:
|
||||||
|
adjusted_ep_num = episode_number + offset_value
|
||||||
|
logger.warning(f"Will try using adjusted episode number {adjusted_ep_num} first")
|
||||||
|
url_params = {'episode': adjusted_ep_num}
|
||||||
|
|
||||||
|
url = f"entries/{entry_id}/files"
|
||||||
|
data = self._search_for_subtitles(url, url_params)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
if isinstance(video, Episode) and not is_movie and has_offset and retry_count < 1:
|
||||||
|
logger.warning(f"Found no subtitles for adjusted episode number, but will retry with normal episode number {episode_number}")
|
||||||
|
url_params = {'episode': episode_number}
|
||||||
|
elif isinstance(video, Episode) and not is_movie and retry_count < 1:
|
||||||
|
logger.warning(f"Found no subtitles for episode number {episode_number}, but will retry without 'episode' parameter")
|
||||||
|
url_params = {}
|
||||||
|
only_look_for_archives = True
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
retry_count += 1
|
||||||
|
else:
|
||||||
|
if adjusted_ep_num:
|
||||||
|
video.episode = adjusted_ep_num
|
||||||
|
logger.debug(f"This videos episode attribute has been updated to: {video.episode}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Filter subtitles
|
||||||
|
list_of_subtitles = []
|
||||||
|
|
||||||
|
data = [item for item in data if not item['name'].endswith(unhandled_archive_formats)]
|
||||||
|
|
||||||
|
# Detect only archives being uploaded
|
||||||
|
archive_entries = [item for item in data if item['name'].endswith(accepted_archive_formats)]
|
||||||
|
subtitle_entries = [item for item in data if not item['name'].endswith(accepted_archive_formats)]
|
||||||
|
has_only_archives = len(archive_entries) > 0 and len(subtitle_entries) == 0
|
||||||
|
if has_only_archives:
|
||||||
|
logger.warning("Have only found archived subtitles")
|
||||||
|
|
||||||
|
elif only_look_for_archives:
|
||||||
|
data = [item for item in data if item['name'].endswith(accepted_archive_formats)]
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
filename = item.get('name')
|
||||||
|
download_url = item.get('url')
|
||||||
|
is_archive = filename.endswith(accepted_archive_formats)
|
||||||
|
|
||||||
|
# Archives will still be considered if they're the only files available, as is mostly the case for movies.
|
||||||
|
if is_archive and not has_only_archives and not self.download_archives:
|
||||||
|
logger.warning(f"Skipping archive '{filename}' because normal subtitles are available instead")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self.enable_ai_subs:
|
||||||
|
p = re.compile(r'[\[\(]?(whisperai)[\]\)]?|[\[\(]whisper[\]\)]', re.IGNORECASE)
|
||||||
|
if p.search(filename):
|
||||||
|
logger.warning(f"Skipping subtitle '{filename}' as it's suspected of being AI generated")
|
||||||
|
continue
|
||||||
|
|
||||||
|
sub_languages = self._try_determine_subtitle_languages(filename)
|
||||||
|
if len(sub_languages) > 1:
|
||||||
|
logger.warning(f"Skipping subtitle '{filename}' as it's suspected of containing multiple languages")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if file is obviously corrupt. If no size is returned, assume OK
|
||||||
|
filesize = item.get('size', self.corrupted_file_size_threshold)
|
||||||
|
if filesize < self.corrupted_file_size_threshold:
|
||||||
|
logger.warning(f"Skipping possibly corrupt file '{filename}': Filesize is just {filesize} bytes")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not filename.endswith(unhandled_archive_formats):
|
||||||
|
lang = sub_languages[0] if len(sub_languages) > 1 else Language("jpn")
|
||||||
|
list_of_subtitles.append(JimakuSubtitle(lang, video, download_url, filename))
|
||||||
|
else:
|
||||||
|
logger.debug(f"Skipping archive '{filename}' as it's not a supported format")
|
||||||
|
|
||||||
|
return list_of_subtitles
|
||||||
|
|
||||||
|
def list_subtitles(self, video, languages=None):
|
||||||
|
subtitles = self._query(video)
|
||||||
|
if not subtitles:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [s for s in subtitles]
|
||||||
|
|
||||||
|
def download_subtitle(self, subtitle: JimakuSubtitle):
|
||||||
|
target_url = subtitle.download_url
|
||||||
|
response = self.session.get(target_url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
if subtitle.is_archive:
|
||||||
|
archive = get_archive_from_bytes(response.content)
|
||||||
|
if archive:
|
||||||
|
if isinstance(subtitle.video, Episode):
|
||||||
|
subtitle.content = get_subtitle_from_archive(
|
||||||
|
archive,
|
||||||
|
episode=subtitle.video.episode,
|
||||||
|
episode_title=subtitle.video.title
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
subtitle.content = get_subtitle_from_archive(
|
||||||
|
archive
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning("Archive seems to not be an archive! File possibly corrupt?")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
subtitle.content = response.content
|
||||||
|
|
||||||
|
def _do_jimaku_request(self, url_path, url_params={}):
|
||||||
|
url = urljoin(f"{self.api_url}/{url_path}", '?' + urlencode(url_params))
|
||||||
|
|
||||||
|
retry_count = 0
|
||||||
|
while retry_count < self.api_ratelimit_backoff_limit:
|
||||||
|
response = self.session.get(url, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 429:
|
||||||
|
reset_time = 5
|
||||||
|
retry_count + 1
|
||||||
|
|
||||||
|
logger.warning(f"Jimaku ratelimit hit, waiting for '{reset_time}' seconds ({retry_count}/{self.api_ratelimit_backoff_limit} tries)")
|
||||||
|
time.sleep(reset_time)
|
||||||
|
continue
|
||||||
|
elif response.status_code == 401:
|
||||||
|
raise AuthenticationError("Unauthorized. API key possibly invalid")
|
||||||
|
else:
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
logger.debug(f"Length of response on {url}: {len(data)}")
|
||||||
|
if len(data) == 0:
|
||||||
|
logger.error(f"Jimaku returned no items for our our query: {url}")
|
||||||
|
return None
|
||||||
|
elif 'error' in data:
|
||||||
|
raise ServiceUnavailable(f"Jimaku returned an error: '{data.get('error')}', Code: '{data.get('code')}'")
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
|
raise APIThrottled(f"Jimaku ratelimit max backoff limit of {self.api_ratelimit_backoff_limit} reached, aborting")
|
||||||
|
|
||||||
|
# Wrapper functions to indirectly call _do_jimaku_request with different cache configs
|
||||||
|
@region.cache_on_arguments(expiration_time=REFINER_EXPIRATION_TIME)
|
||||||
|
def _search_for_entry(self, url_path, url_params={}):
|
||||||
|
return self._do_jimaku_request(url_path, url_params)
|
||||||
|
|
||||||
|
@region.cache_on_arguments(expiration_time=timedelta(minutes=1).total_seconds())
|
||||||
|
def _search_for_subtitles(self, url_path, url_params={}):
|
||||||
|
return self._do_jimaku_request(url_path, url_params)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _try_determine_subtitle_languages(filename):
|
||||||
|
# This is more like a guess and not a 100% fool-proof way of detecting multi-lang subs:
|
||||||
|
# It assumes that language codes, if present, are in the last metadata group of the subs filename.
|
||||||
|
# If such codes are not present, or we failed to match any at all, then we'll just assume that the sub is purely Japanese.
|
||||||
|
default_language = Language("jpn")
|
||||||
|
|
||||||
|
dot_delimit = filename.split(".")
|
||||||
|
bracket_delimit = re.split(r'[\[\]\(\)]+', filename)
|
||||||
|
|
||||||
|
candidate_list = list()
|
||||||
|
if len(dot_delimit) > 2:
|
||||||
|
candidate_list = dot_delimit[-2]
|
||||||
|
elif len(bracket_delimit) > 2:
|
||||||
|
candidate_list = bracket_delimit[-2]
|
||||||
|
|
||||||
|
candidates = [] if len(candidate_list) == 0 else re.split(r'[,\-\+\& ]+', candidate_list)
|
||||||
|
|
||||||
|
# Discard match group if any candidate...
|
||||||
|
# ...contains any numbers, as the group is likely encoding information
|
||||||
|
if any(re.compile(r'\d').search(string) for string in candidates):
|
||||||
|
return [default_language]
|
||||||
|
# ...is >= 5 chars long, as the group is likely other unrelated metadata
|
||||||
|
if any(len(string) >= 5 for string in candidates):
|
||||||
|
return [default_language]
|
||||||
|
|
||||||
|
languages = list()
|
||||||
|
for candidate in candidates:
|
||||||
|
candidate = candidate.lower()
|
||||||
|
if candidate in ["ass", "srt"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sometimes, languages are hidden in 4 character blocks, i.e. "JPSC"
|
||||||
|
if len(candidate) == 4:
|
||||||
|
for addendum in [candidate[:2], candidate[2:]]:
|
||||||
|
candidates.append(addendum)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sometimes, language codes can have additional info such as 'cc' or 'sdh'. For example: "ja[cc]"
|
||||||
|
if len(dot_delimit) > 2 and any(c in candidate for c in '[]()'):
|
||||||
|
candidate = re.split(r'[\[\]\(\)]+', candidate)[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
language_squash = {
|
||||||
|
"jp": "ja",
|
||||||
|
"jap": "ja",
|
||||||
|
"chs": "zho",
|
||||||
|
"cht": "zho",
|
||||||
|
"zhi": "zho",
|
||||||
|
"cn": "zho"
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate = language_squash[candidate] if candidate in language_squash else candidate
|
||||||
|
if len(candidate) > 2:
|
||||||
|
language = Language(candidate)
|
||||||
|
else:
|
||||||
|
language = Language.fromietf(candidate)
|
||||||
|
|
||||||
|
if not any(l.alpha3 == language.alpha3 for l in languages):
|
||||||
|
languages.append(language)
|
||||||
|
except:
|
||||||
|
if candidate in FULL_LANGUAGE_LIST:
|
||||||
|
# Create a dummy for the unknown language
|
||||||
|
languages.append(Language("zul"))
|
||||||
|
|
||||||
|
if len(languages) > 1:
|
||||||
|
# Sometimes a metadata group that actually contains info about codecs gets processed as valid languages.
|
||||||
|
# To prevent false positives, we'll check if Japanese language codes are in the processed languages list.
|
||||||
|
# If not, then it's likely that we didn't actually match language codes -> Assume Japanese only subtitle.
|
||||||
|
contains_jpn = any([l for l in languages if l.alpha3 == "jpn"])
|
||||||
|
|
||||||
|
return languages if contains_jpn else [Language("jpn")]
|
||||||
|
else:
|
||||||
|
return [default_language]
|
||||||
|
|
||||||
|
def _assemble_jimaku_search_url(self, video, media_name, additional_params={}):
|
||||||
|
endpoint = "entries/search"
|
||||||
|
anilist_id = video.anilist_id
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if anilist_id:
|
||||||
|
params = {'anilist_id': anilist_id}
|
||||||
|
else:
|
||||||
|
if self.enable_name_search_fallback or isinstance(video, Movie):
|
||||||
|
params = {'query': media_name}
|
||||||
|
else:
|
||||||
|
logger.error(f"Skipping '{media_name}': Got no AniList ID and fuzzy matching using name is disabled")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if additional_params:
|
||||||
|
params.update(additional_params)
|
||||||
|
|
||||||
|
logger.info(f"Will search for entry based on params: {params}")
|
||||||
|
return urljoin(endpoint, '?' + urlencode(params))
|
|
@ -29,6 +29,7 @@ from dogpile.cache.api import NO_VALUE
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LegendasdivxSubtitle(Subtitle):
|
class LegendasdivxSubtitle(Subtitle):
|
||||||
"""Legendasdivx Subtitle."""
|
"""Legendasdivx Subtitle."""
|
||||||
provider_name = 'legendasdivx'
|
provider_name = 'legendasdivx'
|
||||||
|
@ -69,10 +70,12 @@ class LegendasdivxSubtitle(Subtitle):
|
||||||
self.wrong_fps = True
|
self.wrong_fps = True
|
||||||
|
|
||||||
if self.skip_wrong_fps:
|
if self.skip_wrong_fps:
|
||||||
logger.debug("Legendasdivx :: Skipping subtitle due to FPS mismatch (expected: %s, got: %s)", video.fps, self.sub_frame_rate)
|
logger.debug("Legendasdivx :: Skipping subtitle due to FPS mismatch (expected: %s, got: %s)", video.fps,
|
||||||
|
self.sub_frame_rate)
|
||||||
# not a single match :)
|
# not a single match :)
|
||||||
return set()
|
return set()
|
||||||
logger.debug("Legendasdivx :: Frame rate mismatch (expected: %s, got: %s, but continuing...)", video.fps, self.sub_frame_rate)
|
logger.debug("Legendasdivx :: Frame rate mismatch (expected: %s, got: %s, but continuing...)", video.fps,
|
||||||
|
self.sub_frame_rate)
|
||||||
|
|
||||||
description = sanitize(self.description)
|
description = sanitize(self.description)
|
||||||
|
|
||||||
|
@ -112,6 +115,11 @@ class LegendasdivxSubtitle(Subtitle):
|
||||||
matches.update(['season'])
|
matches.update(['season'])
|
||||||
if video.episode and 'e{:02d}'.format(video.episode) in description:
|
if video.episode and 'e{:02d}'.format(video.episode) in description:
|
||||||
matches.update(['episode'])
|
matches.update(['episode'])
|
||||||
|
# All the search is already based on the series_imdb_id when present in the video and controlled via the
|
||||||
|
# the legendasdivx backend it, so if there is a result, it matches, either inside of a pack or a specific
|
||||||
|
# series and episode, so we can assume the season and episode matches.
|
||||||
|
if video.series_imdb_id:
|
||||||
|
matches.update(['series', 'series_imdb_id', 'season', 'episode'])
|
||||||
|
|
||||||
# release_group
|
# release_group
|
||||||
if video.release_group and sanitize_release_group(video.release_group) in sanitize_release_group(description):
|
if video.release_group and sanitize_release_group(video.release_group) in sanitize_release_group(description):
|
||||||
|
@ -121,6 +129,7 @@ class LegendasdivxSubtitle(Subtitle):
|
||||||
|
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
|
||||||
class LegendasdivxProvider(Provider):
|
class LegendasdivxProvider(Provider):
|
||||||
"""Legendasdivx Provider."""
|
"""Legendasdivx Provider."""
|
||||||
languages = {Language('por', 'BR')} | {Language('por')}
|
languages = {Language('por', 'BR')} | {Language('por')}
|
||||||
|
@ -135,7 +144,7 @@ class LegendasdivxProvider(Provider):
|
||||||
'Referer': 'https://www.legendasdivx.pt'
|
'Referer': 'https://www.legendasdivx.pt'
|
||||||
}
|
}
|
||||||
loginpage = site + '/forum/ucp.php?mode=login'
|
loginpage = site + '/forum/ucp.php?mode=login'
|
||||||
searchurl = site + '/modules.php?name=Downloads&file=jz&d_op=search&op=_jz00&query={query}'
|
searchurl = site + '/modules.php?name=Downloads&file=jz&d_op={d_op}&op={op}&query={query}&temporada={season}&episodio={episode}&imdb={imdbid}'
|
||||||
download_link = site + '/modules.php{link}'
|
download_link = site + '/modules.php{link}'
|
||||||
|
|
||||||
def __init__(self, username, password, skip_wrong_fps=True):
|
def __init__(self, username, password, skip_wrong_fps=True):
|
||||||
|
@ -186,7 +195,8 @@ class LegendasdivxProvider(Provider):
|
||||||
res = self.session.post(self.loginpage, data)
|
res = self.session.post(self.loginpage, data)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
# make sure we're logged in
|
# make sure we're logged in
|
||||||
logger.debug('Legendasdivx.pt :: Logged in successfully: PHPSESSID: %s', self.session.cookies.get_dict()['PHPSESSID'])
|
logger.debug('Legendasdivx.pt :: Logged in successfully: PHPSESSID: %s',
|
||||||
|
self.session.cookies.get_dict()['PHPSESSID'])
|
||||||
cj = self.session.cookies.copy()
|
cj = self.session.cookies.copy()
|
||||||
store_cks = ("PHPSESSID", "phpbb3_2z8zs_sid", "phpbb3_2z8zs_k", "phpbb3_2z8zs_u", "lang")
|
store_cks = ("PHPSESSID", "phpbb3_2z8zs_sid", "phpbb3_2z8zs_k", "phpbb3_2z8zs_u", "lang")
|
||||||
for cn in iter(self.session.cookies.keys()):
|
for cn in iter(self.session.cookies.keys()):
|
||||||
|
@ -252,7 +262,7 @@ class LegendasdivxProvider(Provider):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# get subtitle uploader
|
# get subtitle uploader
|
||||||
sub_header = _subbox.find("div", {"class" :"sub_header"})
|
sub_header = _subbox.find("div", {"class": "sub_header"})
|
||||||
uploader = sub_header.find("a").text if sub_header else 'anonymous'
|
uploader = sub_header.find("a").text if sub_header else 'anonymous'
|
||||||
|
|
||||||
exact_match = False
|
exact_match = False
|
||||||
|
@ -278,12 +288,24 @@ class LegendasdivxProvider(Provider):
|
||||||
|
|
||||||
subtitles = []
|
subtitles = []
|
||||||
|
|
||||||
|
# Set the default search criteria
|
||||||
|
d_op = 'search'
|
||||||
|
op = '_jz00'
|
||||||
|
|
||||||
|
lang_filter_key = 'form_cat'
|
||||||
|
|
||||||
if isinstance(video, Movie):
|
if isinstance(video, Movie):
|
||||||
querytext = video.imdb_id if video.imdb_id else video.title
|
querytext = video.imdb_id if video.imdb_id else video.title
|
||||||
|
|
||||||
if isinstance(video, Episode):
|
if isinstance(video, Episode):
|
||||||
querytext = '%22{}%20S{:02d}E{:02d}%22'.format(video.series, video.season, video.episode)
|
# Overwrite the parameters to refine via imdb_id
|
||||||
querytext = quote(querytext.lower())
|
if video.series_imdb_id:
|
||||||
|
querytext = '&faz=pesquisa_episodio'
|
||||||
|
lang_filter_key = 'idioma'
|
||||||
|
d_op = 'jz_00'
|
||||||
|
op = ''
|
||||||
|
else:
|
||||||
|
querytext = '%22{}%22%20S{:02d}E{:02d}'.format(video.series, video.season, video.episode)
|
||||||
|
querytext = quote(querytext.lower())
|
||||||
|
|
||||||
# language query filter
|
# language query filter
|
||||||
if not isinstance(languages, (tuple, list, set)):
|
if not isinstance(languages, (tuple, list, set)):
|
||||||
|
@ -293,21 +315,30 @@ class LegendasdivxProvider(Provider):
|
||||||
logger.debug("Legendasdivx.pt :: searching for %s subtitles.", language)
|
logger.debug("Legendasdivx.pt :: searching for %s subtitles.", language)
|
||||||
language_id = language.opensubtitles
|
language_id = language.opensubtitles
|
||||||
if 'por' in language_id:
|
if 'por' in language_id:
|
||||||
lang_filter = '&form_cat=28'
|
lang_filter = '&{}=28'.format(lang_filter_key)
|
||||||
elif 'pob' in language_id:
|
elif 'pob' in language_id:
|
||||||
lang_filter = '&form_cat=29'
|
lang_filter = '&{}=29'.format(lang_filter_key)
|
||||||
else:
|
else:
|
||||||
lang_filter = ''
|
lang_filter = ''
|
||||||
|
|
||||||
querytext = querytext + lang_filter if lang_filter else querytext
|
querytext = querytext + lang_filter if lang_filter else querytext
|
||||||
|
|
||||||
|
search_url = _searchurl.format(
|
||||||
|
query=querytext,
|
||||||
|
season='' if isinstance(video, Movie) else video.season,
|
||||||
|
episode='' if isinstance(video, Movie) else video.episode,
|
||||||
|
imdbid='' if isinstance(video, Movie) else video.series_imdb_id.replace('tt', '') if video.series_imdb_id else None,
|
||||||
|
op=op,
|
||||||
|
d_op=d_op,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# sleep for a 1 second before another request
|
# sleep for a 1 second before another request
|
||||||
sleep(1)
|
sleep(1)
|
||||||
searchLimitReached = False
|
searchLimitReached = False
|
||||||
self.headers['Referer'] = self.site + '/index.php'
|
self.headers['Referer'] = self.site + '/index.php'
|
||||||
self.session.headers.update(self.headers)
|
self.session.headers.update(self.headers)
|
||||||
res = self.session.get(_searchurl.format(query=querytext), allow_redirects=False)
|
res = self.session.get(search_url, allow_redirects=False)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
if res.status_code == 200 and "<!--pesquisas:" in res.text:
|
if res.status_code == 200 and "<!--pesquisas:" in res.text:
|
||||||
searches_count_groups = re.search(r'<!--pesquisas: (\d*)-->', res.text)
|
searches_count_groups = re.search(r'<!--pesquisas: (\d*)-->', res.text)
|
||||||
|
@ -327,7 +358,7 @@ class LegendasdivxProvider(Provider):
|
||||||
querytext = re.sub(r"(e|E)(\d{2})", "", querytext)
|
querytext = re.sub(r"(e|E)(\d{2})", "", querytext)
|
||||||
# sleep for a 1 second before another request
|
# sleep for a 1 second before another request
|
||||||
sleep(1)
|
sleep(1)
|
||||||
res = self.session.get(_searchurl.format(query=querytext), allow_redirects=False)
|
res = self.session.get(search_url, allow_redirects=False)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
if res.status_code == 200 and "<!--pesquisas:" in res.text:
|
if res.status_code == 200 and "<!--pesquisas:" in res.text:
|
||||||
searches_count_groups = re.search(r'<!--pesquisas: (\d*)-->', res.text)
|
searches_count_groups = re.search(r'<!--pesquisas: (\d*)-->', res.text)
|
||||||
|
@ -340,9 +371,11 @@ class LegendasdivxProvider(Provider):
|
||||||
if searches_count >= self.SAFE_SEARCH_LIMIT:
|
if searches_count >= self.SAFE_SEARCH_LIMIT:
|
||||||
searchLimitReached = True
|
searchLimitReached = True
|
||||||
if (res.status_code == 200 and "A legenda não foi encontrada" in res.text):
|
if (res.status_code == 200 and "A legenda não foi encontrada" in res.text):
|
||||||
logger.warning('Legendasdivx.pt :: query {0} return no results for language {1}(for series and season only).'.format(querytext, language_id))
|
logger.warning(
|
||||||
|
'Legendasdivx.pt :: query {0} return no results for language {1}(for series and season only).'.format(
|
||||||
|
querytext, language_id))
|
||||||
continue
|
continue
|
||||||
if res.status_code == 302: # got redirected to login page.
|
if res.status_code == 302: # got redirected to login page.
|
||||||
# seems that our session cookies are no longer valid... clean them from cache
|
# seems that our session cookies are no longer valid... clean them from cache
|
||||||
region.delete("legendasdivx_cookies2")
|
region.delete("legendasdivx_cookies2")
|
||||||
logger.debug("Legendasdivx.pt :: Logging in again. Cookies have expired!")
|
logger.debug("Legendasdivx.pt :: Logging in again. Cookies have expired!")
|
||||||
|
@ -350,7 +383,7 @@ class LegendasdivxProvider(Provider):
|
||||||
self.login()
|
self.login()
|
||||||
# sleep for a 1 second before another request
|
# sleep for a 1 second before another request
|
||||||
sleep(1)
|
sleep(1)
|
||||||
res = self.session.get(_searchurl.format(query=querytext))
|
res = self.session.get(search_url, allow_redirects=False)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
if res.status_code == 200 and "<!--pesquisas:" in res.text:
|
if res.status_code == 200 and "<!--pesquisas:" in res.text:
|
||||||
searches_count_groups = re.search(r'<!--pesquisas: (\d*)-->', res.text)
|
searches_count_groups = re.search(r'<!--pesquisas: (\d*)-->', res.text)
|
||||||
|
@ -394,9 +427,9 @@ class LegendasdivxProvider(Provider):
|
||||||
|
|
||||||
# more pages?
|
# more pages?
|
||||||
if num_pages > 1:
|
if num_pages > 1:
|
||||||
for num_page in range(2, num_pages+1):
|
for num_page in range(2, num_pages + 1):
|
||||||
sleep(1) # another 1 sec before requesting...
|
sleep(1) # another 1 sec before requesting...
|
||||||
_search_next = self.searchurl.format(query=querytext) + "&page={0}".format(str(num_page))
|
_search_next = search_url + "&page={0}".format(str(num_page))
|
||||||
logger.debug("Legendasdivx.pt :: Moving on to next page: %s", _search_next)
|
logger.debug("Legendasdivx.pt :: Moving on to next page: %s", _search_next)
|
||||||
# sleep for a 1 second before another request
|
# sleep for a 1 second before another request
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
@ -409,7 +442,7 @@ class LegendasdivxProvider(Provider):
|
||||||
|
|
||||||
def list_subtitles(self, video, languages):
|
def list_subtitles(self, video, languages):
|
||||||
return self.query(video, languages)
|
return self.query(video, languages)
|
||||||
|
|
||||||
@reinitialize_on_error((RequestException,), attempts=1)
|
@reinitialize_on_error((RequestException,), attempts=1)
|
||||||
def download_subtitle(self, subtitle):
|
def download_subtitle(self, subtitle):
|
||||||
|
|
||||||
|
@ -478,7 +511,8 @@ class LegendasdivxProvider(Provider):
|
||||||
if isinstance(subtitle.video, Episode):
|
if isinstance(subtitle.video, Episode):
|
||||||
if all(key in _guess for key in ('season', 'episode')):
|
if all(key in _guess for key in ('season', 'episode')):
|
||||||
logger.debug("Legendasdivx.pt :: guessing %s", name)
|
logger.debug("Legendasdivx.pt :: guessing %s", name)
|
||||||
logger.debug("Legendasdivx.pt :: subtitle S%sE%s video S%sE%s", _guess['season'], _guess['episode'], subtitle.video.season, subtitle.video.episode)
|
logger.debug("Legendasdivx.pt :: subtitle S%sE%s video S%sE%s", _guess['season'], _guess['episode'],
|
||||||
|
subtitle.video.season, subtitle.video.episode)
|
||||||
|
|
||||||
if subtitle.video.episode != _guess['episode'] or subtitle.video.season != _guess['season']:
|
if subtitle.video.episode != _guess['episode'] or subtitle.video.season != _guess['season']:
|
||||||
logger.debug('Legendasdivx.pt :: subtitle does not match video, skipping')
|
logger.debug('Legendasdivx.pt :: subtitle does not match video, skipping')
|
||||||
|
|
264
custom_libs/subliminal_patch/providers/legendasnet.py
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
|
||||||
|
from zipfile import ZipFile, is_zipfile
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
from subzero.language import Language
|
||||||
|
from subliminal import Episode, Movie
|
||||||
|
from subliminal.exceptions import ConfigurationError, ProviderError, DownloadLimitExceeded
|
||||||
|
from subliminal_patch.exceptions import APIThrottled
|
||||||
|
from .mixins import ProviderRetryMixin
|
||||||
|
from subliminal_patch.subtitle import Subtitle
|
||||||
|
from subliminal.subtitle import fix_line_ending
|
||||||
|
from subliminal_patch.providers import Provider
|
||||||
|
from subliminal_patch.providers import utils
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
retry_amount = 3
|
||||||
|
retry_timeout = 5
|
||||||
|
|
||||||
|
|
||||||
|
class LegendasNetSubtitle(Subtitle):
|
||||||
|
provider_name = 'legendasnet'
|
||||||
|
hash_verifiable = False
|
||||||
|
|
||||||
|
def __init__(self, language, forced, page_link, download_link, file_id, release_names, uploader,
|
||||||
|
season=None, episode=None):
|
||||||
|
super().__init__(language)
|
||||||
|
language = Language.rebuild(language, forced=forced)
|
||||||
|
|
||||||
|
self.season = season
|
||||||
|
self.episode = episode
|
||||||
|
self.releases = release_names
|
||||||
|
self.release_info = ', '.join(release_names)
|
||||||
|
self.language = language
|
||||||
|
self.forced = forced
|
||||||
|
self.file_id = file_id
|
||||||
|
self.page_link = page_link
|
||||||
|
self.download_link = download_link
|
||||||
|
self.uploader = uploader
|
||||||
|
self.matches = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self.file_id
|
||||||
|
|
||||||
|
def get_matches(self, video):
|
||||||
|
matches = set()
|
||||||
|
|
||||||
|
# handle movies and series separately
|
||||||
|
if isinstance(video, Episode):
|
||||||
|
# series
|
||||||
|
matches.add('series')
|
||||||
|
# season
|
||||||
|
if video.season == self.season:
|
||||||
|
matches.add('season')
|
||||||
|
# episode
|
||||||
|
if video.episode == self.episode:
|
||||||
|
matches.add('episode')
|
||||||
|
# imdb
|
||||||
|
matches.add('series_imdb_id')
|
||||||
|
else:
|
||||||
|
# title
|
||||||
|
matches.add('title')
|
||||||
|
# imdb
|
||||||
|
matches.add('imdb_id')
|
||||||
|
|
||||||
|
utils.update_matches(matches, video, self.release_info)
|
||||||
|
|
||||||
|
self.matches = matches
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
class LegendasNetProvider(ProviderRetryMixin, Provider):
|
||||||
|
"""Legendas.Net Provider"""
|
||||||
|
server_hostname = 'legendas.net/api'
|
||||||
|
|
||||||
|
languages = {Language('por', 'BR')}
|
||||||
|
video_types = (Episode, Movie)
|
||||||
|
|
||||||
|
def __init__(self, username, password):
|
||||||
|
self.session = Session()
|
||||||
|
self.session.headers = {'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")}
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.access_token = None
|
||||||
|
self.video = None
|
||||||
|
self._started = None
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
headersList = {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": self.session.headers['User-Agent'],
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"email": self.username,
|
||||||
|
"password": self.password
|
||||||
|
})
|
||||||
|
|
||||||
|
response = self.session.request("POST", self.server_url() + 'login', data=payload, headers=headersList)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise ConfigurationError('Failed to login and retrieve access token')
|
||||||
|
self.access_token = response.json().get('access_token')
|
||||||
|
if not self.access_token:
|
||||||
|
raise ConfigurationError('Access token not found in login response')
|
||||||
|
self.session.headers.update({'Authorization': f'Bearer {self.access_token}'})
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self._started = time.time()
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
self.session.close()
|
||||||
|
|
||||||
|
def server_url(self):
|
||||||
|
return f'https://{self.server_hostname}/v1/'
|
||||||
|
|
||||||
|
def query(self, languages, video):
|
||||||
|
self.video = video
|
||||||
|
|
||||||
|
# query the server
|
||||||
|
if isinstance(self.video, Episode):
|
||||||
|
res = self.retry(
|
||||||
|
lambda: self.session.get(self.server_url() + 'search/tv',
|
||||||
|
json={
|
||||||
|
'name': video.series,
|
||||||
|
'page': 1,
|
||||||
|
'per_page': 25,
|
||||||
|
'tv_episode': video.episode,
|
||||||
|
'tv_season': video.season,
|
||||||
|
'imdb_id': video.series_imdb_id
|
||||||
|
},
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=30),
|
||||||
|
amount=retry_amount,
|
||||||
|
retry_timeout=retry_timeout
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
res = self.retry(
|
||||||
|
lambda: self.session.get(self.server_url() + 'search/movie',
|
||||||
|
json={
|
||||||
|
'name': video.title,
|
||||||
|
'page': 1,
|
||||||
|
'per_page': 25,
|
||||||
|
'imdb_id': video.imdb_id
|
||||||
|
},
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=30),
|
||||||
|
amount=retry_amount,
|
||||||
|
retry_timeout=retry_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.status_code == 404:
|
||||||
|
logger.error(f"Endpoint not found: {res.url}")
|
||||||
|
raise ProviderError("Endpoint not found")
|
||||||
|
elif res.status_code == 429:
|
||||||
|
raise APIThrottled("Too many requests")
|
||||||
|
elif res.status_code == 403:
|
||||||
|
raise ConfigurationError("Invalid access token")
|
||||||
|
elif res.status_code != 200:
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
subtitles = []
|
||||||
|
|
||||||
|
result = res.json()
|
||||||
|
|
||||||
|
if ('success' in result and not result['success']) or ('status' in result and not result['status']):
|
||||||
|
logger.debug(result["error"])
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(self.video, Episode):
|
||||||
|
if len(result['tv_shows']):
|
||||||
|
for item in result['tv_shows']:
|
||||||
|
subtitle = LegendasNetSubtitle(
|
||||||
|
language=Language('por', 'BR'),
|
||||||
|
forced=self._is_forced(item),
|
||||||
|
page_link=f"https://legendas.net/tv_legenda?movie_id={result['tv_shows'][0]['tmdb_id']}&"
|
||||||
|
f"legenda_id={item['id']}",
|
||||||
|
download_link=item['path'],
|
||||||
|
file_id=item['id'],
|
||||||
|
release_names=[item.get('release_name', '')],
|
||||||
|
uploader=item['uploader'],
|
||||||
|
season=item.get('season', ''),
|
||||||
|
episode=item.get('episode', '')
|
||||||
|
)
|
||||||
|
subtitle.get_matches(self.video)
|
||||||
|
if subtitle.language in languages:
|
||||||
|
subtitles.append(subtitle)
|
||||||
|
else:
|
||||||
|
if len(result['movies']):
|
||||||
|
for item in result['movies']:
|
||||||
|
subtitle = LegendasNetSubtitle(
|
||||||
|
language=Language('por', 'BR'),
|
||||||
|
forced=self._is_forced(item),
|
||||||
|
page_link=f"https://legendas.net/legenda?movie_id={result['movies'][0]['tmdb_id']}&"
|
||||||
|
f"legenda_id={item['id']}",
|
||||||
|
download_link=item['path'],
|
||||||
|
file_id=item['id'],
|
||||||
|
release_names=[item.get('release_name', '')],
|
||||||
|
uploader=item['uploader'],
|
||||||
|
season=None,
|
||||||
|
episode=None
|
||||||
|
)
|
||||||
|
subtitle.get_matches(self.video)
|
||||||
|
if subtitle.language in languages:
|
||||||
|
subtitles.append(subtitle)
|
||||||
|
|
||||||
|
return subtitles
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_forced(item):
|
||||||
|
forced_tags = ['forced', 'foreign']
|
||||||
|
for tag in forced_tags:
|
||||||
|
if tag in item.get('comment', '').lower():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# nothing match so we consider it as normal subtitles
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_subtitles(self, video, languages):
|
||||||
|
return self.query(languages, video)
|
||||||
|
|
||||||
|
def download_subtitle(self, subtitle):
|
||||||
|
logger.debug('Downloading subtitle %r', subtitle)
|
||||||
|
download_link = urljoin("https://legendas.net", subtitle.download_link)
|
||||||
|
|
||||||
|
r = self.retry(
|
||||||
|
lambda: self.session.get(download_link, timeout=30),
|
||||||
|
amount=retry_amount,
|
||||||
|
retry_timeout=retry_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.status_code == 429:
|
||||||
|
raise DownloadLimitExceeded("Daily download limit exceeded")
|
||||||
|
elif r.status_code == 403:
|
||||||
|
raise ConfigurationError("Invalid access token")
|
||||||
|
elif r.status_code != 200:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
logger.error(f'Could not download subtitle from {download_link}')
|
||||||
|
subtitle.content = None
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
archive_stream = io.BytesIO(r.content)
|
||||||
|
if is_zipfile(archive_stream):
|
||||||
|
archive = ZipFile(archive_stream)
|
||||||
|
for name in archive.namelist():
|
||||||
|
subtitle_content = archive.read(name)
|
||||||
|
subtitle.content = fix_line_ending(subtitle_content)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
subtitle_content = r.content
|
||||||
|
subtitle.content = fix_line_ending(subtitle_content)
|
||||||
|
return
|
|
@ -218,7 +218,7 @@ class OpenSubtitlesComProvider(ProviderRetryMixin, Provider):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.token = r.json()['token']
|
self.token = r.json()['token']
|
||||||
except (ValueError, JSONDecodeError):
|
except (ValueError, JSONDecodeError, AttributeError):
|
||||||
log_request_response(r)
|
log_request_response(r)
|
||||||
raise ProviderError("Cannot get token from provider login response")
|
raise ProviderError("Cannot get token from provider login response")
|
||||||
else:
|
else:
|
||||||
|
@ -543,10 +543,6 @@ class OpenSubtitlesComProvider(ProviderRetryMixin, Provider):
|
||||||
elif status_code == 429:
|
elif status_code == 429:
|
||||||
log_request_response(response)
|
log_request_response(response)
|
||||||
raise TooManyRequests()
|
raise TooManyRequests()
|
||||||
elif status_code == 500:
|
|
||||||
logger.debug("Server side exception raised while downloading from opensubtitles.com website. They "
|
|
||||||
"should mitigate this soon.")
|
|
||||||
return None
|
|
||||||
elif status_code == 502:
|
elif status_code == 502:
|
||||||
# this one should deal with Bad Gateway issue on their side.
|
# this one should deal with Bad Gateway issue on their side.
|
||||||
raise APIThrottled()
|
raise APIThrottled()
|
||||||
|
|
|
@ -209,7 +209,8 @@ class PodnapisiProvider(_PodnapisiProvider, ProviderSubtitleArchiveMixin):
|
||||||
break
|
break
|
||||||
|
|
||||||
# exit if no results
|
# exit if no results
|
||||||
if not xml.find('pagination/results') or not int(xml.find('pagination/results').text):
|
if (not xml.find('pagination/results') or not xml.find('pagination/results').text or not
|
||||||
|
int(xml.find('pagination/results').text)):
|
||||||
logger.debug('No subtitles found')
|
logger.debug('No subtitles found')
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -277,7 +277,11 @@ class SoustitreseuProvider(Provider, ProviderSubtitleArchiveMixin):
|
||||||
release = name[:-4].lower().rstrip('tag').rstrip('en').rstrip('fr')
|
release = name[:-4].lower().rstrip('tag').rstrip('en').rstrip('fr')
|
||||||
_guess = guessit(release)
|
_guess = guessit(release)
|
||||||
if isinstance(video, Episode):
|
if isinstance(video, Episode):
|
||||||
if video.episode != _guess['episode'] or video.season != _guess['season']:
|
try:
|
||||||
|
if video.episode != _guess['episode'] or video.season != _guess['season']:
|
||||||
|
continue
|
||||||
|
except KeyError:
|
||||||
|
# episode or season are missing from guessit result
|
||||||
continue
|
continue
|
||||||
|
|
||||||
matches = set()
|
matches = set()
|
||||||
|
|
|
@ -172,7 +172,7 @@ class SubdivxSubtitlesProvider(Provider):
|
||||||
|
|
||||||
logger.debug("Query: %s", query)
|
logger.debug("Query: %s", query)
|
||||||
|
|
||||||
response = self.session.post(search_link, data=payload)
|
response = self.session.post(search_link, data=payload, timeout=30)
|
||||||
|
|
||||||
if response.status_code == 500:
|
if response.status_code == 500:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
279
custom_libs/subliminal_patch/providers/subdl.py
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import io
|
||||||
|
|
||||||
|
from zipfile import ZipFile, is_zipfile
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
from babelfish import language_converters
|
||||||
|
from subzero.language import Language
|
||||||
|
from subliminal import Episode, Movie
|
||||||
|
from subliminal.exceptions import ConfigurationError, ProviderError, DownloadLimitExceeded
|
||||||
|
from subliminal_patch.exceptions import APIThrottled
|
||||||
|
from .mixins import ProviderRetryMixin
|
||||||
|
from subliminal_patch.subtitle import Subtitle
|
||||||
|
from subliminal.subtitle import fix_line_ending
|
||||||
|
from subliminal_patch.providers import Provider
|
||||||
|
from subliminal_patch.providers import utils
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
retry_amount = 3
|
||||||
|
retry_timeout = 5
|
||||||
|
|
||||||
|
language_converters.register('subdl = subliminal_patch.converters.subdl:SubdlConverter')
|
||||||
|
|
||||||
|
|
||||||
|
class SubdlSubtitle(Subtitle):
|
||||||
|
provider_name = 'subdl'
|
||||||
|
hash_verifiable = False
|
||||||
|
hearing_impaired_verifiable = True
|
||||||
|
|
||||||
|
def __init__(self, language, forced, hearing_impaired, page_link, download_link, file_id, release_names, uploader,
|
||||||
|
season=None, episode=None):
|
||||||
|
super().__init__(language)
|
||||||
|
language = Language.rebuild(language, hi=hearing_impaired, forced=forced)
|
||||||
|
|
||||||
|
self.season = season
|
||||||
|
self.episode = episode
|
||||||
|
self.releases = release_names
|
||||||
|
self.release_info = ', '.join(release_names)
|
||||||
|
self.language = language
|
||||||
|
self.forced = forced
|
||||||
|
self.hearing_impaired = hearing_impaired
|
||||||
|
self.file_id = file_id
|
||||||
|
self.page_link = page_link
|
||||||
|
self.download_link = download_link
|
||||||
|
self.uploader = uploader
|
||||||
|
self.matches = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self.file_id
|
||||||
|
|
||||||
|
def get_matches(self, video):
|
||||||
|
matches = set()
|
||||||
|
|
||||||
|
# handle movies and series separately
|
||||||
|
if isinstance(video, Episode):
|
||||||
|
# series
|
||||||
|
matches.add('series')
|
||||||
|
# season
|
||||||
|
if video.season == self.season:
|
||||||
|
matches.add('season')
|
||||||
|
# episode
|
||||||
|
if video.episode == self.episode:
|
||||||
|
matches.add('episode')
|
||||||
|
# imdb
|
||||||
|
matches.add('series_imdb_id')
|
||||||
|
else:
|
||||||
|
# title
|
||||||
|
matches.add('title')
|
||||||
|
# imdb
|
||||||
|
matches.add('imdb_id')
|
||||||
|
|
||||||
|
utils.update_matches(matches, video, self.release_info)
|
||||||
|
|
||||||
|
self.matches = matches
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
class SubdlProvider(ProviderRetryMixin, Provider):
|
||||||
|
"""Subdl Provider"""
|
||||||
|
server_hostname = 'api.subdl.com'
|
||||||
|
|
||||||
|
languages = {Language(*lang) for lang in list(language_converters['subdl'].to_subdl.keys())}
|
||||||
|
languages.update(set(Language.rebuild(lang, forced=True) for lang in languages))
|
||||||
|
languages.update(set(Language.rebuild(l, hi=True) for l in languages))
|
||||||
|
|
||||||
|
video_types = (Episode, Movie)
|
||||||
|
|
||||||
|
def __init__(self, api_key=None):
|
||||||
|
if not api_key:
|
||||||
|
raise ConfigurationError('Api_key must be specified')
|
||||||
|
|
||||||
|
self.session = Session()
|
||||||
|
self.session.headers = {'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")}
|
||||||
|
self.api_key = api_key
|
||||||
|
self.video = None
|
||||||
|
self._started = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self._started = time.time()
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
self.session.close()
|
||||||
|
|
||||||
|
def server_url(self):
|
||||||
|
return f'https://{self.server_hostname}/api/v1/'
|
||||||
|
|
||||||
|
def query(self, languages, video):
|
||||||
|
self.video = video
|
||||||
|
if isinstance(self.video, Episode):
|
||||||
|
title = self.video.series
|
||||||
|
else:
|
||||||
|
title = self.video.title
|
||||||
|
|
||||||
|
imdb_id = None
|
||||||
|
if isinstance(self.video, Episode) and self.video.series_imdb_id:
|
||||||
|
imdb_id = self.video.series_imdb_id
|
||||||
|
elif isinstance(self.video, Movie) and self.video.imdb_id:
|
||||||
|
imdb_id = self.video.imdb_id
|
||||||
|
|
||||||
|
# be sure to remove duplicates using list(set())
|
||||||
|
langs_list = sorted(list(set([language_converters['subdl'].convert(lang.alpha3, lang.country, lang.script) for
|
||||||
|
lang in languages])))
|
||||||
|
|
||||||
|
langs = ','.join(langs_list)
|
||||||
|
logger.debug(f'Searching for those languages: {langs}')
|
||||||
|
|
||||||
|
# query the server
|
||||||
|
if isinstance(self.video, Episode):
|
||||||
|
res = self.retry(
|
||||||
|
lambda: self.session.get(self.server_url() + 'subtitles',
|
||||||
|
params=(('api_key', self.api_key),
|
||||||
|
('episode_number', self.video.episode),
|
||||||
|
('film_name', title if not imdb_id else None),
|
||||||
|
('imdb_id', imdb_id if imdb_id else None),
|
||||||
|
('languages', langs),
|
||||||
|
('season_number', self.video.season),
|
||||||
|
('subs_per_page', 30),
|
||||||
|
('type', 'tv'),
|
||||||
|
('comment', 1),
|
||||||
|
('releases', 1),
|
||||||
|
('bazarr', 1)), # this argument filter incompatible image based or
|
||||||
|
# txt subtitles
|
||||||
|
timeout=30),
|
||||||
|
amount=retry_amount,
|
||||||
|
retry_timeout=retry_timeout
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
res = self.retry(
|
||||||
|
lambda: self.session.get(self.server_url() + 'subtitles',
|
||||||
|
params=(('api_key', self.api_key),
|
||||||
|
('film_name', title if not imdb_id else None),
|
||||||
|
('imdb_id', imdb_id if imdb_id else None),
|
||||||
|
('languages', langs),
|
||||||
|
('subs_per_page', 30),
|
||||||
|
('type', 'movie'),
|
||||||
|
('comment', 1),
|
||||||
|
('releases', 1),
|
||||||
|
('bazarr', 1)), # this argument filter incompatible image based or
|
||||||
|
# txt subtitles
|
||||||
|
timeout=30),
|
||||||
|
amount=retry_amount,
|
||||||
|
retry_timeout=retry_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.status_code == 429:
|
||||||
|
raise APIThrottled("Too many requests")
|
||||||
|
elif res.status_code == 403:
|
||||||
|
raise ConfigurationError("Invalid API key")
|
||||||
|
elif res.status_code != 200:
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
subtitles = []
|
||||||
|
|
||||||
|
result = res.json()
|
||||||
|
|
||||||
|
if ('success' in result and not result['success']) or ('status' in result and not result['status']):
|
||||||
|
logger.debug(result["error"])
|
||||||
|
return []
|
||||||
|
|
||||||
|
logger.debug(f"Query returned {len(result['subtitles'])} subtitles")
|
||||||
|
|
||||||
|
if len(result['subtitles']):
|
||||||
|
for item in result['subtitles']:
|
||||||
|
if item.get('episode_from', False) == item.get('episode_end', False): # ignore season packs
|
||||||
|
subtitle = SubdlSubtitle(
|
||||||
|
language=Language.fromsubdl(item['language']),
|
||||||
|
forced=self._is_forced(item),
|
||||||
|
hearing_impaired=item.get('hi', False) or self._is_hi(item),
|
||||||
|
page_link=urljoin("https://subdl.com", item.get('subtitlePage', '')),
|
||||||
|
download_link=item['url'],
|
||||||
|
file_id=item['name'],
|
||||||
|
release_names=item.get('releases', []),
|
||||||
|
uploader=item.get('author', ''),
|
||||||
|
season=item.get('season', None),
|
||||||
|
episode=item.get('episode', None),
|
||||||
|
)
|
||||||
|
subtitle.get_matches(self.video)
|
||||||
|
if subtitle.language in languages: # make sure only desired subtitles variants are returned
|
||||||
|
subtitles.append(subtitle)
|
||||||
|
|
||||||
|
return subtitles
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_hi(item):
|
||||||
|
# Comments include specific mention of removed or non HI
|
||||||
|
non_hi_tag = ['hi remove', 'non hi', 'nonhi', 'non-hi', 'non-sdh', 'non sdh', 'nonsdh', 'sdh remove']
|
||||||
|
for tag in non_hi_tag:
|
||||||
|
if tag in item.get('comment', '').lower():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Archive filename include _HI_
|
||||||
|
if '_hi_' in item.get('name', '').lower():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Comments or release names include some specific strings
|
||||||
|
hi_keys = [item.get('comment', '').lower(), [x.lower() for x in item.get('releases', [])]]
|
||||||
|
hi_tag = ['_hi_', ' hi ', '.hi.', 'hi ', ' hi', 'sdh', '𝓢𝓓𝓗']
|
||||||
|
for key in hi_keys:
|
||||||
|
if any(x in key for x in hi_tag):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# nothing match so we consider it as non-HI
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_forced(item):
|
||||||
|
# Comments include specific mention of forced subtitles
|
||||||
|
forced_tags = ['forced', 'foreign']
|
||||||
|
for tag in forced_tags:
|
||||||
|
if tag in item.get('comment', '').lower():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# nothing match so we consider it as normal subtitles
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_subtitles(self, video, languages):
|
||||||
|
return self.query(languages, video)
|
||||||
|
|
||||||
|
def download_subtitle(self, subtitle):
|
||||||
|
logger.debug('Downloading subtitle %r', subtitle)
|
||||||
|
download_link = urljoin("https://dl.subdl.com", subtitle.download_link)
|
||||||
|
|
||||||
|
r = self.retry(
|
||||||
|
lambda: self.session.get(download_link, timeout=30),
|
||||||
|
amount=retry_amount,
|
||||||
|
retry_timeout=retry_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.status_code == 429:
|
||||||
|
raise DownloadLimitExceeded("Daily download limit exceeded")
|
||||||
|
elif r.status_code == 403:
|
||||||
|
raise ConfigurationError("Invalid API key")
|
||||||
|
elif r.status_code != 200:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
logger.error(f'Could not download subtitle from {download_link}')
|
||||||
|
subtitle.content = None
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
archive_stream = io.BytesIO(r.content)
|
||||||
|
if is_zipfile(archive_stream):
|
||||||
|
archive = ZipFile(archive_stream)
|
||||||
|
for name in archive.namelist():
|
||||||
|
# TODO when possible, deal with season pack / multiple files archive
|
||||||
|
subtitle_content = archive.read(name)
|
||||||
|
subtitle.content = fix_line_ending(subtitle_content)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.error(f'Could not unzip subtitle from {download_link}')
|
||||||
|
subtitle.content = None
|
||||||
|
return
|
|
@ -132,9 +132,9 @@ _DEFAULT_HEADERS = {
|
||||||
class Subf2mProvider(Provider):
|
class Subf2mProvider(Provider):
|
||||||
provider_name = "subf2m"
|
provider_name = "subf2m"
|
||||||
|
|
||||||
_movie_title_regex = re.compile(r"^(.+?)( \((\d{4})\))?$")
|
_movie_title_regex = re.compile(r"^(.+?)(\s+\((\d{4})\))?$")
|
||||||
_tv_show_title_regex = re.compile(
|
_tv_show_title_regex = re.compile(
|
||||||
r"^(.+?) [-\(]\s?(.*?) (season|series)\)?( \((\d{4})\))?$"
|
r"^(.+?)\s+[-\(]\s?(.*?)\s+(season|series)\)?(\s+\((\d{4})\))?$"
|
||||||
)
|
)
|
||||||
_tv_show_title_alt_regex = re.compile(r"(.+)\s(\d{1,2})(?:\s|$)")
|
_tv_show_title_alt_regex = re.compile(r"(.+)\s(\d{1,2})(?:\s|$)")
|
||||||
_supported_languages = {}
|
_supported_languages = {}
|
||||||
|
@ -220,7 +220,7 @@ class Subf2mProvider(Provider):
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for result in self._gen_results(title):
|
for result in self._gen_results(title):
|
||||||
text = result.text.lower()
|
text = result.text.strip().lower()
|
||||||
match = self._movie_title_regex.match(text)
|
match = self._movie_title_regex.match(text)
|
||||||
if not match:
|
if not match:
|
||||||
continue
|
continue
|
||||||
|
@ -254,7 +254,7 @@ class Subf2mProvider(Provider):
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for result in self._gen_results(title):
|
for result in self._gen_results(title):
|
||||||
text = result.text.lower()
|
text = result.text.strip().lower()
|
||||||
|
|
||||||
match = self._tv_show_title_regex.match(text)
|
match = self._tv_show_title_regex.match(text)
|
||||||
if not match:
|
if not match:
|
||||||
|
|
|
@ -455,7 +455,13 @@ class SuperSubtitlesProvider(Provider, ProviderSubtitleArchiveMixin):
|
||||||
|
|
||||||
soup = ParserBeautifulSoup(r, ['lxml'])
|
soup = ParserBeautifulSoup(r, ['lxml'])
|
||||||
tables = soup.find_all("table")
|
tables = soup.find_all("table")
|
||||||
tables = tables[0].find_all("tr")
|
|
||||||
|
try:
|
||||||
|
tables = tables[0].find_all("tr")
|
||||||
|
except IndexError:
|
||||||
|
logger.debug("No tables found for %s", url)
|
||||||
|
return []
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
for table in tables:
|
for table in tables:
|
||||||
|
|
|
@ -65,7 +65,7 @@ def _get_matching_sub(
|
||||||
guess = guessit(sub_name, options=guess_options)
|
guess = guessit(sub_name, options=guess_options)
|
||||||
|
|
||||||
matched_episode_num = guess.get("episode")
|
matched_episode_num = guess.get("episode")
|
||||||
if matched_episode_num:
|
if not matched_episode_num:
|
||||||
logger.debug("No episode number found in file: %s", sub_name)
|
logger.debug("No episode number found in file: %s", sub_name)
|
||||||
|
|
||||||
if episode_title is not None:
|
if episode_title is not None:
|
||||||
|
@ -86,11 +86,13 @@ def _get_matching_sub(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _analize_sub_name(sub_name: str, title_):
|
def _analize_sub_name(sub_name: str, title_: str):
|
||||||
titles = re.split(r"[.-]", os.path.splitext(sub_name)[0])
|
titles = re.split(r"[\s_\.\+]?[.-][\s_\.\+]?", os.path.splitext(sub_name)[0])
|
||||||
|
|
||||||
for title in titles:
|
for title in titles:
|
||||||
title = title.strip()
|
title = title.strip()
|
||||||
ratio = SequenceMatcher(None, title, title_).ratio()
|
ratio = SequenceMatcher(None, title.lower(), title_.lower()).ratio()
|
||||||
|
|
||||||
if ratio > 0.85:
|
if ratio > 0.85:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Episode title matched: '%s' -> '%s' [%s]", title, sub_name, ratio
|
"Episode title matched: '%s' -> '%s' [%s]", title, sub_name, ratio
|
||||||
|
|
|
@ -316,7 +316,7 @@ class ZimukuProvider(Provider):
|
||||||
r = self.yunsuo_bypass(download_link, headers={'Referer': subtitle.page_link}, timeout=30)
|
r = self.yunsuo_bypass(download_link, headers={'Referer': subtitle.page_link}, timeout=30)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
try:
|
try:
|
||||||
filename = r.headers["Content-Disposition"]
|
filename = r.headers["Content-Disposition"].lower()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.debug("Unable to parse subtitles filename. Dropping this subtitles.")
|
logger.debug("Unable to parse subtitles filename. Dropping this subtitles.")
|
||||||
return
|
return
|
||||||
|
|
|
@ -12,8 +12,9 @@ import chardet
|
||||||
import pysrt
|
import pysrt
|
||||||
import pysubs2
|
import pysubs2
|
||||||
from bs4 import UnicodeDammit
|
from bs4 import UnicodeDammit
|
||||||
|
from copy import deepcopy
|
||||||
from pysubs2 import SSAStyle
|
from pysubs2 import SSAStyle
|
||||||
from pysubs2.subrip import parse_tags, MAX_REPRESENTABLE_TIME
|
from pysubs2.formats.subrip import parse_tags, MAX_REPRESENTABLE_TIME
|
||||||
from pysubs2.time import ms_to_times
|
from pysubs2.time import ms_to_times
|
||||||
from subzero.modification import SubtitleModifications
|
from subzero.modification import SubtitleModifications
|
||||||
from subzero.language import Language
|
from subzero.language import Language
|
||||||
|
@ -62,9 +63,14 @@ class Subtitle(Subtitle_):
|
||||||
_guessed_encoding = None
|
_guessed_encoding = None
|
||||||
_is_valid = False
|
_is_valid = False
|
||||||
use_original_format = False
|
use_original_format = False
|
||||||
format = "srt" # default format is srt
|
# format = "srt" # default format is srt
|
||||||
|
|
||||||
def __init__(self, language, hearing_impaired=False, page_link=None, encoding=None, mods=None, original_format=False):
|
def __init__(self, language, hearing_impaired=False, page_link=None, encoding=None, mods=None, original_format=False):
|
||||||
|
# language needs to be cloned because it is actually a reference to the provider language object
|
||||||
|
# if a new copy is not created then all subsequent subtitles for this provider will incorrectly be modified
|
||||||
|
# at least until Bazarr is restarted or the provider language object is recreated somehow
|
||||||
|
language = deepcopy(language)
|
||||||
|
|
||||||
# set subtitle language to hi if it's hearing_impaired
|
# set subtitle language to hi if it's hearing_impaired
|
||||||
if hearing_impaired:
|
if hearing_impaired:
|
||||||
language = Language.rebuild(language, hi=True)
|
language = Language.rebuild(language, hi=True)
|
||||||
|
@ -74,6 +80,21 @@ class Subtitle(Subtitle_):
|
||||||
self.mods = mods
|
self.mods = mods
|
||||||
self._is_valid = False
|
self._is_valid = False
|
||||||
self.use_original_format = original_format
|
self.use_original_format = original_format
|
||||||
|
self._og_format = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format(self):
|
||||||
|
if self.use_original_format and self._og_format is not None:
|
||||||
|
logger.debug("Original format requested [%s]", self._og_format)
|
||||||
|
return self._og_format
|
||||||
|
|
||||||
|
logger.debug("Will assume srt format")
|
||||||
|
return "srt"
|
||||||
|
|
||||||
|
# Compatibility
|
||||||
|
@format.setter
|
||||||
|
def format(self, val):
|
||||||
|
self._og_format = val
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
r_info = str(self.release_info or "").replace("\n", " | ").strip()
|
r_info = str(self.release_info or "").replace("\n", " | ").strip()
|
||||||
|
@ -260,7 +281,7 @@ class Subtitle(Subtitle_):
|
||||||
return encoding
|
return encoding
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
"""Check if a :attr:`text` is a valid SubRip format. Note that orignal format will pypass the checking
|
"""Check if a :attr:`text` is a valid SubRip format. Note that original format will bypass the checking
|
||||||
|
|
||||||
:return: whether or not the subtitle is valid.
|
:return: whether or not the subtitle is valid.
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
@ -292,11 +313,13 @@ class Subtitle(Subtitle_):
|
||||||
logger.info("Got FPS from MicroDVD subtitle: %s", subs.fps)
|
logger.info("Got FPS from MicroDVD subtitle: %s", subs.fps)
|
||||||
else:
|
else:
|
||||||
logger.info("Got format: %s", subs.format)
|
logger.info("Got format: %s", subs.format)
|
||||||
if self.use_original_format:
|
self._og_format = subs.format
|
||||||
self.format = subs.format
|
self._is_valid = True
|
||||||
self._is_valid = True
|
# if self.use_original_format:
|
||||||
logger.debug("Using original format")
|
# self.format = subs.format
|
||||||
return True
|
# self._is_valid = True
|
||||||
|
# logger.debug("Using original format")
|
||||||
|
return True
|
||||||
|
|
||||||
except pysubs2.UnknownFPSError:
|
except pysubs2.UnknownFPSError:
|
||||||
# if parsing failed, use frame rate from provider
|
# if parsing failed, use frame rate from provider
|
||||||
|
@ -340,7 +363,7 @@ class Subtitle(Subtitle_):
|
||||||
fragment = fragment.replace(r"\n", u"\n")
|
fragment = fragment.replace(r"\n", u"\n")
|
||||||
fragment = fragment.replace(r"\N", u"\n")
|
fragment = fragment.replace(r"\N", u"\n")
|
||||||
if sty.drawing:
|
if sty.drawing:
|
||||||
raise pysubs2.ContentNotUsable
|
return None
|
||||||
|
|
||||||
if format == "srt":
|
if format == "srt":
|
||||||
if sty.italic:
|
if sty.italic:
|
||||||
|
@ -373,9 +396,10 @@ class Subtitle(Subtitle_):
|
||||||
for i, line in enumerate(visible_lines, 1):
|
for i, line in enumerate(visible_lines, 1):
|
||||||
start = ms_to_timestamp(line.start, mssep=mssep)
|
start = ms_to_timestamp(line.start, mssep=mssep)
|
||||||
end = ms_to_timestamp(line.end, mssep=mssep)
|
end = ms_to_timestamp(line.end, mssep=mssep)
|
||||||
try:
|
|
||||||
text = prepare_text(line.text, sub.styles.get(line.style, SSAStyle.DEFAULT_STYLE))
|
text = prepare_text(line.text, sub.styles.get(line.style, SSAStyle.DEFAULT_STYLE))
|
||||||
except pysubs2.ContentNotUsable:
|
|
||||||
|
if text is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
out.append(u"%d\n" % i)
|
out.append(u"%d\n" % i)
|
||||||
|
|
|
@ -35,6 +35,8 @@ class Video(Video_):
|
||||||
info_url=None,
|
info_url=None,
|
||||||
series_anidb_id=None,
|
series_anidb_id=None,
|
||||||
series_anidb_episode_id=None,
|
series_anidb_episode_id=None,
|
||||||
|
series_anidb_season_episode_offset=None,
|
||||||
|
anilist_id=None,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
super(Video, self).__init__(
|
super(Video, self).__init__(
|
||||||
|
@ -61,3 +63,5 @@ class Video(Video_):
|
||||||
self.info_url = info_url
|
self.info_url = info_url
|
||||||
self.series_anidb_series_id = series_anidb_id,
|
self.series_anidb_series_id = series_anidb_id,
|
||||||
self.series_anidb_episode_id = series_anidb_episode_id,
|
self.series_anidb_episode_id = series_anidb_episode_id,
|
||||||
|
self.series_anidb_season_episode_offset = series_anidb_season_episode_offset,
|
||||||
|
self.anilist_id = anilist_id,
|
||||||
|
|
|
@ -2,18 +2,24 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "error",
|
"no-console": "error",
|
||||||
"camelcase": "warn",
|
"camelcase": "warn",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": ["..*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
"@typescript-eslint/no-empty-function": "warn",
|
"@typescript-eslint/no-empty-function": "warn",
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "warn"
|
"@typescript-eslint/no-unused-vars": "warn"
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended"
|
||||||
],
|
],
|
||||||
"plugins": ["testing-library"],
|
"plugins": ["testing-library", "simple-import-sort", "react-refresh"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -21,6 +27,44 @@
|
||||||
"**/?(*.)+(spec|test).[jt]s?(x)"
|
"**/?(*.)+(spec|test).[jt]s?(x)"
|
||||||
],
|
],
|
||||||
"extends": ["plugin:testing-library/react"]
|
"extends": ["plugin:testing-library/react"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {
|
||||||
|
"simple-import-sort/imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
[
|
||||||
|
// React Packages
|
||||||
|
"^react",
|
||||||
|
// Mantine Packages
|
||||||
|
"^@mantine/",
|
||||||
|
// Vendor Packages
|
||||||
|
"^(\\w|@\\w)",
|
||||||
|
// Side Effect Imports
|
||||||
|
"^\\u0000",
|
||||||
|
// Internal Packages
|
||||||
|
"^@/\\w",
|
||||||
|
// Parent Imports
|
||||||
|
"^\\.\\.(?!/?$)",
|
||||||
|
"^\\.\\./?$",
|
||||||
|
// Relative Imports
|
||||||
|
"^\\./(?=.*/)(?!/?$)",
|
||||||
|
"^\\.(?!/?$)",
|
||||||
|
"^\\./?$",
|
||||||
|
// Style Imports
|
||||||
|
"^.+\\.?(css)$"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaVersion": "latest"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
8
frontend/.gitignore
vendored
|
@ -1,7 +1,7 @@
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
*.local
|
*.local
|
||||||
|
*.tsbuildinfo
|
||||||
build
|
build
|
||||||
coverage
|
coverage
|
||||||
|
dev-dist
|
||||||
*.tsbuildinfo
|
dist
|
||||||
|
node_modules
|
||||||
|
|
1
frontend/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
20.13
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/)
|
- Either [Node.js](https://nodejs.org/) installed manually or using [Node Version Manager](https://github.com/nvm-sh/nvm)
|
||||||
- npm (included in Node.js)
|
- npm (included in Node.js)
|
||||||
|
|
||||||
|
> The recommended Node version to use and maintained is managed on the `.nvmrc` file. You can either install manually
|
||||||
|
> or use `nvm install` followed by `nvm use`.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Clone or download this repository
|
1. Clone or download this repository
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { dependencies } from "../package.json";
|
import { dependencies } from "../package.json";
|
||||||
|
|
||||||
const vendors = [
|
const vendors = [
|
||||||
"react",
|
"react",
|
||||||
"react-router-dom",
|
"react-router-dom",
|
||||||
"react-dom",
|
"react-dom",
|
||||||
"react-query",
|
"@tanstack/react-query",
|
||||||
"axios",
|
"axios",
|
||||||
"socket.io-client",
|
"socket.io-client",
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,7 +5,17 @@
|
||||||
<base href="{{baseUrl}}" />
|
<base href="{{baseUrl}}" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/x-icon" href="./images/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="./images/favicon.ico" />
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
href="./images/apple-touch-icon-180x180.png"
|
||||||
|
sizes="180x180"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="mask-icon"
|
||||||
|
href="./images/maskable-icon-512x512.png"
|
||||||
|
color="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#be4bdb" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
|
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
|
||||||
|
|
4627
frontend/package-lock.json
generated
|
@ -13,70 +13,81 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^6.0.21",
|
"@mantine/core": "^7.12.2",
|
||||||
"@mantine/dropzone": "^6.0.21",
|
"@mantine/dropzone": "^7.12.2",
|
||||||
"@mantine/form": "^6.0.21",
|
"@mantine/form": "^7.12.2",
|
||||||
"@mantine/hooks": "^6.0.21",
|
"@mantine/hooks": "^7.12.2",
|
||||||
"@mantine/modals": "^6.0.21",
|
"@mantine/modals": "^7.12.2",
|
||||||
"@mantine/notifications": "^6.0.21",
|
"@mantine/notifications": "^7.12.2",
|
||||||
|
"@tanstack/react-query": "^5.40.1",
|
||||||
|
"@tanstack/react-table": "^8.19.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"react": "^18.2.0",
|
"braces": "^3.0.3",
|
||||||
"react-dom": "^18.2.0",
|
"react": "^18.3.1",
|
||||||
"react-query": "^3.39.3",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.23.1",
|
||||||
"socket.io-client": "^4.7.5"
|
"socket.io-client": "^4.7.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/roboto": "^5.0.12",
|
"@fontsource/roboto": "^5.0.12",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
|
"@tanstack/react-query-devtools": "^5.40.1",
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"@testing-library/jest-dom": "^6.4.2",
|
||||||
"@testing-library/react": "^15.0.5",
|
"@testing-library/react": "^15.0.5",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/lodash": "^4.17.1",
|
"@types/lodash": "^4.17.1",
|
||||||
"@types/node": "^20.12.6",
|
"@types/node": "^20.12.6",
|
||||||
"@types/react": "^18.2.75",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.2.24",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/react-table": "^7.7.20",
|
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||||
|
"@typescript-eslint/parser": "^7.16.0",
|
||||||
|
"@vite-pwa/assets-generator": "^0.2.4",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@vitest/coverage-v8": "^1.4.0",
|
"@vitest/coverage-v8": "^1.4.0",
|
||||||
"@vitest/ui": "^1.2.2",
|
"@vitest/ui": "^1.2.2",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.7",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.0",
|
||||||
"eslint-plugin-testing-library": "^6.2.0",
|
"eslint-plugin-testing-library": "^6.2.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"postcss-preset-mantine": "^1.14.4",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"pretty-quick": "^4.0.0",
|
"pretty-quick": "^4.0.0",
|
||||||
"react-table": "^7.8.0",
|
"recharts": "^2.12.7",
|
||||||
"recharts": "^2.12.6",
|
|
||||||
"sass": "^1.74.1",
|
"sass": "^1.74.1",
|
||||||
"typescript": "^5.4.4",
|
"typescript": "^5.4.4",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"vite-plugin-checker": "^0.6.4",
|
"vite-plugin-checker": "^0.6.4",
|
||||||
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
"vitest": "^1.2.2",
|
"vitest": "^1.2.2",
|
||||||
"yaml": "^2.4.1"
|
"yaml": "^2.4.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:ci": "vite build -m development",
|
"build:ci": "vite build -m development",
|
||||||
"check": "eslint --ext .ts,.tsx src",
|
"check": "eslint --ext .ts,.tsx src",
|
||||||
|
"check:fix": "eslint --ext .ts,.tsx src --fix",
|
||||||
"check:ts": "tsc --noEmit --incremental false",
|
"check:ts": "tsc --noEmit --incremental false",
|
||||||
"check:fmt": "prettier -c .",
|
"check:fmt": "prettier -c .",
|
||||||
"test": "vitest",
|
|
||||||
"test:ui": "vitest --ui",
|
|
||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage",
|
||||||
"format": "prettier -w .",
|
"format": "prettier -w .",
|
||||||
"prepare": "cd .. && husky frontend/.husky"
|
"pwa-assets:generate": "pwa-assets-generator --preset minimal-2023 public/images/logo128.png",
|
||||||
|
"prepare": "cd .. && husky frontend/.husky",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"start": "vite",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
14
frontend/postcss.config.cjs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-preset-mantine": {},
|
||||||
|
"postcss-simple-vars": {
|
||||||
|
variables: {
|
||||||
|
"mantine-breakpoint-xs": "36em",
|
||||||
|
"mantine-breakpoint-sm": "48em",
|
||||||
|
"mantine-breakpoint-md": "62em",
|
||||||
|
"mantine-breakpoint-lg": "75em",
|
||||||
|
"mantine-breakpoint-xl": "88em",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
BIN
frontend/public/images/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.6 KiB |
BIN
frontend/public/images/maskable-icon-512x512.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
frontend/public/images/pwa-192x192.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
frontend/public/images/pwa-512x512.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/images/pwa-64x64.png
Normal file
After Width: | Height: | Size: 866 B |
BIN
frontend/public/images/pwa-narrow-series-list.jpeg
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
frontend/public/images/pwa-narrow-series-overview.jpeg
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
frontend/public/images/pwa-wide-series-list.jpeg
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
frontend/public/images/pwa-wide-series-overview.jpeg
Normal file
After Width: | Height: | Size: 241 KiB |
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Bazarr",
|
|
||||||
"short_name": "Bazarr",
|
|
||||||
"description": "Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements.",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#be4bdb",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/images/android-chrome-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/images/apple-touch-icon.png",
|
|
||||||
"sizes": "180x180",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/images/mstile-150x150.png",
|
|
||||||
"sizes": "150x150",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
9
frontend/src/App/Header.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.header {
|
||||||
|
@include light {
|
||||||
|
color: var(--mantine-color-gray-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
color: var(--mantine-color-dark-0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +1,26 @@
|
||||||
import { useSystem, useSystemSettings } from "@/apis/hooks";
|
import { FunctionComponent } from "react";
|
||||||
import { Action, Search } from "@/components";
|
import {
|
||||||
import { Layout } from "@/constants";
|
Anchor,
|
||||||
import { useNavbar } from "@/contexts/Navbar";
|
AppShell,
|
||||||
import { useIsOnline } from "@/contexts/Online";
|
Avatar,
|
||||||
import { Environment, useGotoHomepage } from "@/utilities";
|
Badge,
|
||||||
|
Burger,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
Menu,
|
||||||
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
faArrowRotateLeft,
|
faArrowRotateLeft,
|
||||||
faGear,
|
faGear,
|
||||||
faPowerOff,
|
faPowerOff,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import { useSystem, useSystemSettings } from "@/apis/hooks";
|
||||||
Anchor,
|
import { Action, Search } from "@/components";
|
||||||
Avatar,
|
import { useNavbar } from "@/contexts/Navbar";
|
||||||
Badge,
|
import { useIsOnline } from "@/contexts/Online";
|
||||||
Burger,
|
import { Environment, useGotoHomepage } from "@/utilities";
|
||||||
Divider,
|
import styles from "./Header.module.scss";
|
||||||
Group,
|
|
||||||
Header,
|
|
||||||
MediaQuery,
|
|
||||||
Menu,
|
|
||||||
createStyles,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { FunctionComponent } from "react";
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => {
|
|
||||||
const headerBackgroundColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[4];
|
|
||||||
return {
|
|
||||||
header: {
|
|
||||||
backgroundColor: headerBackgroundColor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const AppHeader: FunctionComponent = () => {
|
const AppHeader: FunctionComponent = () => {
|
||||||
const { data: settings } = useSystemSettings();
|
const { data: settings } = useSystemSettings();
|
||||||
|
@ -47,39 +35,28 @@ const AppHeader: FunctionComponent = () => {
|
||||||
|
|
||||||
const goHome = useGotoHomepage();
|
const goHome = useGotoHomepage();
|
||||||
|
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header p="md" height={Layout.HEADER_HEIGHT} className={classes.header}>
|
<AppShell.Header p="md" className={styles.header}>
|
||||||
<Group position="apart" noWrap>
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Group noWrap>
|
<Group wrap="nowrap">
|
||||||
<MediaQuery
|
<Anchor onClick={goHome} visibleFrom="sm">
|
||||||
smallerThan={Layout.MOBILE_BREAKPOINT}
|
<Avatar
|
||||||
styles={{ display: "none" }}
|
alt="brand"
|
||||||
>
|
size={32}
|
||||||
<Anchor onClick={goHome}>
|
src={`${Environment.baseUrl}/images/logo64.png`}
|
||||||
<Avatar
|
></Avatar>
|
||||||
alt="brand"
|
</Anchor>
|
||||||
size={32}
|
<Burger
|
||||||
src={`${Environment.baseUrl}/images/logo64.png`}
|
opened={showed}
|
||||||
></Avatar>
|
onClick={() => show(!showed)}
|
||||||
</Anchor>
|
size="sm"
|
||||||
</MediaQuery>
|
hiddenFrom="sm"
|
||||||
<MediaQuery
|
></Burger>
|
||||||
largerThan={Layout.MOBILE_BREAKPOINT}
|
<Badge size="lg" radius="sm" variant="brand">
|
||||||
styles={{ display: "none" }}
|
|
||||||
>
|
|
||||||
<Burger
|
|
||||||
opened={showed}
|
|
||||||
onClick={() => show(!showed)}
|
|
||||||
size="sm"
|
|
||||||
></Burger>
|
|
||||||
</MediaQuery>
|
|
||||||
<Badge size="lg" radius="sm">
|
|
||||||
Bazarr
|
Bazarr
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
<Group spacing="xs" position="right" noWrap>
|
<Group gap="xs" justify="right" wrap="nowrap">
|
||||||
<Search></Search>
|
<Search></Search>
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
|
@ -87,21 +64,20 @@ const AppHeader: FunctionComponent = () => {
|
||||||
label="System"
|
label="System"
|
||||||
tooltip={{ position: "left", openDelay: 2000 }}
|
tooltip={{ position: "left", openDelay: 2000 }}
|
||||||
loading={offline}
|
loading={offline}
|
||||||
color={offline ? "yellow" : undefined}
|
c={offline ? "yellow" : undefined}
|
||||||
icon={faGear}
|
icon={faGear}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="light"
|
|
||||||
></Action>
|
></Action>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
leftSection={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
||||||
onClick={() => restart()}
|
onClick={() => restart()}
|
||||||
>
|
>
|
||||||
Restart
|
Restart
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<FontAwesomeIcon icon={faPowerOff} />}
|
leftSection={<FontAwesomeIcon icon={faPowerOff} />}
|
||||||
onClick={() => shutdown()}
|
onClick={() => shutdown()}
|
||||||
>
|
>
|
||||||
Shutdown
|
Shutdown
|
||||||
|
@ -114,7 +90,7 @@ const AppHeader: FunctionComponent = () => {
|
||||||
</Menu>
|
</Menu>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Header>
|
</AppShell.Header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
56
frontend/src/App/Navbar.module.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
.anchor {
|
||||||
|
border-color: var(--mantine-color-gray-5);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
border-color: var(--mantine-color-dark-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-left: 2px solid $color-brand-4;
|
||||||
|
background-color: var(--mantine-color-gray-1);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
border-left: 2px solid $color-brand-8;
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover {
|
||||||
|
background-color: var(--mantine-color-gray-0);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
background-color: var(--mantine-color-dark-7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: auto;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: var(--mantine-shadow-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.4rem;
|
||||||
|
margin-right: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
background-color: var(--mantine-color-gray-2);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--mantine-color-gray-8);
|
||||||
|
|
||||||
|
@include dark {
|
||||||
|
color: var(--mantine-color-gray-5);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,4 @@
|
||||||
import { Action } from "@/components";
|
import React, {
|
||||||
import { Layout } from "@/constants";
|
|
||||||
import { useNavbar } from "@/contexts/Navbar";
|
|
||||||
import { useRouteItems } from "@/Router";
|
|
||||||
import { CustomRouteObject, Route } from "@/Router/type";
|
|
||||||
import { BuildKey, pathJoin } from "@/utilities";
|
|
||||||
import { LOG } from "@/utilities/console";
|
|
||||||
import {
|
|
||||||
faHeart,
|
|
||||||
faMoon,
|
|
||||||
faSun,
|
|
||||||
IconDefinition,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import {
|
|
||||||
Anchor,
|
|
||||||
Badge,
|
|
||||||
Collapse,
|
|
||||||
createStyles,
|
|
||||||
Divider,
|
|
||||||
Group,
|
|
||||||
Navbar as MantineNavbar,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useHover } from "@mantine/hooks";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import {
|
|
||||||
createContext,
|
createContext,
|
||||||
FunctionComponent,
|
FunctionComponent,
|
||||||
useContext,
|
useContext,
|
||||||
|
@ -35,6 +7,34 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom";
|
import { matchPath, NavLink, RouteObject, useLocation } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
AppShell,
|
||||||
|
Badge,
|
||||||
|
Collapse,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useComputedColorScheme,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useHover } from "@mantine/hooks";
|
||||||
|
import {
|
||||||
|
faHeart,
|
||||||
|
faMoon,
|
||||||
|
faSun,
|
||||||
|
IconDefinition,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { Action } from "@/components";
|
||||||
|
import { useNavbar } from "@/contexts/Navbar";
|
||||||
|
import { useRouteItems } from "@/Router";
|
||||||
|
import { CustomRouteObject, Route } from "@/Router/type";
|
||||||
|
import { BuildKey, pathJoin } from "@/utilities";
|
||||||
|
import { LOG } from "@/utilities/console";
|
||||||
|
import styles from "./Navbar.module.scss";
|
||||||
|
|
||||||
const Selection = createContext<{
|
const Selection = createContext<{
|
||||||
selection: string | null;
|
selection: string | null;
|
||||||
|
@ -97,11 +97,12 @@ function useIsActive(parent: string, route: RouteObject) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppNavbar: FunctionComponent = () => {
|
const AppNavbar: FunctionComponent = () => {
|
||||||
const { showed } = useNavbar();
|
|
||||||
const [selection, select] = useState<string | null>(null);
|
const [selection, select] = useState<string | null>(null);
|
||||||
|
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const computedColorScheme = useComputedColorScheme("light");
|
||||||
|
|
||||||
|
const dark = computedColorScheme === "dark";
|
||||||
|
|
||||||
const routes = useRouteItems();
|
const routes = useRouteItems();
|
||||||
|
|
||||||
|
@ -111,23 +112,10 @@ const AppNavbar: FunctionComponent = () => {
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineNavbar
|
<AppShell.Navbar p="xs" className={styles.nav}>
|
||||||
p="xs"
|
|
||||||
hiddenBreakpoint={Layout.MOBILE_BREAKPOINT}
|
|
||||||
hidden={!showed}
|
|
||||||
width={{ [Layout.MOBILE_BREAKPOINT]: Layout.NAVBAR_WIDTH }}
|
|
||||||
styles={(theme) => ({
|
|
||||||
root: {
|
|
||||||
backgroundColor:
|
|
||||||
theme.colorScheme === "light"
|
|
||||||
? theme.colors.gray[2]
|
|
||||||
: theme.colors.dark[6],
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Selection.Provider value={{ selection, select }}>
|
<Selection.Provider value={{ selection, select }}>
|
||||||
<MantineNavbar.Section grow>
|
<AppShell.Section grow>
|
||||||
<Stack spacing={0}>
|
<Stack gap={0}>
|
||||||
{routes.map((route, idx) => (
|
{routes.map((route, idx) => (
|
||||||
<RouteItem
|
<RouteItem
|
||||||
key={BuildKey("nav", idx)}
|
key={BuildKey("nav", idx)}
|
||||||
|
@ -136,14 +124,13 @@ const AppNavbar: FunctionComponent = () => {
|
||||||
></RouteItem>
|
></RouteItem>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</MantineNavbar.Section>
|
</AppShell.Section>
|
||||||
<Divider></Divider>
|
<Divider></Divider>
|
||||||
<MantineNavbar.Section mt="xs">
|
<AppShell.Section mt="xs">
|
||||||
<Group spacing="xs">
|
<Group gap="xs">
|
||||||
<Action
|
<Action
|
||||||
label="Change Theme"
|
label="Change Theme"
|
||||||
color={dark ? "yellow" : "indigo"}
|
c={dark ? "yellow" : "indigo"}
|
||||||
variant="subtle"
|
|
||||||
onClick={() => toggleColorScheme()}
|
onClick={() => toggleColorScheme()}
|
||||||
icon={dark ? faSun : faMoon}
|
icon={dark ? faSun : faMoon}
|
||||||
></Action>
|
></Action>
|
||||||
|
@ -151,17 +138,12 @@ const AppNavbar: FunctionComponent = () => {
|
||||||
href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XHHRWXT9YB7WE&source=url"
|
href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XHHRWXT9YB7WE&source=url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Action
|
<Action label="Donate" icon={faHeart} c="red"></Action>
|
||||||
label="Donate"
|
|
||||||
icon={faHeart}
|
|
||||||
variant="subtle"
|
|
||||||
color="red"
|
|
||||||
></Action>
|
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
</MantineNavbar.Section>
|
</AppShell.Section>
|
||||||
</Selection.Provider>
|
</Selection.Provider>
|
||||||
</MantineNavbar>
|
</AppShell.Navbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -186,7 +168,7 @@ const RouteItem: FunctionComponent<{
|
||||||
|
|
||||||
if (children !== undefined) {
|
if (children !== undefined) {
|
||||||
const elements = (
|
const elements = (
|
||||||
<Stack spacing={0}>
|
<Stack gap={0}>
|
||||||
{children.map((child, idx) => (
|
{children.map((child, idx) => (
|
||||||
<RouteItem
|
<RouteItem
|
||||||
parent={link}
|
parent={link}
|
||||||
|
@ -199,7 +181,7 @@ const RouteItem: FunctionComponent<{
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={0}>
|
<Stack gap={0}>
|
||||||
<NavbarItem
|
<NavbarItem
|
||||||
primary
|
primary
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -244,53 +226,6 @@ const RouteItem: FunctionComponent<{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => {
|
|
||||||
const borderColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[5] : theme.colors.dark[4];
|
|
||||||
|
|
||||||
const activeBorderColor =
|
|
||||||
theme.colorScheme === "light"
|
|
||||||
? theme.colors.brand[4]
|
|
||||||
: theme.colors.brand[8];
|
|
||||||
|
|
||||||
const activeBackgroundColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[1] : theme.colors.dark[8];
|
|
||||||
|
|
||||||
const hoverBackgroundColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[7];
|
|
||||||
|
|
||||||
const textColor =
|
|
||||||
theme.colorScheme === "light" ? theme.colors.gray[8] : theme.colors.gray[5];
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: {
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
color: textColor,
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
textDecoration: "none",
|
|
||||||
borderLeft: `2px solid ${borderColor}`,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
backgroundColor: activeBackgroundColor,
|
|
||||||
borderLeft: `2px solid ${activeBorderColor}`,
|
|
||||||
boxShadow: theme.shadows.xs,
|
|
||||||
},
|
|
||||||
hover: {
|
|
||||||
backgroundColor: hoverBackgroundColor,
|
|
||||||
},
|
|
||||||
icon: { width: "1.4rem", marginRight: theme.spacing.xs },
|
|
||||||
badge: {
|
|
||||||
marginLeft: "auto",
|
|
||||||
textDecoration: "none",
|
|
||||||
boxShadow: theme.shadows.xs,
|
|
||||||
color: textColor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface NavbarItemProps {
|
interface NavbarItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
@ -308,8 +243,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
||||||
onClick,
|
onClick,
|
||||||
primary = false,
|
primary = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
const { show } = useNavbar();
|
const { show } = useNavbar();
|
||||||
|
|
||||||
const { ref, hovered } = useHover();
|
const { ref, hovered } = useHover();
|
||||||
|
@ -335,9 +268,9 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
||||||
}}
|
}}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
clsx(
|
clsx(
|
||||||
clsx(classes.anchor, {
|
clsx(styles.anchor, {
|
||||||
[classes.active]: isActive,
|
[styles.active]: isActive,
|
||||||
[classes.hover]: hovered,
|
[styles.hover]: hovered,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -347,18 +280,19 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
||||||
inline
|
inline
|
||||||
p="xs"
|
p="xs"
|
||||||
size="sm"
|
size="sm"
|
||||||
weight={primary ? "bold" : "normal"}
|
fw={primary ? "bold" : "normal"}
|
||||||
className={classes.text}
|
className={styles.text}
|
||||||
|
span
|
||||||
>
|
>
|
||||||
{icon && (
|
{icon && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className={classes.icon}
|
className={styles.icon}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
></FontAwesomeIcon>
|
></FontAwesomeIcon>
|
||||||
)}
|
)}
|
||||||
{name}
|
{name}
|
||||||
{shouldHideBadge === false && (
|
{!shouldHideBadge && (
|
||||||
<Badge className={classes.badge} radius="xs">
|
<Badge className={styles.badge} radius="xs">
|
||||||
{badge}
|
{badge}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
39
frontend/src/App/ThemeLoader.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { MantineColorScheme, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { useSystemSettings } from "@/apis/hooks";
|
||||||
|
|
||||||
|
const ThemeProvider = () => {
|
||||||
|
const [localScheme, setLocalScheme] = useState<MantineColorScheme | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const { setColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
const settings = useSystemSettings();
|
||||||
|
|
||||||
|
const settingsColorScheme = settings.data?.general
|
||||||
|
.theme as MantineColorScheme;
|
||||||
|
|
||||||
|
const setScheme = useCallback(
|
||||||
|
(colorScheme: MantineColorScheme) => {
|
||||||
|
setColorScheme(colorScheme);
|
||||||
|
},
|
||||||
|
[setColorScheme],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!settingsColorScheme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localScheme === settingsColorScheme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScheme(settingsColorScheme);
|
||||||
|
setLocalScheme(settingsColorScheme);
|
||||||
|
}, [settingsColorScheme, setScheme, localScheme]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProvider;
|
61
frontend/src/App/ThemeProvider.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { FunctionComponent, PropsWithChildren } from "react";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
createTheme,
|
||||||
|
MantineProvider,
|
||||||
|
Pagination,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import ThemeLoader from "@/App/ThemeLoader";
|
||||||
|
import "@mantine/core/styles.layer.css";
|
||||||
|
import "@mantine/notifications/styles.layer.css";
|
||||||
|
import styleVars from "@/assets/_variables.module.scss";
|
||||||
|
import actionIconClasses from "@/assets/action_icon.module.scss";
|
||||||
|
import badgeClasses from "@/assets/badge.module.scss";
|
||||||
|
import buttonClasses from "@/assets/button.module.scss";
|
||||||
|
import paginationClasses from "@/assets/pagination.module.scss";
|
||||||
|
|
||||||
|
const themeProvider = createTheme({
|
||||||
|
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
|
||||||
|
colors: {
|
||||||
|
brand: [
|
||||||
|
styleVars.colorBrand0,
|
||||||
|
styleVars.colorBrand1,
|
||||||
|
styleVars.colorBrand2,
|
||||||
|
styleVars.colorBrand3,
|
||||||
|
styleVars.colorBrand4,
|
||||||
|
styleVars.colorBrand5,
|
||||||
|
styleVars.colorBrand6,
|
||||||
|
styleVars.colorBrand7,
|
||||||
|
styleVars.colorBrand8,
|
||||||
|
styleVars.colorBrand9,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryColor: "brand",
|
||||||
|
components: {
|
||||||
|
ActionIcon: ActionIcon.extend({
|
||||||
|
classNames: actionIconClasses,
|
||||||
|
}),
|
||||||
|
Badge: Badge.extend({
|
||||||
|
classNames: badgeClasses,
|
||||||
|
}),
|
||||||
|
Button: Button.extend({
|
||||||
|
classNames: buttonClasses,
|
||||||
|
}),
|
||||||
|
Pagination: Pagination.extend({
|
||||||
|
classNames: paginationClasses,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<MantineProvider theme={themeProvider} defaultColorScheme="auto">
|
||||||
|
<ThemeLoader />
|
||||||
|
{children}
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProvider;
|
|
@ -1,5 +1,5 @@
|
||||||
import { render } from "@/tests";
|
|
||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
|
import { render } from "@/tests";
|
||||||
import App from ".";
|
import App from ".";
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
|
import { FunctionComponent, useEffect, useState } from "react";
|
||||||
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
|
import { AppShell } from "@mantine/core";
|
||||||
|
import { useWindowEvent } from "@mantine/hooks";
|
||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
import AppNavbar from "@/App/Navbar";
|
import AppNavbar from "@/App/Navbar";
|
||||||
import { RouterNames } from "@/Router/RouterNames";
|
|
||||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
import { Layout } from "@/constants";
|
|
||||||
import NavbarProvider from "@/contexts/Navbar";
|
import NavbarProvider from "@/contexts/Navbar";
|
||||||
import OnlineProvider from "@/contexts/Online";
|
import OnlineProvider from "@/contexts/Online";
|
||||||
import { notification } from "@/modules/task";
|
import { notification } from "@/modules/task";
|
||||||
import CriticalError from "@/pages/errors/CriticalError";
|
import CriticalError from "@/pages/errors/CriticalError";
|
||||||
|
import { RouterNames } from "@/Router/RouterNames";
|
||||||
import { Environment } from "@/utilities";
|
import { Environment } from "@/utilities";
|
||||||
import { AppShell } from "@mantine/core";
|
|
||||||
import { useWindowEvent } from "@mantine/hooks";
|
|
||||||
import { showNotification } from "@mantine/notifications";
|
|
||||||
import { FunctionComponent, useEffect, useState } from "react";
|
|
||||||
import { Outlet, useNavigate } from "react-router-dom";
|
|
||||||
import AppHeader from "./Header";
|
import AppHeader from "./Header";
|
||||||
|
import styleVars from "@/assets/_variables.module.scss";
|
||||||
|
|
||||||
const App: FunctionComponent = () => {
|
const App: FunctionComponent = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -55,13 +55,19 @@ const App: FunctionComponent = () => {
|
||||||
<NavbarProvider value={{ showed: navbar, show: setNavbar }}>
|
<NavbarProvider value={{ showed: navbar, show: setNavbar }}>
|
||||||
<OnlineProvider value={{ online, setOnline }}>
|
<OnlineProvider value={{ online, setOnline }}>
|
||||||
<AppShell
|
<AppShell
|
||||||
navbarOffsetBreakpoint={Layout.MOBILE_BREAKPOINT}
|
navbar={{
|
||||||
header={<AppHeader></AppHeader>}
|
width: styleVars.navBarWidth,
|
||||||
navbar={<AppNavbar></AppNavbar>}
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !navbar },
|
||||||
|
}}
|
||||||
|
header={{ height: { base: styleVars.headerHeight } }}
|
||||||
padding={0}
|
padding={0}
|
||||||
fixed
|
|
||||||
>
|
>
|
||||||
<Outlet></Outlet>
|
<AppHeader></AppHeader>
|
||||||
|
<AppNavbar></AppNavbar>
|
||||||
|
<AppShell.Main>
|
||||||
|
<Outlet></Outlet>
|
||||||
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</OnlineProvider>
|
</OnlineProvider>
|
||||||
</NavbarProvider>
|
</NavbarProvider>
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { useSystemSettings } from "@/apis/hooks";
|
|
||||||
import {
|
|
||||||
ColorScheme,
|
|
||||||
ColorSchemeProvider,
|
|
||||||
createEmotionCache,
|
|
||||||
MantineProvider,
|
|
||||||
MantineThemeOverride,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useColorScheme } from "@mantine/hooks";
|
|
||||||
import {
|
|
||||||
FunctionComponent,
|
|
||||||
PropsWithChildren,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
const theme: MantineThemeOverride = {
|
|
||||||
fontFamily: "Roboto, open sans, Helvetica Neue, Helvetica, Arial, sans-serif",
|
|
||||||
colors: {
|
|
||||||
brand: [
|
|
||||||
"#F8F0FC",
|
|
||||||
"#F3D9FA",
|
|
||||||
"#EEBEFA",
|
|
||||||
"#E599F7",
|
|
||||||
"#DA77F2",
|
|
||||||
"#CC5DE8",
|
|
||||||
"#BE4BDB",
|
|
||||||
"#AE3EC9",
|
|
||||||
"#9C36B5",
|
|
||||||
"#862E9C",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
primaryColor: "brand",
|
|
||||||
};
|
|
||||||
|
|
||||||
function useAutoColorScheme() {
|
|
||||||
const settings = useSystemSettings();
|
|
||||||
const settingsColorScheme = settings.data?.general.theme;
|
|
||||||
|
|
||||||
let preferredColorScheme: ColorScheme = useColorScheme();
|
|
||||||
switch (settingsColorScheme) {
|
|
||||||
case "light":
|
|
||||||
preferredColorScheme = "light" as ColorScheme;
|
|
||||||
break;
|
|
||||||
case "dark":
|
|
||||||
preferredColorScheme = "dark" as ColorScheme;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [colorScheme, setColorScheme] = useState(preferredColorScheme);
|
|
||||||
|
|
||||||
// automatically switch dark/light theme
|
|
||||||
useEffect(() => {
|
|
||||||
setColorScheme(preferredColorScheme);
|
|
||||||
}, [preferredColorScheme]);
|
|
||||||
|
|
||||||
const toggleColorScheme = useCallback((value?: ColorScheme) => {
|
|
||||||
setColorScheme((scheme) => value || (scheme === "dark" ? "light" : "dark"));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { colorScheme, setColorScheme, toggleColorScheme };
|
|
||||||
}
|
|
||||||
|
|
||||||
const emotionCache = createEmotionCache({ key: "bazarr" });
|
|
||||||
|
|
||||||
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
|
|
||||||
const { colorScheme, toggleColorScheme } = useAutoColorScheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ColorSchemeProvider
|
|
||||||
colorScheme={colorScheme}
|
|
||||||
toggleColorScheme={toggleColorScheme}
|
|
||||||
>
|
|
||||||
<MantineProvider
|
|
||||||
withGlobalStyles
|
|
||||||
withNormalizeCSS
|
|
||||||
theme={{ colorScheme, ...theme }}
|
|
||||||
emotionCache={emotionCache}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</MantineProvider>
|
|
||||||
</ColorSchemeProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ThemeProvider;
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useSystemSettings } from "@/apis/hooks";
|
|
||||||
import { LoadingOverlay } from "@mantine/core";
|
|
||||||
import { FunctionComponent, useEffect } from "react";
|
import { FunctionComponent, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { LoadingOverlay } from "@mantine/core";
|
||||||
|
import { useSystemSettings } from "@/apis/hooks";
|
||||||
|
|
||||||
const Redirector: FunctionComponent = () => {
|
const Redirector: FunctionComponent = () => {
|
||||||
const { data } = useSystemSettings();
|
const { data } = useSystemSettings();
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
import App from "@/App";
|
import {
|
||||||
|
createContext,
|
||||||
|
FunctionComponent,
|
||||||
|
lazy,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
faClock,
|
||||||
|
faCogs,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faFileExcel,
|
||||||
|
faFilm,
|
||||||
|
faLaptop,
|
||||||
|
faPlay,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useBadges } from "@/apis/hooks";
|
import { useBadges } from "@/apis/hooks";
|
||||||
import { useEnabledStatus } from "@/apis/hooks/site";
|
import { useEnabledStatus } from "@/apis/hooks/site";
|
||||||
|
import App from "@/App";
|
||||||
import { Lazy } from "@/components/async";
|
import { Lazy } from "@/components/async";
|
||||||
import Authentication from "@/pages/Authentication";
|
import Authentication from "@/pages/Authentication";
|
||||||
import BlacklistMoviesView from "@/pages/Blacklist/Movies";
|
import BlacklistMoviesView from "@/pages/Blacklist/Movies";
|
||||||
import BlacklistSeriesView from "@/pages/Blacklist/Series";
|
import BlacklistSeriesView from "@/pages/Blacklist/Series";
|
||||||
import Episodes from "@/pages/Episodes";
|
import Episodes from "@/pages/Episodes";
|
||||||
|
import NotFound from "@/pages/errors/NotFound";
|
||||||
import MoviesHistoryView from "@/pages/History/Movies";
|
import MoviesHistoryView from "@/pages/History/Movies";
|
||||||
import SeriesHistoryView from "@/pages/History/Series";
|
import SeriesHistoryView from "@/pages/History/Series";
|
||||||
import MovieView from "@/pages/Movies";
|
import MovieView from "@/pages/Movies";
|
||||||
|
@ -30,30 +48,14 @@ import SystemReleasesView from "@/pages/System/Releases";
|
||||||
import SystemTasksView from "@/pages/System/Tasks";
|
import SystemTasksView from "@/pages/System/Tasks";
|
||||||
import WantedMoviesView from "@/pages/Wanted/Movies";
|
import WantedMoviesView from "@/pages/Wanted/Movies";
|
||||||
import WantedSeriesView from "@/pages/Wanted/Series";
|
import WantedSeriesView from "@/pages/Wanted/Series";
|
||||||
import NotFound from "@/pages/errors/NotFound";
|
|
||||||
import { Environment } from "@/utilities";
|
import { Environment } from "@/utilities";
|
||||||
import {
|
|
||||||
faClock,
|
|
||||||
faCogs,
|
|
||||||
faExclamationTriangle,
|
|
||||||
faFileExcel,
|
|
||||||
faFilm,
|
|
||||||
faLaptop,
|
|
||||||
faPlay,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import {
|
|
||||||
FunctionComponent,
|
|
||||||
createContext,
|
|
||||||
lazy,
|
|
||||||
useContext,
|
|
||||||
useMemo,
|
|
||||||
} from "react";
|
|
||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
|
||||||
import Redirector from "./Redirector";
|
import Redirector from "./Redirector";
|
||||||
import { RouterNames } from "./RouterNames";
|
import { RouterNames } from "./RouterNames";
|
||||||
import { CustomRouteObject } from "./type";
|
import { CustomRouteObject } from "./type";
|
||||||
|
|
||||||
const HistoryStats = lazy(() => import("@/pages/History/Statistics"));
|
const HistoryStats = lazy(
|
||||||
|
() => import("@/pages/History/Statistics/HistoryStats"),
|
||||||
|
);
|
||||||
const SystemStatusView = lazy(() => import("@/pages/System/Status"));
|
const SystemStatusView = lazy(() => import("@/pages/System/Status"));
|
||||||
|
|
||||||
function useRoutes(): CustomRouteObject[] {
|
function useRoutes(): CustomRouteObject[] {
|
||||||
|
|
2
frontend/src/Router/type.d.ts
vendored
|
@ -1,5 +1,5 @@
|
||||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { RouteObject } from "react-router-dom";
|
import { RouteObject } from "react-router-dom";
|
||||||
|
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
declare namespace Route {
|
declare namespace Route {
|
||||||
export type Item = {
|
export type Item = {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from "react-query";
|
} from "@tanstack/react-query";
|
||||||
import { usePaginationQuery } from "../queries/hooks";
|
import { usePaginationQuery } from "@/apis/queries/hooks";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
import api from "../raw";
|
import api from "@/apis/raw";
|
||||||
|
|
||||||
const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
|
const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
|
||||||
episodes.forEach((item) => {
|
episodes.forEach((item) => {
|
||||||
|
@ -24,30 +25,21 @@ const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useEpisodesByIds(ids: number[]) {
|
|
||||||
const client = useQueryClient();
|
|
||||||
return useQuery(
|
|
||||||
[QueryKeys.Series, QueryKeys.Episodes, ids],
|
|
||||||
() => api.episodes.byEpisodeId(ids),
|
|
||||||
{
|
|
||||||
onSuccess: (data) => {
|
|
||||||
cacheEpisodes(client, data);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useEpisodesBySeriesId(id: number) {
|
export function useEpisodesBySeriesId(id: number) {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useQuery(
|
|
||||||
[QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All],
|
const query = useQuery({
|
||||||
() => api.episodes.bySeriesId([id]),
|
queryKey: [QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All],
|
||||||
{
|
queryFn: () => api.episodes.bySeriesId([id]),
|
||||||
onSuccess: (data) => {
|
});
|
||||||
cacheEpisodes(client, data);
|
|
||||||
},
|
useEffect(() => {
|
||||||
},
|
if (query.isSuccess && query.data) {
|
||||||
);
|
cacheEpisodes(client, query.data);
|
||||||
|
}
|
||||||
|
}, [query.isSuccess, query.data, client]);
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodeWantedPagination() {
|
export function useEpisodeWantedPagination() {
|
||||||
|
@ -57,17 +49,18 @@ export function useEpisodeWantedPagination() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodeBlacklist() {
|
export function useEpisodeBlacklist() {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||||
() => api.episodes.blacklist(),
|
queryFn: () => api.episodes.blacklist(),
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodeAddBlacklist() {
|
export function useEpisodeAddBlacklist() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
mutationKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||||
(param: {
|
|
||||||
|
mutationFn: (param: {
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
episodeId: number;
|
episodeId: number;
|
||||||
form: FormType.AddBlacklist;
|
form: FormType.AddBlacklist;
|
||||||
|
@ -75,35 +68,33 @@ export function useEpisodeAddBlacklist() {
|
||||||
const { seriesId, episodeId, form } = param;
|
const { seriesId, episodeId, form } = param;
|
||||||
return api.episodes.addBlacklist(seriesId, episodeId, form);
|
return api.episodes.addBlacklist(seriesId, episodeId, form);
|
||||||
},
|
},
|
||||||
{
|
|
||||||
onSuccess: (_, { seriesId, episodeId }) => {
|
onSuccess: (_, { seriesId }) => {
|
||||||
client.invalidateQueries([
|
void client.invalidateQueries({
|
||||||
QueryKeys.Series,
|
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||||
QueryKeys.Episodes,
|
});
|
||||||
QueryKeys.Blacklist,
|
|
||||||
]);
|
void client.invalidateQueries({
|
||||||
client.invalidateQueries([QueryKeys.Series, seriesId]);
|
queryKey: [QueryKeys.Series, seriesId],
|
||||||
},
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodeDeleteBlacklist() {
|
export function useEpisodeDeleteBlacklist() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
mutationKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||||
(param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
|
|
||||||
|
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
|
||||||
api.episodes.deleteBlacklist(param.all, param.form),
|
api.episodes.deleteBlacklist(param.all, param.form),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([
|
void client.invalidateQueries({
|
||||||
QueryKeys.Series,
|
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||||
QueryKeys.Episodes,
|
});
|
||||||
QueryKeys.Blacklist,
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodeHistoryPagination() {
|
export function useEpisodeHistoryPagination() {
|
||||||
|
@ -115,12 +106,20 @@ export function useEpisodeHistoryPagination() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodeHistory(episodeId?: number) {
|
export function useEpisodeHistory(episodeId?: number) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History, episodeId],
|
queryKey: [
|
||||||
() => {
|
QueryKeys.Series,
|
||||||
|
QueryKeys.Episodes,
|
||||||
|
QueryKeys.History,
|
||||||
|
episodeId,
|
||||||
|
],
|
||||||
|
|
||||||
|
queryFn: () => {
|
||||||
if (episodeId) {
|
if (episodeId) {
|
||||||
return api.episodes.historyBy(episodeId);
|
return api.episodes.historyBy(episodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
import api from "../raw";
|
import api from "@/apis/raw";
|
||||||
|
|
||||||
export function useHistoryStats(
|
export function useHistoryStats(
|
||||||
time: History.TimeFrameOptions,
|
time: History.TimeFrameOptions,
|
||||||
|
@ -8,14 +8,19 @@ export function useHistoryStats(
|
||||||
provider: System.Provider | null,
|
provider: System.Provider | null,
|
||||||
language: Language.Info | null,
|
language: Language.Info | null,
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.History, { time, action, provider, language }],
|
queryKey: [
|
||||||
() =>
|
QueryKeys.System,
|
||||||
|
QueryKeys.History,
|
||||||
|
{ time, action, provider, language },
|
||||||
|
],
|
||||||
|
|
||||||
|
queryFn: () =>
|
||||||
api.history.stats(
|
api.history.stats(
|
||||||
time,
|
time,
|
||||||
action ?? undefined,
|
action ?? undefined,
|
||||||
provider?.name,
|
provider?.name,
|
||||||
language?.code2,
|
language?.code2,
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
import api from "../raw";
|
import api from "@/apis/raw";
|
||||||
|
|
||||||
export function useLanguages(history?: boolean) {
|
export function useLanguages(history?: boolean) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Languages, history ?? false],
|
queryKey: [QueryKeys.System, QueryKeys.Languages, history ?? false],
|
||||||
() => api.system.languages(history),
|
queryFn: () => api.system.languages(history),
|
||||||
{
|
staleTime: Infinity,
|
||||||
staleTime: Infinity,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLanguageProfiles() {
|
export function useLanguageProfiles() {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.LanguagesProfiles],
|
queryKey: [QueryKeys.System, QueryKeys.LanguagesProfiles],
|
||||||
() => api.system.languagesProfileList(),
|
queryFn: () => api.system.languagesProfileList(),
|
||||||
{
|
staleTime: Infinity,
|
||||||
staleTime: Infinity,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from "react-query";
|
} from "@tanstack/react-query";
|
||||||
import { usePaginationQuery } from "../queries/hooks";
|
import { usePaginationQuery } from "@/apis/queries/hooks";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
import api from "../raw";
|
import api from "@/apis/raw";
|
||||||
|
|
||||||
const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
|
const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
|
||||||
movies.forEach((item) => {
|
movies.forEach((item) => {
|
||||||
|
@ -14,33 +15,32 @@ const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useMoviesByIds(ids: number[]) {
|
|
||||||
const client = useQueryClient();
|
|
||||||
return useQuery([QueryKeys.Movies, ...ids], () => api.movies.movies(ids), {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
cacheMovies(client, data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMovieById(id: number) {
|
export function useMovieById(id: number) {
|
||||||
return useQuery([QueryKeys.Movies, id], async () => {
|
return useQuery({
|
||||||
const response = await api.movies.movies([id]);
|
queryKey: [QueryKeys.Movies, id],
|
||||||
return response.length > 0 ? response[0] : undefined;
|
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.movies.movies([id]);
|
||||||
|
return response.length > 0 ? response[0] : undefined;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovies() {
|
export function useMovies() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useQuery(
|
|
||||||
[QueryKeys.Movies, QueryKeys.All],
|
const query = useQuery({
|
||||||
() => api.movies.movies(),
|
queryKey: [QueryKeys.Movies, QueryKeys.All],
|
||||||
{
|
queryFn: () => api.movies.movies(),
|
||||||
onSuccess: (data) => {
|
});
|
||||||
cacheMovies(client, data);
|
|
||||||
},
|
useEffect(() => {
|
||||||
},
|
if (query.isSuccess && query.data) {
|
||||||
);
|
cacheMovies(client, query.data);
|
||||||
|
}
|
||||||
|
}, [query.isSuccess, query.data, client]);
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMoviesPagination() {
|
export function useMoviesPagination() {
|
||||||
|
@ -51,32 +51,37 @@ export function useMoviesPagination() {
|
||||||
|
|
||||||
export function useMovieModification() {
|
export function useMovieModification() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Movies],
|
mutationKey: [QueryKeys.Movies],
|
||||||
(form: FormType.ModifyItem) => api.movies.modify(form),
|
mutationFn: (form: FormType.ModifyItem) => api.movies.modify(form),
|
||||||
{
|
|
||||||
onSuccess: (_, form) => {
|
onSuccess: (_, form) => {
|
||||||
form.id.forEach((v) => {
|
form.id.forEach((v) => {
|
||||||
client.invalidateQueries([QueryKeys.Movies, v]);
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Movies, v],
|
||||||
});
|
});
|
||||||
// TODO: query less
|
});
|
||||||
client.invalidateQueries([QueryKeys.Movies]);
|
|
||||||
},
|
// TODO: query less
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Movies],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovieAction() {
|
export function useMovieAction() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Actions, QueryKeys.Movies],
|
mutationKey: [QueryKeys.Actions, QueryKeys.Movies],
|
||||||
(form: FormType.MoviesAction) => api.movies.action(form),
|
mutationFn: (form: FormType.MoviesAction) => api.movies.action(form),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.Movies]);
|
void client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.Movies],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovieWantedPagination() {
|
export function useMovieWantedPagination() {
|
||||||
|
@ -86,40 +91,49 @@ export function useMovieWantedPagination() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovieBlacklist() {
|
export function useMovieBlacklist() {
|
||||||
return useQuery([QueryKeys.Movies, QueryKeys.Blacklist], () =>
|
return useQuery({
|
||||||
api.movies.blacklist(),
|
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
|
||||||
);
|
|
||||||
|
queryFn: () => api.movies.blacklist(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovieAddBlacklist() {
|
export function useMovieAddBlacklist() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Movies, QueryKeys.Blacklist],
|
mutationKey: [QueryKeys.Movies, QueryKeys.Blacklist],
|
||||||
(param: { id: number; form: FormType.AddBlacklist }) => {
|
|
||||||
|
mutationFn: (param: { id: number; form: FormType.AddBlacklist }) => {
|
||||||
const { id, form } = param;
|
const { id, form } = param;
|
||||||
return api.movies.addBlacklist(id, form);
|
return api.movies.addBlacklist(id, form);
|
||||||
},
|
},
|
||||||
{
|
|
||||||
onSuccess: (_, { id }) => {
|
onSuccess: (_, { id }) => {
|
||||||
client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
|
void client.invalidateQueries({
|
||||||
client.invalidateQueries([QueryKeys.Movies, id]);
|
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
|
||||||
},
|
});
|
||||||
|
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Movies, id],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovieDeleteBlacklist() {
|
export function useMovieDeleteBlacklist() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Movies, QueryKeys.Blacklist],
|
mutationKey: [QueryKeys.Movies, QueryKeys.Blacklist],
|
||||||
(param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
|
|
||||||
|
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
|
||||||
api.movies.deleteBlacklist(param.all, param.form),
|
api.movies.deleteBlacklist(param.all, param.form),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
|
void client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovieHistoryPagination() {
|
export function useMovieHistoryPagination() {
|
||||||
|
@ -131,9 +145,15 @@ export function useMovieHistoryPagination() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMovieHistory(radarrId?: number) {
|
export function useMovieHistory(radarrId?: number) {
|
||||||
return useQuery([QueryKeys.Movies, QueryKeys.History, radarrId], () => {
|
return useQuery({
|
||||||
if (radarrId) {
|
queryKey: [QueryKeys.Movies, QueryKeys.History, radarrId],
|
||||||
return api.movies.historyBy(radarrId);
|
|
||||||
}
|
queryFn: () => {
|
||||||
|
if (radarrId) {
|
||||||
|
return api.movies.historyBy(radarrId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +1,82 @@
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
import api from "../raw";
|
import api from "@/apis/raw";
|
||||||
|
|
||||||
export function useSystemProviders(history?: boolean) {
|
export function useSystemProviders(history?: boolean) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Providers, history ?? false],
|
queryKey: [QueryKeys.System, QueryKeys.Providers, history ?? false],
|
||||||
() => api.providers.providers(history),
|
queryFn: () => api.providers.providers(history),
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMoviesProvider(radarrId?: number) {
|
export function useMoviesProvider(radarrId?: number) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Providers, QueryKeys.Movies, radarrId],
|
queryKey: [
|
||||||
() => {
|
QueryKeys.System,
|
||||||
|
QueryKeys.Providers,
|
||||||
|
QueryKeys.Movies,
|
||||||
|
radarrId,
|
||||||
|
],
|
||||||
|
|
||||||
|
queryFn: () => {
|
||||||
if (radarrId) {
|
if (radarrId) {
|
||||||
return api.providers.movies(radarrId);
|
return api.providers.movies(radarrId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
},
|
},
|
||||||
{
|
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodesProvider(episodeId?: number) {
|
export function useEpisodesProvider(episodeId?: number) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Providers, QueryKeys.Episodes, episodeId],
|
queryKey: [
|
||||||
() => {
|
QueryKeys.System,
|
||||||
|
QueryKeys.Providers,
|
||||||
|
QueryKeys.Episodes,
|
||||||
|
episodeId,
|
||||||
|
],
|
||||||
|
|
||||||
|
queryFn: () => {
|
||||||
if (episodeId) {
|
if (episodeId) {
|
||||||
return api.providers.episodes(episodeId);
|
return api.providers.episodes(episodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
},
|
},
|
||||||
{
|
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useResetProvider() {
|
export function useResetProvider() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Providers],
|
mutationKey: [QueryKeys.System, QueryKeys.Providers],
|
||||||
() => api.providers.reset(),
|
mutationFn: () => api.providers.reset(),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Providers]);
|
client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.System, QueryKeys.Providers],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDownloadEpisodeSubtitles() {
|
export function useDownloadEpisodeSubtitles() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[
|
mutationKey: [
|
||||||
QueryKeys.System,
|
QueryKeys.System,
|
||||||
QueryKeys.Providers,
|
QueryKeys.Providers,
|
||||||
QueryKeys.Subtitles,
|
QueryKeys.Subtitles,
|
||||||
QueryKeys.Episodes,
|
QueryKeys.Episodes,
|
||||||
],
|
],
|
||||||
(param: {
|
|
||||||
|
mutationFn: (param: {
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
episodeId: number;
|
episodeId: number;
|
||||||
form: FormType.ManualDownload;
|
form: FormType.ManualDownload;
|
||||||
|
@ -70,30 +86,33 @@ export function useDownloadEpisodeSubtitles() {
|
||||||
param.episodeId,
|
param.episodeId,
|
||||||
param.form,
|
param.form,
|
||||||
),
|
),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
onSuccess: (_, param) => {
|
||||||
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
|
client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.Series, param.seriesId],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDownloadMovieSubtitles() {
|
export function useDownloadMovieSubtitles() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[
|
mutationKey: [
|
||||||
QueryKeys.System,
|
QueryKeys.System,
|
||||||
QueryKeys.Providers,
|
QueryKeys.Providers,
|
||||||
QueryKeys.Subtitles,
|
QueryKeys.Subtitles,
|
||||||
QueryKeys.Movies,
|
QueryKeys.Movies,
|
||||||
],
|
],
|
||||||
(param: { radarrId: number; form: FormType.ManualDownload }) =>
|
|
||||||
|
mutationFn: (param: { radarrId: number; form: FormType.ManualDownload }) =>
|
||||||
api.providers.downloadMovieSubtitle(param.radarrId, param.form),
|
api.providers.downloadMovieSubtitle(param.radarrId, param.form),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
onSuccess: (_, param) => {
|
||||||
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
|
client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.Movies, param.radarrId],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from "react-query";
|
} from "@tanstack/react-query";
|
||||||
import { usePaginationQuery } from "../queries/hooks";
|
import { usePaginationQuery } from "@/apis/queries/hooks";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
import api from "../raw";
|
import api from "@/apis/raw";
|
||||||
|
|
||||||
function cacheSeries(client: QueryClient, series: Item.Series[]) {
|
function cacheSeries(client: QueryClient, series: Item.Series[]) {
|
||||||
series.forEach((item) => {
|
series.forEach((item) => {
|
||||||
|
@ -16,31 +17,47 @@ function cacheSeries(client: QueryClient, series: Item.Series[]) {
|
||||||
|
|
||||||
export function useSeriesByIds(ids: number[]) {
|
export function useSeriesByIds(ids: number[]) {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useQuery([QueryKeys.Series, ...ids], () => api.series.series(ids), {
|
|
||||||
onSuccess: (data) => {
|
const query = useQuery({
|
||||||
cacheSeries(client, data);
|
queryKey: [QueryKeys.Series, ...ids],
|
||||||
},
|
queryFn: () => api.series.series(ids),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.isSuccess && query.data) {
|
||||||
|
cacheSeries(client, query.data);
|
||||||
|
}
|
||||||
|
}, [query.isSuccess, query.data, client]);
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSeriesById(id: number) {
|
export function useSeriesById(id: number) {
|
||||||
return useQuery([QueryKeys.Series, id], async () => {
|
return useQuery({
|
||||||
const response = await api.series.series([id]);
|
queryKey: [QueryKeys.Series, id],
|
||||||
return response.length > 0 ? response[0] : undefined;
|
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.series.series([id]);
|
||||||
|
return response.length > 0 ? response[0] : undefined;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSeries() {
|
export function useSeries() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useQuery(
|
|
||||||
[QueryKeys.Series, QueryKeys.All],
|
const query = useQuery({
|
||||||
() => api.series.series(),
|
queryKey: [QueryKeys.Series, QueryKeys.All],
|
||||||
{
|
queryFn: () => api.series.series(),
|
||||||
onSuccess: (data) => {
|
});
|
||||||
cacheSeries(client, data);
|
|
||||||
},
|
useEffect(() => {
|
||||||
},
|
if (query.isSuccess && query.data) {
|
||||||
);
|
cacheSeries(client, query.data);
|
||||||
|
}
|
||||||
|
}, [query.isSuccess, query.data, client]);
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSeriesPagination() {
|
export function useSeriesPagination() {
|
||||||
|
@ -51,29 +68,33 @@ export function useSeriesPagination() {
|
||||||
|
|
||||||
export function useSeriesModification() {
|
export function useSeriesModification() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Series],
|
mutationKey: [QueryKeys.Series],
|
||||||
(form: FormType.ModifyItem) => api.series.modify(form),
|
mutationFn: (form: FormType.ModifyItem) => api.series.modify(form),
|
||||||
{
|
|
||||||
onSuccess: (_, form) => {
|
onSuccess: (_, form) => {
|
||||||
form.id.forEach((v) => {
|
form.id.forEach((v) => {
|
||||||
client.invalidateQueries([QueryKeys.Series, v]);
|
client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Series, v],
|
||||||
});
|
});
|
||||||
client.invalidateQueries([QueryKeys.Series]);
|
});
|
||||||
},
|
client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Series],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSeriesAction() {
|
export function useSeriesAction() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Actions, QueryKeys.Series],
|
mutationKey: [QueryKeys.Actions, QueryKeys.Series],
|
||||||
(form: FormType.SeriesAction) => api.series.action(form),
|
mutationFn: (form: FormType.SeriesAction) => api.series.action(form),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.Series]);
|
client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.Series],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
import { useIsMutating } from "react-query";
|
import { useIsMutating } from "@tanstack/react-query";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
|
|
||||||
export function useIsAnyActionRunning() {
|
export function useIsAnyActionRunning() {
|
||||||
return useIsMutating([QueryKeys.Actions]) > 0;
|
return (
|
||||||
|
useIsMutating({
|
||||||
|
mutationKey: [QueryKeys.Actions],
|
||||||
|
}) > 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsMovieActionRunning() {
|
export function useIsMovieActionRunning() {
|
||||||
return useIsMutating([QueryKeys.Actions, QueryKeys.Movies]) > 0;
|
return (
|
||||||
|
useIsMutating({
|
||||||
|
mutationKey: [QueryKeys.Actions, QueryKeys.Movies],
|
||||||
|
}) > 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsSeriesActionRunning() {
|
export function useIsSeriesActionRunning() {
|
||||||
return useIsMutating([QueryKeys.Actions, QueryKeys.Series]) > 0;
|
return (
|
||||||
|
useIsMutating({
|
||||||
|
mutationKey: [QueryKeys.Actions, QueryKeys.Series],
|
||||||
|
}) > 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsAnyMutationRunning() {
|
export function useIsAnyMutationRunning() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { QueryKeys } from "../queries/keys";
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
import api from "../raw";
|
import api from "@/apis/raw";
|
||||||
|
|
||||||
export function useSubtitleAction() {
|
export function useSubtitleAction() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -8,23 +8,29 @@ export function useSubtitleAction() {
|
||||||
action: string;
|
action: string;
|
||||||
form: FormType.ModifySubtitle;
|
form: FormType.ModifySubtitle;
|
||||||
}
|
}
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.Subtitles],
|
mutationKey: [QueryKeys.Subtitles],
|
||||||
(param: Param) => api.subtitles.modify(param.action, param.form),
|
mutationFn: (param: Param) =>
|
||||||
{
|
api.subtitles.modify(param.action, param.form),
|
||||||
onSuccess: (_, param) => {
|
|
||||||
client.invalidateQueries([QueryKeys.History]);
|
|
||||||
|
|
||||||
// TODO: Query less
|
onSuccess: (_, param) => {
|
||||||
const { type, id } = param.form;
|
client.invalidateQueries({
|
||||||
if (type === "episode") {
|
queryKey: [QueryKeys.History],
|
||||||
client.invalidateQueries([QueryKeys.Series, id]);
|
});
|
||||||
} else {
|
|
||||||
client.invalidateQueries([QueryKeys.Movies, id]);
|
// TODO: Query less
|
||||||
}
|
const { type, id } = param.form;
|
||||||
},
|
if (type === "episode") {
|
||||||
|
client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Series, id],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Movies, id],
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEpisodeSubtitleModification() {
|
export function useEpisodeSubtitleModification() {
|
||||||
|
@ -36,42 +42,48 @@ export function useEpisodeSubtitleModification() {
|
||||||
form: T;
|
form: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = useMutation(
|
const download = useMutation({
|
||||||
[QueryKeys.Subtitles, QueryKeys.Episodes],
|
mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes],
|
||||||
(param: Param<FormType.Subtitle>) =>
|
|
||||||
|
mutationFn: (param: Param<FormType.Subtitle>) =>
|
||||||
api.episodes.downloadSubtitles(
|
api.episodes.downloadSubtitles(
|
||||||
param.seriesId,
|
param.seriesId,
|
||||||
param.episodeId,
|
param.episodeId,
|
||||||
param.form,
|
param.form,
|
||||||
),
|
),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
|
||||||
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const remove = useMutation(
|
onSuccess: (_, param) => {
|
||||||
[QueryKeys.Subtitles, QueryKeys.Episodes],
|
client.invalidateQueries({
|
||||||
(param: Param<FormType.DeleteSubtitle>) =>
|
queryKey: [QueryKeys.Series, param.seriesId],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const remove = useMutation({
|
||||||
|
mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes],
|
||||||
|
|
||||||
|
mutationFn: (param: Param<FormType.DeleteSubtitle>) =>
|
||||||
api.episodes.deleteSubtitles(param.seriesId, param.episodeId, param.form),
|
api.episodes.deleteSubtitles(param.seriesId, param.episodeId, param.form),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
|
||||||
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const upload = useMutation(
|
onSuccess: (_, param) => {
|
||||||
[QueryKeys.Subtitles, QueryKeys.Episodes],
|
client.invalidateQueries({
|
||||||
(param: Param<FormType.UploadSubtitle>) =>
|
queryKey: [QueryKeys.Series, param.seriesId],
|
||||||
api.episodes.uploadSubtitles(param.seriesId, param.episodeId, param.form),
|
});
|
||||||
{
|
|
||||||
onSuccess: (_, { seriesId }) => {
|
|
||||||
client.invalidateQueries([QueryKeys.Series, seriesId]);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
|
const upload = useMutation({
|
||||||
|
mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes],
|
||||||
|
|
||||||
|
mutationFn: (param: Param<FormType.UploadSubtitle>) =>
|
||||||
|
api.episodes.uploadSubtitles(param.seriesId, param.episodeId, param.form),
|
||||||
|
|
||||||
|
onSuccess: (_, { seriesId }) => {
|
||||||
|
client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Series, seriesId],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return { download, remove, upload };
|
return { download, remove, upload };
|
||||||
}
|
}
|
||||||
|
@ -84,46 +96,54 @@ export function useMovieSubtitleModification() {
|
||||||
form: T;
|
form: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = useMutation(
|
const download = useMutation({
|
||||||
[QueryKeys.Subtitles, QueryKeys.Movies],
|
mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies],
|
||||||
(param: Param<FormType.Subtitle>) =>
|
|
||||||
|
mutationFn: (param: Param<FormType.Subtitle>) =>
|
||||||
api.movies.downloadSubtitles(param.radarrId, param.form),
|
api.movies.downloadSubtitles(param.radarrId, param.form),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
|
||||||
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const remove = useMutation(
|
onSuccess: (_, param) => {
|
||||||
[QueryKeys.Subtitles, QueryKeys.Movies],
|
client.invalidateQueries({
|
||||||
(param: Param<FormType.DeleteSubtitle>) =>
|
queryKey: [QueryKeys.Movies, param.radarrId],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const remove = useMutation({
|
||||||
|
mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies],
|
||||||
|
|
||||||
|
mutationFn: (param: Param<FormType.DeleteSubtitle>) =>
|
||||||
api.movies.deleteSubtitles(param.radarrId, param.form),
|
api.movies.deleteSubtitles(param.radarrId, param.form),
|
||||||
{
|
|
||||||
onSuccess: (_, param) => {
|
|
||||||
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const upload = useMutation(
|
onSuccess: (_, param) => {
|
||||||
[QueryKeys.Subtitles, QueryKeys.Movies],
|
client.invalidateQueries({
|
||||||
(param: Param<FormType.UploadSubtitle>) =>
|
queryKey: [QueryKeys.Movies, param.radarrId],
|
||||||
api.movies.uploadSubtitles(param.radarrId, param.form),
|
});
|
||||||
{
|
|
||||||
onSuccess: (_, { radarrId }) => {
|
|
||||||
client.invalidateQueries([QueryKeys.Movies, radarrId]);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
|
const upload = useMutation({
|
||||||
|
mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies],
|
||||||
|
|
||||||
|
mutationFn: (param: Param<FormType.UploadSubtitle>) =>
|
||||||
|
api.movies.uploadSubtitles(param.radarrId, param.form),
|
||||||
|
|
||||||
|
onSuccess: (_, { radarrId }) => {
|
||||||
|
client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Movies, radarrId],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return { download, remove, upload };
|
return { download, remove, upload };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubtitleInfos(names: string[]) {
|
export function useSubtitleInfos(names: string[]) {
|
||||||
return useQuery([QueryKeys.Subtitles, QueryKeys.Infos, names], () =>
|
return useQuery({
|
||||||
api.subtitles.info(names),
|
queryKey: [QueryKeys.Subtitles, QueryKeys.Infos, names],
|
||||||
);
|
|
||||||
|
queryFn: () => api.subtitles.info(names),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRefTracksByEpisodeId(
|
export function useRefTracksByEpisodeId(
|
||||||
|
@ -131,11 +151,17 @@ export function useRefTracksByEpisodeId(
|
||||||
sonarrEpisodeId: number,
|
sonarrEpisodeId: number,
|
||||||
isEpisode: boolean,
|
isEpisode: boolean,
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.Episodes, sonarrEpisodeId, QueryKeys.Subtitles, subtitlesPath],
|
queryKey: [
|
||||||
() => api.subtitles.getRefTracksByEpisodeId(subtitlesPath, sonarrEpisodeId),
|
QueryKeys.Episodes,
|
||||||
{ enabled: isEpisode },
|
sonarrEpisodeId,
|
||||||
);
|
QueryKeys.Subtitles,
|
||||||
|
subtitlesPath,
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
api.subtitles.getRefTracksByEpisodeId(subtitlesPath, sonarrEpisodeId),
|
||||||
|
enabled: isEpisode,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRefTracksByMovieId(
|
export function useRefTracksByMovieId(
|
||||||
|
@ -143,9 +169,15 @@ export function useRefTracksByMovieId(
|
||||||
radarrMovieId: number,
|
radarrMovieId: number,
|
||||||
isMovie: boolean,
|
isMovie: boolean,
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.Movies, radarrMovieId, QueryKeys.Subtitles, subtitlesPath],
|
queryKey: [
|
||||||
() => api.subtitles.getRefTracksByMovieId(subtitlesPath, radarrMovieId),
|
QueryKeys.Movies,
|
||||||
{ enabled: isMovie },
|
radarrMovieId,
|
||||||
);
|
QueryKeys.Subtitles,
|
||||||
|
subtitlesPath,
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
api.subtitles.getRefTracksByMovieId(subtitlesPath, radarrMovieId),
|
||||||
|
enabled: isMovie,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { QueryKeys } from "@/apis/queries/keys";
|
||||||
|
import api from "@/apis/raw";
|
||||||
import { Environment } from "@/utilities";
|
import { Environment } from "@/utilities";
|
||||||
import { setAuthenticated } from "@/utilities/event";
|
import { setAuthenticated } from "@/utilities/event";
|
||||||
import { useMemo } from "react";
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
|
||||||
import { QueryKeys } from "../queries/keys";
|
|
||||||
import api from "../raw";
|
|
||||||
|
|
||||||
export function useBadges() {
|
export function useBadges() {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Badges],
|
queryKey: [QueryKeys.System, QueryKeys.Badges],
|
||||||
() => api.badges.all(),
|
queryFn: () => api.badges.all(),
|
||||||
{
|
refetchOnWindowFocus: "always",
|
||||||
refetchOnWindowFocus: "always",
|
refetchInterval: 1000 * 60,
|
||||||
refetchInterval: 1000 * 60,
|
staleTime: 1000 * 10,
|
||||||
staleTime: 1000 * 10,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFileSystem(
|
export function useFileSystem(
|
||||||
|
@ -22,9 +20,10 @@ export function useFileSystem(
|
||||||
path: string,
|
path: string,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.FileSystem, type, path],
|
queryKey: [QueryKeys.FileSystem, type, path],
|
||||||
() => {
|
|
||||||
|
queryFn: () => {
|
||||||
if (type === "bazarr") {
|
if (type === "bazarr") {
|
||||||
return api.files.bazarr(path);
|
return api.files.bazarr(path);
|
||||||
} else if (type === "radarr") {
|
} else if (type === "radarr") {
|
||||||
|
@ -32,53 +31,68 @@ export function useFileSystem(
|
||||||
} else if (type === "sonarr") {
|
} else if (type === "sonarr") {
|
||||||
return api.files.sonarr(path);
|
return api.files.sonarr(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
},
|
},
|
||||||
{
|
|
||||||
enabled,
|
enabled,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemSettings() {
|
export function useSystemSettings() {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Settings],
|
queryKey: [QueryKeys.System, QueryKeys.Settings],
|
||||||
() => api.system.settings(),
|
queryFn: () => api.system.settings(),
|
||||||
{
|
staleTime: Infinity,
|
||||||
staleTime: Infinity,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSettingsMutation() {
|
export function useSettingsMutation() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Settings],
|
mutationKey: [QueryKeys.System, QueryKeys.Settings],
|
||||||
(data: LooseObject) => api.system.updateSettings(data),
|
mutationFn: (data: LooseObject) => api.system.updateSettings(data),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System]);
|
void client.invalidateQueries({
|
||||||
client.invalidateQueries([QueryKeys.Series]);
|
queryKey: [QueryKeys.System],
|
||||||
client.invalidateQueries([QueryKeys.Episodes]);
|
});
|
||||||
client.invalidateQueries([QueryKeys.Movies]);
|
|
||||||
client.invalidateQueries([QueryKeys.Wanted]);
|
void client.invalidateQueries({
|
||||||
client.invalidateQueries([QueryKeys.Badges]);
|
queryKey: [QueryKeys.Series],
|
||||||
},
|
});
|
||||||
|
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Episodes],
|
||||||
|
});
|
||||||
|
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Movies],
|
||||||
|
});
|
||||||
|
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Wanted],
|
||||||
|
});
|
||||||
|
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.Badges],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useServerSearch(query: string, enabled: boolean) {
|
export function useServerSearch(query: string, enabled: boolean) {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Search, query],
|
queryKey: [QueryKeys.System, QueryKeys.Search, query],
|
||||||
() => api.system.search(query),
|
queryFn: () => api.system.search(query),
|
||||||
{
|
enabled,
|
||||||
enabled,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemLogs() {
|
export function useSystemLogs() {
|
||||||
return useQuery([QueryKeys.System, QueryKeys.Logs], () => api.system.logs(), {
|
return useQuery({
|
||||||
|
queryKey: [QueryKeys.System, QueryKeys.Logs],
|
||||||
|
queryFn: () => api.system.logs(),
|
||||||
refetchOnWindowFocus: "always",
|
refetchOnWindowFocus: "always",
|
||||||
refetchInterval: 1000 * 60,
|
refetchInterval: 1000 * 60,
|
||||||
staleTime: 1000 * 10,
|
staleTime: 1000 * 10,
|
||||||
|
@ -87,171 +101,189 @@ export function useSystemLogs() {
|
||||||
|
|
||||||
export function useDeleteLogs() {
|
export function useDeleteLogs() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Logs],
|
mutationKey: [QueryKeys.System, QueryKeys.Logs],
|
||||||
() => api.system.deleteLogs(),
|
mutationFn: () => api.system.deleteLogs(),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Logs]);
|
void client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.System, QueryKeys.Logs],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemAnnouncements() {
|
export function useSystemAnnouncements() {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Announcements],
|
queryKey: [QueryKeys.System, QueryKeys.Announcements],
|
||||||
() => api.system.announcements(),
|
queryFn: () => api.system.announcements(),
|
||||||
{
|
refetchOnWindowFocus: "always",
|
||||||
refetchOnWindowFocus: "always",
|
refetchInterval: 1000 * 60,
|
||||||
refetchInterval: 1000 * 60,
|
staleTime: 1000 * 10,
|
||||||
staleTime: 1000 * 10,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemAnnouncementsAddDismiss() {
|
export function useSystemAnnouncementsAddDismiss() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Announcements],
|
mutationKey: [QueryKeys.System, QueryKeys.Announcements],
|
||||||
(param: { hash: string }) => {
|
|
||||||
|
mutationFn: (param: { hash: string }) => {
|
||||||
const { hash } = param;
|
const { hash } = param;
|
||||||
return api.system.addAnnouncementsDismiss(hash);
|
return api.system.addAnnouncementsDismiss(hash);
|
||||||
},
|
},
|
||||||
{
|
|
||||||
onSuccess: (_, { hash }) => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Announcements]);
|
void client.invalidateQueries({
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Badges]);
|
queryKey: [QueryKeys.System, QueryKeys.Announcements],
|
||||||
},
|
});
|
||||||
|
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.System, QueryKeys.Badges],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemTasks() {
|
export function useSystemTasks() {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
[QueryKeys.System, QueryKeys.Tasks],
|
queryKey: [QueryKeys.System, QueryKeys.Tasks],
|
||||||
() => api.system.tasks(),
|
queryFn: () => api.system.tasks(),
|
||||||
{
|
refetchOnWindowFocus: "always",
|
||||||
refetchOnWindowFocus: "always",
|
refetchInterval: 1000 * 60,
|
||||||
refetchInterval: 1000 * 60,
|
staleTime: 1000 * 10,
|
||||||
staleTime: 1000 * 10,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRunTask() {
|
export function useRunTask() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Tasks],
|
mutationKey: [QueryKeys.System, QueryKeys.Tasks],
|
||||||
(id: string) => api.system.runTask(id),
|
mutationFn: (id: string) => api.system.runTask(id),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Tasks]);
|
void client.invalidateQueries({
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
|
queryKey: [QueryKeys.System, QueryKeys.Tasks],
|
||||||
},
|
});
|
||||||
|
|
||||||
|
void client.invalidateQueries({
|
||||||
|
queryKey: [QueryKeys.System, QueryKeys.Backups],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemBackups() {
|
export function useSystemBackups() {
|
||||||
return useQuery([QueryKeys.System, "backups"], () => api.system.backups());
|
return useQuery({
|
||||||
|
queryKey: [QueryKeys.System, "backups"],
|
||||||
|
queryFn: () => api.system.backups(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateBackups() {
|
export function useCreateBackups() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Backups],
|
mutationKey: [QueryKeys.System, QueryKeys.Backups],
|
||||||
() => api.system.createBackups(),
|
mutationFn: () => api.system.createBackups(),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
|
void client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.System, QueryKeys.Backups],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRestoreBackups() {
|
export function useRestoreBackups() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Backups],
|
mutationKey: [QueryKeys.System, QueryKeys.Backups],
|
||||||
(filename: string) => api.system.restoreBackups(filename),
|
mutationFn: (filename: string) => api.system.restoreBackups(filename),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
|
void client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.System, QueryKeys.Backups],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteBackups() {
|
export function useDeleteBackups() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation(
|
return useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Backups],
|
mutationKey: [QueryKeys.System, QueryKeys.Backups],
|
||||||
(filename: string) => api.system.deleteBackups(filename),
|
mutationFn: (filename: string) => api.system.deleteBackups(filename),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
|
void client.invalidateQueries({
|
||||||
},
|
queryKey: [QueryKeys.System, QueryKeys.Backups],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemStatus() {
|
export function useSystemStatus() {
|
||||||
return useQuery([QueryKeys.System, "status"], () => api.system.status());
|
return useQuery({
|
||||||
|
queryKey: [QueryKeys.System, "status"],
|
||||||
|
queryFn: () => api.system.status(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemHealth() {
|
export function useSystemHealth() {
|
||||||
return useQuery([QueryKeys.System, "health"], () => api.system.health());
|
return useQuery({
|
||||||
|
queryKey: [QueryKeys.System, "health"],
|
||||||
|
queryFn: () => api.system.health(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemReleases() {
|
export function useSystemReleases() {
|
||||||
return useQuery([QueryKeys.System, "releases"], () => api.system.releases());
|
return useQuery({
|
||||||
|
queryKey: [QueryKeys.System, "releases"],
|
||||||
|
queryFn: () => api.system.releases(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystem() {
|
export function useSystem() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const { mutate: logout, isLoading: isLoggingOut } = useMutation(
|
const { mutate: logout, isPending: isLoggingOut } = useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Actions],
|
mutationKey: [QueryKeys.System, QueryKeys.Actions],
|
||||||
() => api.system.logout(),
|
mutationFn: () => api.system.logout(),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
setAuthenticated(false);
|
|
||||||
client.clear();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutate: login, isLoading: isLoggingIn } = useMutation(
|
onSuccess: () => {
|
||||||
[QueryKeys.System, QueryKeys.Actions],
|
setAuthenticated(false);
|
||||||
(param: { username: string; password: string }) =>
|
client.clear();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: login, isPending: isLoggingIn } = useMutation({
|
||||||
|
mutationKey: [QueryKeys.System, QueryKeys.Actions],
|
||||||
|
|
||||||
|
mutationFn: (param: { username: string; password: string }) =>
|
||||||
api.system.login(param.username, param.password),
|
api.system.login(param.username, param.password),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
// TODO: Hard-coded value
|
|
||||||
window.location.replace(Environment.baseUrl);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutate: shutdown, isLoading: isShuttingDown } = useMutation(
|
onSuccess: () => {
|
||||||
[QueryKeys.System, QueryKeys.Actions],
|
// TODO: Hard-coded value
|
||||||
() => api.system.shutdown(),
|
window.location.replace(Environment.baseUrl);
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
client.clear();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const { mutate: restart, isLoading: isRestarting } = useMutation(
|
const { mutate: shutdown, isPending: isShuttingDown } = useMutation({
|
||||||
[QueryKeys.System, QueryKeys.Actions],
|
mutationKey: [QueryKeys.System, QueryKeys.Actions],
|
||||||
() => api.system.restart(),
|
mutationFn: () => api.system.shutdown(),
|
||||||
{
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.clear();
|
client.clear();
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
|
const { mutate: restart, isPending: isRestarting } = useMutation({
|
||||||
|
mutationKey: [QueryKeys.System, QueryKeys.Actions],
|
||||||
|
mutationFn: () => api.system.restart(),
|
||||||
|
|
||||||
|
onSuccess: () => {
|
||||||
|
client.clear();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { GetItemId, useOnValueChange } from "@/utilities";
|
|
||||||
import { usePageSize } from "@/utilities/storage";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
QueryKey,
|
QueryKey,
|
||||||
UseQueryResult,
|
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from "react-query";
|
UseQueryResult,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import { GetItemId, useOnValueChange } from "@/utilities";
|
||||||
|
import { usePageSize } from "@/utilities/storage";
|
||||||
import { QueryKeys } from "./keys";
|
import { QueryKeys } from "./keys";
|
||||||
|
|
||||||
export type UsePaginationQueryResult<T extends object> = UseQueryResult<
|
export type UsePaginationQueryResult<T extends object> = UseQueryResult<
|
||||||
|
@ -39,31 +39,31 @@ export function usePaginationQuery<
|
||||||
|
|
||||||
const start = page * pageSize;
|
const start = page * pageSize;
|
||||||
|
|
||||||
const results = useQuery(
|
const results = useQuery({
|
||||||
[...queryKey, QueryKeys.Range, { start, size: pageSize }],
|
queryKey: [...queryKey, QueryKeys.Range, { start, size: pageSize }],
|
||||||
() => {
|
|
||||||
|
queryFn: () => {
|
||||||
const param: Parameter.Range = {
|
const param: Parameter.Range = {
|
||||||
start,
|
start,
|
||||||
length: pageSize,
|
length: pageSize,
|
||||||
};
|
};
|
||||||
return queryFn(param);
|
return queryFn(param);
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
onSuccess: ({ data }) => {
|
|
||||||
if (cacheIndividual) {
|
|
||||||
data.forEach((item) => {
|
|
||||||
const id = GetItemId(item);
|
|
||||||
if (id) {
|
|
||||||
client.setQueryData([...queryKey, id], item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = results;
|
const { data } = results;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (results.isSuccess && results.data && cacheIndividual) {
|
||||||
|
results.data.data.forEach((item) => {
|
||||||
|
const id = GetItemId(item);
|
||||||
|
if (id) {
|
||||||
|
client.setQueryData([...queryKey, id], item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [results.isSuccess, results.data, client, cacheIndividual, queryKey]);
|
||||||
|
|
||||||
const totalCount = data?.total ?? 0;
|
const totalCount = data?.total ?? 0;
|
||||||
const pageCount = Math.ceil(totalCount / pageSize);
|
const pageCount = Math.ceil(totalCount / pageSize);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { QueryClient } from "react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
@ -6,7 +6,11 @@ const queryClient = new QueryClient({
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: false,
|
retry: false,
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
keepPreviousData: true,
|
networkMode: "offlineFirst",
|
||||||
|
placeholderData: (previousData: object) => previousData,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
networkMode: "offlineFirst",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|