mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-24 06:37:16 -04:00
Added new feature: Tag-Based Automatic Language Profile Selection
This commit is contained in:
parent
c852458b8c
commit
b304f6f1ef
12 changed files with 150 additions and 44 deletions
|
@ -73,6 +73,7 @@ class SystemSettings(Resource):
|
|||
mustNotContain=str(item['mustNotContain']),
|
||||
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
||||
None,
|
||||
tag=item['tag'] if 'tag' in item else None,
|
||||
)
|
||||
.where(TableLanguagesProfiles.profileId == item['profileId']))
|
||||
existing.remove(item['profileId'])
|
||||
|
@ -89,6 +90,7 @@ class SystemSettings(Resource):
|
|||
mustNotContain=str(item['mustNotContain']),
|
||||
originalFormat=int(item['originalFormat']) if item['originalFormat'] not in None_Keys else
|
||||
None,
|
||||
tag=item['tag'] if 'tag' in item else None,
|
||||
))
|
||||
for profileId in existing:
|
||||
# Remove deleted profiles
|
||||
|
|
|
@ -88,6 +88,8 @@ validators = [
|
|||
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.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.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.movie_default_enabled', must_exist=True, default=False, is_type_of=bool),
|
||||
|
|
|
@ -379,6 +379,7 @@ def update_profile_id_list():
|
|||
'mustContain': ast.literal_eval(x.mustContain) if x.mustContain else [],
|
||||
'mustNotContain': ast.literal_eval(x.mustNotContain) if x.mustNotContain else [],
|
||||
'originalFormat': x.originalFormat,
|
||||
'tag': x.tag,
|
||||
} for x in database.execute(
|
||||
select(TableLanguagesProfiles.profileId,
|
||||
TableLanguagesProfiles.name,
|
||||
|
@ -386,7 +387,8 @@ def update_profile_id_list():
|
|||
TableLanguagesProfiles.items,
|
||||
TableLanguagesProfiles.mustContain,
|
||||
TableLanguagesProfiles.mustNotContain,
|
||||
TableLanguagesProfiles.originalFormat))
|
||||
TableLanguagesProfiles.originalFormat,
|
||||
TableLanguagesProfiles.tag))
|
||||
.all()
|
||||
]
|
||||
|
||||
|
@ -421,7 +423,7 @@ def get_profile_cutoff(profile_id):
|
|||
if profile_id and profile_id != 'null':
|
||||
cutoff_language = []
|
||||
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 profileId == int(profile_id):
|
||||
for item in items:
|
||||
|
@ -511,7 +513,8 @@ def upgrade_languages_profile_hi_values():
|
|||
TableLanguagesProfiles.items,
|
||||
TableLanguagesProfiles.mustContain,
|
||||
TableLanguagesProfiles.mustNotContain,
|
||||
TableLanguagesProfiles.originalFormat)
|
||||
TableLanguagesProfiles.originalFormat,
|
||||
TableLanguagesProfiles.tag)
|
||||
))\
|
||||
.all():
|
||||
items = json.loads(languages_profile.items)
|
||||
|
|
|
@ -28,6 +28,11 @@ def trace(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():
|
||||
movies_full_scan_subtitles()
|
||||
logging.info('BAZARR All existing movie subtitles indexed from disk.')
|
||||
|
@ -108,6 +113,7 @@ def update_movies(send_event=True):
|
|||
else:
|
||||
audio_profiles = get_profile_list()
|
||||
tagsDict = get_tags()
|
||||
language_profiles = get_language_profiles()
|
||||
|
||||
# Get movies data from 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:
|
||||
parsed_movie = movieParser(movie, action='update',
|
||||
tags_dict=tagsDict,
|
||||
language_profiles=language_profiles,
|
||||
movie_default_profile=movie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
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:
|
||||
parsed_movie = movieParser(movie, action='insert',
|
||||
tags_dict=tagsDict,
|
||||
language_profiles=language_profiles,
|
||||
movie_default_profile=movie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
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()
|
||||
tagsDict = get_tags()
|
||||
language_profiles = get_language_profiles()
|
||||
|
||||
try:
|
||||
# Get movie data from radarr api
|
||||
|
@ -256,10 +265,10 @@ def update_one_movie(movie_id, action, defer_search=False):
|
|||
return
|
||||
else:
|
||||
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)
|
||||
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)
|
||||
except Exception:
|
||||
logging.exception('BAZARR cannot get movie returned by SignalR feed from Radarr API.')
|
||||
|
|
|
@ -11,7 +11,17 @@ from utilities.path_mappings import path_mappings
|
|||
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:
|
||||
try:
|
||||
overview = str(movie['overview'])
|
||||
|
@ -140,6 +150,11 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
|
|||
parsed_movie['subtitles'] = '[]'
|
||||
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
|
||||
|
||||
return parsed_movie
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,17 @@ from sonarr.info import get_sonarr_info
|
|||
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 ''
|
||||
poster = ''
|
||||
fanart = ''
|
||||
|
@ -42,39 +52,33 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
|
|||
else:
|
||||
audio_language = []
|
||||
|
||||
if action == 'update':
|
||||
return {'title': show["title"],
|
||||
'path': show["path"],
|
||||
'tvdbId': int(show["tvdbId"]),
|
||||
'sonarrSeriesId': int(show["id"]),
|
||||
'overview': overview,
|
||||
'poster': poster,
|
||||
'fanart': fanart,
|
||||
'audio_language': str(audio_language),
|
||||
'sortTitle': show['sortTitle'],
|
||||
'year': str(show['year']),
|
||||
'alternativeTitles': alternate_titles,
|
||||
'tags': str(tags),
|
||||
'seriesType': show['seriesType'],
|
||||
'imdbId': imdbId,
|
||||
'monitored': str(bool(show['monitored']))}
|
||||
else:
|
||||
return {'title': show["title"],
|
||||
'path': show["path"],
|
||||
'tvdbId': show["tvdbId"],
|
||||
'sonarrSeriesId': show["id"],
|
||||
'overview': overview,
|
||||
'poster': poster,
|
||||
'fanart': fanart,
|
||||
'audio_language': str(audio_language),
|
||||
'sortTitle': show['sortTitle'],
|
||||
'year': str(show['year']),
|
||||
'alternativeTitles': alternate_titles,
|
||||
'tags': str(tags),
|
||||
'seriesType': show['seriesType'],
|
||||
'imdbId': imdbId,
|
||||
'profileId': serie_default_profile,
|
||||
'monitored': str(bool(show['monitored']))}
|
||||
parsed_series = {
|
||||
'title': show["title"],
|
||||
'path': show["path"],
|
||||
'tvdbId': int(show["tvdbId"]),
|
||||
'sonarrSeriesId': int(show["id"]),
|
||||
'overview': overview,
|
||||
'poster': poster,
|
||||
'fanart': fanart,
|
||||
'audio_language': str(audio_language),
|
||||
'sortTitle': show['sortTitle'],
|
||||
'year': str(show['year']),
|
||||
'alternativeTitles': alternate_titles,
|
||||
'tags': str(tags),
|
||||
'seriesType': show['seriesType'],
|
||||
'imdbId': imdbId,
|
||||
'monitored': str(bool(show['monitored']))
|
||||
}
|
||||
|
||||
if action == 'insert':
|
||||
parsed_series['profileId'] = serie_default_profile
|
||||
|
||||
if settings.general.serie_tag_enabled:
|
||||
tag_profile = get_matching_profile(tags, language_profiles)
|
||||
if tag_profile:
|
||||
parsed_series['profileId'] = tag_profile
|
||||
|
||||
return parsed_series
|
||||
|
||||
|
||||
def profile_id_to_language(id_, profiles):
|
||||
|
|
|
@ -26,6 +26,11 @@ def trace(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():
|
||||
series_monitored = database.execute(
|
||||
select(TableShows.tvdbId, TableShows.monitored))\
|
||||
|
@ -58,6 +63,7 @@ def update_series(send_event=True):
|
|||
|
||||
audio_profiles = get_profile_list()
|
||||
tagsDict = get_tags()
|
||||
language_profiles = get_language_profiles()
|
||||
|
||||
# Get shows data from 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:
|
||||
updated_series = seriesParser(show, action='update', tags_dict=tagsDict,
|
||||
language_profiles=language_profiles,
|
||||
serie_default_profile=serie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
|
||||
|
@ -132,6 +139,7 @@ def update_series(send_event=True):
|
|||
event_stream(type='series', payload=show['id'])
|
||||
else:
|
||||
added_series = seriesParser(show, action='insert', tags_dict=tagsDict,
|
||||
language_profiles=language_profiles,
|
||||
serie_default_profile=serie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
|
||||
|
@ -203,7 +211,7 @@ def update_one_series(series_id, action):
|
|||
|
||||
audio_profiles = get_profile_list()
|
||||
tagsDict = get_tags()
|
||||
|
||||
language_profiles = get_language_profiles()
|
||||
try:
|
||||
# Get series data from sonarr api
|
||||
series = None
|
||||
|
@ -215,10 +223,12 @@ def update_one_series(series_id, action):
|
|||
else:
|
||||
if action == 'updated' and existing_series:
|
||||
series = seriesParser(series_data[0], action='update', tags_dict=tagsDict,
|
||||
language_profiles=language_profiles,
|
||||
serie_default_profile=serie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
elif action == 'updated' and not existing_series:
|
||||
series = seriesParser(series_data[0], action='insert', tags_dict=tagsDict,
|
||||
language_profiles=language_profiles,
|
||||
serie_default_profile=serie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
except Exception:
|
||||
|
|
|
@ -3,3 +3,11 @@
|
|||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.evenly {
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
Accordion,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Select,
|
||||
Stack,
|
||||
Switch,
|
||||
|
@ -72,9 +73,16 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
(value) => value.length > 0,
|
||||
"Must have a name",
|
||||
),
|
||||
tag: FormUtils.validation((value) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /^[a-z_0-9-]+$/.test(value);
|
||||
}, "Only lowercase alphanumeric characters, underscores (_) and hyphens (-) are allowed"),
|
||||
items: FormUtils.validation(
|
||||
(value) => value.length > 0,
|
||||
"Must contain at lease 1 language",
|
||||
"Must contain at least 1 language",
|
||||
),
|
||||
},
|
||||
});
|
||||
|
@ -265,7 +273,24 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label="Name" {...form.getInputProps("name")}></TextInput>
|
||||
<Flex
|
||||
direction={{ base: "column", sm: "row" }}
|
||||
gap="sm"
|
||||
className={styles.evenly}
|
||||
>
|
||||
<TextInput label="Name" {...form.getInputProps("name")}></TextInput>
|
||||
<TextInput
|
||||
label="Tag"
|
||||
{...form.getInputProps("tag")}
|
||||
onBlur={() =>
|
||||
form.setFieldValue(
|
||||
"tag",
|
||||
(prev) =>
|
||||
prev?.toLowerCase().trim().replace(/\s+/g, "_") ?? undefined,
|
||||
)
|
||||
}
|
||||
></TextInput>
|
||||
</Flex>
|
||||
<Accordion
|
||||
multiple
|
||||
chevronPosition="right"
|
||||
|
@ -274,7 +299,6 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
>
|
||||
<Accordion.Item value="Languages">
|
||||
<Stack>
|
||||
{form.errors.items}
|
||||
<SimpleTable
|
||||
columns={columns}
|
||||
data={form.values.items}
|
||||
|
@ -282,6 +306,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
<Button fullWidth onClick={addItem}>
|
||||
Add Language
|
||||
</Button>
|
||||
<Text c="var(--mantine-color-error)">{form.errors.items}</Text>
|
||||
<Selector
|
||||
clearable
|
||||
label="Cutoff"
|
||||
|
|
|
@ -115,6 +115,28 @@ const SettingsLanguagesView: FunctionComponent = () => {
|
|||
<Section header="Languages Profile">
|
||||
<Table></Table>
|
||||
</Section>
|
||||
<Section header="Tag-Based Automatic Language Profile Selection Settings">
|
||||
<Message>
|
||||
If enabled, Bazarr will look at the names of all tags of a Series from
|
||||
Sonarr (or a Movie from Radarr) to find a matching Bazarr language
|
||||
profile tag. It will use as the language profile the FIRST tag from
|
||||
Sonarr/Radarr that matches the tag of a Bazarr language profile
|
||||
EXACTLY. If mutiple tags match, there is no guarantee as to which one
|
||||
will be used, so choose your tag names carefully. Also, if you update
|
||||
the tag names in Sonarr/Radarr, Bazarr will detect this and repeat the
|
||||
matching process for the affected shows. However, if a show's only
|
||||
matching tag is removed from Sonarr/Radarr, Bazarr will NOT remove the
|
||||
show's existing language profile, but keep it, as is.
|
||||
</Message>
|
||||
<Check
|
||||
label="Series"
|
||||
settingKey="settings-general-serie_tag_enabled"
|
||||
></Check>
|
||||
<Check
|
||||
label="Movies"
|
||||
settingKey="settings-general-movie_tag_enabled"
|
||||
></Check>
|
||||
</Section>
|
||||
<Section header="Default Settings">
|
||||
<Check
|
||||
label="Series"
|
||||
|
|
|
@ -65,6 +65,10 @@ const Table: FunctionComponent = () => {
|
|||
header: "Name",
|
||||
accessorKey: "name",
|
||||
},
|
||||
{
|
||||
header: "Tag",
|
||||
accessorKey: "tag",
|
||||
},
|
||||
{
|
||||
header: "Languages",
|
||||
accessorKey: "items",
|
||||
|
@ -178,6 +182,7 @@ const Table: FunctionComponent = () => {
|
|||
const profile = {
|
||||
profileId: nextProfileId,
|
||||
name: "",
|
||||
tag: undefined,
|
||||
items: [],
|
||||
cutoff: null,
|
||||
mustContain: [],
|
||||
|
|
1
frontend/src/types/api.d.ts
vendored
1
frontend/src/types/api.d.ts
vendored
|
@ -40,6 +40,7 @@ declare namespace Language {
|
|||
mustContain: string[];
|
||||
mustNotContain: string[];
|
||||
originalFormat: boolean | null;
|
||||
tag: string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue