feat: simplify language support in quality profiles

- moved language field in quality profile general page
- simplify transformation for sonarr by making languages optional
This commit is contained in:
Sam Chau
2026-01-22 14:02:43 +10:30
parent 12ba7540f7
commit 4efefe63ca
23 changed files with 5153 additions and 440 deletions

View File

@@ -9,6 +9,7 @@ export interface CreateQualityProfileInput {
name: string;
description: string | null;
tags: string[];
language: string | null;
}
export interface CreateQualityProfileOptions {
@@ -89,6 +90,16 @@ export async function create(options: CreateQualityProfileOptions) {
queries.push(insertQuality);
}
// 4. Insert language if one is selected
if (input.language !== null) {
const insertLanguage = {
sql: `INSERT INTO quality_profile_languages (quality_profile_name, language_name, type) VALUES ('${esc(input.name)}', '${esc(input.language)}', 'simple')`,
parameters: [],
query: {} as never
};
queries.push(insertLanguage);
}
// Write the operation
const result = await writeOperation({
databaseId,

View File

@@ -32,6 +32,13 @@ export async function general(
.orderBy('t.name')
.execute();
// Get language for this profile (first one if exists)
const languageRow = await db
.selectFrom('quality_profile_languages as qpl')
.select(['qpl.language_name'])
.where('qpl.quality_profile_name', '=', profile.name)
.executeTakeFirst();
return {
id: profile.id,
name: profile.name,
@@ -39,6 +46,7 @@ export async function general(
tags: tags.map((tag) => ({
name: tag.tag_name,
created_at: tag.tag_created_at
}))
})),
language: languageRow?.language_name ?? null
};
}

View File

@@ -40,6 +40,7 @@ export interface QualityProfileGeneral {
name: string;
description: string; // Raw markdown
tags: Tag[];
language: string | null; // Language name, null means "Any"
}
/** Language configuration for a quality profile */

View File

@@ -11,6 +11,7 @@ export interface UpdateGeneralInput {
name: string;
description: string;
tags: string[];
language: string | null; // Language name, null means no language set
}
export interface UpdateGeneralOptions {
@@ -92,6 +93,27 @@ export async function updateGeneral(options: UpdateGeneralOptions) {
queries.push(linkTag);
}
// 3. Handle language changes
const profileNameForLanguage = input.name !== current.name ? input.name : current.name;
// Delete existing language for this profile
const deleteLanguage = {
sql: `DELETE FROM quality_profile_languages WHERE quality_profile_name = '${esc(profileNameForLanguage)}'`,
parameters: [],
query: {} as never
};
queries.push(deleteLanguage);
// Insert new language if one is selected
if (input.language !== null) {
const insertLanguage = {
sql: `INSERT INTO quality_profile_languages (quality_profile_name, language_name, type) VALUES ('${esc(profileNameForLanguage)}', '${esc(input.language)}', 'simple')`,
parameters: [],
query: {} as never
};
queries.push(insertLanguage);
}
// Log what's being changed
const changes: Record<string, { from: unknown; to: unknown }> = {};
@@ -104,6 +126,9 @@ export async function updateGeneral(options: UpdateGeneralOptions) {
if (tagsToAdd.length > 0 || tagsToRemove.length > 0) {
changes.tags = { from: currentTagNames, to: input.tags };
}
if (current.language !== input.language) {
changes.language = { from: current.language, to: input.language };
}
await logger.info(`Save quality profile "${input.name}"`, {
source: 'QualityProfile',

View File

@@ -246,7 +246,13 @@ export const LANGUAGES: Record<SyncArrType, Record<string, LanguageDefinition>>
malayalam: { id: 48, name: 'Malayalam' },
kannada: { id: 49, name: 'Kannada' },
albanian: { id: 50, name: 'Albanian' },
afrikaans: { id: 51, name: 'Afrikaans' }
afrikaans: { id: 51, name: 'Afrikaans' },
marathi: { id: 52, name: 'Marathi' },
tagalog: { id: 53, name: 'Tagalog' },
urdu: { id: 54, name: 'Urdu' },
romansh: { id: 55, name: 'Romansh' },
mongolian: { id: 56, name: 'Mongolian' },
georgian: { id: 57, name: 'Georgian' }
},
sonarr: {
unknown: { id: 0, name: 'Unknown' },
@@ -479,3 +485,60 @@ export function getLanguageForProfile(name: string, arrType: SyncArrType): Langu
return getLanguage(name, arrType);
}
/**
* Get all Radarr languages as an array (for UI dropdowns)
* Returns languages sorted by name, with Any and Original at the top
*/
export function getRadarrLanguages(): LanguageDefinition[] {
const languages = Object.values(LANGUAGES.radarr);
// Sort: Any first, Original second, then alphabetically
return languages.sort((a, b) => {
if (a.id === -1) return -1;
if (b.id === -1) return 1;
if (a.id === -2) return -1;
if (b.id === -2) return 1;
return a.name.localeCompare(b.name);
});
}
/**
* Language with arr type support information (for conditions UI)
*/
export interface LanguageWithSupport {
name: string;
radarr: boolean;
sonarr: boolean;
}
/**
* Get all languages with their arr type support (for conditions page)
* Returns sorted array with Original first, then alphabetically
*/
export function getLanguagesWithSupport(): LanguageWithSupport[] {
const radarrLangs = new Set(Object.values(LANGUAGES.radarr).map((l) => l.name));
const sonarrLangs = new Set(Object.values(LANGUAGES.sonarr).map((l) => l.name));
// Combine all language names
const allNames = new Set([...radarrLangs, ...sonarrLangs]);
// Build result with support flags
const result: LanguageWithSupport[] = [];
for (const name of allNames) {
// Skip "Any" - it's only for quality profiles, not conditions
if (name === 'Any') continue;
result.push({
name,
radarr: radarrLangs.has(name),
sonarr: sonarrLangs.has(name)
});
}
// Sort: Original first, then alphabetically
return result.sort((a, b) => {
if (a.name === 'Original') return -1;
if (b.name === 'Original') return 1;
return a.name.localeCompare(b.name);
});
}

View File

@@ -345,7 +345,7 @@ export class QualityProfileSyncer extends BaseSyncer {
syncedProfiles.push({
name: arrProfile.name,
action: isUpdate ? 'updated' : 'created',
language: arrProfile.language.name,
language: arrProfile.language?.name ?? 'N/A',
cutoffFormatScore: arrProfile.cutoffFormatScore,
minFormatScore: arrProfile.minFormatScore,
formats: scoredFormats

View File

@@ -34,7 +34,7 @@ export interface ArrQualityProfile {
id?: number;
name: string;
items: ArrQualityItem[];
language: { id: number; name: string };
language?: { id: number; name: string }; // Radarr only, Sonarr ignores this
upgradeAllowed: boolean;
cutoff: number;
minFormatScore: number;
@@ -201,9 +201,11 @@ export function transformQualityProfile(
// Reverse items to match arr expected order
items.reverse();
// Build language config
const languageName = profile.language?.name ?? 'any';
const language = getLanguageForProfile(languageName, arrType);
// Build language config (Radarr only - Sonarr uses custom formats for language filtering)
const language =
arrType === 'radarr'
? getLanguageForProfile(profile.language?.name ?? 'any', arrType)
: undefined;
// Build format items
const formatItems: ArrFormatItem[] = [];
@@ -236,7 +238,7 @@ export function transformQualityProfile(
return {
name: profile.name,
items,
language,
...(language && { language }), // Only include for Radarr
upgradeAllowed: profile.upgradesAllowed,
cutoff: cutoffId ?? items[items.length - 1]?.quality?.id ?? 0,
minFormatScore: profile.minimumCustomFormatScore,

View File

@@ -607,7 +607,7 @@ export interface ArrQualityProfilePayload {
id?: number;
name: string;
items: ArrQualityProfileItem[];
language: ArrLanguage;
language?: ArrLanguage; // Radarr only - Sonarr uses custom formats for language filtering
upgradeAllowed: boolean;
cutoff: number;
minFormatScore: number;