diff --git a/package-lock.json b/package-lock.json index 6ceb6cd..86d2d58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index cf26f2f..1f68515 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx index fc9f60b..eafb36e 100644 --- a/src/screens/CatalogScreen.tsx +++ b/src/screens/CatalogScreen.tsx @@ -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; @@ -52,11 +53,22 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [error, setError] = useState(null); + const [dataSource, setDataSource] = useState(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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ route, navigation }) => { setLoading(false); setRefreshing(false); } - }, [addonId, type, id, genreFilter]); + }, [addonId, type, id, genreFilter, dataSource]); useEffect(() => { loadItems(1); diff --git a/src/screens/DiscoverScreen.tsx b/src/screens/DiscoverScreen.tsx index 43c3bf9..9672ef2 100644 --- a/src/screens/DiscoverScreen.tsx +++ b/src/screens/DiscoverScreen.tsx @@ -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 --- diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 5389680..6632664 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -142,7 +142,7 @@ const ActionButtons = React.memo(({ } }} > - + )} diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 8b0a4cc..6aab5b4 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -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(0); const [catalogCount, setCatalogCount] = useState(0); const [mdblistKeySet, setMdblistKeySet] = useState(false); + const [discoverDataSource, setDiscoverDataSource] = useState(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 ( { /> + + ( + + handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)} + > + Addons + + handleDiscoverDataSourceChange(DataSource.TMDB)} + > + TMDB + + + )} + /> + + 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; \ No newline at end of file diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index d4ab5c8..f60d0f6 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -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 { + // 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 { + 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 { + 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 { + 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 { + 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 { try { // Try up to 3 times with increasing delays diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 1be1d50..fc890bb 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -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 []; diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index fb65552..a216196 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -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 { + 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 { + 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 { + 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();