summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--bazarr/api/system/settings.py2
-rw-r--r--bazarr/app/config.py2
-rw-r--r--bazarr/app/database.py9
-rw-r--r--bazarr/radarr/sync/movies.py13
-rw-r--r--bazarr/radarr/sync/parser.py17
-rw-r--r--bazarr/sonarr/sync/parser.py72
-rw-r--r--bazarr/sonarr/sync/series.py12
-rw-r--r--frontend/src/components/forms/ProfileEditForm.module.scss8
-rw-r--r--frontend/src/components/forms/ProfileEditForm.tsx31
-rw-r--r--frontend/src/pages/Settings/Languages/index.tsx22
-rw-r--r--frontend/src/pages/Settings/Languages/table.tsx5
-rw-r--r--frontend/src/types/api.d.ts1
12 files changed, 150 insertions, 44 deletions
diff --git a/bazarr/api/system/settings.py b/bazarr/api/system/settings.py
index 103df6304..126e2f7a5 100644
--- a/bazarr/api/system/settings.py
+++ b/bazarr/api/system/settings.py
@@ -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
diff --git a/bazarr/app/config.py b/bazarr/app/config.py
index f5203da84..aebdf5dc3 100644
--- a/bazarr/app/config.py
+++ b/bazarr/app/config.py
@@ -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),
diff --git a/bazarr/app/database.py b/bazarr/app/database.py
index fa612c4eb..3780befea 100644
--- a/bazarr/app/database.py
+++ b/bazarr/app/database.py
@@ -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)
diff --git a/bazarr/radarr/sync/movies.py b/bazarr/radarr/sync/movies.py
index 82416cffb..6273a0a8d 100644
--- a/bazarr/radarr/sync/movies.py
+++ b/bazarr/radarr/sync/movies.py
@@ -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.')
diff --git a/bazarr/radarr/sync/parser.py b/bazarr/radarr/sync/parser.py
index 9648152c2..d5f3e08c8 100644
--- a/bazarr/radarr/sync/parser.py
+++ b/bazarr/radarr/sync/parser.py
@@ -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
diff --git a/bazarr/sonarr/sync/parser.py b/bazarr/sonarr/sync/parser.py
index d8fce1697..32980cbab 100644
--- a/bazarr/sonarr/sync/parser.py
+++ b/bazarr/sonarr/sync/parser.py
@@ -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):
diff --git a/bazarr/sonarr/sync/series.py b/bazarr/sonarr/sync/series.py
index 96a00009c..f8bd84990 100644
--- a/bazarr/sonarr/sync/series.py
+++ b/bazarr/sonarr/sync/series.py
@@ -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:
diff --git a/frontend/src/components/forms/ProfileEditForm.module.scss b/frontend/src/components/forms/ProfileEditForm.module.scss
index d98b850ff..3d4a8e177 100644
--- a/frontend/src/components/forms/ProfileEditForm.module.scss
+++ b/frontend/src/components/forms/ProfileEditForm.module.scss
@@ -3,3 +3,11 @@
padding: 0;
}
}
+
+.evenly {
+ flex-wrap: wrap;
+
+ & > div {
+ flex: 1;
+ }
+}
diff --git a/frontend/src/components/forms/ProfileEditForm.tsx b/frontend/src/components/forms/ProfileEditForm.tsx
index 75e2f9df7..267951fcb 100644
--- a/frontend/src/components/forms/ProfileEditForm.tsx
+++ b/frontend/src/components/forms/ProfileEditForm.tsx
@@ -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"
diff --git a/frontend/src/pages/Settings/Languages/index.tsx b/frontend/src/pages/Settings/Languages/index.tsx
index 9fe562920..6a5b5309b 100644
--- a/frontend/src/pages/Settings/Languages/index.tsx
+++ b/frontend/src/pages/Settings/Languages/index.tsx
@@ -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"
diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx
index c32300628..03971a5cc 100644
--- a/frontend/src/pages/Settings/Languages/table.tsx
+++ b/frontend/src/pages/Settings/Languages/table.tsx
@@ -66,6 +66,10 @@ const Table: FunctionComponent = () => {
accessorKey: "name",
},
{
+ header: "Tag",
+ accessorKey: "tag",
+ },
+ {
header: "Languages",
accessorKey: "items",
cell: ({
@@ -178,6 +182,7 @@ const Table: FunctionComponent = () => {
const profile = {
profileId: nextProfileId,
name: "",
+ tag: undefined,
items: [],
cutoff: null,
mustContain: [],
diff --git a/frontend/src/types/api.d.ts b/frontend/src/types/api.d.ts
index 069be3029..e8bd4483e 100644
--- a/frontend/src/types/api.d.ts
+++ b/frontend/src/types/api.d.ts
@@ -40,6 +40,7 @@ declare namespace Language {
mustContain: string[];
mustNotContain: string[];
originalFormat: boolean | null;
+ tag: string | undefined;
}
}