mirror of
https://github.com/morpheus65535/bazarr
synced 2024-12-21 23:32:31 +00:00
Merge development into master
This commit is contained in:
commit
5c56866d56
36 changed files with 394 additions and 2692 deletions
|
@ -62,6 +62,7 @@ If you need something that is not already part of Bazarr, feel free to create a
|
|||
- Karagarga.in
|
||||
- Ktuvit (Get `hashed_password` using method described [here](https://github.com/XBMCil/service.subtitles.ktuvit))
|
||||
- LegendasDivx
|
||||
- Legendas.net
|
||||
- Napiprojekt
|
||||
- Napisy24
|
||||
- Nekur
|
||||
|
|
|
@ -6,10 +6,12 @@ import logging
|
|||
|
||||
from flask_restx import Resource, Namespace
|
||||
from tzlocal import get_localzone_name
|
||||
from alembic.migration import MigrationContext
|
||||
|
||||
from radarr.info import get_radarr_info
|
||||
from sonarr.info import get_sonarr_info
|
||||
from app.get_args import args
|
||||
from app.database import engine, database, select
|
||||
from init import startTime
|
||||
|
||||
from ..utils import authenticate
|
||||
|
@ -34,6 +36,16 @@ class SystemStatus(Resource):
|
|||
timezone = "Exception while getting time zone name."
|
||||
logging.exception("BAZARR is unable to get configured time zone name.")
|
||||
|
||||
try:
|
||||
database_version = ".".join([str(x) for x in engine.dialect.server_version_info])
|
||||
except Exception:
|
||||
database_version = ""
|
||||
|
||||
try:
|
||||
database_migration = MigrationContext.configure(engine.connect()).get_current_revision()
|
||||
except Exception:
|
||||
database_migration = "unknown"
|
||||
|
||||
system_status = {}
|
||||
system_status.update({'bazarr_version': os.environ["BAZARR_VERSION"]})
|
||||
system_status.update({'package_version': package_version})
|
||||
|
@ -41,6 +53,8 @@ class SystemStatus(Resource):
|
|||
system_status.update({'radarr_version': get_radarr_info.version()})
|
||||
system_status.update({'operating_system': platform.platform()})
|
||||
system_status.update({'python_version': platform.python_version()})
|
||||
system_status.update({'database_engine': f'{engine.dialect.name.capitalize()} {database_version}'})
|
||||
system_status.update({'database_migration': database_migration})
|
||||
system_status.update({'bazarr_directory': os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(__file__))))})
|
||||
system_status.update({'bazarr_config_directory': args.config_dir})
|
||||
|
|
|
@ -528,3 +528,32 @@ def upgrade_languages_profile_hi_values():
|
|||
.values({"items": json.dumps(items)})
|
||||
.where(TableLanguagesProfiles.profileId == languages_profile.profileId)
|
||||
)
|
||||
|
||||
|
||||
def fix_languages_profiles_with_duplicate_ids():
|
||||
languages_profiles = database.execute(
|
||||
select(TableLanguagesProfiles.profileId, TableLanguagesProfiles.items, TableLanguagesProfiles.cutoff)).all()
|
||||
for languages_profile in languages_profiles:
|
||||
if languages_profile.cutoff:
|
||||
# ignore profiles that have a cutoff set
|
||||
continue
|
||||
languages_profile_ids = []
|
||||
languages_profile_has_duplicate = False
|
||||
languages_profile_items = json.loads(languages_profile.items)
|
||||
for items in languages_profile_items:
|
||||
if items['id'] in languages_profile_ids:
|
||||
languages_profile_has_duplicate = True
|
||||
break
|
||||
else:
|
||||
languages_profile_ids.append(items['id'])
|
||||
|
||||
if languages_profile_has_duplicate:
|
||||
item_id = 0
|
||||
for items in languages_profile_items:
|
||||
item_id += 1
|
||||
items['id'] = item_id
|
||||
database.execute(
|
||||
update(TableLanguagesProfiles)
|
||||
.values({"items": json.dumps(languages_profile_items)})
|
||||
.where(TableLanguagesProfiles.profileId == languages_profile.profileId)
|
||||
)
|
||||
|
|
|
@ -62,7 +62,7 @@ class UnwantedWaitressMessageFilter(logging.Filter):
|
|||
# no filtering in debug mode or if originating from us
|
||||
return True
|
||||
|
||||
if record.level != loggin.ERROR:
|
||||
if record.levelno < logging.ERROR:
|
||||
return False
|
||||
|
||||
unwantedMessages = [
|
||||
|
@ -172,9 +172,14 @@ def configure_logging(debug=False):
|
|||
logging.getLogger("rebulk").setLevel(logging.WARNING)
|
||||
logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL)
|
||||
|
||||
def empty_file(filename):
|
||||
# Open the log file in write mode to clear its contents
|
||||
with open(filename, 'w'):
|
||||
pass # Just opening and closing the file will clear it
|
||||
|
||||
def empty_log():
|
||||
fh.doRollover()
|
||||
empty_file(get_log_file_path())
|
||||
logging.info('BAZARR Log file emptied')
|
||||
|
||||
|
||||
|
|
|
@ -159,6 +159,8 @@ class ChineseTraditional(CustomLanguage):
|
|||
)
|
||||
_extensions_hi = (
|
||||
".cht.hi", ".tc.hi", ".zht.hi", "hant.hi", ".big5.hi", "繁體中文.hi", "雙語.hi", ".zh-tw.hi",
|
||||
".cht.cc", ".tc.cc", ".zht.cc", "hant.cc", ".big5.cc", "繁體中文.cc", "雙語.cc", ".zh-tw.cc",
|
||||
".cht.sdh", ".tc.sdh", ".zht.sdh", "hant.sdh", ".big5.sdh", "繁體中文.sdh", "雙語.sdh", ".zh-tw.sdh",
|
||||
)
|
||||
_extensions_fuzzy = ("繁", "雙語")
|
||||
_extensions_disamb_fuzzy = ("简", "双语")
|
||||
|
|
|
@ -35,7 +35,8 @@ else:
|
|||
# there's missing embedded packages after a commit
|
||||
check_if_new_update()
|
||||
|
||||
from app.database import System, database, update, migrate_db, create_db_revision, upgrade_languages_profile_hi_values # noqa E402
|
||||
from app.database import (System, database, update, migrate_db, create_db_revision, upgrade_languages_profile_hi_values,
|
||||
fix_languages_profiles_with_duplicate_ids) # noqa E402
|
||||
from app.notifier import update_notifier # noqa E402
|
||||
from languages.get_languages import load_language_in_db # noqa E402
|
||||
from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402
|
||||
|
@ -50,6 +51,7 @@ if args.create_db_revision:
|
|||
else:
|
||||
migrate_db(app)
|
||||
upgrade_languages_profile_hi_values()
|
||||
fix_languages_profiles_with_duplicate_ids()
|
||||
|
||||
configure_proxy_func()
|
||||
|
||||
|
|
|
@ -218,10 +218,12 @@ def refine_anidb_ids(video):
|
|||
)
|
||||
|
||||
if not anidb_series_id:
|
||||
logger.error(f'Could not find anime series {video.series}')
|
||||
return video
|
||||
|
||||
logger.debug(f'AniDB refinement identified {video.series} as {anidb_series_id}.')
|
||||
|
||||
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}')
|
||||
|
|
|
@ -36,40 +36,47 @@ def delete_subtitles(media_type, language, forced, hi, media_path, subtitles_pat
|
|||
language_log += ':forced'
|
||||
language_string += ' forced'
|
||||
|
||||
if media_type == 'series':
|
||||
pr = path_mappings.path_replace
|
||||
prr = path_mappings.path_replace_reverse
|
||||
else:
|
||||
pr = path_mappings.path_replace_movie
|
||||
prr = path_mappings.path_replace_reverse_movie
|
||||
|
||||
result = ProcessSubtitlesResult(message=f"{language_string} subtitles deleted from disk.",
|
||||
reversed_path=path_mappings.path_replace_reverse(media_path),
|
||||
reversed_path=prr(media_path),
|
||||
downloaded_language_code2=language_log,
|
||||
downloaded_provider=None,
|
||||
score=None,
|
||||
forced=None,
|
||||
subtitle_id=None,
|
||||
reversed_subtitles_path=path_mappings.path_replace_reverse(subtitles_path),
|
||||
reversed_subtitles_path=prr(subtitles_path),
|
||||
hearing_impaired=None)
|
||||
|
||||
if media_type == 'series':
|
||||
try:
|
||||
os.remove(path_mappings.path_replace(subtitles_path))
|
||||
os.remove(pr(subtitles_path))
|
||||
except OSError:
|
||||
logging.exception(f'BAZARR cannot delete subtitles file: {subtitles_path}')
|
||||
store_subtitles(path_mappings.path_replace_reverse(media_path), media_path)
|
||||
store_subtitles(prr(media_path), media_path)
|
||||
return False
|
||||
else:
|
||||
history_log(0, sonarr_series_id, sonarr_episode_id, result)
|
||||
store_subtitles(path_mappings.path_replace_reverse(media_path), media_path)
|
||||
store_subtitles(prr(media_path), media_path)
|
||||
notify_sonarr(sonarr_series_id)
|
||||
event_stream(type='series', action='update', payload=sonarr_series_id)
|
||||
event_stream(type='episode-wanted', action='update', payload=sonarr_episode_id)
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
os.remove(path_mappings.path_replace_movie(subtitles_path))
|
||||
os.remove(pr(subtitles_path))
|
||||
except OSError:
|
||||
logging.exception(f'BAZARR cannot delete subtitles file: {subtitles_path}')
|
||||
store_subtitles_movie(path_mappings.path_replace_reverse_movie(media_path), media_path)
|
||||
store_subtitles_movie(prr(media_path), media_path)
|
||||
return False
|
||||
else:
|
||||
history_log_movie(0, radarr_id, result)
|
||||
store_subtitles_movie(path_mappings.path_replace_reverse_movie(media_path), media_path)
|
||||
store_subtitles_movie(prr(media_path), media_path)
|
||||
notify_radarr(radarr_id)
|
||||
event_stream(type='movie-wanted', action='update', payload=radarr_id)
|
||||
return True
|
||||
|
|
|
@ -112,14 +112,19 @@ class SubSyncer:
|
|||
f"{offset_seconds} seconds and a framerate scale factor of "
|
||||
f"{f'{framerate_scale_factor:.2f}'}.")
|
||||
|
||||
if sonarr_series_id:
|
||||
prr = path_mappings.path_replace_reverse
|
||||
else:
|
||||
prr = path_mappings.path_replace_reverse_movie
|
||||
|
||||
result = ProcessSubtitlesResult(message=message,
|
||||
reversed_path=path_mappings.path_replace_reverse(self.reference),
|
||||
reversed_path=prr(self.reference),
|
||||
downloaded_language_code2=srt_lang,
|
||||
downloaded_provider=None,
|
||||
score=None,
|
||||
forced=forced,
|
||||
subtitle_id=None,
|
||||
reversed_subtitles_path=srt_path,
|
||||
reversed_subtitles_path=prr(self.srtin),
|
||||
hearing_impaired=hi)
|
||||
|
||||
if sonarr_episode_id:
|
||||
|
|
|
@ -16,6 +16,7 @@ from radarr.history import history_log_movie
|
|||
from sonarr.history import history_log
|
||||
from subtitles.processing import ProcessSubtitlesResult
|
||||
from app.event_handler import show_progress, hide_progress
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
||||
|
||||
def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, forced, hi, media_type, sonarr_series_id,
|
||||
|
@ -27,9 +28,15 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
|||
}
|
||||
|
||||
to_lang = alpha3_from_alpha2(to_lang)
|
||||
lang_obj = CustomLanguage.from_value(to_lang, "alpha3")
|
||||
if not lang_obj:
|
||||
try:
|
||||
lang_obj = Language(to_lang)
|
||||
except ValueError:
|
||||
custom_lang_obj = CustomLanguage.from_value(to_lang, "alpha3")
|
||||
if custom_lang_obj:
|
||||
lang_obj = CustomLanguage.subzero_language(custom_lang_obj)
|
||||
else:
|
||||
logging.debug(f'BAZARR is unable to translate to {to_lang} for this subtitles: {source_srt_file}')
|
||||
return False
|
||||
if forced:
|
||||
lang_obj = Language.rebuild(lang_obj, forced=True)
|
||||
if hi:
|
||||
|
@ -104,14 +111,19 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo
|
|||
|
||||
message = f"{language_from_alpha2(from_lang)} subtitles translated to {language_from_alpha3(to_lang)}."
|
||||
|
||||
if media_type == 'series':
|
||||
prr = path_mappings.path_replace_reverse
|
||||
else:
|
||||
prr = path_mappings.path_replace_reverse_movie
|
||||
|
||||
result = ProcessSubtitlesResult(message=message,
|
||||
reversed_path=video_path,
|
||||
reversed_path=prr(video_path),
|
||||
downloaded_language_code2=to_lang,
|
||||
downloaded_provider=None,
|
||||
score=None,
|
||||
forced=forced,
|
||||
subtitle_id=None,
|
||||
reversed_subtitles_path=dest_srt_file,
|
||||
reversed_subtitles_path=prr(dest_srt_file),
|
||||
hearing_impaired=hi)
|
||||
|
||||
if media_type == 'series':
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# coding=utf-8
|
||||
|
||||
import json
|
||||
|
||||
from app.config import settings
|
||||
from app.database import TableShowsRootfolder, TableMoviesRootfolder, database, select
|
||||
from app.database import TableShowsRootfolder, TableMoviesRootfolder, TableLanguagesProfiles, database, select
|
||||
from app.event_handler import event_stream
|
||||
from .path_mappings import path_mappings
|
||||
from sonarr.rootfolder import check_sonarr_rootfolder
|
||||
|
@ -47,4 +49,21 @@ def get_health_issues():
|
|||
health_issues.append({'object': path_mappings.path_replace_movie(item.path),
|
||||
'issue': item.error})
|
||||
|
||||
# get languages profiles duplicate ids issues when there's a cutoff set
|
||||
languages_profiles = database.execute(
|
||||
select(TableLanguagesProfiles.items, TableLanguagesProfiles.name, TableLanguagesProfiles.cutoff)).all()
|
||||
for languages_profile in languages_profiles:
|
||||
if not languages_profile.cutoff:
|
||||
# ignore profiles that don't have a cutoff set
|
||||
continue
|
||||
languages_profile_ids = []
|
||||
for items in json.loads(languages_profile.items):
|
||||
if items['id'] in languages_profile_ids:
|
||||
health_issues.append({'object': languages_profile.name,
|
||||
'issue': 'This languages profile has duplicate IDs. You need to edit this profile'
|
||||
' and make sure to select the proper cutoff if required.'})
|
||||
break
|
||||
else:
|
||||
languages_profile_ids.append(items['id'])
|
||||
|
||||
return health_issues
|
||||
|
|
|
@ -7,15 +7,12 @@ import random
|
|||
import re
|
||||
|
||||
from requests import Session
|
||||
from subliminal import __short_version__
|
||||
from subliminal.video import Episode
|
||||
from subliminal.video import Movie
|
||||
from subliminal import ProviderError
|
||||
from subliminal.video import Episode, Movie
|
||||
from subliminal_patch.exceptions import APIThrottled
|
||||
from subliminal_patch.providers import Provider
|
||||
from subliminal_patch.providers.utils import get_archive_from_bytes
|
||||
from subliminal_patch.providers.utils import get_subtitle_from_archive
|
||||
from subliminal_patch.providers.utils import update_matches
|
||||
from subliminal_patch.providers.utils import USER_AGENTS
|
||||
from subliminal_patch.providers.utils import (get_archive_from_bytes, get_subtitle_from_archive, update_matches,
|
||||
USER_AGENTS)
|
||||
from subliminal_patch.subtitle import Subtitle
|
||||
from subzero.language import Language
|
||||
|
||||
|
@ -111,7 +108,6 @@ class SubdivxSubtitlesProvider(Provider):
|
|||
self.session = Session()
|
||||
|
||||
def initialize(self):
|
||||
# self.session.headers["User-Agent"] = f"Subliminal/{__short_version__}"
|
||||
self.session.headers["User-Agent"] = random.choice(USER_AGENTS)
|
||||
self.session.cookies.update({"iduser_cookie": _IDUSER_COOKIE})
|
||||
|
||||
|
@ -166,9 +162,26 @@ class SubdivxSubtitlesProvider(Provider):
|
|||
return subtitles
|
||||
|
||||
def _query_results(self, query, video):
|
||||
token_link = f"{_SERVER_URL}/inc/gt.php?gt=1"
|
||||
|
||||
token_response = self.session.get(token_link, timeout=30)
|
||||
|
||||
if token_response.status_code != 200:
|
||||
raise ProviderError("Unable to obtain a token")
|
||||
|
||||
try:
|
||||
token_response_json = token_response.json()
|
||||
except JSONDecodeError:
|
||||
raise ProviderError("Unable to parse JSON response")
|
||||
else:
|
||||
if 'token' in token_response_json and token_response_json['token']:
|
||||
token = token_response_json['token']
|
||||
else:
|
||||
raise ProviderError("Response doesn't include a token")
|
||||
|
||||
search_link = f"{_SERVER_URL}/inc/ajax.php"
|
||||
|
||||
payload = {"tabla": "resultados", "filtros": "", "buscar": query}
|
||||
payload = {"tabla": "resultados", "filtros": "", "buscar393": query, "token": token}
|
||||
|
||||
logger.debug("Query: %s", query)
|
||||
|
||||
|
@ -197,7 +210,7 @@ class SubdivxSubtitlesProvider(Provider):
|
|||
# Iterate over each subtitle in the response
|
||||
for item in data["aaData"]:
|
||||
id = item["id"]
|
||||
page_link = f"{_SERVER_URL}/descargar.php?id={id}"
|
||||
page_link = f"{_SERVER_URL}/{id}"
|
||||
title = _clean_title(item["titulo"])
|
||||
description = item["descripcion"]
|
||||
uploader = item["nick"]
|
||||
|
|
|
@ -16,6 +16,7 @@ from babelfish.exceptions import LanguageReverseError
|
|||
|
||||
import ffmpeg
|
||||
import functools
|
||||
from pycountry import languages
|
||||
|
||||
# These are all the languages Whisper supports.
|
||||
# from whisper.tokenizer import LANGUAGES
|
||||
|
@ -132,6 +133,18 @@ def set_log_level(newLevel="INFO"):
|
|||
# initialize to default above
|
||||
set_log_level()
|
||||
|
||||
# ffmpeg uses the older ISO 639-2 code when extracting audio streams based on language
|
||||
# if we give it the newer ISO 639-3 code it can't find that audio stream by name because it's different
|
||||
# for example it wants 'ger' instead of 'deu' for the German language
|
||||
# or 'fre' instead of 'fra' for the French language
|
||||
def get_ISO_639_2_code(iso639_3_code):
|
||||
# find the language using ISO 639-3 code
|
||||
language = languages.get(alpha_3=iso639_3_code)
|
||||
# get the ISO 639-2 code or use the original input if there isn't a match
|
||||
iso639_2_code = language.bibliographic if language and hasattr(language, 'bibliographic') else iso639_3_code
|
||||
logger.debug(f"ffmpeg using language code '{iso639_2_code}' (instead of '{iso639_3_code}')")
|
||||
return iso639_2_code
|
||||
|
||||
@functools.lru_cache(2)
|
||||
def encode_audio_stream(path, ffmpeg_path, audio_stream_language=None):
|
||||
logger.debug("Encoding audio stream to WAV with ffmpeg")
|
||||
|
@ -140,10 +153,13 @@ def encode_audio_stream(path, ffmpeg_path, audio_stream_language=None):
|
|||
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
|
||||
inp = ffmpeg.input(path, threads=0)
|
||||
if audio_stream_language:
|
||||
logger.debug(f"Whisper will only use the {audio_stream_language} audio stream for {path}")
|
||||
# There is more than one audio stream, so pick the requested one by name
|
||||
# Use the ISO 639-2 code if available
|
||||
audio_stream_language = get_ISO_639_2_code(audio_stream_language)
|
||||
logger.debug(f"Whisper will use the '{audio_stream_language}' audio stream for {path}")
|
||||
inp = inp[f'a:m:language:{audio_stream_language}']
|
||||
|
||||
out, _ = inp.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=16000) \
|
||||
out, _ = inp.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=16000, af="aresample=async=1") \
|
||||
.run(cmd=[ffmpeg_path, "-nostdin"], capture_stdout=True, capture_stderr=True)
|
||||
|
||||
except ffmpeg.Error as e:
|
||||
|
|
|
@ -162,14 +162,4 @@ class Language(Language_):
|
|||
return Language(*Language_.fromalpha3b(s).__getstate__())
|
||||
|
||||
|
||||
IETF_MATCH = ".+\.([^-.]+)(?:-[A-Za-z]+)?$"
|
||||
ENDSWITH_LANGUAGECODE_RE = re.compile("\.([^-.]{2,3})(?:-[A-Za-z]{2,})?$")
|
||||
|
||||
|
||||
def match_ietf_language(s, ietf=False):
|
||||
language_match = re.match(".+\.([^\.]+)$" if not ietf
|
||||
else IETF_MATCH, s)
|
||||
if language_match and len(language_match.groups()) == 1:
|
||||
language = language_match.groups()[0]
|
||||
return language
|
||||
return s
|
||||
ENDSWITH_LANGUAGECODE_RE = re.compile(r"\.([^-.]{2,3})(?:-[A-Za-z]{2,})?$")
|
||||
|
|
|
@ -270,6 +270,7 @@ function useRoutes(): CustomRouteObject[] {
|
|||
{
|
||||
path: "status",
|
||||
name: "Status",
|
||||
badge: data?.status,
|
||||
element: (
|
||||
<Lazy>
|
||||
<SystemStatusView></SystemStatusView>
|
||||
|
@ -309,6 +310,7 @@ function useRoutes(): CustomRouteObject[] {
|
|||
data?.sonarr_signalr,
|
||||
data?.radarr_signalr,
|
||||
data?.announcements,
|
||||
data?.status,
|
||||
radarr,
|
||||
sonarr,
|
||||
],
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||
import { Autocomplete, ComboboxItem, OptionsFilter, Text } from "@mantine/core";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { chain, includes } from "lodash";
|
||||
import { useServerSearch } from "@/apis/hooks";
|
||||
import { useDebouncedValue } from "@/utilities";
|
||||
|
||||
|
@ -15,23 +16,45 @@ function useSearch(query: string) {
|
|||
const debouncedQuery = useDebouncedValue(query, 500);
|
||||
const { data } = useServerSearch(debouncedQuery, debouncedQuery.length >= 0);
|
||||
|
||||
const duplicates = chain(data)
|
||||
.groupBy((item) => `${item.title} (${item.year})`)
|
||||
.filter((group) => group.length > 1)
|
||||
.map((group) => `${group[0].title} (${group[0].year})`)
|
||||
.value();
|
||||
|
||||
return useMemo<SearchResultItem[]>(
|
||||
() =>
|
||||
data?.map((v) => {
|
||||
let link: string;
|
||||
if (v.sonarrSeriesId) {
|
||||
link = `/series/${v.sonarrSeriesId}`;
|
||||
} else if (v.radarrId) {
|
||||
link = `/movies/${v.radarrId}`;
|
||||
} else {
|
||||
const { link, displayName } = (() => {
|
||||
const hasDuplicate = includes(duplicates, `${v.title} (${v.year})`);
|
||||
|
||||
if (v.sonarrSeriesId) {
|
||||
return {
|
||||
link: `/series/${v.sonarrSeriesId}`,
|
||||
displayName: hasDuplicate
|
||||
? `${v.title} (${v.year}) (S)`
|
||||
: `${v.title} (${v.year})`,
|
||||
};
|
||||
}
|
||||
|
||||
if (v.radarrId) {
|
||||
return {
|
||||
link: `/movies/${v.radarrId}`,
|
||||
displayName: hasDuplicate
|
||||
? `${v.title} (${v.year}) (M)`
|
||||
: `${v.title} (${v.year})`,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Unknown search result");
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
value: `${v.title} (${v.year})`,
|
||||
value: displayName,
|
||||
link,
|
||||
};
|
||||
}) ?? [],
|
||||
[data],
|
||||
[data, duplicates],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import React, { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
MantineColor,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
|
@ -17,8 +17,9 @@ import {
|
|||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { isString } from "lodash";
|
||||
import { isString, uniqBy } from "lodash";
|
||||
import { useMovieSubtitleModification } from "@/apis/hooks";
|
||||
import { subtitlesTypeOptions } from "@/components/forms/uploadFormSelectorTypes";
|
||||
import { Action, Selector } from "@/components/inputs";
|
||||
import SimpleTable from "@/components/tables/SimpleTable";
|
||||
import TextPopover from "@/components/TextPopover";
|
||||
|
@ -88,7 +89,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
|||
|
||||
const languages = useProfileItemsToLanguages(profile);
|
||||
const languageOptions = useSelectorOptions(
|
||||
languages,
|
||||
uniqBy(languages, "code2"),
|
||||
(v) => v.name,
|
||||
(v) => v.code2,
|
||||
);
|
||||
|
@ -207,34 +208,6 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
|||
return <Text className="table-primary">{file.name}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Forced",
|
||||
accessorKey: "forced",
|
||||
cell: ({ row: { original, index } }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={original.forced}
|
||||
onChange={({ currentTarget: { checked } }) => {
|
||||
action.mutate(index, { ...original, forced: checked });
|
||||
}}
|
||||
></Checkbox>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "HI",
|
||||
accessorKey: "hi",
|
||||
cell: ({ row: { original, index } }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={original.hi}
|
||||
onChange={({ currentTarget: { checked } }) => {
|
||||
action.mutate(index, { ...original, hi: checked });
|
||||
}}
|
||||
></Checkbox>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Language",
|
||||
accessorKey: "language",
|
||||
|
@ -251,6 +224,61 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: () => (
|
||||
<Selector
|
||||
options={subtitlesTypeOptions}
|
||||
value={null}
|
||||
placeholder="Type"
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
action.update((item) => {
|
||||
switch (value) {
|
||||
case "hi":
|
||||
return { ...item, hi: true, forced: false };
|
||||
case "forced":
|
||||
return { ...item, hi: false, forced: true };
|
||||
case "normal":
|
||||
return { ...item, hi: false, forced: false };
|
||||
default:
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
></Selector>
|
||||
),
|
||||
accessorKey: "type",
|
||||
cell: ({ row: { original, index } }) => {
|
||||
return (
|
||||
<Select
|
||||
value={
|
||||
subtitlesTypeOptions.find((s) => {
|
||||
if (original.hi) {
|
||||
return s.value === "hi";
|
||||
}
|
||||
|
||||
if (original.forced) {
|
||||
return s.value === "forced";
|
||||
}
|
||||
|
||||
return s.value === "normal";
|
||||
})?.value
|
||||
}
|
||||
data={subtitlesTypeOptions}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
action.mutate(index, {
|
||||
...original,
|
||||
hi: value === "hi",
|
||||
forced: value === "forced",
|
||||
});
|
||||
}
|
||||
}}
|
||||
></Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
cell: ({ row: { index } }) => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import React, { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
MantineColor,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
|
@ -17,12 +17,13 @@ import {
|
|||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { isString } from "lodash";
|
||||
import { isString, uniqBy } from "lodash";
|
||||
import {
|
||||
useEpisodesBySeriesId,
|
||||
useEpisodeSubtitleModification,
|
||||
useSubtitleInfos,
|
||||
} from "@/apis/hooks";
|
||||
import { subtitlesTypeOptions } from "@/components/forms/uploadFormSelectorTypes";
|
||||
import { Action, Selector } from "@/components/inputs";
|
||||
import SimpleTable from "@/components/tables/SimpleTable";
|
||||
import TextPopover from "@/components/TextPopover";
|
||||
|
@ -100,7 +101,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
|||
const profile = useLanguageProfileBy(series.profileId);
|
||||
const languages = useProfileItemsToLanguages(profile);
|
||||
const languageOptions = useSelectorOptions(
|
||||
languages,
|
||||
uniqBy(languages, "code2"),
|
||||
(v) => v.name,
|
||||
(v) => v.code2,
|
||||
);
|
||||
|
@ -235,42 +236,6 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
|||
return <Text className="table-primary">{name}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Forced",
|
||||
accessorKey: "forced",
|
||||
cell: ({ row: { original, index } }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={original.forced}
|
||||
onChange={({ currentTarget: { checked } }) => {
|
||||
action.mutate(index, {
|
||||
...original,
|
||||
forced: checked,
|
||||
hi: checked ? false : original.hi,
|
||||
});
|
||||
}}
|
||||
></Checkbox>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "HI",
|
||||
accessorKey: "hi",
|
||||
cell: ({ row: { original, index } }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={original.hi}
|
||||
onChange={({ currentTarget: { checked } }) => {
|
||||
action.mutate(index, {
|
||||
...original,
|
||||
hi: checked,
|
||||
forced: checked ? false : original.forced,
|
||||
});
|
||||
}}
|
||||
></Checkbox>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: () => (
|
||||
<Selector
|
||||
|
@ -280,8 +245,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
|||
onChange={(value) => {
|
||||
if (value) {
|
||||
action.update((item) => {
|
||||
item.language = value;
|
||||
return item;
|
||||
return { ...item, language: value };
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
@ -301,6 +265,61 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: () => (
|
||||
<Selector
|
||||
options={subtitlesTypeOptions}
|
||||
value={null}
|
||||
placeholder="Type"
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
action.update((item) => {
|
||||
switch (value) {
|
||||
case "hi":
|
||||
return { ...item, hi: true, forced: false };
|
||||
case "forced":
|
||||
return { ...item, hi: false, forced: true };
|
||||
case "normal":
|
||||
return { ...item, hi: false, forced: false };
|
||||
default:
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
></Selector>
|
||||
),
|
||||
accessorKey: "type",
|
||||
cell: ({ row: { original, index } }) => {
|
||||
return (
|
||||
<Select
|
||||
value={
|
||||
subtitlesTypeOptions.find((s) => {
|
||||
if (original.hi) {
|
||||
return s.value === "hi";
|
||||
}
|
||||
|
||||
if (original.forced) {
|
||||
return s.value === "forced";
|
||||
}
|
||||
|
||||
return s.value === "normal";
|
||||
})?.value
|
||||
}
|
||||
data={subtitlesTypeOptions}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
action.mutate(index, {
|
||||
...original,
|
||||
hi: value === "hi",
|
||||
forced: value === "forced",
|
||||
});
|
||||
}
|
||||
}}
|
||||
></Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "episode",
|
||||
header: "Episode",
|
||||
|
|
16
frontend/src/components/forms/uploadFormSelectorTypes.tsx
Normal file
16
frontend/src/components/forms/uploadFormSelectorTypes.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { SelectorOption } from "@/components";
|
||||
|
||||
export const subtitlesTypeOptions: SelectorOption<string>[] = [
|
||||
{
|
||||
label: "Normal",
|
||||
value: "normal",
|
||||
},
|
||||
{
|
||||
label: "Hearing-Impaired",
|
||||
value: "hi",
|
||||
},
|
||||
{
|
||||
label: "Forced",
|
||||
value: "forced",
|
||||
},
|
||||
];
|
|
@ -6,6 +6,7 @@ import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
|||
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { uniqueId } from "lodash";
|
||||
import { useMovieModification, useMoviesPagination } from "@/apis/hooks";
|
||||
import { Action } from "@/components";
|
||||
import { AudioList } from "@/components/bazarr";
|
||||
|
@ -95,7 +96,7 @@ const MovieView: FunctionComponent = () => {
|
|||
<Badge
|
||||
mr="xs"
|
||||
color="yellow"
|
||||
key={BuildKey(v.code2, v.hi, v.forced)}
|
||||
key={uniqueId(`${BuildKey(v.code2, v.hi, v.forced)}_`)}
|
||||
>
|
||||
<Language.Text value={v}></Language.Text>
|
||||
</Badge>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { FunctionComponent, useCallback, useMemo } from "react";
|
|||
import { Badge, Button, Group } from "@mantine/core";
|
||||
import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { cloneDeep, includes, maxBy } from "lodash";
|
||||
import { Action } from "@/components";
|
||||
import {
|
||||
anyCutoff,
|
||||
|
@ -79,10 +79,10 @@ const Table: FunctionComponent = () => {
|
|||
}) => {
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{items.map((v) => {
|
||||
{items.map((v, i) => {
|
||||
const isCutoff = v.id === cutoff || cutoff === anyCutoff;
|
||||
return (
|
||||
<ItemBadge key={v.id} cutoff={isCutoff} item={v}></ItemBadge>
|
||||
<ItemBadge key={i} cutoff={isCutoff} item={v}></ItemBadge>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
|
@ -148,9 +148,45 @@ const Table: FunctionComponent = () => {
|
|||
icon={faWrench}
|
||||
c="gray"
|
||||
onClick={() => {
|
||||
const lastId = maxBy(profile.items, "id")?.id || 0;
|
||||
|
||||
// We once had an issue on the past where there were duplicated
|
||||
// item ids that needs to become unique upon editing.
|
||||
const sanitizedProfile = {
|
||||
...cloneDeep(profile),
|
||||
items: profile.items.reduce(
|
||||
(acc, value) => {
|
||||
const { ids, duplicatedIds, items } = acc;
|
||||
|
||||
// We once had an issue on the past where there were duplicated
|
||||
// item ids that needs to become unique upon editing.
|
||||
if (includes(ids, value.id)) {
|
||||
duplicatedIds.push(value.id);
|
||||
items.push({
|
||||
...value,
|
||||
id: lastId + duplicatedIds.length,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
ids.push(value.id);
|
||||
items.push(value);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
ids: [] as number[],
|
||||
duplicatedIds: [] as number[],
|
||||
items: [] as typeof profile.items,
|
||||
},
|
||||
).items,
|
||||
tag: profile.tag || undefined,
|
||||
};
|
||||
|
||||
modals.openContextModal(ProfileEditModal, {
|
||||
languages,
|
||||
profile: cloneDeep(profile),
|
||||
profile: sanitizedProfile,
|
||||
onComplete: updateProfile,
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -144,6 +144,8 @@ const SystemStatusView: FunctionComponent = () => {
|
|||
<Row title="Radarr Version">{status?.radarr_version}</Row>
|
||||
<Row title="Operating System">{status?.operating_system}</Row>
|
||||
<Row title="Python Version">{status?.python_version}</Row>
|
||||
<Row title="Database Engine">{status?.database_engine}</Row>
|
||||
<Row title="Database Version">{status?.database_migration}</Row>
|
||||
<Row title="Bazarr Directory">{status?.bazarr_directory}</Row>
|
||||
<Row title="Bazarr Config Directory">
|
||||
{status?.bazarr_config_directory}
|
||||
|
|
2
frontend/src/types/system.d.ts
vendored
2
frontend/src/types/system.d.ts
vendored
|
@ -20,6 +20,8 @@ declare namespace System {
|
|||
bazarr_config_directory: string;
|
||||
bazarr_directory: string;
|
||||
bazarr_version: string;
|
||||
database_engine: string;
|
||||
database_migration: string;
|
||||
operating_system: string;
|
||||
package_version: string;
|
||||
python_version: string;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
pip
|
|
@ -1,20 +0,0 @@
|
|||
argparse is (c) 2006-2009 Steven J. Bethard <steven.bethard@gmail.com>.
|
||||
|
||||
The argparse module was contributed to Python as of Python 2.7 and thus
|
||||
was licensed under the Python license. Same license applies to all files in
|
||||
the argparse package project.
|
||||
|
||||
For details about the Python License, please see doc/Python-License.txt.
|
||||
|
||||
History
|
||||
-------
|
||||
|
||||
Before (and including) argparse 1.1, the argparse package was licensed under
|
||||
Apache License v2.0.
|
||||
|
||||
After argparse 1.1, all project files from the argparse project were deleted
|
||||
due to license compatibility issues between Apache License 2.0 and GNU GPL v2.
|
||||
|
||||
The project repository then had a clean start with some files taken from
|
||||
Python 2.7.1, so definitely all files are under Python License now.
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: argparse
|
||||
Version: 1.4.0
|
||||
Summary: Python command-line parsing library
|
||||
Home-page: https://github.com/ThomasWaldmann/argparse/
|
||||
Author: Thomas Waldmann
|
||||
Author-email: tw@waldmann-edv.de
|
||||
License: Python Software Foundation License
|
||||
Keywords: argparse command line parser parsing
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 2.3
|
||||
Classifier: Programming Language :: Python :: 2.4
|
||||
Classifier: Programming Language :: Python :: 2.5
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3.0
|
||||
Classifier: Programming Language :: Python :: 3.1
|
||||
Classifier: Programming Language :: Python :: 3.2
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Topic :: Software Development
|
||||
License-File: LICENSE.txt
|
||||
|
||||
The argparse module makes it easy to write user friendly command line
|
||||
interfaces.
|
||||
|
||||
The program defines what arguments it requires, and argparse will figure out
|
||||
how to parse those out of sys.argv. The argparse module also automatically
|
||||
generates help and usage messages and issues errors when users give the
|
||||
program invalid arguments.
|
||||
|
||||
As of Python >= 2.7 and >= 3.2, the argparse module is maintained within the
|
||||
Python standard library. For users who still need to support Python < 2.7 or
|
||||
< 3.2, it is also provided as a separate package, which tries to stay
|
||||
compatible with the module in the standard library, but also supports older
|
||||
Python versions.
|
||||
|
||||
Also, we can fix bugs here for users who are stuck on some non-current python
|
||||
version, like e.g. 3.2.3 (which has bugs that were fixed in a later 3.2.x
|
||||
release).
|
||||
|
||||
argparse is licensed under the Python license, for details see LICENSE.txt.
|
||||
|
||||
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
argparse should work on Python >= 2.3, it was tested on:
|
||||
|
||||
* 2.3, 2.4, 2.5, 2.6 and 2.7
|
||||
* 3.1, 3.2, 3.3, 3.4
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Try one of these:
|
||||
|
||||
python setup.py install
|
||||
|
||||
easy_install argparse
|
||||
|
||||
pip install argparse
|
||||
|
||||
putting argparse.py in some directory listed in sys.path should also work
|
||||
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
If you find a bug in argparse (pypi), please try to reproduce it with latest
|
||||
python 2.7 and 3.4 (and use argparse from stdlib).
|
||||
|
||||
If it happens there also, please file a bug in the python.org issue tracker.
|
||||
If it does not happen there, file a bug in the argparse package issue tracker.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
argparse-1.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
argparse-1.4.0.dist-info/LICENSE.txt,sha256=bVBNRcTRCfkl7wWJYLbRzicSu2tXk-kmv8FRcWrHQEg,741
|
||||
argparse-1.4.0.dist-info/METADATA,sha256=yZGPMA4uvkui2P7qaaiI89zqwjDbyFcehJG4j5Pk8Yk,2816
|
||||
argparse-1.4.0.dist-info/RECORD,,
|
||||
argparse-1.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
argparse-1.4.0.dist-info/WHEEL,sha256=P2T-6epvtXQ2cBOE_U1K4_noqlJFN3tj15djMgEu4NM,110
|
||||
argparse-1.4.0.dist-info/top_level.txt,sha256=TgiWrQsF0mKWwqS2KHLORD0ZtqYHPRGdCAAzKwtVvJ4,9
|
||||
argparse.py,sha256=0ksYqisQDQvhoiuo19JERCSpg51tc641GFJIx7pTA0g,89214
|
|
@ -1,6 +0,0 @@
|
|||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.41.3)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
|
@ -1 +0,0 @@
|
|||
argparse
|
2392
libs/argparse.py
2392
libs/argparse.py
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,6 @@
|
|||
# Bazarr dependencies
|
||||
alembic==1.13.1
|
||||
aniso8601==9.0.1
|
||||
argparse==1.4.0
|
||||
apprise==1.7.6
|
||||
apscheduler<=3.10.4
|
||||
attrs==23.2.0
|
||||
|
|
|
@ -7,6 +7,7 @@ Create Date: 2024-02-16 10:32:39.123456
|
|||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from app.database import TableLanguagesProfiles
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
@ -19,6 +20,9 @@ bind = op.get_context().bind
|
|||
|
||||
|
||||
def upgrade():
|
||||
op.execute(sa.update(TableLanguagesProfiles)
|
||||
.values({TableLanguagesProfiles.originalFormat: 0})
|
||||
.where(TableLanguagesProfiles.originalFormat.is_(None)))
|
||||
if bind.engine.name == 'postgresql':
|
||||
with op.batch_alter_table('table_languages_profiles') as batch_op:
|
||||
batch_op.alter_column('originalFormat', type_=sa.Integer())
|
||||
|
|
15
tests/bazarr/test_logging_filters.py
Normal file
15
tests/bazarr/test_logging_filters.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import logging
|
||||
|
||||
from bazarr.app.logger import UnwantedWaitressMessageFilter
|
||||
|
||||
def test_true_for_bazarr():
|
||||
record = logging.LogRecord("", logging.INFO, "", 0, "a message from BAZARR for logging", (), None)
|
||||
assert UnwantedWaitressMessageFilter().filter(record)
|
||||
|
||||
def test_false_below_error():
|
||||
record = logging.LogRecord("", logging.INFO, "", 0, "", (), None)
|
||||
assert not UnwantedWaitressMessageFilter().filter(record)
|
||||
|
||||
def test_true_above_error():
|
||||
record = logging.LogRecord("", logging.CRITICAL, "", 0, "", (), None)
|
||||
assert UnwantedWaitressMessageFilter().filter(record)
|
|
@ -1,50 +0,0 @@
|
|||
from subliminal_patch.providers import subscene_cloudscraper as subscene
|
||||
|
||||
|
||||
def test_provider_scraper_call():
|
||||
with subscene.SubsceneProvider() as provider:
|
||||
result = provider._scraper_call(
|
||||
"https://subscene.com/subtitles/breaking-bad-fifth-season"
|
||||
)
|
||||
assert result.status_code == 200
|
||||
|
||||
|
||||
def test_provider_gen_results():
|
||||
with subscene.SubsceneProvider() as provider:
|
||||
assert list(provider._gen_results("Breaking Bad"))
|
||||
|
||||
|
||||
def test_provider_search_movie():
|
||||
with subscene.SubsceneProvider() as provider:
|
||||
result = provider._search_movie("Taxi Driver", 1976)
|
||||
assert result == "/subtitles/taxi-driver"
|
||||
|
||||
|
||||
def test_provider_find_movie_subtitles(languages):
|
||||
with subscene.SubsceneProvider() as provider:
|
||||
result = provider._find_movie_subtitles(
|
||||
"/subtitles/taxi-driver", languages["en"]
|
||||
)
|
||||
assert result
|
||||
|
||||
|
||||
def test_provider_search_tv_show_season():
|
||||
with subscene.SubsceneProvider() as provider:
|
||||
result = provider._search_tv_show_season("The Wire", 1)
|
||||
assert result == "/subtitles/the-wire--first-season"
|
||||
|
||||
|
||||
def test_provider_find_episode_subtitles(languages):
|
||||
with subscene.SubsceneProvider() as provider:
|
||||
result = provider._find_episode_subtitles(
|
||||
"/subtitles/the-wire--first-season", 1, 1, languages["en"]
|
||||
)
|
||||
assert result
|
||||
|
||||
|
||||
def test_provider_download_subtitle(languages):
|
||||
path = "https://subscene.com/subtitles/the-wire--first-season/english/115904"
|
||||
subtitle = subscene.SubsceneSubtitle(languages["en"], path, "", 1)
|
||||
with subscene.SubsceneProvider() as provider:
|
||||
provider.download_subtitle(subtitle)
|
||||
assert subtitle.is_valid()
|
Loading…
Reference in a new issue