mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-19 09:17:38 +00:00
Merge pull request #639 from chrisk325/catalogs
remove hardcoded catalog type + seperate search result for anime + fix for tvdb+kitsu
This commit is contained in:
commit
d0bfd3550a
6 changed files with 174 additions and 184 deletions
|
|
@ -14,6 +14,24 @@ interface AddonSectionProps {
|
|||
currentTheme: any;
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
'movie': 'search.movies',
|
||||
'series': 'search.tv_shows',
|
||||
'anime.movie': 'search.anime_movies',
|
||||
'anime.series': 'search.anime_series',
|
||||
};
|
||||
|
||||
const subtitleStyle = (currentTheme: any) => ({
|
||||
color: currentTheme.colors.lightGray,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16,
|
||||
});
|
||||
|
||||
const containerStyle = {
|
||||
marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24,
|
||||
};
|
||||
|
||||
export const AddonSection = React.memo(({
|
||||
addonGroup,
|
||||
addonIndex,
|
||||
|
|
@ -23,18 +41,27 @@ export const AddonSection = React.memo(({
|
|||
}: AddonSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const movieResults = useMemo(() =>
|
||||
addonGroup.results.filter(item => item.type === 'movie'),
|
||||
[addonGroup.results]
|
||||
);
|
||||
const seriesResults = useMemo(() =>
|
||||
addonGroup.results.filter(item => item.type === 'series'),
|
||||
[addonGroup.results]
|
||||
);
|
||||
const otherResults = useMemo(() =>
|
||||
addonGroup.results.filter(item => item.type !== 'movie' && item.type !== 'series'),
|
||||
[addonGroup.results]
|
||||
);
|
||||
// Group results by their exact type, preserving order of first appearance
|
||||
const groupedByType = useMemo(() => {
|
||||
const order: string[] = [];
|
||||
const groups: Record<string, StreamingContent[]> = {};
|
||||
|
||||
for (const item of addonGroup.results) {
|
||||
if (!groups[item.type]) {
|
||||
order.push(item.type);
|
||||
groups[item.type] = [];
|
||||
}
|
||||
groups[item.type].push(item);
|
||||
}
|
||||
|
||||
return order.map(type => ({ type, items: groups[type] }));
|
||||
}, [addonGroup.results]);
|
||||
|
||||
const getLabelForType = (type: string): string => {
|
||||
if (TYPE_LABELS[type]) return t(TYPE_LABELS[type]);
|
||||
// Fallback: capitalise and replace dots/underscores for unknown types
|
||||
return type.replace(/[._]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
|
|
@ -50,22 +77,13 @@ export const AddonSection = React.memo(({
|
|||
</View>
|
||||
</View>
|
||||
|
||||
{/* Movies */}
|
||||
{movieResults.length > 0 && (
|
||||
<View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]}>
|
||||
<Text style={[
|
||||
styles.carouselSubtitle,
|
||||
{
|
||||
color: currentTheme.colors.lightGray,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
{t('search.movies')} ({movieResults.length})
|
||||
{groupedByType.map(({ type, items }) => (
|
||||
<View key={type} style={[styles.carouselContainer, containerStyle]}>
|
||||
<Text style={[styles.carouselSubtitle, subtitleStyle(currentTheme)]}>
|
||||
{getLabelForType(type)} ({items.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={movieResults}
|
||||
data={items}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResultItem
|
||||
item={item}
|
||||
|
|
@ -74,81 +92,16 @@ export const AddonSection = React.memo(({
|
|||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `${addonGroup.addonId}-movie-${item.id}`}
|
||||
keyExtractor={item => `${addonGroup.addonId}-${type}-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* TV Shows */}
|
||||
{seriesResults.length > 0 && (
|
||||
<View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]}>
|
||||
<Text style={[
|
||||
styles.carouselSubtitle,
|
||||
{
|
||||
color: currentTheme.colors.lightGray,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
{t('search.tv_shows')} ({seriesResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={seriesResults}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResultItem
|
||||
item={item}
|
||||
index={index}
|
||||
onPress={onItemPress}
|
||||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `${addonGroup.addonId}-series-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Other types */}
|
||||
{otherResults.length > 0 && (
|
||||
<View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]}>
|
||||
<Text style={[
|
||||
styles.carouselSubtitle,
|
||||
{
|
||||
color: currentTheme.colors.lightGray,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
{otherResults[0].type.charAt(0).toUpperCase() + otherResults[0].type.slice(1)} ({otherResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={otherResults}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResultItem
|
||||
item={item}
|
||||
index={index}
|
||||
onPress={onItemPress}
|
||||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `${addonGroup.addonId}-${item.type}-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}, (prev, next) => {
|
||||
// Only re-render if this section's reference changed
|
||||
return prev.addonGroup === next.addonGroup && prev.addonIndex === next.addonIndex;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ interface DiscoverBottomSheetsProps {
|
|||
typeSheetRef: RefObject<BottomSheetModal>;
|
||||
catalogSheetRef: RefObject<BottomSheetModal>;
|
||||
genreSheetRef: RefObject<BottomSheetModal>;
|
||||
selectedDiscoverType: 'movie' | 'series';
|
||||
selectedDiscoverType: string;
|
||||
selectedCatalog: DiscoverCatalog | null;
|
||||
selectedDiscoverGenre: string | null;
|
||||
filteredCatalogs: DiscoverCatalog[];
|
||||
availableGenres: string[];
|
||||
onTypeSelect: (type: 'movie' | 'series') => void;
|
||||
availableTypes: string[];
|
||||
onTypeSelect: (type: string) => void;
|
||||
onCatalogSelect: (catalog: DiscoverCatalog) => void;
|
||||
onGenreSelect: (genre: string | null) => void;
|
||||
currentTheme: any;
|
||||
|
|
@ -31,6 +32,7 @@ export const DiscoverBottomSheets = ({
|
|||
selectedDiscoverGenre,
|
||||
filteredCatalogs,
|
||||
availableGenres,
|
||||
availableTypes,
|
||||
onTypeSelect,
|
||||
onCatalogSelect,
|
||||
onGenreSelect,
|
||||
|
|
@ -38,7 +40,20 @@ export const DiscoverBottomSheets = ({
|
|||
}: DiscoverBottomSheetsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const typeSnapPoints = useMemo(() => ['25%'], []);
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
'movie': t('search.movies'),
|
||||
'series': t('search.tv_shows'),
|
||||
'anime.movie': t('search.anime_movies'),
|
||||
'anime.series': t('search.anime_series'),
|
||||
};
|
||||
const getLabelForType = (type: string) =>
|
||||
TYPE_LABELS[type] ?? type.replace(/[._]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
|
||||
const typeSnapPoints = useMemo(() => {
|
||||
const itemCount = availableTypes.length;
|
||||
const snapPct = Math.min(20 + itemCount * 10, 60);
|
||||
return [`${snapPct}%`];
|
||||
}, [availableTypes]);
|
||||
const catalogSnapPoints = useMemo(() => ['50%'], []);
|
||||
const genreSnapPoints = useMemo(() => ['50%'], []);
|
||||
const [activeBottomSheetRef, setActiveBottomSheetRef] = useState(null);
|
||||
|
|
@ -225,47 +240,25 @@ export const DiscoverBottomSheets = ({
|
|||
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
|
||||
contentContainerStyle={styles.bottomSheetContent}
|
||||
>
|
||||
{/* Movies option */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
selectedDiscoverType === 'movie' && styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onTypeSelect('movie')}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.movies')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.browse_movies')}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverType === 'movie' && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* TV Shows option */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
selectedDiscoverType === 'series' && styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onTypeSelect('series')}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.tv_shows')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.browse_tv')}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverType === 'series' && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{availableTypes.map((type) => (
|
||||
<TouchableOpacity
|
||||
key={type}
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
selectedDiscoverType === type && styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onTypeSelect(type)}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{getLabelForType(type)}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverType === type && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ interface DiscoverSectionProps {
|
|||
pendingDiscoverResults: StreamingContent[];
|
||||
loadingMore: boolean;
|
||||
selectedCatalog: DiscoverCatalog | null;
|
||||
selectedDiscoverType: 'movie' | 'series';
|
||||
selectedDiscoverType: string;
|
||||
selectedDiscoverGenre: string | null;
|
||||
availableGenres: string[];
|
||||
typeSheetRef: React.RefObject<BottomSheetModal>;
|
||||
|
|
@ -78,7 +78,11 @@ export const DiscoverSection = ({
|
|||
onPress={() => typeSheetRef.current?.present()}
|
||||
>
|
||||
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
|
||||
{selectedDiscoverType === 'movie' ? t('search.movies') : t('search.tv_shows')}
|
||||
{selectedDiscoverType === 'movie' ? t('search.movies')
|
||||
: selectedDiscoverType === 'series' ? t('search.tv_shows')
|
||||
: selectedDiscoverType === 'anime.movie' ? t('search.anime_movies')
|
||||
: selectedDiscoverType === 'anime.series' ? t('search.anime_series')
|
||||
: selectedDiscoverType.replace(/[._]/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
|
||||
</Text>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
|
|
@ -112,8 +116,13 @@ export const DiscoverSection = ({
|
|||
{selectedCatalog && (
|
||||
<View style={styles.discoverFilterSummary}>
|
||||
<Text style={[styles.discoverFilterSummaryText, { color: currentTheme.colors.lightGray }]}>
|
||||
{selectedCatalog.addonName} • {selectedCatalog.type === 'movie' ? t('search.movies') : t('search.tv_shows')}
|
||||
{selectedDiscoverGenre ? ` • ${selectedDiscoverGenre}` : ''}
|
||||
{selectedCatalog.addonName} • {
|
||||
selectedCatalog.type === 'movie' ? t('search.movies')
|
||||
: selectedCatalog.type === 'series' ? t('search.tv_shows')
|
||||
: selectedCatalog.type === 'anime.movie' ? t('search.anime_movies')
|
||||
: selectedCatalog.type === 'anime.series' ? t('search.anime_series')
|
||||
: selectedCatalog.type.replace(/[._]/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}{selectedDiscoverGenre ? ` • ${selectedDiscoverGenre}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,13 @@ interface UseMetadataReturn {
|
|||
|
||||
export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadataReturn => {
|
||||
const { settings, isLoaded: settingsLoaded } = useSettings();
|
||||
|
||||
// Normalize anime subtypes to their base types for all internal logic.
|
||||
// anime.series behaves like series; anime.movie behaves like movie.
|
||||
const normalizedType = type === 'anime.series' ? 'series'
|
||||
: type === 'anime.movie' ? 'movie'
|
||||
: type;
|
||||
|
||||
const [metadata, setMetadata] = useState<StreamingContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -427,7 +434,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
return;
|
||||
}
|
||||
// Check cache first
|
||||
const cachedCast = cacheService.getCast(id, type);
|
||||
const cachedCast = cacheService.getCast(id, normalizedType);
|
||||
if (cachedCast) {
|
||||
if (__DEV__) logger.log('[loadCast] Using cached cast data');
|
||||
setCast(cachedCast);
|
||||
|
|
@ -439,7 +446,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
if (id.startsWith('tmdb:')) {
|
||||
const tmdbId = id.split(':')[1];
|
||||
if (__DEV__) logger.log('[loadCast] Using TMDB ID directly:', tmdbId);
|
||||
const castData = await tmdbService.getCredits(parseInt(tmdbId), type);
|
||||
const castData = await tmdbService.getCredits(parseInt(tmdbId), normalizedType);
|
||||
if (castData && castData.cast) {
|
||||
const formattedCast = castData.cast.map((actor: any) => ({
|
||||
id: actor.id,
|
||||
|
|
@ -464,7 +471,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
|
||||
if (tmdbId) {
|
||||
if (__DEV__) logger.log('[loadCast] Fetching cast using TMDB ID:', tmdbId);
|
||||
const castData = await tmdbService.getCredits(tmdbId, type);
|
||||
const castData = await tmdbService.getCredits(tmdbId, normalizedType);
|
||||
if (castData && castData.cast) {
|
||||
const formattedCast = castData.cast.map((actor: any) => ({
|
||||
id: actor.id,
|
||||
|
|
@ -511,7 +518,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
setLoadAttempts(prev => prev + 1);
|
||||
|
||||
// Check metadata screen cache
|
||||
const cachedScreen = cacheService.getMetadataScreen(id, type);
|
||||
const cachedScreen = cacheService.getMetadataScreen(id, normalizedType);
|
||||
if (cachedScreen) {
|
||||
console.log('🔍 [useMetadata] Using cached metadata:', {
|
||||
id,
|
||||
|
|
@ -523,7 +530,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
});
|
||||
setMetadata(cachedScreen.metadata);
|
||||
setCast(cachedScreen.cast);
|
||||
if (type === 'series' && cachedScreen.episodes) {
|
||||
if (normalizedType === 'series' && cachedScreen.episodes) {
|
||||
setGroupedEpisodes(cachedScreen.episodes.groupedEpisodes);
|
||||
setEpisodes(cachedScreen.episodes.currentEpisodes);
|
||||
setSelectedSeason(cachedScreen.episodes.selectedSeason);
|
||||
|
|
@ -567,7 +574,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
} else {
|
||||
const tmdbId = id.split(':')[1];
|
||||
// For TMDB IDs, we need to handle metadata differently
|
||||
if (type === 'movie') {
|
||||
if (normalizedType === 'movie') {
|
||||
if (__DEV__) logger.log('Fetching movie details from TMDB for:', tmdbId);
|
||||
const movieDetails = await tmdbService.getMovieDetails(
|
||||
tmdbId,
|
||||
|
|
@ -639,7 +646,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
}
|
||||
|
||||
setMetadata(formattedMovie);
|
||||
cacheService.setMetadata(id, type, formattedMovie);
|
||||
cacheService.setMetadata(id, normalizedType, formattedMovie);
|
||||
(async () => {
|
||||
const items = await catalogService.getLibraryItems();
|
||||
const isInLib = items.some(item => item.id === id);
|
||||
|
|
@ -649,7 +656,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
return;
|
||||
}
|
||||
}
|
||||
} else if (type === 'series') {
|
||||
} else if (normalizedType === 'series') {
|
||||
// Handle TV shows with TMDB IDs
|
||||
if (__DEV__) logger.log('Fetching TV show details from TMDB for:', tmdbId);
|
||||
try {
|
||||
|
|
@ -719,7 +726,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
}
|
||||
|
||||
setMetadata(formattedShow);
|
||||
cacheService.setMetadata(id, type, formattedShow);
|
||||
cacheService.setMetadata(id, normalizedType, formattedShow);
|
||||
|
||||
// Load series data (episodes)
|
||||
setTmdbId(parseInt(tmdbId));
|
||||
|
|
@ -779,7 +786,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
for (const addon of externalMetaAddons) {
|
||||
try {
|
||||
const result = await withTimeout(
|
||||
stremioService.getMetaDetails(type, actualId, addon.id),
|
||||
stremioService.getMetaDetails(normalizedType, actualId, addon.id),
|
||||
API_TIMEOUT
|
||||
);
|
||||
|
||||
|
|
@ -799,7 +806,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// If no external addon worked, fall back to catalog addon
|
||||
console.log('🔍 [useMetadata] No external meta addon worked, falling back to catalog addon');
|
||||
const result = await withTimeout(
|
||||
catalogService.getEnhancedContentDetails(type, actualId, addonId),
|
||||
catalogService.getEnhancedContentDetails(normalizedType, actualId, addonId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
if (actualId.startsWith('tt')) {
|
||||
|
|
@ -831,7 +838,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
console.log('⚡ [useMetadata] Calling catalogService.getEnhancedContentDetails...');
|
||||
console.log('🔍 [useMetadata] Calling catalogService.getEnhancedContentDetails:', { type, actualId, addonId });
|
||||
const result = await withTimeout(
|
||||
catalogService.getEnhancedContentDetails(type, actualId, addonId),
|
||||
catalogService.getEnhancedContentDetails(normalizedType, actualId, addonId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
console.log('✅ [useMetadata] catalogService returned:', result ? 'DATA' : 'NULL');
|
||||
|
|
@ -871,13 +878,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
console.log('🔍 [useMetadata] Original TMDB ID failed, trying ID conversion fallback');
|
||||
const tmdbRaw = id.split(':')[1];
|
||||
try {
|
||||
const stremioId = await catalogService.getStremioId(type === 'series' ? 'tv' : 'movie', tmdbRaw);
|
||||
const stremioId = await catalogService.getStremioId(normalizedType === 'series' ? 'tv' : 'movie', tmdbRaw);
|
||||
if (stremioId && stremioId !== id) {
|
||||
console.log('🔍 [useMetadata] Trying converted ID:', { originalId: id, convertedId: stremioId });
|
||||
const [content, castData] = await Promise.allSettled([
|
||||
withRetry(async () => {
|
||||
const result = await withTimeout(
|
||||
catalogService.getEnhancedContentDetails(type, stremioId, addonId),
|
||||
catalogService.getEnhancedContentDetails(normalizedType, stremioId, addonId),
|
||||
API_TIMEOUT
|
||||
);
|
||||
if (stremioId.startsWith('tt')) {
|
||||
|
|
@ -934,7 +941,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
|
||||
if (finalTmdbId) {
|
||||
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||
if (type === 'movie') {
|
||||
if (normalizedType === 'movie') {
|
||||
const localized = await tmdbSvc.getMovieDetails(String(finalTmdbId), lang);
|
||||
if (localized) {
|
||||
const movieDetailsObj = {
|
||||
|
|
@ -1011,7 +1018,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) {
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||
const contentType = type === 'series' ? 'tv' : 'movie';
|
||||
const contentType = normalizedType === 'series' ? 'tv' : 'movie';
|
||||
|
||||
// Get TMDB ID
|
||||
let tmdbIdForLogo = null;
|
||||
|
|
@ -1080,7 +1087,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
}
|
||||
return updated;
|
||||
});
|
||||
cacheService.setMetadata(id, type, finalMetadata);
|
||||
cacheService.setMetadata(id, normalizedType, finalMetadata);
|
||||
(async () => {
|
||||
const items = await catalogService.getLibraryItems();
|
||||
const isInLib = items.some(item => item.id === id);
|
||||
|
|
@ -1597,10 +1604,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Convert TMDB ID to IMDb ID for Stremio addons (they expect IMDb format)
|
||||
try {
|
||||
let externalIds = null;
|
||||
if (type === 'movie') {
|
||||
if (normalizedType === 'movie') {
|
||||
const movieDetails = await withTimeout(tmdbService.getMovieDetails(tmdbId), API_TIMEOUT);
|
||||
externalIds = movieDetails?.external_ids;
|
||||
} else if (type === 'series') {
|
||||
} else if (normalizedType === 'series') {
|
||||
externalIds = await withTimeout(tmdbService.getShowExternalIds(parseInt(tmdbId)), API_TIMEOUT);
|
||||
}
|
||||
|
||||
|
|
@ -1829,7 +1836,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
return false;
|
||||
});
|
||||
|
||||
const requestedEpisodeType = type;
|
||||
const requestedEpisodeType = normalizedType;
|
||||
let streamAddons = pickStreamCapableAddons(requestedEpisodeType);
|
||||
|
||||
if (streamAddons.length === 0) {
|
||||
|
|
@ -2029,12 +2036,17 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Remove 'series:' prefix if present to be safe, though parsing logic above usually handles it
|
||||
stremioEpisodeId = episodeId.replace(/^series:/, '');
|
||||
} else if (!seasonNum) {
|
||||
// No season (e.g., mal:57658:1) - use id:episode format
|
||||
stremioEpisodeId = `${id}:${episodeNum}`;
|
||||
// No season (e.g., kitsu:12345:1, mal:57658:1) - use showIdStr:episode format.
|
||||
// Use showIdStr (parsed from episodeId) rather than outer `id` so that when the
|
||||
// show has multiple IDs (e.g. tvdb+kitsu), we preserve the namespace that the
|
||||
// episode actually belongs to (e.g. kitsu:animeId:epNum, not tvdb:showId:epNum).
|
||||
const baseId = showIdStr && showIdStr !== id ? showIdStr : id;
|
||||
stremioEpisodeId = `${baseId}:${episodeNum}`;
|
||||
} else {
|
||||
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
|
||||
const baseId = showIdStr && showIdStr !== id ? showIdStr : id;
|
||||
stremioEpisodeId = `${baseId}:${seasonNum}:${episodeNum}`;
|
||||
}
|
||||
if (__DEV__) console.log('ℹ️ [loadEpisodeStreams] Using ID as both TMDB and Stremio ID:', tmdbId);
|
||||
if (__DEV__) console.log('ℹ️ [loadEpisodeStreams] Using ID as both TMDB and Stremio ID:', tmdbId, '| stremioEpisodeId:', stremioEpisodeId);
|
||||
}
|
||||
|
||||
// Extract episode info from the episodeId for logging
|
||||
|
|
@ -2111,7 +2123,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
if (!metadata) return;
|
||||
|
||||
if (inLibrary) {
|
||||
catalogService.removeFromLibrary(type, id);
|
||||
catalogService.removeFromLibrary(normalizedType, id);
|
||||
} else {
|
||||
catalogService.addToLibrary(metadata);
|
||||
}
|
||||
|
|
@ -2190,12 +2202,12 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
try {
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||
const results = await tmdbService.getRecommendations(type === 'movie' ? 'movie' : 'tv', String(tmdbId), lang);
|
||||
const results = await tmdbService.getRecommendations(normalizedType === 'movie' ? 'movie' : 'tv', String(tmdbId), lang);
|
||||
|
||||
// Convert TMDB results to StreamingContent format (simplified)
|
||||
const formattedRecommendations: StreamingContent[] = results.map((item: any) => ({
|
||||
id: `tmdb:${item.id}`,
|
||||
type: type === 'movie' ? 'movie' : 'series',
|
||||
type: normalizedType === 'movie' ? 'movie' : 'series',
|
||||
name: item.title || item.name || 'Untitled',
|
||||
poster: tmdbService.getImageUrl(item.poster_path) || 'https://via.placeholder.com/300x450', // Provide fallback
|
||||
year: (item.release_date || item.first_air_date)?.substring(0, 4) || 'N/A', // Ensure string and provide fallback
|
||||
|
|
@ -2226,7 +2238,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
setTmdbId(fetchedTmdbId);
|
||||
// Fetch certification only if granular setting is enabled
|
||||
if (settings.tmdbEnrichCertification) {
|
||||
const certification = await tmdbService.getCertification(type, fetchedTmdbId);
|
||||
const certification = await tmdbService.getCertification(normalizedType, fetchedTmdbId);
|
||||
if (certification) {
|
||||
if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification });
|
||||
setMetadata(prev => prev ? {
|
||||
|
|
@ -2299,7 +2311,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
return;
|
||||
}
|
||||
const tmdbSvc = TMDBService.getInstance();
|
||||
const cert = await tmdbSvc.getCertification(type, tmdbId);
|
||||
const cert = await tmdbSvc.getCertification(normalizedType, tmdbId);
|
||||
if (cert) {
|
||||
if (__DEV__) console.log('[useMetadata] fetched certification (attach path)', { type, tmdbId, cert });
|
||||
setMetadata(prev => prev ? { ...prev, tmdbId, certification: cert } : prev);
|
||||
|
|
@ -2326,7 +2338,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
return;
|
||||
}
|
||||
|
||||
const contentKey = `${type}-${tmdbId}`;
|
||||
const contentKey = `${normalizedType}-${tmdbId}`;
|
||||
if (productionInfoFetchedRef.current === contentKey) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -2334,7 +2346,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Only skip if networks are set AND collection is already set (for movies)
|
||||
const hasNetworks = !!(metadata as any).networks;
|
||||
const hasCollection = !!(metadata as any).collection;
|
||||
if (hasNetworks && (type !== 'movie' || hasCollection)) {
|
||||
if (hasNetworks && (normalizedType !== 'movie' || hasCollection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2357,7 +2369,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
collectionsEnabled: settings.tmdbEnrichCollections
|
||||
});
|
||||
|
||||
if (type === 'series') {
|
||||
if (normalizedType === 'series') {
|
||||
// Fetch networks and additional details for TV shows
|
||||
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||
const showDetails = await tmdbService.getTVShowDetails(tmdbId, lang);
|
||||
|
|
@ -2406,7 +2418,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
}));
|
||||
}
|
||||
}
|
||||
} else if (type === 'movie') {
|
||||
} else if (normalizedType === 'movie') {
|
||||
// Fetch production companies and additional details for movies
|
||||
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||
const movieDetails = await tmdbService.getMovieDetails(String(tmdbId), lang);
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ const SearchScreen = () => {
|
|||
// Discover section state
|
||||
const [discoverCatalogs, setDiscoverCatalogs] = useState<DiscoverCatalog[]>([]);
|
||||
const [selectedCatalog, setSelectedCatalog] = useState<DiscoverCatalog | null>(null);
|
||||
const [selectedDiscoverType, setSelectedDiscoverType] = useState<'movie' | 'series'>('movie');
|
||||
const [selectedDiscoverType, setSelectedDiscoverType] = useState<string>('movie');
|
||||
const [selectedDiscoverGenre, setSelectedDiscoverGenre] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
|
@ -127,7 +127,7 @@ const SearchScreen = () => {
|
|||
try {
|
||||
// Load saved type
|
||||
const savedType = await mmkvStorage.getItem(DISCOVER_TYPE_KEY);
|
||||
if (savedType && (savedType === 'movie' || savedType === 'series')) {
|
||||
if (savedType) {
|
||||
setSelectedDiscoverType(savedType);
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ const SearchScreen = () => {
|
|||
}, []);
|
||||
|
||||
// Save discover settings when they change
|
||||
const saveDiscoverSettings = useCallback(async (type: 'movie' | 'series', catalog: DiscoverCatalog | null, genre: string | null) => {
|
||||
const saveDiscoverSettings = useCallback(async (type: string, catalog: DiscoverCatalog | null, genre: string | null) => {
|
||||
try {
|
||||
// Save type
|
||||
await mmkvStorage.setItem(DISCOVER_TYPE_KEY, type);
|
||||
|
|
@ -267,10 +267,8 @@ const SearchScreen = () => {
|
|||
if (isMounted.current) {
|
||||
const allCatalogs: DiscoverCatalog[] = [];
|
||||
for (const [type, catalogs] of Object.entries(filters.catalogsByType)) {
|
||||
if (type === 'movie' || type === 'series') {
|
||||
for (const catalog of catalogs) {
|
||||
allCatalogs.push({ ...catalog, type });
|
||||
}
|
||||
for (const catalog of catalogs) {
|
||||
allCatalogs.push({ ...catalog, type });
|
||||
}
|
||||
}
|
||||
setDiscoverCatalogs(allCatalogs);
|
||||
|
|
@ -636,9 +634,10 @@ const SearchScreen = () => {
|
|||
};
|
||||
|
||||
const availableGenres = useMemo(() => selectedCatalog?.genres || [], [selectedCatalog]);
|
||||
const availableTypes = useMemo(() => [...new Set(discoverCatalogs.map(c => c.type))], [discoverCatalogs]);
|
||||
const filteredCatalogs = useMemo(() => discoverCatalogs.filter(c => c.type === selectedDiscoverType), [discoverCatalogs, selectedDiscoverType]);
|
||||
|
||||
const handleTypeSelect = (type: 'movie' | 'series') => {
|
||||
const handleTypeSelect = (type: string) => {
|
||||
setSelectedDiscoverType(type);
|
||||
|
||||
// Save type setting
|
||||
|
|
@ -893,6 +892,7 @@ const SearchScreen = () => {
|
|||
selectedDiscoverGenre={selectedDiscoverGenre}
|
||||
filteredCatalogs={filteredCatalogs}
|
||||
availableGenres={availableGenres}
|
||||
availableTypes={availableTypes}
|
||||
onTypeSelect={handleTypeSelect}
|
||||
onCatalogSelect={handleCatalogSelect}
|
||||
onGenreSelect={handleGenreSelect}
|
||||
|
|
|
|||
|
|
@ -363,6 +363,8 @@ class CatalogService {
|
|||
}
|
||||
|
||||
private canBrowseCatalog(catalog: StreamingCatalog): boolean {
|
||||
// Exclude non-standard types like anime.series, anime.movie from discover browsing
|
||||
if (catalog.type && catalog.type.includes('.')) return false;
|
||||
const requiredExtras = this.getRequiredCatalogExtras(catalog);
|
||||
return requiredExtras.every(extraName => extraName === 'genre');
|
||||
}
|
||||
|
|
@ -1534,9 +1536,24 @@ class CatalogService {
|
|||
return;
|
||||
}
|
||||
|
||||
// Dedupe within addon and against global
|
||||
// Within this addon's results, if the same ID appears under both a generic
|
||||
// type (e.g. "series") and a specific type (e.g. "anime.series"), keep only
|
||||
// the specific one. This handles addons that expose both catalog types.
|
||||
const bestByIdWithinAddon = new Map<string, StreamingContent>();
|
||||
for (const item of addonResults) {
|
||||
const existing = bestByIdWithinAddon.get(item.id);
|
||||
if (!existing) {
|
||||
bestByIdWithinAddon.set(item.id, item);
|
||||
} else if (!existing.type.includes('.') && item.type.includes('.')) {
|
||||
// Prefer the more specific type
|
||||
bestByIdWithinAddon.set(item.id, item);
|
||||
}
|
||||
}
|
||||
const deduped = Array.from(bestByIdWithinAddon.values());
|
||||
|
||||
// Dedupe against global seen (keyed by type:id to avoid cross-addon ID collisions)
|
||||
const localSeen = new Set<string>();
|
||||
const unique = addonResults.filter(item => {
|
||||
const unique = deduped.filter(item => {
|
||||
const key = `${item.type}:${item.id}`;
|
||||
if (localSeen.has(key) || globalSeen.has(key)) return false;
|
||||
localSeen.add(key);
|
||||
|
|
@ -1626,6 +1643,12 @@ class CatalogService {
|
|||
const items = metas.map(meta => {
|
||||
const content = this.convertMetaToStreamingContent(meta);
|
||||
content.addonId = manifest.id;
|
||||
// The meta's own type field may be generic (e.g. "series") even when
|
||||
// the catalog it came from is more specific (e.g. "anime.series").
|
||||
// Stamp the catalog type so grouping in the UI is correct.
|
||||
if (type && content.type !== type) {
|
||||
content.type = type;
|
||||
}
|
||||
return content;
|
||||
});
|
||||
logger.log(`Found ${items.length} results from ${manifest.name}`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue