mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-25 22:46:31 -04:00
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user