some fixes

This commit is contained in:
tapframe 2025-08-08 18:22:55 +05:30
parent dff3a66d7b
commit d8950caf04
6 changed files with 238 additions and 133 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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' : 'Builtin in use'}
{useCustomSubtitles ? 'Addon in use' : 'Builtin 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: 'Builtin' },
{ 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) ? (

View file

@ -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';

View file

@ -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>

View file

@ -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