diff --git a/src/common/index.js b/src/common/index.js index 1b248c1ff..1d564ad38 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -26,6 +26,7 @@ const { default: useSettings } = require('./useSettings'); const { default: useShell } = require('./useShell'); const useStreamingServer = require('./useStreamingServer'); const { default: useTimeout } = require('./useTimeout'); +const { default: usePlayUrl } = require('./usePlayUrl'); const useTorrent = require('./useTorrent'); const useTranslate = require('./useTranslate'); const { default: useOrientation } = require('./useOrientation'); @@ -65,6 +66,7 @@ module.exports = { useShell, useStreamingServer, useTimeout, + usePlayUrl, useTorrent, useTranslate, useOrientation, diff --git a/src/common/usePlayUrl.ts b/src/common/usePlayUrl.ts new file mode 100644 index 000000000..4e537a592 --- /dev/null +++ b/src/common/usePlayUrl.ts @@ -0,0 +1,59 @@ +import { useCallback } from 'react'; +import magnet from 'magnet-uri'; +import { useServices } from 'stremio/services'; +import useToast from 'stremio/common/Toast/useToast'; +import useTorrent from 'stremio/common/useTorrent'; +import useStreamingServer from 'stremio/common/useStreamingServer'; + +const HTTP_REGEX = /^https?:\/\/.+/i; + +const usePlayUrl = () => { + const { core } = useServices(); + const toast = useToast(); + const { createTorrentFromMagnet } = useTorrent(); + const streamingServer = useStreamingServer(); + + const handlePlayUrl = useCallback(async (text: string): Promise => { + if (!text || !text.trim()) return false; + const trimmed = text.trim(); + + if (HTTP_REGEX.test(trimmed)) { + try { + const encoded = await core.transport.encodeStream({ url: trimmed }); + if (typeof encoded === 'string') { + window.location.hash = `#/player/${encodeURIComponent(encoded)}`; + return true; + } + } catch (e) { + console.error('Failed to encode stream:', e); + } + return false; + } + + try { + const parsed = magnet.decode(trimmed); + if (parsed && typeof parsed.infoHash === 'string') { + const serverReady = streamingServer.settings !== null + && streamingServer.settings.type === 'Ready'; + if (!serverReady) { + toast.show({ + type: 'error', + title: 'Streaming server is not available. Cannot play magnet links.', + timeout: 5000 + }); + return false; + } + createTorrentFromMagnet(trimmed); + return true; + } + } catch (e) { + // Not a valid magnet + } + + return false; + }, [streamingServer.settings, createTorrentFromMagnet]); + + return { handlePlayUrl }; +}; + +export default usePlayUrl; diff --git a/src/common/useTorrent.js b/src/common/useTorrent.js index 0ae117d3a..64b529b04 100644 --- a/src/common/useTorrent.js +++ b/src/common/useTorrent.js @@ -6,6 +6,8 @@ const { useServices } = require('stremio/services'); const useToast = require('stremio/common/Toast/useToast'); const useStreamingServer = require('stremio/common/useStreamingServer'); +const CREATE_TORRENT_TIMEOUT = 20000; + const useTorrent = () => { const { core } = useServices(); const streamingServer = useStreamingServer(); @@ -25,10 +27,10 @@ const useTorrent = () => { createTorrentTimeout.current = setTimeout(() => { toast.show({ type: 'error', - title: 'It\'s taking a long time to get metadata from the torrent.', - timeout: 10000 + title: 'Failed to get metadata from the torrent. No peers found.', + timeout: 8000 }); - }, 10000); + }, CREATE_TORRENT_TIMEOUT); } }, []); React.useEffect(() => { diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index 4b2fdc87e..b6309a44c 100644 --- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -10,7 +10,8 @@ const { Button } = require('stremio/components'); const { default: useFullscreen } = require('stremio/common/useFullscreen'); const useProfile = require('stremio/common/useProfile'); const usePWA = require('stremio/common/usePWA'); -const useTorrent = require('stremio/common/useTorrent'); +const { default: usePlayUrl } = require('stremio/common/usePlayUrl'); +const useToast = require('stremio/common/Toast/useToast'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const useStreamingServer = require('stremio/common/useStreamingServer'); const styles = require('./styles'); @@ -20,7 +21,8 @@ const NavMenuContent = ({ onClick }) => { const { core } = useServices(); const profile = useProfile(); const streamingServer = useStreamingServer(); - const { createTorrentFromMagnet } = useTorrent(); + const { handlePlayUrl } = usePlayUrl(); + const toast = useToast(); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); const [isIOSPWA, isAndroidPWA] = usePWA(); const streamingServerWarningDismissed = React.useMemo(() => { @@ -40,11 +42,18 @@ const NavMenuContent = ({ onClick }) => { const onPlayMagnetLinkClick = React.useCallback(async () => { try { const clipboardText = await navigator.clipboard.readText(); - createTorrentFromMagnet(clipboardText); + const handled = await handlePlayUrl(clipboardText); + if (!handled) { + toast.show({ + type: 'error', + title: 'Clipboard does not contain a valid URL or magnet link.', + timeout: 5000 + }); + } } catch(e) { console.error(e); } - }, []); + }, [handlePlayUrl]); return (
diff --git a/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js b/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js index 0bc95cd73..e8f58bdff 100644 --- a/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js +++ b/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js @@ -9,7 +9,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { useRouteFocused } = require('stremio-router'); const Button = require('stremio/components/Button').default; const TextInput = require('stremio/components/TextInput').default; -const useTorrent = require('stremio/common/useTorrent'); +const { default: usePlayUrl } = require('stremio/common/usePlayUrl'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const useSearchHistory = require('./useSearchHistory'); const useLocalSearch = require('./useLocalSearch'); @@ -21,7 +21,7 @@ const SearchBar = React.memo(({ className, query, active }) => { const routeFocused = useRouteFocused(); const searchHistory = useSearchHistory(); const localSearch = useLocalSearch(); - const { createTorrentFromMagnet } = useTorrent(); + const { handlePlayUrl } = usePlayUrl(); const [historyOpen, openHistory, closeHistory, ] = useBinaryState(query === null ? true : false); const [currentQuery, setCurrentQuery] = React.useState(query || ''); @@ -52,12 +52,14 @@ const SearchBar = React.memo(({ className, query, active }) => { const value = searchInputRef.current.value; setCurrentQuery(value); openHistory(); - try { - createTorrentFromMagnet(value); - } catch (error) { - console.error('Failed to create torrent from magnet:', error); + }, []); + + const queryInputOnPaste = React.useCallback((event) => { + const pasted = event.clipboardData.getData('text'); + if (pasted) { + handlePlayUrl(pasted); } - }, [createTorrentFromMagnet]); + }, [handlePlayUrl]); const queryInputOnSubmit = React.useCallback((event) => { event.preventDefault(); @@ -108,6 +110,7 @@ const SearchBar = React.memo(({ className, query, active }) => { defaultValue={query} tabIndex={-1} onChange={queryInputOnChange} + onPaste={queryInputOnPaste} onSubmit={queryInputOnSubmit} onClick={openHistory} /> diff --git a/src/services/Core/CoreTransport.js b/src/services/Core/CoreTransport.js index b140551f6..c497d75c6 100644 --- a/src/services/Core/CoreTransport.js +++ b/src/services/Core/CoreTransport.js @@ -52,6 +52,9 @@ function CoreTransport(args) { this.decodeStream = async function(stream) { return bridge.call(['decodeStream'], [stream]); }; + this.encodeStream = async function(stream) { + return bridge.call(['encodeStream'], [stream]); + }; } module.exports = CoreTransport; diff --git a/src/services/Core/types.d.ts b/src/services/Core/types.d.ts index 7e4249ac8..668479e12 100644 --- a/src/services/Core/types.d.ts +++ b/src/services/Core/types.d.ts @@ -17,6 +17,7 @@ interface CoreTransport { getState: (model: string) => Promise, dispatch: (action: Action, model?: string) => Promise, decodeStream: (stream: string) => Promise, + encodeStream: (stream: object) => Promise, analytics: (event: AnalyticsEvent) => Promise, on: (name: string, listener: () => void) => void, off: (name: string, listener: () => void) => void,