mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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:
parent
12a18c057d
commit
869bedba72
9 changed files with 551 additions and 13 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue