From da6340e421cd432d88c2eac73850b0b6eda55471 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 22 Feb 2026 20:27:21 -0800 Subject: [PATCH] New: Add Original Country information Closes #5143 --- frontend/src/Helpers/Props/icons.ts | 2 + .../Internationalization/getCountryCode.ts | 262 ++++++++++++++++++ .../Internationalization/useCountryName.ts | 32 +++ frontend/src/Language/useLanguageName.ts | 2 +- frontend/src/Series/Details/SeriesDetails.css | 1 + .../src/Series/Details/SeriesDetails.css.d.ts | 1 + frontend/src/Series/Details/SeriesDetails.tsx | 18 ++ .../Index/Menus/SeriesIndexSortMenu.tsx | 9 + .../Index/Posters/SeriesIndexPoster.tsx | 2 + .../Index/Posters/SeriesIndexPosterInfo.tsx | 13 + .../src/Series/Index/Table/SeriesIndexRow.css | 1 + .../Index/Table/SeriesIndexRow.css.d.ts | 1 + .../src/Series/Index/Table/SeriesIndexRow.tsx | 10 + .../Index/Table/SeriesIndexTableHeader.css | 1 + .../Table/SeriesIndexTableHeader.css.d.ts | 1 + frontend/src/Series/Series.ts | 1 + frontend/src/Series/seriesOptionsStore.ts | 6 + .../OriginalCountrySpecification.cs | 47 ++++ .../Migration/227_original_country.cs | 15 + src/NzbDrone.Core/Localization/Core/en.json | 2 + .../SkyHook/Resource/ShowResource.cs | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 1 + .../CustomScript/CustomScript.cs | 191 ++++--------- .../Notifications/Webhook/WebhookSeries.cs | 2 + src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 1 + src/NzbDrone.Core/Tv/Series.cs | 2 +- src/Sonarr.Api.V5/Series/SeriesResource.cs | 2 + 27 files changed, 487 insertions(+), 140 deletions(-) create mode 100644 frontend/src/Internationalization/getCountryCode.ts create mode 100644 frontend/src/Internationalization/useCountryName.ts create mode 100644 src/NzbDrone.Core/AutoTagging/Specifications/OriginalCountrySpecification.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/227_original_country.cs diff --git a/frontend/src/Helpers/Props/icons.ts b/frontend/src/Helpers/Props/icons.ts index 4f0f24f73..7550e1dd4 100644 --- a/frontend/src/Helpers/Props/icons.ts +++ b/frontend/src/Helpers/Props/icons.ts @@ -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; diff --git a/frontend/src/Internationalization/getCountryCode.ts b/frontend/src/Internationalization/getCountryCode.ts new file mode 100644 index 000000000..e412b9996 --- /dev/null +++ b/frontend/src/Internationalization/getCountryCode.ts @@ -0,0 +1,262 @@ +// Mapping from ISO 3166-1 alpha-3 (3-letter) to alpha-2 (2-letter) country codes +const alpha3ToAlpha2: Record = { + 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; diff --git a/frontend/src/Internationalization/useCountryName.ts b/frontend/src/Internationalization/useCountryName.ts new file mode 100644 index 000000000..a5431a53a --- /dev/null +++ b/frontend/src/Internationalization/useCountryName.ts @@ -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; diff --git a/frontend/src/Language/useLanguageName.ts b/frontend/src/Language/useLanguageName.ts index 7e880384f..737d60c87 100644 --- a/frontend/src/Language/useLanguageName.ts +++ b/frontend/src/Language/useLanguageName.ts @@ -12,7 +12,7 @@ function getDisplayName(code: string) { : null; } -const useLanguage = () => { +export const useLanguage = () => { return useApiQuery({ path: '/localization/language', queryOptions: { diff --git a/frontend/src/Series/Details/SeriesDetails.css b/frontend/src/Series/Details/SeriesDetails.css index ce05386ba..5b747a376 100644 --- a/frontend/src/Series/Details/SeriesDetails.css +++ b/frontend/src/Series/Details/SeriesDetails.css @@ -133,6 +133,7 @@ .path, .sizeOnDisk, .qualityProfileName, +.originalCountry, .originalLanguageName, .statusName, .network, diff --git a/frontend/src/Series/Details/SeriesDetails.css.d.ts b/frontend/src/Series/Details/SeriesDetails.css.d.ts index ad8d08312..efb070edc 100644 --- a/frontend/src/Series/Details/SeriesDetails.css.d.ts +++ b/frontend/src/Series/Details/SeriesDetails.css.d.ts @@ -16,6 +16,7 @@ interface CssExports { 'links': string; 'monitorToggleButton': string; 'network': string; + 'originalCountry': string; 'originalLanguageName': string; 'overview': string; 'path': string; diff --git a/frontend/src/Series/Details/SeriesDetails.tsx b/frontend/src/Series/Details/SeriesDetails.tsx index efda5a65a..605710b5f 100644 --- a/frontend/src/Series/Details/SeriesDetails.tsx +++ b/frontend/src/Series/Details/SeriesDetails.tsx @@ -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) { ) : null} + {originalCountryName ? ( + + ) : null} + {network ? (