1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-25 22:46:31 -04:00

New: Add Original Country information

Closes #5143
This commit is contained in:
Mark McDowall
2026-02-22 20:27:21 -08:00
parent 965b6144e3
commit da6340e421
27 changed files with 487 additions and 140 deletions
+2
View File
@@ -68,6 +68,7 @@ import {
faFolderOpen as fasFolderOpen,
faFolderTree as farFolderTree,
faForward as fasForward,
faGlobe as fasGlobe,
faHeart as fasHeart,
faHistory as fasHistory,
faHome as fasHome,
@@ -166,6 +167,7 @@ export const FOOTNOTE = fasAsterisk;
export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen;
export const GENRE = fasTheaterMasks;
export const GLOBE = fasGlobe;
export const GROUP = farObjectGroup;
export const HEALTH = fasMedkit;
export const HEART = fasHeart;
@@ -0,0 +1,262 @@
// Mapping from ISO 3166-1 alpha-3 (3-letter) to alpha-2 (2-letter) country codes
const alpha3ToAlpha2: Record<string, string> = {
AFG: 'AF',
ALB: 'AL',
DZA: 'DZ',
ASM: 'AS',
AND: 'AD',
AGO: 'AO',
AIA: 'AI',
ATA: 'AQ',
ATG: 'AG',
ARG: 'AR',
ARM: 'AM',
ABW: 'AW',
AUS: 'AU',
AUT: 'AT',
AZE: 'AZ',
BHS: 'BS',
BHR: 'BH',
BGD: 'BD',
BRB: 'BB',
BLR: 'BY',
BEL: 'BE',
BLZ: 'BZ',
BEN: 'BJ',
BMU: 'BM',
BTN: 'BT',
BOL: 'BO',
BES: 'BQ',
BIH: 'BA',
BWA: 'BW',
BVT: 'BV',
BRA: 'BR',
IOT: 'IO',
BRN: 'BN',
BGR: 'BG',
BFA: 'BF',
BDI: 'BI',
CPV: 'CV',
KHM: 'KH',
CMR: 'CM',
CAN: 'CA',
CYM: 'KY',
CAF: 'CF',
TCD: 'TD',
CHL: 'CL',
CHN: 'CN',
CXR: 'CX',
CCK: 'CC',
COL: 'CO',
COM: 'KM',
COG: 'CG',
COD: 'CD',
COK: 'CK',
CRI: 'CR',
CIV: 'CI',
HRV: 'HR',
CUB: 'CU',
CUW: 'CW',
CYP: 'CY',
CZE: 'CZ',
DNK: 'DK',
DJI: 'DJ',
DMA: 'DM',
DOM: 'DO',
ECU: 'EC',
EGY: 'EG',
SLV: 'SV',
GNQ: 'GQ',
ERI: 'ER',
EST: 'EE',
SWZ: 'SZ',
ETH: 'ET',
FLK: 'FK',
FRO: 'FO',
FJI: 'FJ',
FIN: 'FI',
FRA: 'FR',
GUF: 'GF',
PYF: 'PF',
ATF: 'TF',
GAB: 'GA',
GMB: 'GM',
GEO: 'GE',
DEU: 'DE',
GHA: 'GH',
GIB: 'GI',
GRC: 'GR',
GRL: 'GL',
GRD: 'GD',
GLP: 'GP',
GUM: 'GU',
GTM: 'GT',
GGY: 'GG',
GIN: 'GN',
GNB: 'GW',
GUY: 'GY',
HTI: 'HT',
HMD: 'HM',
VAT: 'VA',
HND: 'HN',
HKG: 'HK',
HUN: 'HU',
ISL: 'IS',
IND: 'IN',
IDN: 'ID',
IRN: 'IR',
IRQ: 'IQ',
IRL: 'IE',
IMN: 'IM',
ISR: 'IL',
ITA: 'IT',
JAM: 'JM',
JPN: 'JP',
JEY: 'JE',
JOR: 'JO',
KAZ: 'KZ',
KEN: 'KE',
KIR: 'KI',
PRK: 'KP',
KOR: 'KR',
KWT: 'KW',
KGZ: 'KG',
LAO: 'LA',
LVA: 'LV',
LBN: 'LB',
LSO: 'LS',
LBR: 'LR',
LBY: 'LY',
LIE: 'LI',
LTU: 'LT',
LUX: 'LU',
MAC: 'MO',
MDG: 'MG',
MWI: 'MW',
MYS: 'MY',
MDV: 'MV',
MLI: 'ML',
MLT: 'MT',
MHL: 'MH',
MTQ: 'MQ',
MRT: 'MR',
MUS: 'MU',
MYT: 'YT',
MEX: 'MX',
FSM: 'FM',
MDA: 'MD',
MCO: 'MC',
MNG: 'MN',
MNE: 'ME',
MSR: 'MS',
MAR: 'MA',
MOZ: 'MZ',
MMR: 'MM',
NAM: 'NA',
NRU: 'NR',
NPL: 'NP',
NLD: 'NL',
NCL: 'NC',
NZL: 'NZ',
NIC: 'NI',
NER: 'NE',
NGA: 'NG',
NIU: 'NU',
NFK: 'NF',
MKD: 'MK',
MNP: 'MP',
NOR: 'NO',
OMN: 'OM',
PAK: 'PK',
PLW: 'PW',
PSE: 'PS',
PAN: 'PA',
PNG: 'PG',
PRY: 'PY',
PER: 'PE',
PHL: 'PH',
PCN: 'PN',
POL: 'PL',
PRT: 'PT',
PRI: 'PR',
QAT: 'QA',
REU: 'RE',
ROU: 'RO',
RUS: 'RU',
RWA: 'RW',
BLM: 'BL',
SHN: 'SH',
KNA: 'KN',
LCA: 'LC',
MAF: 'MF',
SPM: 'PM',
VCT: 'VC',
WSM: 'WS',
SMR: 'SM',
STP: 'ST',
SAU: 'SA',
SEN: 'SN',
SRB: 'RS',
SYC: 'SC',
SLE: 'SL',
SGP: 'SG',
SXM: 'SX',
SVK: 'SK',
SVN: 'SI',
SLB: 'SB',
SOM: 'SO',
ZAF: 'ZA',
SGS: 'GS',
SSD: 'SS',
ESP: 'ES',
LKA: 'LK',
SDN: 'SD',
SUR: 'SR',
SJM: 'SJ',
SWE: 'SE',
CHE: 'CH',
SYR: 'SY',
TWN: 'TW',
TJK: 'TJ',
TZA: 'TZ',
THA: 'TH',
TLS: 'TL',
TGO: 'TG',
TKL: 'TK',
TON: 'TO',
TTO: 'TT',
TUN: 'TN',
TUR: 'TR',
TKM: 'TM',
TCA: 'TC',
TUV: 'TV',
UGA: 'UG',
UKR: 'UA',
ARE: 'AE',
GBR: 'GB',
USA: 'US',
UMI: 'UM',
URY: 'UY',
UZB: 'UZ',
VUT: 'VU',
VEN: 'VE',
VNM: 'VN',
VGB: 'VG',
VIR: 'VI',
WLF: 'WF',
ESH: 'EH',
YEM: 'YE',
ZMB: 'ZM',
ZWE: 'ZW',
};
const getCountryCode = (countryCode: string) => {
const normalizedCode =
countryCode.length === 3
? alpha3ToAlpha2[countryCode.toUpperCase()]
: countryCode;
return normalizedCode;
};
export default getCountryCode;
@@ -0,0 +1,32 @@
import { useMemo } from 'react';
import { useLanguage } from 'Language/useLanguageName';
import getCountryCode from './getCountryCode';
const useCountryName = (countryCode: string | undefined) => {
const { data } = useLanguage();
return useMemo(() => {
if (!countryCode) {
return '';
}
const locale = data?.identifier ?? 'en';
const getDisplayName = Intl.DisplayNames
? new Intl.DisplayNames([locale], { type: 'region', fallback: 'code' })
: null;
if (!getDisplayName) {
return countryCode;
}
try {
return getDisplayName.of(getCountryCode(countryCode)) ?? countryCode;
} catch (e) {
console.warn('Error getting country name for code:', countryCode, e);
return countryCode;
}
}, [countryCode, data]);
};
export default useCountryName;
+1 -1
View File
@@ -12,7 +12,7 @@ function getDisplayName(code: string) {
: null;
}
const useLanguage = () => {
export const useLanguage = () => {
return useApiQuery<LanguageResponse>({
path: '/localization/language',
queryOptions: {
@@ -133,6 +133,7 @@
.path,
.sizeOnDisk,
.qualityProfileName,
.originalCountry,
.originalLanguageName,
.statusName,
.network,
+1
View File
@@ -16,6 +16,7 @@ interface CssExports {
'links': string;
'monitorToggleButton': string;
'network': string;
'originalCountry': string;
'originalLanguageName': string;
'overview': string;
'path': string;
@@ -30,6 +30,7 @@ import {
tooltipPositions,
} from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import useCountryName from 'Internationalization/useCountryName';
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModal from 'Series/Edit/EditSeriesModal';
@@ -348,6 +349,8 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
refetchEpisodeFiles();
}, [refetchEpisodes, refetchEpisodeFiles]);
const originalCountryName = useCountryName(series?.originalCountry);
useEffect(() => {
populate();
}, [populate]);
@@ -694,6 +697,21 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
</Label>
) : null}
{originalCountryName ? (
<Label
className={styles.detailsLabel}
title={translate('OriginalCountry')}
size={sizes.LARGE}
>
<div>
<Icon name={icons.GLOBE} size={17} />
<span className={styles.originalCountry}>
{originalCountryName}
</span>
</div>
</Label>
) : null}
{network ? (
<Label
className={styles.detailsLabel}
@@ -46,6 +46,15 @@ function SeriesIndexSortMenu(props: SeriesIndexSortMenuProps) {
{translate('Network')}
</SortMenuItem>
<SortMenuItem
name="originalCountry"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('OriginalCountry')}
</SortMenuItem>
<SortMenuItem
name="originalLanguage"
sortKey={sortKey}
@@ -103,6 +103,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
status,
path,
titleSlug,
originalCountry,
originalLanguage,
network,
nextAiring,
@@ -256,6 +257,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
) : null}
<SeriesIndexPosterInfo
originalCountry={originalCountry}
originalLanguage={originalLanguage}
network={network}
previousAiring={previousAiring}
@@ -1,6 +1,7 @@
import React from 'react';
import HeartRating from 'Components/HeartRating';
import SeriesTagList from 'Components/SeriesTagList';
import useCountryName from 'Internationalization/useCountryName';
import Language from 'Language/Language';
import { Ratings } from 'Series/Series';
import { QualityProfileModel } from 'Settings/Profiles/Quality/useQualityProfiles';
@@ -11,6 +12,7 @@ import translate from 'Utilities/String/translate';
import styles from './SeriesIndexPosterInfo.css';
interface SeriesIndexPosterInfoProps {
originalCountry?: string;
originalLanguage?: Language;
network?: string;
showQualityProfile: boolean;
@@ -32,6 +34,7 @@ interface SeriesIndexPosterInfoProps {
function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
const {
originalCountry,
originalLanguage,
network,
qualityProfile,
@@ -51,6 +54,8 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
showTags,
} = props;
const originalCountryName = useCountryName(originalCountry);
if (sortKey === 'network' && network) {
return (
<div className={styles.info} title={translate('Network')}>
@@ -59,6 +64,14 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
);
}
if (sortKey === 'originalCountry' && !!originalCountryName) {
return (
<div className={styles.info} title={translate('OriginalCountry')}>
{originalCountryName}
</div>
);
}
if (sortKey === 'originalLanguage' && !!originalLanguage?.name) {
return (
<div className={styles.info} title={translate('OriginalLanguage')}>
@@ -66,6 +66,7 @@
flex: 2 0 90px;
}
.originalCountry,
.originalLanguage,
.qualityProfileId {
composes: cell;
@@ -17,6 +17,7 @@ interface CssExports {
'monitorNewItems': string;
'network': string;
'nextAiring': string;
'originalCountry': string;
'originalLanguage': string;
'overlayTitle': string;
'path': string;
@@ -14,6 +14,7 @@ import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
import { icons } from 'Helpers/Props';
import useCountryName from 'Internationalization/useCountryName';
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModal from 'Series/Edit/EditSeriesModal';
import { Statistics } from 'Series/Series';
@@ -56,6 +57,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
const [isEditSeriesModalOpen, setIsEditSeriesModalOpen] = useState(false);
const [isDeleteSeriesModalOpen, setIsDeleteSeriesModalOpen] = useState(false);
const { getIsSelected, toggleSelected } = useSelect();
const originalCountryName = useCountryName(series?.originalCountry);
const onRefreshPress = useCallback(() => {
executeCommand({
@@ -230,6 +232,14 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
);
}
if (name === 'originalCountry') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{originalCountryName}
</VirtualTableRowCell>
);
}
if (name === 'originalLanguage') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
@@ -30,6 +30,7 @@
flex: 2 0 90px;
}
.originalCountry,
.originalLanguage,
.qualityProfileId {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
@@ -13,6 +13,7 @@ interface CssExports {
'monitorNewItems': string;
'network': string;
'nextAiring': string;
'originalCountry': string;
'originalLanguage': string;
'path': string;
'previousAiring': string;
+1
View File
@@ -78,6 +78,7 @@ interface Series extends ModelBase {
monitored: boolean;
monitorNewItems: MonitorNewItems;
network: string;
originalCountry: string;
originalLanguage: Language;
overview: string;
path: string;
@@ -125,6 +125,12 @@ const { useOptions, useOption, setOptions, setOption, setSort, getOptions } =
isSortable: true,
isVisible: false,
},
{
name: 'originalCountry',
label: () => translate('OriginalCountry'),
isSortable: true,
isVisible: false,
},
{
name: 'originalLanguage',
label: () => translate('OriginalLanguage'),