Integrate data source preference functionality in catalog service; add support for TMDB as a content source, enhance CatalogScreen and SettingsScreen to manage data source selection, and implement genre filtering improvements for better content discovery.

This commit is contained in:
Nayif Noushad 2025-04-22 13:43:23 +05:30
parent 12a18c057d
commit 869bedba72
9 changed files with 551 additions and 13 deletions

14
package-lock.json generated
View file

@ -15,6 +15,7 @@
"@react-native-community/blur": "^4.4.1",
"@react-native-community/slider": "^4.5.6",
"@react-native-masked-view/masked-view": "github:react-native-masked-view/masked-view",
"@react-native-picker/picker": "^2.11.0",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
@ -3346,6 +3347,19 @@
"react-native": ">=0.57"
}
},
"node_modules/@react-native-picker/picker": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.0.tgz",
"integrity": "sha512-QuZU6gbxmOID5zZgd/H90NgBnbJ3VV6qVzp6c7/dDrmWdX8S0X5YFYgDcQFjE3dRen9wB9FWnj2VVdPU64adSg==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",

View file

@ -16,6 +16,7 @@
"@react-native-community/blur": "^4.4.1",
"@react-native-community/slider": "^4.5.6",
"@react-native-masked-view/masked-view": "github:react-native-masked-view/masked-view",
"@react-native-picker/picker": "^2.11.0",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",

View file

