Merge development into master

pull/2699/head
github-actions[bot] 2 months ago committed by GitHub
commit 5c56866d56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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;
const { link, displayName } = (() => {
const hasDuplicate = includes(duplicates, `${v.title} (${v.year})`);
if (v.sonarrSeriesId) {
link = `/series/${v.sonarrSeriesId}`;
} else if (v.radarrId) {
link = `/movies/${v.radarrId}`;
} else {
throw new Error("Unknown search result");
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",

@ -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}

@ -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,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

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())

@ -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…
Cancel
Save