mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-04 08:49:52 +00:00
some fixes
This commit is contained in:
parent
dff3a66d7b
commit
d8950caf04
6 changed files with 238 additions and 133 deletions
|
|
@ -31,7 +31,7 @@ import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUt
|
|||
import { styles } from './utils/playerStyles';
|
||||
import { SubtitleModals } from './modals/SubtitleModals';
|
||||
import { AudioTrackModal } from './modals/AudioTrackModal';
|
||||
import ResumeOverlay from './modals/ResumeOverlay';
|
||||
// Removed ResumeOverlay usage when alwaysResume is enabled
|
||||
import PlayerControls from './controls/PlayerControls';
|
||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||
import { SourcesModal } from './modals/SourcesModal';
|
||||
|
|
@ -118,6 +118,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const [showResumeOverlay, setShowResumeOverlay] = useState(false);
|
||||
const [resumePosition, setResumePosition] = useState<number | null>(null);
|
||||
const [savedDuration, setSavedDuration] = useState<number | null>(null);
|
||||
const initialSeekTargetRef = useRef<number | null>(null);
|
||||
const initialSeekVerifiedRef = useRef(false);
|
||||
const isSourceSeekableRef = useRef<boolean | null>(null);
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
const [isOpeningAnimationComplete, setIsOpeningAnimationComplete] = useState(false);
|
||||
const openingFadeAnim = useRef(new Animated.Value(0)).current;
|
||||
|
|
@ -185,12 +188,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
// Get metadata to access logo (only if we have a valid id)
|
||||
const shouldLoadMetadata = Boolean(id && type);
|
||||
const metadataResult = useMetadata({
|
||||
id: id || 'placeholder',
|
||||
type: type || 'movie'
|
||||
});
|
||||
const metadataResult = useMetadata({ id: id || 'placeholder', type: (type as any) });
|
||||
const { settings: appSettings } = useSettings();
|
||||
const { metadata, loading: metadataLoading } = shouldLoadMetadata ? metadataResult : { metadata: null, loading: false };
|
||||
const { settings } = useSettings();
|
||||
|
||||
// Logo animation values
|
||||
const logoScaleAnim = useRef(new Animated.Value(0.8)).current;
|
||||
|
|
@ -369,10 +369,17 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
if (progressPercent < 85) {
|
||||
setResumePosition(savedProgress.currentTime);
|
||||
setSavedDuration(savedProgress.duration);
|
||||
logger.log(`[AndroidVideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`);
|
||||
setShowResumeOverlay(true);
|
||||
logger.log(`[AndroidVideoPlayer] Showing resume overlay`);
|
||||
setSavedDuration(savedProgress.duration);
|
||||
setInitialPosition(savedProgress.currentTime);
|
||||
initialSeekTargetRef.current = savedProgress.currentTime;
|
||||
logger.log(`[AndroidVideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`);
|
||||
if (appSettings.alwaysResume) {
|
||||
logger.log(`[AndroidVideoPlayer] AlwaysResume enabled. Auto-seeking to ${savedProgress.currentTime}`);
|
||||
seekToTime(savedProgress.currentTime);
|
||||
} else {
|
||||
setShowResumeOverlay(true);
|
||||
logger.log(`[AndroidVideoPlayer] Showing resume overlay`);
|
||||
}
|
||||
} else {
|
||||
logger.log(`[AndroidVideoPlayer] Progress too high (${progressPercent.toFixed(1)}%), not showing resume overlay`);
|
||||
}
|
||||
|
|
@ -387,7 +394,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
loadWatchProgress();
|
||||
}, [id, type, episodeId]);
|
||||
}, [id, type, episodeId, appSettings.alwaysResume]);
|
||||
|
||||
const saveWatchProgress = async () => {
|
||||
if (id && type && currentTime > 0 && duration > 0) {
|
||||
|
|
@ -944,7 +951,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
setIsLoadingSubtitleList(true);
|
||||
try {
|
||||
// Fetch from installed OpenSubtitles v3 addon via Stremio only
|
||||
// Fetch from all installed subtitle-capable addons via Stremio
|
||||
const stremioType = type === 'series' ? 'series' : 'movie';
|
||||
const stremioVideoId = stremioType === 'series' && season && episode
|
||||
? `series:${targetImdbId}:${season}:${episode}`
|
||||
|
|
@ -956,37 +963,28 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
flagUrl: '',
|
||||
format: 'srt',
|
||||
encoding: 'utf-8',
|
||||
media: 'opensubtitles',
|
||||
media: sub.addonName || sub.addon || '',
|
||||
display: sub.lang || 'Unknown',
|
||||
language: (sub.lang || '').toLowerCase(),
|
||||
isHearingImpaired: false,
|
||||
source: sub.addonName || 'OpenSubtitles v3',
|
||||
source: sub.addonName || sub.addon || 'Addon',
|
||||
}));
|
||||
|
||||
// De-duplicate by language
|
||||
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
|
||||
const exists = acc.find(item => item.language === current.language);
|
||||
if (!exists) {
|
||||
acc.push(current);
|
||||
}
|
||||
return acc;
|
||||
}, [] as WyzieSubtitle[]);
|
||||
// Sort with English languages first, then alphabetical
|
||||
// Sort with English languages first, then alphabetical over full list
|
||||
const isEnglish = (s: WyzieSubtitle) => {
|
||||
const lang = (s.language || '').toLowerCase();
|
||||
const disp = (s.display || '').toLowerCase();
|
||||
return lang === 'en' || lang === 'eng' || /^en([-_]|$)/.test(lang) || disp.includes('english');
|
||||
};
|
||||
uniqueSubtitles.sort((a, b) => {
|
||||
stremioSubs.sort((a, b) => {
|
||||
const aIsEn = isEnglish(a);
|
||||
const bIsEn = isEnglish(b);
|
||||
if (aIsEn && !bIsEn) return -1;
|
||||
if (!aIsEn && bIsEn) return 1;
|
||||
return (a.display || '').localeCompare(b.display || '');
|
||||
});
|
||||
setAvailableSubtitles(uniqueSubtitles);
|
||||
setAvailableSubtitles(stremioSubs);
|
||||
if (autoSelectEnglish) {
|
||||
const englishSubtitle = uniqueSubtitles.find(sub =>
|
||||
const englishSubtitle = stremioSubs.find(sub =>
|
||||
sub.language.toLowerCase() === 'eng' ||
|
||||
sub.language.toLowerCase() === 'en' ||
|
||||
sub.display.toLowerCase().includes('english')
|
||||
|
|
@ -1333,6 +1331,32 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isVideoLoaded && initialPosition && !isInitialSeekComplete && duration > 0) {
|
||||
logger.log(`[AndroidVideoPlayer] Post-load initial seek to: ${initialPosition}s`);
|
||||
seekToTime(initialPosition);
|
||||
setIsInitialSeekComplete(true);
|
||||
// Verify whether the seek actually took effect (detect non-seekable sources)
|
||||
if (!initialSeekVerifiedRef.current) {
|
||||
initialSeekVerifiedRef.current = true;
|
||||
const target = initialSeekTargetRef.current ?? initialPosition;
|
||||
setTimeout(() => {
|
||||
const delta = Math.abs(currentTime - (target || 0));
|
||||
if (target && (currentTime < target - 1.5)) {
|
||||
logger.warn(`[AndroidVideoPlayer] Initial seek appears ignored (delta=${delta.toFixed(2)}). Treating source as non-seekable; starting from 0`);
|
||||
isSourceSeekableRef.current = false;
|
||||
// Reset resume intent and continue from 0
|
||||
setInitialPosition(null);
|
||||
setResumePosition(null);
|
||||
setShowResumeOverlay(false);
|
||||
} else {
|
||||
isSourceSeekableRef.current = true;
|
||||
}
|
||||
}, 1200);
|
||||
}
|
||||
}
|
||||
}, [isVideoLoaded, initialPosition, duration]);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, {
|
||||
width: screenDimensions.width,
|
||||
|
|
@ -1564,16 +1588,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
lineHeightMultiplier={subtitleLineHeightMultiplier}
|
||||
/>
|
||||
|
||||
<ResumeOverlay
|
||||
showResumeOverlay={showResumeOverlay}
|
||||
resumePosition={resumePosition}
|
||||
duration={savedDuration || duration}
|
||||
title={episodeTitle || title}
|
||||
season={season}
|
||||
episode={episode}
|
||||
handleResume={handleResume}
|
||||
handleStartFromBeginning={handleStartFromBeginning}
|
||||
/>
|
||||
{/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */}
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUt
|
|||
import { styles } from './utils/playerStyles';
|
||||
import { SubtitleModals } from './modals/SubtitleModals';
|
||||
import { AudioTrackModal } from './modals/AudioTrackModal';
|
||||
import ResumeOverlay from './modals/ResumeOverlay';
|
||||
// Removed ResumeOverlay usage when alwaysResume is enabled
|
||||
import PlayerControls from './controls/PlayerControls';
|
||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||
import { SourcesModal } from './modals/SourcesModal';
|
||||
|
|
@ -95,8 +95,8 @@ const VideoPlayer: React.FC = () => {
|
|||
episodeId: episodeId
|
||||
});
|
||||
|
||||
// Get the Trakt autosync settings to use the user-configured sync frequency
|
||||
const { settings: traktSettings } = useTraktAutosyncSettings();
|
||||
// App settings
|
||||
const { settings: appSettings } = useSettings();
|
||||
|
||||
safeDebugLog("Component mounted with props", {
|
||||
uri, title, season, episode, episodeTitle, quality, year,
|
||||
|
|
@ -401,10 +401,17 @@ const VideoPlayer: React.FC = () => {
|
|||
|
||||
if (progressPercent < 85) {
|
||||
setResumePosition(savedProgress.currentTime);
|
||||
setSavedDuration(savedProgress.duration);
|
||||
logger.log(`[VideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`);
|
||||
setShowResumeOverlay(true);
|
||||
logger.log(`[VideoPlayer] Showing resume overlay`);
|
||||
setSavedDuration(savedProgress.duration);
|
||||
setInitialPosition(savedProgress.currentTime);
|
||||
logger.log(`[VideoPlayer] Set resume position to: ${savedProgress.currentTime} of ${savedProgress.duration}`);
|
||||
if (appSettings.alwaysResume) {
|
||||
logger.log(`[VideoPlayer] AlwaysResume enabled. Auto-seeking to ${savedProgress.currentTime}`);
|
||||
// Seek immediately after load
|
||||
seekToTime(savedProgress.currentTime);
|
||||
} else {
|
||||
setShowResumeOverlay(true);
|
||||
logger.log(`[VideoPlayer] Showing resume overlay`);
|
||||
}
|
||||
} else {
|
||||
logger.log(`[VideoPlayer] Progress too high (${progressPercent.toFixed(1)}%), not showing resume overlay`);
|
||||
}
|
||||
|
|
@ -419,7 +426,7 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
loadWatchProgress();
|
||||
}, [id, type, episodeId]);
|
||||
}, [id, type, episodeId, appSettings.alwaysResume]);
|
||||
|
||||
const saveWatchProgress = async () => {
|
||||
if (id && type && currentTime > 0 && duration > 0) {
|
||||
|
|
@ -919,7 +926,7 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
setIsLoadingSubtitleList(true);
|
||||
try {
|
||||
// Fetch from installed OpenSubtitles v3 addon via Stremio only
|
||||
// Fetch from all installed subtitle-capable addons via Stremio
|
||||
const stremioType = type === 'series' ? 'series' : 'movie';
|
||||
const stremioVideoId = stremioType === 'series' && season && episode
|
||||
? `series:${targetImdbId}:${season}:${episode}`
|
||||
|
|
@ -931,37 +938,28 @@ const VideoPlayer: React.FC = () => {
|
|||
flagUrl: '',
|
||||
format: 'srt',
|
||||
encoding: 'utf-8',
|
||||
media: 'opensubtitles',
|
||||
media: sub.addonName || sub.addon || '',
|
||||
display: sub.lang || 'Unknown',
|
||||
language: (sub.lang || '').toLowerCase(),
|
||||
isHearingImpaired: false,
|
||||
source: sub.addonName || 'OpenSubtitles v3',
|
||||
source: sub.addonName || sub.addon || 'Addon',
|
||||
}));
|
||||
|
||||
// De-duplicate by language
|
||||
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
|
||||
const exists = acc.find(item => item.language === current.language);
|
||||
if (!exists) {
|
||||
acc.push(current);
|
||||
}
|
||||
return acc;
|
||||
}, [] as WyzieSubtitle[]);
|
||||
// Sort with English languages first, then alphabetical
|
||||
// Sort with English languages first, then alphabetical over full list
|
||||
const isEnglish = (s: WyzieSubtitle) => {
|
||||
const lang = (s.language || '').toLowerCase();
|
||||
const disp = (s.display || '').toLowerCase();
|
||||
return lang === 'en' || lang === 'eng' || /^en([-_]|$)/.test(lang) || disp.includes('english');
|
||||
};
|
||||
uniqueSubtitles.sort((a, b) => {
|
||||
stremioSubs.sort((a, b) => {
|
||||
const aIsEn = isEnglish(a);
|
||||
const bIsEn = isEnglish(b);
|
||||
if (aIsEn && !bIsEn) return -1;
|
||||
if (!aIsEn && bIsEn) return 1;
|
||||
return (a.display || '').localeCompare(b.display || '');
|
||||
});
|
||||
setAvailableSubtitles(uniqueSubtitles);
|
||||
setAvailableSubtitles(stremioSubs);
|
||||
if (autoSelectEnglish) {
|
||||
const englishSubtitle = uniqueSubtitles.find(sub =>
|
||||
const englishSubtitle = stremioSubs.find(sub =>
|
||||
sub.language.toLowerCase() === 'eng' ||
|
||||
sub.language.toLowerCase() === 'en' ||
|
||||
sub.display.toLowerCase().includes('english')
|
||||
|
|
@ -1277,6 +1275,14 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isVideoLoaded && initialPosition && !isInitialSeekComplete && duration > 0) {
|
||||
logger.log(`[VideoPlayer] Post-load initial seek to: ${initialPosition}s`);
|
||||
seekToTime(initialPosition);
|
||||
setIsInitialSeekComplete(true);
|
||||
}
|
||||
}, [isVideoLoaded, initialPosition, duration]);
|
||||
|
||||
return (
|
||||
<View style={[
|
||||
styles.container,
|
||||
|
|
@ -1496,16 +1502,7 @@ const VideoPlayer: React.FC = () => {
|
|||
lineHeightMultiplier={subtitleLineHeightMultiplier}
|
||||
/>
|
||||
|
||||
<ResumeOverlay
|
||||
showResumeOverlay={showResumeOverlay}
|
||||
resumePosition={resumePosition}
|
||||
duration={savedDuration || duration}
|
||||
title={episodeTitle || title}
|
||||
season={season}
|
||||
episode={episode}
|
||||
handleResume={handleResume}
|
||||
handleStartFromBeginning={handleStartFromBeginning}
|
||||
/>
|
||||
{/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */}
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
|
|
|
|||
|
|
@ -102,12 +102,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
subtitleOffsetSec,
|
||||
setSubtitleOffsetSec,
|
||||
}) => {
|
||||
// Track which specific online subtitle is currently loaded
|
||||
// Track which specific addon subtitle is currently loaded
|
||||
const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState<string | null>(null);
|
||||
// Track which online subtitle is currently loading to show spinner per-item
|
||||
// Track which addon subtitle is currently loading to show spinner per-item
|
||||
const [loadingSubtitleId, setLoadingSubtitleId] = React.useState<string | null>(null);
|
||||
// Active tab for better organization
|
||||
const [activeTab, setActiveTab] = React.useState<'built-in' | 'online' | 'appearance'>(useCustomSubtitles ? 'online' : 'built-in');
|
||||
const [activeTab, setActiveTab] = React.useState<'built-in' | 'addon' | 'appearance'>(useCustomSubtitles ? 'addon' : 'built-in');
|
||||
// Responsive tuning
|
||||
const isCompact = width < 360 || height < 640;
|
||||
const sectionPad = isCompact ? 12 : 16;
|
||||
|
|
@ -122,7 +122,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
}
|
||||
}, [showSubtitleModal]);
|
||||
|
||||
// Reset selected online subtitle when switching to built-in tracks
|
||||
// Reset selected addon subtitle when switching to built-in tracks
|
||||
React.useEffect(() => {
|
||||
if (!useCustomSubtitles) {
|
||||
setSelectedOnlineSubtitleId(null);
|
||||
|
|
@ -138,7 +138,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
|
||||
// Keep tab in sync with current usage
|
||||
React.useEffect(() => {
|
||||
setActiveTab(useCustomSubtitles ? 'online' : 'built-in');
|
||||
setActiveTab(useCustomSubtitles ? 'addon' : 'built-in');
|
||||
}, [useCustomSubtitles]);
|
||||
|
||||
const handleClose = () => {
|
||||
|
|
@ -214,7 +214,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
<Text style={{ color: '#FFFFFF', fontSize: 22, fontWeight: '700' }}>Subtitles</Text>
|
||||
<View style={{ paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, backgroundColor: useCustomSubtitles ? 'rgba(34,197,94,0.2)' : 'rgba(59,130,246,0.2)' }}>
|
||||
<Text style={{ color: useCustomSubtitles ? '#22C55E' : '#3B82F6', fontSize: 11, fontWeight: '700' }}>
|
||||
{useCustomSubtitles ? 'Online in use' : 'Built‑in in use'}
|
||||
{useCustomSubtitles ? 'Addon in use' : 'Built‑in in use'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -238,7 +238,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
<View style={{ flexDirection: 'row', gap: 8, paddingHorizontal: 20, paddingTop: 10, paddingBottom: 6 }}>
|
||||
{([
|
||||
{ key: 'built-in', label: 'Built‑in' },
|
||||
{ key: 'online', label: 'Online' },
|
||||
{ key: 'addon', label: 'Addons' },
|
||||
{ key: 'appearance', label: 'Appearance' },
|
||||
] as const).map(tab => (
|
||||
<TouchableOpacity
|
||||
|
|
@ -315,7 +315,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'online' && (
|
||||
{activeTab === 'addon' && (
|
||||
<View style={{ marginBottom: 30 }}>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
|
|
@ -330,7 +330,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
}}>
|
||||
Online Subtitles
|
||||
Addon Subtitles
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
|
|
@ -381,7 +381,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
marginTop: 8,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
Tap to search online
|
||||
Tap to fetch from addons
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : isLoadingSubtitleList ? (
|
||||
|
|
@ -426,7 +426,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
{sub.display}
|
||||
</Text>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 13 }}>
|
||||
{formatLanguage(sub.language)}
|
||||
{formatLanguage(sub.language)}{sub.source ? ` · ${sub.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
{(isLoadingSubtitles && loadingSubtitleId === sub.id) ? (
|
||||
|
|
|
|||
|
|
@ -43,9 +43,11 @@ export interface AppSettings {
|
|||
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
|
||||
streamDisplayMode: 'separate' | 'grouped'; // How to display streaming links - separately by provider or grouped under one name
|
||||
streamSortMode: 'scraper-then-quality' | 'quality-then-scraper'; // How to sort streams - by scraper first or quality first
|
||||
showScraperLogos: boolean; // Show/hide scraper logos next to streaming links
|
||||
showScraperLogos: boolean; // Show scraper logos next to streaming links
|
||||
// Quality filtering settings
|
||||
excludedQualities: string[]; // Array of quality strings to exclude (e.g., ['2160p', '4K', '1080p', '720p'])
|
||||
// Playback behavior
|
||||
alwaysResume: boolean; // If true, resume automatically without prompt when progress < 85%
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
|
|
@ -74,6 +76,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
showScraperLogos: true, // Show scraper logos by default
|
||||
// Quality filtering defaults
|
||||
excludedQualities: [], // No qualities excluded by default
|
||||
// Playback behavior defaults
|
||||
alwaysResume: false,
|
||||
};
|
||||
|
||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||
|
|
|
|||
|
|
@ -265,18 +265,51 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
{ color: currentTheme.colors.textMuted },
|
||||
]}
|
||||
>
|
||||
Automatically play the highest quality stream when available
|
||||
Automatically start the highest quality stream available.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.autoplayBestStream}
|
||||
onValueChange={(value) => updateSetting('autoplayBestStream', value)}
|
||||
trackColor={{
|
||||
false: 'rgba(255,255,255,0.2)',
|
||||
true: currentTheme.colors.primary + '40'
|
||||
}}
|
||||
thumbColor={settings.autoplayBestStream ? currentTheme.colors.primary : 'rgba(255,255,255,0.8)'}
|
||||
ios_backgroundColor="rgba(255,255,255,0.2)"
|
||||
thumbColor={settings.autoplayBestStream ? currentTheme.colors.primary : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={[
|
||||
styles.settingIconContainer,
|
||||
{ backgroundColor: 'rgba(255,255,255,0.1)' }
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name="restore"
|
||||
size={20}
|
||||
color={currentTheme.colors.primary}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.settingText}>
|
||||
<Text
|
||||
style={[
|
||||
styles.settingTitle,
|
||||
{ color: currentTheme.colors.text },
|
||||
]}
|
||||
>
|
||||
Always Resume
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.settingDescription,
|
||||
{ color: currentTheme.colors.textMuted },
|
||||
]}
|
||||
>
|
||||
Skip the resume prompt and automatically continue where you left off (if less than 85% watched).
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.alwaysResume}
|
||||
onValueChange={(value) => updateSetting('alwaysResume', value)}
|
||||
thumbColor={settings.alwaysResume ? currentTheme.colors.primary : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -265,6 +265,40 @@ class StremioService {
|
|||
logger.log('✅ Cinemeta pre-installed with fallback manifest');
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure OpenSubtitles v3 is always installed as a pre-installed addon
|
||||
const opensubsId = 'org.stremio.opensubtitlesv3';
|
||||
if (!this.installedAddons.has(opensubsId)) {
|
||||
try {
|
||||
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||
this.installedAddons.set(opensubsId, opensubsManifest);
|
||||
logger.log('✅ OpenSubtitles v3 pre-installed as default subtitles addon');
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch OpenSubtitles manifest, using fallback:', error);
|
||||
const fallbackManifest: Manifest = {
|
||||
id: opensubsId,
|
||||
name: 'OpenSubtitles v3',
|
||||
version: '1.0.0',
|
||||
description: 'OpenSubtitles v3 Addon for Stremio',
|
||||
url: 'https://opensubtitles-v3.strem.io',
|
||||
originalUrl: 'https://opensubtitles-v3.strem.io/manifest.json',
|
||||
types: ['movie', 'series'],
|
||||
catalogs: [],
|
||||
resources: [
|
||||
{
|
||||
name: 'subtitles',
|
||||
types: ['movie', 'series'],
|
||||
idPrefixes: ['tt']
|
||||
}
|
||||
],
|
||||
behaviorHints: {
|
||||
configurable: false
|
||||
}
|
||||
};
|
||||
this.installedAddons.set(opensubsId, fallbackManifest);
|
||||
logger.log('✅ OpenSubtitles v3 pre-installed with fallback manifest');
|
||||
}
|
||||
}
|
||||
|
||||
// Load addon order if exists
|
||||
const storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY);
|
||||
|
|
@ -285,6 +319,21 @@ class StremioService {
|
|||
this.addonOrder.unshift(cinemetaId);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure OpenSubtitles v3 is present right after Cinemeta (if not already ordered)
|
||||
const ensureOpensubsPosition = () => {
|
||||
const idx = this.addonOrder.indexOf(opensubsId);
|
||||
const cinIdx = this.addonOrder.indexOf(cinemetaId);
|
||||
if (idx === -1) {
|
||||
// Insert after Cinemeta
|
||||
this.addonOrder.splice(cinIdx + 1, 0, opensubsId);
|
||||
} else if (idx <= cinIdx) {
|
||||
// Move it to right after Cinemeta
|
||||
this.addonOrder.splice(idx, 1);
|
||||
this.addonOrder.splice(cinIdx + 1, 0, opensubsId);
|
||||
}
|
||||
};
|
||||
ensureOpensubsPosition();
|
||||
|
||||
// Add any missing addons to the order
|
||||
const installedIds = Array.from(this.installedAddons.keys());
|
||||
|
|
@ -1103,52 +1152,59 @@ class StremioService {
|
|||
|
||||
async getSubtitles(type: string, id: string, videoId?: string): Promise<Subtitle[]> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
// Find the OpenSubtitles v3 addon
|
||||
const openSubtitlesAddon = this.getInstalledAddons().find(
|
||||
addon => addon.id === 'org.stremio.opensubtitlesv3'
|
||||
);
|
||||
|
||||
if (!openSubtitlesAddon) {
|
||||
logger.warn('OpenSubtitles v3 addon not found');
|
||||
// Collect from all installed addons that expose a subtitles resource
|
||||
const addons = this.getInstalledAddons();
|
||||
const subtitleAddons = addons.filter(addon => {
|
||||
if (!addon.resources) return false;
|
||||
return addon.resources.some((resource: any) => {
|
||||
if (typeof resource === 'string') return resource === 'subtitles';
|
||||
return resource && resource.name === 'subtitles';
|
||||
});
|
||||
});
|
||||
|
||||
if (subtitleAddons.length === 0) {
|
||||
logger.warn('No subtitle-capable addons installed');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = this.getAddonBaseURL(openSubtitlesAddon.url || '').baseUrl;
|
||||
|
||||
// Construct the query URL with the correct format
|
||||
// For series episodes, use the videoId directly which includes series ID + episode info
|
||||
let url = '';
|
||||
if (type === 'series' && videoId) {
|
||||
// For series, extract the IMDB ID and episode info from videoId (series:tt12345:1:2)
|
||||
// and construct the proper URL format: /subtitles/series/tt12345:1:2.json
|
||||
const episodeInfo = videoId.replace('series:', '');
|
||||
url = `${baseUrl}/subtitles/series/${episodeInfo}.json`;
|
||||
} else {
|
||||
// For movies, the format is /subtitles/movie/tt12345.json
|
||||
url = `${baseUrl}/subtitles/${type}/${id}.json`;
|
||||
|
||||
const requests = subtitleAddons.map(async (addon) => {
|
||||
if (!addon.url) return [] as Subtitle[];
|
||||
try {
|
||||
const { baseUrl } = this.getAddonBaseURL(addon.url || '');
|
||||
let url = '';
|
||||
if (type === 'series' && videoId) {
|
||||
const episodeInfo = videoId.replace('series:', '');
|
||||
url = `${baseUrl}/subtitles/series/${episodeInfo}.json`;
|
||||
} else {
|
||||
url = `${baseUrl}/subtitles/${type}/${id}.json`;
|
||||
}
|
||||
logger.log(`Fetching subtitles from ${addon.name}: ${url}`);
|
||||
const response = await this.retryRequest(async () => axios.get(url, { timeout: 10000 }));
|
||||
if (response.data && Array.isArray(response.data.subtitles)) {
|
||||
return response.data.subtitles.map((sub: any) => ({
|
||||
...sub,
|
||||
addon: addon.id,
|
||||
addonName: addon.name,
|
||||
})) as Subtitle[];
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch subtitles from ${addon.name}:`, error);
|
||||
}
|
||||
|
||||
logger.log(`Fetching subtitles from: ${url}`);
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
if (response.data && response.data.subtitles) {
|
||||
// Process and return the subtitles
|
||||
return response.data.subtitles.map((sub: any) => ({
|
||||
...sub,
|
||||
addon: openSubtitlesAddon.id,
|
||||
addonName: openSubtitlesAddon.name
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch subtitles:', error);
|
||||
}
|
||||
|
||||
return [];
|
||||
return [] as Subtitle[];
|
||||
});
|
||||
|
||||
const all = await Promise.all(requests);
|
||||
// Flatten and de-duplicate by URL
|
||||
const merged = ([] as Subtitle[]).concat(...all);
|
||||
const seen = new Set<string>();
|
||||
const deduped = merged.filter(s => {
|
||||
const key = s.url;
|
||||
if (!key) return false;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
return deduped;
|
||||
}
|
||||
|
||||
// Add methods to move addons in the order
|
||||
|
|
|
|||
Loading…
Reference in a new issue