@ -21,6 +21,7 @@ import { Image } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
import { logger } from '../utils/logger';
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
type CatalogScreenProps = {
route: RouteProp<RootStackParamList, 'Catalog'>;
@ -52,11 +53,22 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [error, setError] = useState<string | null>(null);
const [dataSource, setDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS);
const isDarkMode = true;
const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames();
const displayName = getCustomName(addonId || '', type || '', id || '', originalName || '');
// Add effect to get data source preference when component mounts
useEffect(() => {
const getDataSourcePreference = async () => {
const preference = await catalogService.getDataSourcePreference();
setDataSource(preference);
};
getDataSourcePreference();
}, []);
const loadItems = useCallback(async (pageNum: number, shouldRefresh: boolean = false) => {
try {
if (shouldRefresh) {
@ -67,6 +79,73 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
setError(null);
// Process the genre filter - ignore "All" and clean up the value
let effectiveGenreFilter = genreFilter;
if (effectiveGenreFilter === 'All') {
effectiveGenreFilter = undefined;
logger.log('Genre "All" detected, removing genre filter');
} else if (effectiveGenreFilter) {
// Clean up the genre filter
effectiveGenreFilter = effectiveGenreFilter.trim();
logger.log(`Using cleaned genre filter: "${effectiveGenreFilter}"`);
}
// Check if using TMDB as data source and not requesting a specific addon
if (dataSource === DataSource.TMDB && !addonId) {
logger.log('Using TMDB data source for CatalogScreen');
try {
const catalogs = await catalogService.getCatalogByType(type, effectiveGenreFilter);
if (catalogs && catalogs.length > 0) {
// Flatten all items from all catalogs
const allItems: StreamingContent[] = [];
catalogs.forEach(catalog => {
allItems.push(...catalog.items);
});
// Convert StreamingContent to Meta format
const metaItems: Meta[] = allItems.map(item => ({
id: item.id,
type: item.type,
name: item.name,
poster: item.poster,
background: item.banner,
logo: item.logo,
description: item.description,
releaseInfo: item.year?.toString() || '',
imdbRating: item.imdbRating,
year: item.year,
genres: item.genres || [],
runtime: item.runtime,
certification: item.certification,
}));
// Remove duplicates
const uniqueItems = metaItems.filter((item, index, self) =>
index === self.findIndex((t) => t.id === item.id)
);
setItems(uniqueItems);
setHasMore(false); // TMDB already returns a full set
setLoading(false);
setRefreshing(false);
return;
} else {
setError("No content found for the selected filters");
setItems([]);
setLoading(false);
setRefreshing(false);
return;
}
} catch (error) {
logger.error('Failed to get TMDB catalog:', error);
setError('Failed to load content from TMDB');
setItems([]);
setLoading(false);
setRefreshing(false);
return;
}
}
// Use this flag to track if we found and processed any items
let foundItems = false;
let allItems: Meta[] = [];
@ -83,7 +162,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
}
// Create filters array for genre filtering if provided
const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : [];
const filters = effectiveGenreFilter ? [{ title: 'genre', value: effectiveGenreFilter }] : [];
// Load items from the catalog
const newItems = await stremioService.getCatalog(addon, type, id, pageNum, filters);
@ -99,12 +178,15 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
} else {
setItems(prev => [...prev, ...newItems]);
}
} else if (genreFilter) {
} else if (effectiveGenreFilter) {
// Get all addons that have catalogs of the specified type
const typeManifests = manifests.filter(manifest =>
manifest.catalogs && manifest.catalogs.some(catalog => catalog.type === type)
);
// Add debug logging for genre filter
logger.log(`Using genre filter: "${effectiveGenreFilter}" for type: ${type}`);
// For each addon, try to get content with the genre filter
for (const manifest of typeManifests) {
try {
@ -114,12 +196,46 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
// For each catalog, try to get content
for (const catalog of typeCatalogs) {
try {
const filters = [{ title: 'genre', value: genreFilter }];
const filters = [{ title: 'genre', value: effectiveGenreFilter }];
// Debug logging for each catalog request
logger.log(`Requesting from ${manifest.name}, catalog ${catalog.id} with genre "${effectiveGenreFilter}"`);
const catalogItems = await stremioService.getCatalog(manifest, type, catalog.id, pageNum, filters);
if (catalogItems && catalogItems.length > 0) {
allItems = [...allItems, ...catalogItems];
foundItems = true;
// Log first few items' genres to debug
const sampleItems = catalogItems.slice(0, 3);
sampleItems.forEach(item => {
logger.log(`Item "${item.name}" has genres: ${JSON.stringify(item.genres)}`);
});
// Filter items client-side to ensure they contain the requested genre
// Some addons might not properly filter by genre on the server
let filteredItems = catalogItems;
if (effectiveGenreFilter) {
const normalizedGenreFilter = effectiveGenreFilter.toLowerCase().trim();
filteredItems = catalogItems.filter(item => {
// Skip items without genres
if (!item.genres || !Array.isArray(item.genres)) {
return false;
}
// Check for genre match (exact or substring)
return item.genres.some(genre => {
const normalizedGenre = genre.toLowerCase().trim();
return normalizedGenre === normalizedGenreFilter ||
normalizedGenre.includes(normalizedGenreFilter) ||
normalizedGenreFilter.includes(normalizedGenre);
});
});
logger.log(`Filtered ${catalogItems.length} items to ${filteredItems.length} matching genre "${effectiveGenreFilter}"`);
}
allItems = [...allItems, ...filteredItems];
foundItems = filteredItems.length > 0;
}
} catch (error) {
logger.log(`Failed to load items from ${manifest.name} catalog ${catalog.id}:`, error);
@ -163,7 +279,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
setLoading(false);
setRefreshing(false);
}
}, [addonId, type, id, genreFilter]);
}, [addonId, type, id, genreFilter, dataSource]);
useEffect(() => {
loadItems(1);

View file

@ -222,14 +222,11 @@ const CatalogSection = React.memo(({
// If not, CatalogScreen needs modification or we fetch IDs here.
// Let's stick to passing genre and type for now, CatalogScreen logic might suffice?
navigation.navigate('Catalog', {
// We don't have a single catalog ID or Addon ID for a genre section.
// Pass the genre as the 'id' and 'name' for CatalogScreen to potentially filter.
// This might require CatalogScreen to be adapted to handle genre-based views.
addonId: 'genre-based', // Placeholder or identifier
// Don't pass an addonId since we want to filter by genre across all addons
id: catalog.genre,
type: selectedCategory.type,
name: `${catalog.genre} ${selectedCategory.name}`, // Pass constructed name for now
genreFilter: catalog.genre // Keep the genre filter
name: `${catalog.genre} ${selectedCategory.name}`,
genreFilter: catalog.genre // This will trigger the genre-based filtering logic in CatalogScreen
});
// --- END TEMPORARY ---

View file

@ -142,7 +142,7 @@ const ActionButtons = React.memo(({
}
}}
>
<MaterialIcons name="star-rate" size={24} color="#fff" />
<MaterialIcons name="assessment" size={24} color="#fff" />
</TouchableOpacity>
)}
</Animated.View>

View file

@ -18,12 +18,14 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { Picker } from '@react-native-picker/picker';
import { colors } from '../styles/colors';
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator';
import { stremioService } from '../services/stremioService';
import { useCatalogContext } from '../contexts/CatalogContext';
import { useTraktContext } from '../contexts/TraktContext';
import { catalogService, DataSource } from '../services/catalogService';
const { width } = Dimensions.get('window');
@ -128,6 +130,7 @@ const SettingsScreen: React.FC = () => {
const [addonCount, setAddonCount] = useState<number>(0);
const [catalogCount, setCatalogCount] = useState<number>(0);
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
const [discoverDataSource, setDiscoverDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS);
const loadData = useCallback(async () => {
try {
@ -161,6 +164,10 @@ const SettingsScreen: React.FC = () => {
// Check MDBList API key status
const mdblistKey = await AsyncStorage.getItem('mdblist_api_key');
setMdblistKeySet(!!mdblistKey);
// Get discover data source preference
const dataSource = await catalogService.getDataSourcePreference();
setDiscoverDataSource(dataSource);
} catch (error) {
console.error('Error loading settings data:', error);
}
@ -217,6 +224,13 @@ const SettingsScreen: React.FC = () => {
/>
);
// Handle data source change
const handleDiscoverDataSourceChange = useCallback(async (value: string) => {
const dataSource = value as DataSource;
setDiscoverDataSource(dataSource);
await catalogService.setDataSourcePreference(dataSource);
}, []);
return (
<SafeAreaView style={[
styles.container,
@ -320,6 +334,43 @@ const SettingsScreen: React.FC = () => {
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Discover">
<SettingItem
title="Content Source"
description="Choose where to get content for the Discover screen"
icon="explore"
isDarkMode={isDarkMode}
renderControl={() => (
<View style={styles.selectorContainer}>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
]}>Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
]}>TMDB</Text>
</TouchableOpacity>
</View>
)}
/>
</SettingsCard>
<View style={styles.versionContainer}>
<Text style={[styles.versionText, {color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}]}>
Version 1.0.0
@ -450,6 +501,39 @@ const styles = StyleSheet.create({
versionText: {
fontSize: 14,
},
pickerContainer: {
flex: 1,
},
picker: {
flex: 1,
},
selectorContainer: {
flexDirection: 'row',
borderRadius: 8,
overflow: 'hidden',
height: 36,
width: 160,
marginRight: 8,
},
selectorButton: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 12,
backgroundColor: 'rgba(255,255,255,0.08)',
},
selectorButtonActive: {
backgroundColor: colors.primary,
},
selectorText: {
fontSize: 14,
fontWeight: '500',
color: colors.mediumEmphasis,
},
selectorTextActive: {
color: colors.white,
fontWeight: '600',
},
});
export default SettingsScreen;

View file

@ -5,6 +5,15 @@ import { TMDBService } from './tmdbService';
import { logger } from '../utils/logger';
import { getCatalogDisplayName } from '../utils/catalogNameUtils';
// Add a constant for storing the data source preference
const DATA_SOURCE_KEY = 'discover_data_source';
// Define data source types
export enum DataSource {
STREMIO_ADDONS = 'stremio_addons',
TMDB = 'tmdb',
}
export interface StreamingAddon {
id: string;
name: string;
@ -202,6 +211,15 @@ class CatalogService {
}
async getCatalogByType(type: string, genreFilter?: string): Promise<CatalogContent[]> {
// Get the data source preference (default to Stremio addons)
const dataSourcePreference = await this.getDataSourcePreference();
// If TMDB is selected as the data source, use TMDB API
if (dataSourcePreference === DataSource.TMDB) {
return this.getCatalogByTypeFromTMDB(type, genreFilter);
}
// Otherwise use the original Stremio addons method
const addons = await this.getAllAddons();
const catalogs: CatalogContent[] = [];
@ -245,6 +263,148 @@ class CatalogService {
return catalogs;
}
/**
* Get catalog content from TMDB by type and genre
*/
private async getCatalogByTypeFromTMDB(type: string, genreFilter?: string): Promise<CatalogContent[]> {
const tmdbService = TMDBService.getInstance();
const catalogs: CatalogContent[] = [];
try {
// Map Stremio content type to TMDB content type
const tmdbType = type === 'movie' ? 'movie' : 'tv';
// If no genre filter or All is selected, get multiple catalogs
if (!genreFilter || genreFilter === 'All') {
// Get trending
const trendingItems = await tmdbService.getTrending(tmdbType, 'week');
const trendingItemsPromises = trendingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
const trendingStreamingItems = await Promise.all(trendingItemsPromises);
catalogs.push({
addon: 'tmdb',
type,
id: 'trending',
name: `Trending ${type === 'movie' ? 'Movies' : 'TV Shows'}`,
items: trendingStreamingItems
});
// Get popular
const popularItems = await tmdbService.getPopular(tmdbType, 1);
const popularItemsPromises = popularItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
const popularStreamingItems = await Promise.all(popularItemsPromises);
catalogs.push({
addon: 'tmdb',
type,
id: 'popular',
name: `Popular ${type === 'movie' ? 'Movies' : 'TV Shows'}`,
items: popularStreamingItems
});
// Get upcoming/on air
const upcomingItems = await tmdbService.getUpcoming(tmdbType, 1);
const upcomingItemsPromises = upcomingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
const upcomingStreamingItems = await Promise.all(upcomingItemsPromises);
catalogs.push({
addon: 'tmdb',
type,
id: 'upcoming',
name: type === 'movie' ? 'Upcoming Movies' : 'On Air TV Shows',
items: upcomingStreamingItems
});
} else {
// Get content by genre
const genreItems = await tmdbService.discoverByGenre(tmdbType, genreFilter);
const streamingItemsPromises = genreItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType));
const streamingItems = await Promise.all(streamingItemsPromises);
catalogs.push({
addon: 'tmdb',
type,
id: 'discover',
name: `${genreFilter} ${type === 'movie' ? 'Movies' : 'TV Shows'}`,
genre: genreFilter,
items: streamingItems
});
}
} catch (error) {
logger.error(`Failed to get catalog from TMDB for type ${type}, genre ${genreFilter}:`, error);
}
return catalogs;
}
/**
* Convert TMDB trending/discover result to StreamingContent format
*/
private async convertTMDBToStreamingContent(item: any, type: 'movie' | 'tv'): Promise<StreamingContent> {
const id = item.external_ids?.imdb_id || `tmdb:${item.id}`;
const name = type === 'movie' ? item.title : item.name;
const posterPath = item.poster_path;
// Get genres from genre_ids
let genres: string[] = [];
if (item.genre_ids && item.genre_ids.length > 0) {
try {
const tmdbService = TMDBService.getInstance();
const genreLists = type === 'movie'
? await tmdbService.getMovieGenres()
: await tmdbService.getTvGenres();
const genreIds: number[] = item.genre_ids;
genres = genreIds
.map(genreId => {
const genre = genreLists.find(g => g.id === genreId);
return genre ? genre.name : null;
})
.filter(Boolean) as string[];
} catch (error) {
logger.error('Failed to get genres for TMDB content:', error);
}
}
return {
id,
type: type === 'movie' ? 'movie' : 'series',
name: name || 'Unknown',
poster: posterPath ? `https://image.tmdb.org/t/p/w500${posterPath}` : 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image',
posterShape: 'poster',
banner: item.backdrop_path ? `https://image.tmdb.org/t/p/original${item.backdrop_path}` : undefined,
year: type === 'movie'
? (item.release_date ? new Date(item.release_date).getFullYear() : undefined)
: (item.first_air_date ? new Date(item.first_air_date).getFullYear() : undefined),
description: item.overview,
genres,
inLibrary: this.library[`${type === 'movie' ? 'movie' : 'series'}:${id}`] !== undefined,
};
}
/**
* Get the current data source preference
*/
async getDataSourcePreference(): Promise<DataSource> {
try {
const dataSource = await AsyncStorage.getItem(DATA_SOURCE_KEY);
return dataSource as DataSource || DataSource.STREMIO_ADDONS;
} catch (error) {
logger.error('Failed to get data source preference:', error);
return DataSource.STREMIO_ADDONS;
}
}
/**
* Set the data source preference
*/
async setDataSourcePreference(dataSource: DataSource): Promise<void> {
try {
await AsyncStorage.setItem(DATA_SOURCE_KEY, dataSource);
} catch (error) {
logger.error('Failed to set data source preference:', error);
}
}
async getContentDetails(type: string, id: string): Promise<StreamingContent | null> {
try {
// Try up to 3 times with increasing delays

View file

@ -351,18 +351,23 @@ class StremioService {
// Add filters
if (filters.length > 0) {
logger.log(`Adding ${filters.length} filters to Cinemeta request`);
filters.forEach(filter => {
if (filter.value) {
logger.log(`Adding filter ${filter.title}=${filter.value}`);
url += `&${encodeURIComponent(filter.title)}=${encodeURIComponent(filter.value)}`;
}
});
}
logger.log(`Cinemeta catalog request URL: ${url}`);
const response = await this.retryRequest(async () => {
return await axios.get(url);
});
if (response.data && response.data.metas && Array.isArray(response.data.metas)) {
logger.log(`Cinemeta returned ${response.data.metas.length} items`);
return response.data.metas;
}
return [];
@ -384,18 +389,23 @@ class StremioService {
// Add filters
if (filters.length > 0) {
logger.log(`Adding ${filters.length} filters to ${manifest.name} request`);
filters.forEach(filter => {
if (filter.value) {
logger.log(`Adding filter ${filter.title}=${filter.value}`);
url += `&${encodeURIComponent(filter.title)}=${encodeURIComponent(filter.value)}`;
}
});
}
logger.log(`${manifest.name} catalog request URL: ${url}`);
const response = await this.retryRequest(async () => {
return await axios.get(url);
});
if (response.data && response.data.metas && Array.isArray(response.data.metas)) {
logger.log(`${manifest.name} returned ${response.data.metas.length} items`);
return response.data.metas;
}
return [];

View file

@ -790,6 +790,99 @@ export class TMDBService {
}
}
/**
* Get popular movies or TV shows
* @param type 'movie' or 'tv'
* @param page Page number for pagination
*/
async getPopular(type: 'movie' | 'tv', page: number = 1): Promise<TMDBTrendingResult[]> {
try {
const response = await axios.get(`${BASE_URL}/${type}/popular`, {
headers: await this.getHeaders(),
params: await this.getParams({
language: 'en-US',
page,
}),
});
// Get external IDs for each popular item
const results = response.data.results || [];
const resultsWithExternalIds = await Promise.all(
results.map(async (item: TMDBTrendingResult) => {
try {
const externalIdsResponse = await axios.get(
`${BASE_URL}/${type}/${item.id}/external_ids`,
{
headers: await this.getHeaders(),
params: await this.getParams(),
}
);
return {
...item,
external_ids: externalIdsResponse.data
};
} catch (error) {
logger.error(`Failed to get external IDs for ${type} ${item.id}:`, error);
return item;
}
})
);
return resultsWithExternalIds;
} catch (error) {
logger.error(`Failed to get popular ${type} content:`, error);
return [];
}
}
/**
* Get upcoming/now playing content
* @param type 'movie' or 'tv'
* @param page Page number for pagination
*/
async getUpcoming(type: 'movie' | 'tv', page: number = 1): Promise<TMDBTrendingResult[]> {
try {
// For movies use upcoming, for TV use on_the_air
const endpoint = type === 'movie' ? 'upcoming' : 'on_the_air';
const response = await axios.get(`${BASE_URL}/${type}/${endpoint}`, {
headers: await this.getHeaders(),
params: await this.getParams({
language: 'en-US',
page,
}),
});
// Get external IDs for each upcoming item
const results = response.data.results || [];
const resultsWithExternalIds = await Promise.all(
results.map(async (item: TMDBTrendingResult) => {
try {
const externalIdsResponse = await axios.get(
`${BASE_URL}/${type}/${item.id}/external_ids`,
{
headers: await this.getHeaders(),
params: await this.getParams(),
}
);
return {
...item,
external_ids: externalIdsResponse.data
};
} catch (error) {
logger.error(`Failed to get external IDs for ${type} ${item.id}:`, error);
return item;
}
})
);
return resultsWithExternalIds;
} catch (error) {
logger.error(`Failed to get upcoming ${type} content:`, error);
return [];
}
}
/**
* Get the list of official movie genres from TMDB
*/
@ -825,6 +918,69 @@ export class TMDBService {
return [];
}
}
/**
* Discover movies or TV shows by genre
* @param type 'movie' or 'tv'
* @param genreName The genre name to filter by
* @param page Page number for pagination
*/
async discoverByGenre(type: 'movie' | 'tv', genreName: string, page: number = 1): Promise<TMDBTrendingResult[]> {
try {
// First get the genre ID from the name
const genreList = type === 'movie'
? await this.getMovieGenres()
: await this.getTvGenres();
const genre = genreList.find(g => g.name.toLowerCase() === genreName.toLowerCase());
if (!genre) {
logger.error(`Genre ${genreName} not found`);
return [];
}
const response = await axios.get(`${BASE_URL}/discover/${type}`, {
headers: await this.getHeaders(),
params: await this.getParams({
language: 'en-US',
sort_by: 'popularity.desc',
include_adult: false,
include_video: false,
page,
with_genres: genre.id.toString(),
with_original_language: 'en',
}),
});
// Get external IDs for each item
const results = response.data.results || [];
const resultsWithExternalIds = await Promise.all(
results.map(async (item: TMDBTrendingResult) => {
try {
const externalIdsResponse = await axios.get(
`${BASE_URL}/${type}/${item.id}/external_ids`,
{
headers: await this.getHeaders(),
params: await this.getParams(),
}
);
return {
...item,
external_ids: externalIdsResponse.data
};
} catch (error) {
logger.error(`Failed to get external IDs for ${type} ${item.id}:`, error);
return item;
}
})
);
return resultsWithExternalIds;
} catch (error) {
logger.error(`Failed to discover ${type} by genre ${genreName}:`, error);
return [];
}
}
}
export const tmdbService = TMDBService.getInstance();