mirror of
https://github.com/agregarr/agregarr.git
synced 2026-01-25 03:48:48 -06:00
fix: adds global networks
This commit is contained in:
@@ -91,13 +91,13 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
*/
|
||||
public async getPlatformTop10(
|
||||
platform: string,
|
||||
region = 'world',
|
||||
region = 'global',
|
||||
requestedMediaType?: 'movie' | 'tv' | 'both'
|
||||
): Promise<FlixPatrolPlatformData> {
|
||||
try {
|
||||
// Construct URL based on region
|
||||
let url: string;
|
||||
if (region === 'world' || region === 'global') {
|
||||
if (region === 'global') {
|
||||
url = '/top10';
|
||||
} else {
|
||||
// Use current date for streaming overview
|
||||
@@ -258,8 +258,8 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
public async getAvailablePlatformsForCountry(
|
||||
country: string
|
||||
): Promise<FlixPatrolPlatformOption[]> {
|
||||
// For global/world, return our static list
|
||||
if (country === 'world' || country === 'global') {
|
||||
// For global, return our static list
|
||||
if (country === 'global') {
|
||||
return this.getGlobalPlatformOptions();
|
||||
}
|
||||
|
||||
@@ -364,33 +364,85 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
let platformSection = null;
|
||||
for (const heading of headings) {
|
||||
const headingText = heading.textContent || '';
|
||||
|
||||
// Handle both formats:
|
||||
// Country-specific: "PLATFORM TOP 10" (e.g., "NETFLIX TOP 10")
|
||||
// Global: "TOP Movies on PLATFORM" (e.g., "TOP Movies on Netflix")
|
||||
let actualPlatformName = null;
|
||||
|
||||
if (headingText.toLowerCase().includes('top 10')) {
|
||||
// Extract the actual platform name from the heading for comparison
|
||||
// Country-specific format: "PLATFORM TOP 10"
|
||||
const match = headingText.match(/^(.+?)\s+TOP 10/i);
|
||||
if (match) {
|
||||
const actualPlatformName = match[1].trim();
|
||||
const normalizedActual = actualPlatformName
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/\+/g, '')
|
||||
.replace(/&/g, 'and')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
actualPlatformName = match[1].trim();
|
||||
}
|
||||
} else if (
|
||||
headingText.toLowerCase().includes('top movies on') ||
|
||||
headingText.toLowerCase().includes('top tv shows on')
|
||||
) {
|
||||
// Global format: Check if heading contains any of our mapped platform names
|
||||
const possibleNames = this.mapPlatformIdToFlixPatrolName(platformName);
|
||||
|
||||
// Compare normalized platform names
|
||||
if (normalizedActual === platformName.toLowerCase()) {
|
||||
logger.debug(`Found platform section: ${headingText}`, {
|
||||
label: 'FlixPatrol API',
|
||||
platform,
|
||||
headingText,
|
||||
actualPlatformName,
|
||||
normalizedActual,
|
||||
platformName,
|
||||
});
|
||||
platformSection = heading;
|
||||
// Instead of parsing, just check if the heading contains our platform names
|
||||
for (const possibleName of possibleNames) {
|
||||
if (headingText.toLowerCase().includes(possibleName.toLowerCase())) {
|
||||
actualPlatformName = possibleName; // Use the mapped name directly
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actualPlatformName) {
|
||||
const normalizedActual = actualPlatformName
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/\+/g, '')
|
||||
.replace(/&/g, 'and')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
|
||||
let isMatch = false;
|
||||
|
||||
// For country-specific pages (with "TOP 10"), use the original logic
|
||||
if (headingText.toLowerCase().includes('top 10')) {
|
||||
const normalizedPlatform = platformName
|
||||
.toLowerCase()
|
||||
.replace(/_/g, '-'); // Keep the original underscore-to-dash conversion
|
||||
isMatch = normalizedActual === normalizedPlatform;
|
||||
} else {
|
||||
// For global pages, use the new mapping logic
|
||||
const possibleNames =
|
||||
this.mapPlatformIdToFlixPatrolName(platformName);
|
||||
isMatch = possibleNames.some((name) => {
|
||||
const normalizedName = name
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/\+/g, '')
|
||||
.replace(/&/g, 'and')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
return normalizedActual === normalizedName;
|
||||
});
|
||||
}
|
||||
|
||||
// Compare normalized platform names
|
||||
if (isMatch) {
|
||||
logger.debug(`Found platform section: ${headingText}`, {
|
||||
label: 'FlixPatrol API',
|
||||
platform,
|
||||
headingText,
|
||||
actualPlatformName,
|
||||
normalizedActual,
|
||||
platformName,
|
||||
format: headingText.toLowerCase().includes('top 10')
|
||||
? 'country'
|
||||
: 'global',
|
||||
matchingMethod: headingText.toLowerCase().includes('top 10')
|
||||
? 'original'
|
||||
: 'mapping',
|
||||
});
|
||||
platformSection = heading;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!platformSection) {
|
||||
@@ -403,6 +455,11 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
return result;
|
||||
}
|
||||
|
||||
// For global pages, use the simpler card-table parsing
|
||||
if (region === 'global') {
|
||||
return this.parseGlobalPlatformData(platformSection, result, platform);
|
||||
}
|
||||
|
||||
// Extract platform logo information from the platform section
|
||||
const platformLogo = await this.extractPlatformLogo(platformSection);
|
||||
if (platformLogo) {
|
||||
@@ -799,7 +856,13 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
const platformName = this.extractPlatformNameFromSubtype(platform);
|
||||
return platformName
|
||||
.split(/[-_]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.map((word) => {
|
||||
// Special case for TV to maintain proper capitalization
|
||||
if (word.toLowerCase() === 'tv') {
|
||||
return 'TV';
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
@@ -811,6 +874,27 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
return platform.replace(/_top_10$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Map our platform IDs to FlixPatrol HTML platform names
|
||||
*/
|
||||
private mapPlatformIdToFlixPatrolName(platformId: string): string[] {
|
||||
// Only platforms actually found in FlixPatrol /top10 page test data
|
||||
const mappings: { [key: string]: string[] } = {
|
||||
netflix: ['Netflix'],
|
||||
hbo: ['HBO'],
|
||||
disney: ['Disney+'], // "TOP Movies on Disney+ on September 6, 2025"
|
||||
amazon_prime: ['Amazon Prime'], // "TOP Movies on Amazon Prime on September 6, 2025"
|
||||
'amazon-prime': ['Amazon Prime'],
|
||||
apple_tv: ['Apple'], // "TOP Movies on Apple on September 6, 2025"
|
||||
'apple-tv': ['Apple'],
|
||||
paramount: ['Paramount+'], // "TOP TV Shows on Paramount+ on September 6, 2025"
|
||||
amazon: ['Amazon'], // "TOP Movies on Amazon on September 6, 2025" (different from Prime)
|
||||
};
|
||||
|
||||
const normalized = platformId.toLowerCase().replace(/_/g, '-');
|
||||
return mappings[normalized] || [platformId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify what type of content each table contains by analyzing the text structure
|
||||
*/
|
||||
@@ -1059,10 +1143,8 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
}
|
||||
});
|
||||
|
||||
// Always include 'world' as the global option
|
||||
countries.add('world');
|
||||
|
||||
const result = Array.from(countries).sort();
|
||||
// Always include 'global' as the global option at the top
|
||||
const result = ['global', ...Array.from(countries).sort()];
|
||||
|
||||
logger.debug(`Total unique countries found: ${result.length}`, {
|
||||
label: 'FlixPatrol API',
|
||||
@@ -1333,6 +1415,196 @@ class FlixPatrolAPI extends ExternalAPI {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse global platform data using card-table structure
|
||||
* Simple method focused only on global pages to avoid breaking country logic
|
||||
*/
|
||||
private parseGlobalPlatformData(
|
||||
platformSection: Element,
|
||||
result: FlixPatrolPlatformData,
|
||||
platform: string
|
||||
): FlixPatrolPlatformData {
|
||||
logger.debug(`Parsing global platform data for ${platform}`, {
|
||||
label: 'FlixPatrol API',
|
||||
platform,
|
||||
});
|
||||
|
||||
// The card tables exist in the document, but not directly after headings
|
||||
// Search the entire document for card-table elements and associate them with platforms
|
||||
const document = platformSection.ownerDocument;
|
||||
const allCardTables = document?.querySelectorAll('table.card-table') || [];
|
||||
|
||||
logger.debug(
|
||||
`Found ${allCardTables.length} total card tables in document`,
|
||||
{
|
||||
label: 'FlixPatrol API',
|
||||
platform,
|
||||
}
|
||||
);
|
||||
|
||||
// Find all global platform headings (exclude country breakdown)
|
||||
const allHeadings = Array.from(document?.querySelectorAll('h2') || []);
|
||||
const globalPlatformHeadings = allHeadings.filter((h) => {
|
||||
const text = h.textContent?.toLowerCase() || '';
|
||||
return (
|
||||
(text.includes('top movies on') || text.includes('top tv shows on')) &&
|
||||
!text.includes('by country')
|
||||
);
|
||||
});
|
||||
|
||||
// Group headings by platform (Movies + TV pairs)
|
||||
const platformGroups: { movies: Element | null; tv: Element | null }[] = [];
|
||||
const platforms: string[] = [];
|
||||
|
||||
globalPlatformHeadings.forEach((heading) => {
|
||||
const text = heading.textContent?.toLowerCase() || '';
|
||||
// Extract platform name from heading like "TOP Movies on Netflix on September 6, 2025"
|
||||
const platformMatch = text.match(
|
||||
/top (?:movies|tv shows) on (.+?) on \w+/
|
||||
);
|
||||
if (platformMatch) {
|
||||
const platformName = platformMatch[1].trim();
|
||||
let platformGroup = platformGroups.find(
|
||||
(_, index) => platforms[index] === platformName
|
||||
);
|
||||
|
||||
if (!platformGroup) {
|
||||
platforms.push(platformName);
|
||||
platformGroup = { movies: null, tv: null };
|
||||
platformGroups.push(platformGroup);
|
||||
}
|
||||
|
||||
if (text.includes('movies')) {
|
||||
platformGroup.movies = heading;
|
||||
} else if (text.includes('tv shows')) {
|
||||
platformGroup.tv = heading;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find which platform group our section belongs to
|
||||
const currentPlatformGroupIndex = platformGroups.findIndex(
|
||||
(group) =>
|
||||
group.movies === platformSection || group.tv === platformSection
|
||||
);
|
||||
|
||||
if (currentPlatformGroupIndex >= 0) {
|
||||
// Each platform gets 2 sequential tables from the global card-table list
|
||||
// Filter out country breakdown tables (they have many rows, typically >50)
|
||||
const globalCardTables = Array.from(allCardTables).filter((table) => {
|
||||
const rows = table.querySelectorAll('tr');
|
||||
return rows.length <= 20; // Global platform tables have ~10 rows each
|
||||
});
|
||||
|
||||
const startTableIndex = currentPlatformGroupIndex * 2;
|
||||
const endTableIndex = startTableIndex + 2;
|
||||
|
||||
logger.debug(
|
||||
`Platform ${platform} should use tables ${startTableIndex}-${
|
||||
endTableIndex - 1
|
||||
}`,
|
||||
{
|
||||
label: 'FlixPatrol API',
|
||||
platform,
|
||||
currentPlatformGroupIndex,
|
||||
totalPlatformGroups: platformGroups.length,
|
||||
globalCardTablesCount: globalCardTables.length,
|
||||
platformName: platforms[currentPlatformGroupIndex],
|
||||
}
|
||||
);
|
||||
|
||||
for (
|
||||
let i = startTableIndex;
|
||||
i < endTableIndex && i < globalCardTables.length;
|
||||
i++
|
||||
) {
|
||||
const table = globalCardTables[i];
|
||||
const items = this.parseCardTable(table);
|
||||
|
||||
// For global pages, each platform typically has 2 tables: Movies then TV Shows
|
||||
// Determine the content type based on table position within the platform's tables
|
||||
const tablePositionInPlatform = i - startTableIndex;
|
||||
const isMovieTable = tablePositionInPlatform % 2 === 0; // Even indices = Movies, Odd = TV
|
||||
|
||||
if (isMovieTable) {
|
||||
result.movies.push(
|
||||
...items.map((item) => ({ ...item, type: 'movie' as const }))
|
||||
);
|
||||
} else {
|
||||
result.tvShows.push(
|
||||
...items.map((item) => ({ ...item, type: 'tv' as const }))
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`Processed table ${i} for ${platform}`, {
|
||||
label: 'FlixPatrol API',
|
||||
platform,
|
||||
tableIndex: i,
|
||||
tablePositionInPlatform,
|
||||
isMovieTable,
|
||||
itemsCount: items.length,
|
||||
contentType: isMovieTable ? 'movies' : 'tv',
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(`Parsed platform data for ${platform}`, {
|
||||
label: 'FlixPatrol API',
|
||||
platform,
|
||||
movieCount: result.movies.length,
|
||||
tvCount: result.tvShows.length,
|
||||
tablesUsed: `${startTableIndex}-${endTableIndex - 1}`,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single card-table element to extract ranking items
|
||||
*/
|
||||
private parseCardTable(table: Element): FlixPatrolListItem[] {
|
||||
const items: FlixPatrolListItem[] = [];
|
||||
const rows = table.querySelectorAll('tr');
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
|
||||
if (cells.length >= 3) {
|
||||
const rankText = cells[0].textContent?.trim() || '';
|
||||
const titleElement = cells[1].querySelector('a');
|
||||
const pointsText = cells[2].textContent?.trim() || '';
|
||||
|
||||
// Extract rank number
|
||||
const rankMatch = rankText.match(/(\d+)/);
|
||||
const rank = rankMatch ? parseInt(rankMatch[1], 10) : index + 1;
|
||||
|
||||
// Extract title
|
||||
const title =
|
||||
titleElement?.textContent?.trim() ||
|
||||
cells[1].textContent?.trim() ||
|
||||
'';
|
||||
|
||||
// Extract FlixPatrol URL
|
||||
const flixpatrolUrl = titleElement?.getAttribute('href') || undefined;
|
||||
|
||||
// Extract points
|
||||
const points = pointsText;
|
||||
|
||||
if (title) {
|
||||
items.push({
|
||||
rank,
|
||||
title,
|
||||
points,
|
||||
flixpatrolUrl,
|
||||
type: 'movie', // Default - will be determined by context or backend
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
export default FlixPatrolAPI;
|
||||
|
||||
7
server/lib/collections/external/networks.ts
vendored
7
server/lib/collections/external/networks.ts
vendored
@@ -837,7 +837,12 @@ export class NetworksCollectionSync extends BaseCollectionSync {
|
||||
* Extract clean platform name from subtype for branding
|
||||
*/
|
||||
private extractPlatformNameFromSubtype(subtype: string): string {
|
||||
return subtype.replace(/_top_10$/, '');
|
||||
// Remove "_top_10" suffix and normalize to match poster generation system
|
||||
const platformName = subtype.replace(/_top_10$/, '');
|
||||
|
||||
// Convert underscores to hyphens for poster generation compatibility
|
||||
// This ensures platform names match the SERVICE_LOGO_MAP in posterGeneration.ts
|
||||
return platformName.replace(/_/g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -644,7 +644,15 @@ export class TemplateEngine {
|
||||
return platform
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
.split(' ')
|
||||
.map((word) => {
|
||||
// Special case for TV to maintain proper capitalization
|
||||
if (word.toLowerCase() === 'tv') {
|
||||
return 'TV';
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -744,6 +744,8 @@ collectionsRoutes.post('/create', isAuthenticated(), async (req, res) => {
|
||||
const settings = getSettings();
|
||||
const { IdGenerator } = await import('@server/utils/idGenerator');
|
||||
|
||||
// Cache warming removed - caused double requests and rate limiting issues
|
||||
|
||||
// Extract libraryIds from request - support both single libraryId and multiple libraryIds
|
||||
const libraryIds = req.body.libraryIds
|
||||
? Array.isArray(req.body.libraryIds)
|
||||
|
||||
@@ -32,12 +32,16 @@ interface NetworksConfigSectionProps {
|
||||
errors: FormikErrors<CollectionFormConfig>;
|
||||
touched: FormikTouched<CollectionFormConfig>;
|
||||
isVisible?: boolean;
|
||||
getTemplatePresets?: (
|
||||
values: CollectionFormConfig
|
||||
) => { label: string; value: string }[];
|
||||
}
|
||||
|
||||
const NetworksConfigSection = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
isVisible = true,
|
||||
getTemplatePresets,
|
||||
}: NetworksConfigSectionProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -90,19 +94,33 @@ const NetworksConfigSection = ({
|
||||
setFieldValue('subtype', '');
|
||||
}
|
||||
}}
|
||||
disabled={isLoadingCountries}
|
||||
disabled={false}
|
||||
>
|
||||
<option value="">
|
||||
{isLoadingCountries
|
||||
? intl.formatMessage(messages.loadingCountries)
|
||||
: intl.formatMessage(messages.selectCountry)}
|
||||
<option value="">{intl.formatMessage(messages.selectCountry)}</option>
|
||||
|
||||
{/* Global option - always available */}
|
||||
<option value="global">Global</option>
|
||||
|
||||
{/* Separator */}
|
||||
<option disabled style={{ borderTop: '1px solid #4a5568' }}>
|
||||
────────────────
|
||||
</option>
|
||||
{Array.isArray(countries) &&
|
||||
countries.map((country) => (
|
||||
<option key={country.value} value={country.value}>
|
||||
{country.label}
|
||||
</option>
|
||||
))}
|
||||
|
||||
{/* Loading state or countries */}
|
||||
{isLoadingCountries ? (
|
||||
<option disabled>
|
||||
{intl.formatMessage(messages.loadingCountries)}
|
||||
</option>
|
||||
) : (
|
||||
Array.isArray(countries) &&
|
||||
countries
|
||||
.filter((country) => country.value !== 'global') // Exclude global since it's shown above
|
||||
.map((country) => (
|
||||
<option key={country.value} value={country.value}>
|
||||
{country.label}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</Field>
|
||||
{countriesError && (
|
||||
<p className="mt-1 text-xs text-red-400">
|
||||
@@ -127,6 +145,21 @@ const NetworksConfigSection = ({
|
||||
name="subtype"
|
||||
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
disabled={isLoadingPlatforms}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newPlatform = e.target.value;
|
||||
setFieldValue('subtype', newPlatform);
|
||||
|
||||
// Auto-select first template option when platform is selected (same as other collection types)
|
||||
if (newPlatform && getTemplatePresets) {
|
||||
setTimeout(() => {
|
||||
const tempValues = { ...values, subtype: newPlatform };
|
||||
const presets = getTemplatePresets(tempValues);
|
||||
if (presets.length > 0) {
|
||||
setFieldValue('template', presets[0].value);
|
||||
}
|
||||
}, 100); // Same delay as other collection types
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">
|
||||
{isLoadingPlatforms
|
||||
|
||||
@@ -133,6 +133,7 @@ const TemplateSection = ({
|
||||
fetchedTitles,
|
||||
detectedMediaTypes
|
||||
);
|
||||
|
||||
return templatePresets.map((preset) => (
|
||||
<option key={preset.value} value={preset.value}>
|
||||
{preset.label}
|
||||
|
||||
@@ -1271,6 +1271,67 @@ const CollectionFormConfigForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Networks collection presets
|
||||
if (values.type === 'networks') {
|
||||
if (values.subtype) {
|
||||
// Get platform name from subtype for display
|
||||
// Handle cases like "netflix_top_10" -> "Netflix"
|
||||
// and "disney-plus" -> "Disney Plus"
|
||||
const platformName = values.subtype
|
||||
.split('_')[0] // Take first part before underscore (removes "_top_10" etc)
|
||||
.split('-') // Split on dashes
|
||||
.map((word) => {
|
||||
// Special case for TV to maintain proper capitalization
|
||||
if (word.toLowerCase() === 'tv') {
|
||||
return 'TV';
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return [
|
||||
{
|
||||
label: `Top 10 {mediaType}s on ${platformName}`,
|
||||
value: `Top 10 {mediaType}s on ${platformName}`,
|
||||
},
|
||||
{
|
||||
label: `Popular on ${platformName}`,
|
||||
value: `Popular on ${platformName}`,
|
||||
},
|
||||
{
|
||||
label: `${platformName} Top 10 {mediaType}s`,
|
||||
value: `${platformName} Top 10 {mediaType}s`,
|
||||
},
|
||||
{
|
||||
label: `${platformName} Top {mediaType}s`,
|
||||
value: `${platformName} Top {mediaType}s`,
|
||||
},
|
||||
{
|
||||
label: `Top {mediaType}s on ${platformName}`,
|
||||
value: `Top {mediaType}s on ${platformName}`,
|
||||
},
|
||||
{
|
||||
label: `${platformName} Trending {mediaType}s`,
|
||||
value: `${platformName} Trending {mediaType}s`,
|
||||
},
|
||||
{
|
||||
label: `Best of ${platformName}`,
|
||||
value: `Best of ${platformName}`,
|
||||
},
|
||||
{ label: 'Custom', value: 'custom' },
|
||||
];
|
||||
} else {
|
||||
// No platform selected yet
|
||||
return [
|
||||
{
|
||||
label: 'Select a Platform First',
|
||||
value: 'select-platform',
|
||||
},
|
||||
{ label: 'Custom', value: 'custom' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for unknown types
|
||||
return [
|
||||
{
|
||||
@@ -1610,6 +1671,7 @@ const CollectionFormConfigForm = ({
|
||||
errors={errors as FormikErrors<CollectionFormConfig>}
|
||||
touched={touched as FormikTouched<CollectionFormConfig>}
|
||||
isVisible={true}
|
||||
getTemplatePresets={getTemplatePresets}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -891,6 +891,23 @@ const AllCollectionsView: React.FC = () => {
|
||||
default:
|
||||
return subtype;
|
||||
}
|
||||
case 'networks':
|
||||
// Format platform names like "netflix_top_10" -> "Netflix"
|
||||
// and "neon-tv" -> "Neon TV"
|
||||
return subtype
|
||||
.split('_')[0] // Take first part before underscore (removes "_top_10" etc)
|
||||
.split('-') // Split on dashes
|
||||
.map((word) => {
|
||||
// Special case for TV to maintain proper capitalization
|
||||
if (word.toLowerCase() === 'tv') {
|
||||
return 'TV';
|
||||
}
|
||||
return (
|
||||
word.charAt(0).toUpperCase() +
|
||||
word.slice(1)
|
||||
);
|
||||
})
|
||||
.join(' ');
|
||||
default:
|
||||
return subtype;
|
||||
}
|
||||
@@ -909,6 +926,8 @@ const AllCollectionsView: React.FC = () => {
|
||||
? 'Tautulli'
|
||||
: config.type === 'overseerr'
|
||||
? 'Overseerr'
|
||||
: config.type === 'networks'
|
||||
? 'Networks'
|
||||
: config.type || '';
|
||||
|
||||
const subtypeLabel = getSubtypeLabel(
|
||||
|
||||
@@ -562,6 +562,20 @@ const SortableItem = ({
|
||||
default:
|
||||
return subtype;
|
||||
}
|
||||
case 'networks':
|
||||
// Format platform names like "netflix_top_10" -> "Netflix"
|
||||
// and "neon-tv" -> "Neon TV"
|
||||
return subtype
|
||||
.split('_')[0] // Take first part before underscore (removes "_top_10" etc)
|
||||
.split('-') // Split on dashes
|
||||
.map((word) => {
|
||||
// Special case for TV to maintain proper capitalization
|
||||
if (word.toLowerCase() === 'tv') {
|
||||
return 'TV';
|
||||
}
|
||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
default:
|
||||
return subtype;
|
||||
}
|
||||
@@ -580,6 +594,8 @@ const SortableItem = ({
|
||||
? 'Tautulli'
|
||||
: collection.type === 'overseerr'
|
||||
? 'Overseerr'
|
||||
: collection.type === 'networks'
|
||||
? 'Networks'
|
||||
: collection.type || '';
|
||||
|
||||
const subtypeLabel = getSubtypeLabel(
|
||||
|
||||
Reference in New Issue
Block a user