diff --git a/src/common/LibItem/LibItem.js b/src/common/LibItem/LibItem.js index 610a39eeb..b1000aa37 100644 --- a/src/common/LibItem/LibItem.js +++ b/src/common/LibItem/LibItem.js @@ -9,22 +9,20 @@ const OPTIONS = [ { label: 'Dismiss', value: 'dismiss' } ]; -const LibItem = ({ id, videoId, ...props }) => { +const LibItem = ({ id, ...props }) => { const { core } = useServices(); const options = React.useMemo(() => { return OPTIONS.filter(({ value }) => { - return value !== 'dismiss' || (props.progress !== null && !isNaN(props.progress)); + switch (value) { + case 'play': + return props.deepLinks && typeof props.deepLinks.player === 'string'; + case 'details': + return props.deepLinks && (typeof props.deepLinks.meta_details_videos === 'string' || typeof props.deepLinks.meta_details_streams === 'string'); + case 'dismiss': + return typeof id === 'string' && props.progress !== null && !isNaN(props.progress); + } }); - }, [props.progress]); - const playIcon = React.useMemo(() => { - return props.progress !== null && !isNaN(props.progress); - }, [props.progress]); - const dataset = React.useMemo(() => ({ - id, - videoId, - type: props.type, - ...props.dataset - }), [id, videoId, props.type, props.dataset]); + }, [id, props.progress, props.deepLinks]); const optionOnSelect = React.useCallback((event) => { if (typeof props.optionOnSelect === 'function') { props.optionOnSelect(event); @@ -33,32 +31,30 @@ const LibItem = ({ id, videoId, ...props }) => { if (!event.nativeEvent.optionSelectPrevented) { switch (event.value) { case 'play': { - if (typeof event.dataset.id === 'string' && typeof event.dataset.type === 'string') { - // TODO check streams storage - // TODO check behaviour_hints - // TODO add videos page to the history stack if needed - window.location = `#/metadetails/${encodeURIComponent(event.dataset.type)}/${encodeURIComponent(event.dataset.id)}${typeof event.dataset.videoId === 'string' ? `/${encodeURIComponent(event.dataset.videoId)}` : ''}`; + if (props.deepLinks && typeof props.deepLinks.player === 'string') { + window.location = props.deepLinks.player; } break; } case 'details': { - if (typeof event.dataset.id === 'string' && typeof event.dataset.type === 'string') { - // TODO check streams storage - // TODO check behaviour_hints - // TODO add videos page to the history stack if needed - window.location = `#/metadetails/${encodeURIComponent(event.dataset.type)}/${encodeURIComponent(event.dataset.id)}${typeof event.dataset.videoId === 'string' ? `/${encodeURIComponent(event.dataset.videoId)}` : ''}`; + if (props.deepLinks) { + if (typeof props.deepLinks.meta_details_videos === 'string') { + window.location = props.deepLinks.meta_details_videos; + } else if (typeof props.deepLinks.meta_details_streams === 'string') { + window.location = props.deepLinks.meta_details_streams; + } } break; } case 'dismiss': { - if (typeof event.dataset.id === 'string') { + if (typeof id === 'string') { core.dispatch({ action: 'Ctx', args: { action: 'RewindLibraryItem', - args: event.dataset.id + args: id } }); } @@ -67,13 +63,12 @@ const LibItem = ({ id, videoId, ...props }) => { } } } - }, [props.optionOnSelect]); + }, [id, props.deepLinks, props.optionOnSelect]); return ( ); @@ -81,10 +76,12 @@ const LibItem = ({ id, videoId, ...props }) => { LibItem.propTypes = { id: PropTypes.string, - videoId: PropTypes.string, - type: PropTypes.string, progress: PropTypes.number, - dataset: PropTypes.object, + deepLinks: PropTypes.shape({ + meta_details_videos: PropTypes.string, + meta_details_streams: PropTypes.string, + player: PropTypes.string + }), optionOnSelect: PropTypes.func }; diff --git a/src/common/MetaItem/MetaItem.js b/src/common/MetaItem/MetaItem.js index acce287ad..95885a018 100644 --- a/src/common/MetaItem/MetaItem.js +++ b/src/common/MetaItem/MetaItem.js @@ -23,8 +23,23 @@ const ICON_FOR_TYPE = new Map([ ['other', 'ic_movies'], ]); -const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, options, dataset, optionOnSelect, ...props }) => { +const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, options, deepLinks, dataset, optionOnSelect, ...props }) => { const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false); + const href = React.useMemo(() => { + return deepLinks ? + typeof deepLinks.player === 'string' ? + deepLinks.player + : + typeof deepLinks.meta_details_streams === 'string' ? + deepLinks.meta_details_streams + : + typeof deepLinks.meta_details_videos === 'string' ? + deepLinks.meta_details_videos + : + null + : + null; + }, [deepLinks]); const metaItemOnClick = React.useCallback((event) => { if (typeof props.onClick === 'function') { props.onClick(event); @@ -58,7 +73,7 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI ), []); return ( - @@ -69,7 +69,9 @@ MetaRow.propTypes = { posterShape: PropTypes.string })), itemComponent: PropTypes.elementType, - href: PropTypes.string + deepLinks: PropTypes.shape({ + discover: PropTypes.string + }) }; module.exports = MetaRow; diff --git a/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js b/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js index 9f0398b9f..79de6b370 100644 --- a/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js +++ b/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js @@ -6,7 +6,7 @@ const Button = require('stremio/common/Button'); const CONSTANTS = require('stremio/common/CONSTANTS'); const styles = require('./styles'); -const MetaRowPlaceholder = ({ className, title, href }) => { +const MetaRowPlaceholder = ({ className, title, deepLinks }) => { return (
@@ -14,8 +14,8 @@ const MetaRowPlaceholder = ({ className, title, href }) => { {typeof title === 'string' && title.length > 0 ? title : null}
{ - typeof href === 'string' && href.length > 0 ? - @@ -40,7 +40,9 @@ const MetaRowPlaceholder = ({ className, title, href }) => { MetaRowPlaceholder.propTypes = { className: PropTypes.string, title: PropTypes.string, - href: PropTypes.string + deepLinks: PropTypes.shape({ + discover: PropTypes.string + }) }; module.exports = MetaRowPlaceholder; diff --git a/src/common/deepLinking.js b/src/common/deepLinking.js new file mode 100644 index 000000000..efe3a11a1 --- /dev/null +++ b/src/common/deepLinking.js @@ -0,0 +1,90 @@ +const pako = require('pako'); + +const serializeStream = (stream) => { + return btoa(pako.deflate(JSON.stringify(stream), { to: 'string' })); +}; + +const deserializeStream = (stream) => { + return JSON.parse(pako.inflate(atob(stream), { to: 'string' })); +}; + +const withMetaItem = ({ metaItem }) => { + return { + meta_details_videos: typeof metaItem.behaviorHints.defaultVideoId !== 'string' ? + `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}` + : + null, + meta_details_streams: typeof metaItem.behaviorHints.defaultVideoId === 'string' ? + `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(metaItem.behaviorHints.defaultVideoId)}` + : + null + }; +}; + +const withLibItem = ({ libItem, streams = {} }) => { + const [stream, streamTransportUrl, metaTransportUrl] = typeof libItem.state.video_id === 'string' && typeof streams[`${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}`] === 'object' ? + streams[`${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}`] + : + []; + return { + meta_details_videos: typeof libItem.behaviorHints.defaultVideoId !== 'string' ? + `#/metadetails/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}` + : + null, + meta_details_streams: typeof libItem.state.video_id === 'string' ? + `#/metadetails/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}` + : + typeof libItem.behaviorHints.defaultVideoId === 'string' ? + `#/metadetails/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.behaviorHints.defaultVideoId)}` + : + null, + // TODO check if stream is external + player: typeof libItem.state.video_id === 'string' && typeof stream === 'object' && typeof streamTransportUrl === 'string' && typeof metaTransportUrl === 'string' ? + `#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(libItem.type)}/${encodeURIComponent(libItem._id)}/${encodeURIComponent(libItem.state.video_id)}` + : + null + }; +}; + +const withVideo = ({ video, metaTransportUrl, metaItem, streams = {} }) => { + const [stream, streamTransportUrl] = typeof streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`] === 'object' ? + streams[`${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`] + : + []; + return { + meta_details_streams: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}`, + // TODO check if stream is external + player: typeof stream === 'object' && typeof streamTransportUrl === 'string' ? + `#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}` + : + Array.isArray(video.streams) && video.streams.length === 1 ? + `#/player/${encodeURIComponent(serializeStream(video.streams[0]))}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}/${encodeURIComponent(video.id)}` + : + null + }; +}; + +const withStream = ({ stream, streamTransportUrl, metaTransportUrl, type, id, videoId }) => { + return { + player: typeof metaTransportUrl === 'string' && typeof type === 'string' && typeof id === 'string' && typeof videoId === 'string' ? + `#/player/${encodeURIComponent(serializeStream(stream))}/${encodeURIComponent(streamTransportUrl)}/${encodeURIComponent(metaTransportUrl)}/${encodeURIComponent(type)}/${encodeURIComponent(id)}/${encodeURIComponent(videoId)}` + : + `#/player/${encodeURIComponent(serializeStream(stream))}` + }; +}; + +const withCatalog = ({ request }) => { + return { + discover: `#/discover/${encodeURIComponent(request.base)}/${encodeURIComponent(request.path.type_name)}/${encodeURIComponent(request.path.id)}?${new URLSearchParams(request.path.extra).toString()}` + }; +}; + +module.exports = { + withCatalog, + withMetaItem, + withLibItem, + withVideo, + withStream, + serializeStream, + deserializeStream +}; diff --git a/src/common/index.js b/src/common/index.js index 4edbb546e..c9fb1587f 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -21,6 +21,7 @@ const TextInput = require('./TextInput'); const { ToastProvider, useToast } = require('./Toast'); const comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); +const deepLinking = require('./deepLinking'); const languageNames = require('./languageNames'); const routesRegexp = require('./routesRegexp'); const useAnimationFrame = require('./useAnimationFrame'); @@ -60,6 +61,7 @@ module.exports = { useToast, comparatorWithPriorities, CONSTANTS, + deepLinking, languageNames, routesRegexp, useAnimationFrame, diff --git a/src/routes/Board/Board.js b/src/routes/Board/Board.js index 9cc5f26e5..ec01a4c63 100644 --- a/src/routes/Board/Board.js +++ b/src/routes/Board/Board.js @@ -18,13 +18,12 @@ const Board = () => { title={'Continue Watching'} items={continueWatchingPreview.lib_items} itemComponent={LibItem} - href={'#/continuewatching'} + deepLinks={continueWatchingPreview.deepLinks} /> : null } {board.catalog_resources.map((catalog_resource, index) => { - const href = `#/discover/${encodeURIComponent(catalog_resource.request.base)}/${encodeURIComponent(catalog_resource.request.path.type_name)}/${encodeURIComponent(catalog_resource.request.path.id)}`; const title = `${catalog_resource.origin} - ${catalog_resource.request.path.id} ${catalog_resource.request.path.type_name}`; switch (catalog_resource.content.type) { case 'Ready': { @@ -39,7 +38,7 @@ const Board = () => { title={title} items={catalog_resource.content.content} itemComponent={MetaItem} - href={href} + deepLinks={catalog_resource.deepLinks} /> ); } @@ -51,7 +50,7 @@ const Board = () => { className={styles['board-row']} title={title} message={message} - href={href} + deepLinks={catalog_resource.deepLinks} /> ); } @@ -61,7 +60,7 @@ const Board = () => { key={index} className={classnames(styles['board-row'], styles['board-row-poster'])} title={title} - href={href} + deepLinks={catalog_resource.deepLinks} /> ); } diff --git a/src/routes/Board/styles.less b/src/routes/Board/styles.less index 38542adfd..a12f2e402 100644 --- a/src/routes/Board/styles.less +++ b/src/routes/Board/styles.less @@ -44,7 +44,7 @@ } } - .board-row-landscape { + .board-row-landscape, .continue-watching-row { .meta-item, .meta-item-placeholder { &:nth-child(n+9) { display: none; @@ -58,14 +58,6 @@ @media only screen and (max-width: @normal) { .board-container { .board-content { - .continue-watching-row { - .meta-item, .meta-item-placeholder { - &:nth-child(n+10) { - display: none; - } - } - } - .board-row-poster, .board-row-square { .meta-item, .meta-item-placeholder { &:nth-child(n+9) { @@ -74,7 +66,7 @@ } } - .board-row-landscape { + .board-row-landscape, .continue-watching-row { .meta-item, .meta-item-placeholder { &:nth-child(n+8) { display: none; @@ -88,14 +80,6 @@ @media only screen and (max-width: @medium) { .board-container { .board-content { - .continue-watching-row { - .meta-item, .meta-item-placeholder { - &:nth-child(n+9) { - display: none; - } - } - } - .board-row-poster, .board-row-square { .meta-item, .meta-item-placeholder { &:nth-child(n+8) { @@ -104,7 +88,7 @@ } } - .board-row-landscape { + .board-row-landscape, .continue-watching-row { .meta-item, .meta-item-placeholder { &:nth-child(n+7) { display: none; @@ -118,14 +102,6 @@ @media only screen and (max-width: @small) { .board-container { .board-content { - .continue-watching-row { - .meta-item, .meta-item-placeholder { - &:nth-child(n+8) { - display: none; - } - } - } - .board-row-poster, .board-row-square { .meta-item, .meta-item-placeholder { &:nth-child(n+7) { @@ -134,7 +110,7 @@ } } - .board-row-landscape { + .board-row-landscape, .continue-watching-row { .meta-item, .meta-item-placeholder { &:nth-child(n+6) { display: none; @@ -148,14 +124,6 @@ @media only screen and (max-width: @xsmall) { .board-container { .board-content { - .continue-watching-row { - .meta-item, .meta-item-placeholder { - &:nth-child(n+7) { - display: none; - } - } - } - .board-row-poster, .board-row-square { .meta-item, .meta-item-placeholder { &:nth-child(n+6) { @@ -164,7 +132,7 @@ } } - .board-row-landscape { + .board-row-landscape, .continue-watching-row { .meta-item, .meta-item-placeholder { &:nth-child(n+5) { display: none; @@ -178,14 +146,6 @@ @media only screen and (max-width: @minimum) { .board-container { .board-content { - .continue-watching-row { - .meta-item, .meta-item-placeholder { - &:nth-child(n+6) { - display: none; - } - } - } - .board-row-poster, .board-row-square { .meta-item, .meta-item-placeholder { &:nth-child(n+5) { @@ -194,7 +154,7 @@ } } - .board-row-landscape { + .board-row-landscape, .continue-watching-row { .meta-item, .meta-item-placeholder { &:nth-child(n+4) { display: none; diff --git a/src/routes/Board/useBoard.js b/src/routes/Board/useBoard.js index 8064c6da7..b59629b38 100644 --- a/src/routes/Board/useBoard.js +++ b/src/routes/Board/useBoard.js @@ -1,5 +1,5 @@ const React = require('react'); -const { useModelState } = require('stremio/common'); +const { deepLinking, useModelState } = require('stremio/common'); const initBoardState = () => ({ selected: null, @@ -18,7 +18,7 @@ const mapBoardStateWithCtx = (board, ctx) => { name: metaItem.name, poster: metaItem.poster, posterShape: metaItems[0].posterShape, - href: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}` // TODO this should redirect with videoId at some cases + deepLinks: deepLinking.withMetaItem({ metaItem }) })) } : @@ -33,7 +33,8 @@ const mapBoardStateWithCtx = (board, ctx) => { return origin; }, catalog_resource.request.base); - return { request, content, origin }; + const deepLinks = deepLinking.withCatalog({ request }); + return { request, content, origin, deepLinks }; }); return { selected, catalog_resources }; }; diff --git a/src/routes/Board/useContinueWatchingPreview.js b/src/routes/Board/useContinueWatchingPreview.js index cebe44b20..d13c8f210 100644 --- a/src/routes/Board/useContinueWatchingPreview.js +++ b/src/routes/Board/useContinueWatchingPreview.js @@ -1,22 +1,22 @@ const React = require('react'); const { useServices } = require('stremio/services'); -const { useModelState } = require('stremio/common'); +const { deepLinking, useModelState } = require('stremio/common'); const mapContinueWatchingPreviewState = (continue_watching_preview) => { - const lib_items = continue_watching_preview.lib_items.map((lib_item) => ({ - id: lib_item._id, - type: lib_item.type, - name: lib_item.name, - poster: lib_item.poster, - posterShape: lib_item.posterShape === 'landscape' ? 'square' : lib_item.posterShape, - videoId: lib_item.state.video_id, - progress: lib_item.state.timeOffset > 0 && lib_item.state.duration > 0 ? - lib_item.state.timeOffset / lib_item.state.duration + const lib_items = continue_watching_preview.lib_items.map((libItem) => ({ + id: libItem._id, + type: libItem.type, + name: libItem.name, + poster: libItem.poster, + posterShape: libItem.posterShape === 'landscape' ? 'square' : libItem.posterShape, + progress: libItem.state.timeOffset > 0 && libItem.state.duration > 0 ? + libItem.state.timeOffset / libItem.state.duration : null, - href: `#/metadetails/${encodeURIComponent(lib_item.type)}/${encodeURIComponent(lib_item._id)}${lib_item.state.video_id !== null ? `/${encodeURIComponent(lib_item.state.video_id)}` : ''}` + deepLinks: deepLinking.withLibItem({ libItem }) })); - return { lib_items }; + const deepLinks = { discover: '#/continuewatching' }; + return { lib_items, deepLinks }; }; const useContinueWatchingPreview = () => { diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index fa59b2900..27b15b0eb 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -135,7 +135,7 @@ const Discover = ({ urlParams, queryParams }) => { poster={metaItem.poster} posterShape={metaItem.posterShape} playIcon={selectedMetaItem === metaItem} - href={metaItem.href} + deepLinks={metaItem.deepLinks} data-index={index} onClick={metaItemOnClick} /> diff --git a/src/routes/Discover/useDiscover.js b/src/routes/Discover/useDiscover.js index 85da2d556..8f14710f0 100644 --- a/src/routes/Discover/useDiscover.js +++ b/src/routes/Discover/useDiscover.js @@ -1,6 +1,6 @@ const React = require('react'); const { useServices } = require('stremio/services'); -const { CONSTANTS, useModelState, comparatorWithPriorities } = require('stremio/common'); +const { CONSTANTS, deepLinking, useModelState, comparatorWithPriorities } = require('stremio/common'); const initDiscoverState = () => ({ selected: null, @@ -41,7 +41,7 @@ const mapDiscoverState = (discover) => { released: new Date(metaItem.released), description: metaItem.description, trailer: metaItem.trailer, - href: `#/metadetails/${encodeURIComponent(metaItem.type)}/${encodeURIComponent(metaItem.id)}` // TODO this should redirect with videoId at some cases + deepLinks: deepLinking.withMetaItem({ metaItem }) })) } } diff --git a/src/routes/Library/useLibrary.js b/src/routes/Library/useLibrary.js index 8ea749b4b..eaf286481 100644 --- a/src/routes/Library/useLibrary.js +++ b/src/routes/Library/useLibrary.js @@ -1,5 +1,5 @@ const React = require('react'); -const { CONSTANTS, useModelState, comparatorWithPriorities } = require('stremio/common'); +const { CONSTANTS, deepLinking, useModelState, comparatorWithPriorities } = require('stremio/common'); const initLibraryState = () => ({ selected: null, @@ -10,18 +10,17 @@ const initLibraryState = () => ({ const mapLibraryState = (library) => { const selected = library.selected; const type_names = library.type_names.sort(comparatorWithPriorities(CONSTANTS.TYPE_PRIORITIES)); - const lib_items = library.lib_items.map((lib_item) => ({ - id: lib_item._id, - type: lib_item.type, - name: lib_item.name, - poster: lib_item.poster, - posterShape: lib_item.posterShape === 'landscape' ? 'square' : lib_item.posterShape, - progress: lib_item.state.timeOffset > 0 && lib_item.state.duration > 0 ? - lib_item.state.timeOffset / lib_item.state.duration + const lib_items = library.lib_items.map((libItem) => ({ + id: libItem._id, + type: libItem.type, + name: libItem.name, + poster: libItem.poster, + posterShape: libItem.posterShape === 'landscape' ? 'square' : libItem.posterShape, + progress: libItem.state.timeOffset > 0 && libItem.state.duration > 0 ? + libItem.state.timeOffset / libItem.state.duration : null, - videoId: lib_item.state.video_id, - href: `#/metadetails/${encodeURIComponent(lib_item.type)}/${encodeURIComponent(lib_item._id)}${lib_item.state.video_id !== null ? `/${encodeURIComponent(lib_item.state.video_id)}` : ''}` + deepLinks: deepLinking.withLibItem({ libItem }) })); return { selected, type_names, lib_items }; }; diff --git a/src/routes/MetaDetails/StreamsList/Stream/Stream.js b/src/routes/MetaDetails/StreamsList/Stream/Stream.js index fbe2a8bcc..bccd9f875 100644 --- a/src/routes/MetaDetails/StreamsList/Stream/Stream.js +++ b/src/routes/MetaDetails/StreamsList/Stream/Stream.js @@ -6,12 +6,21 @@ const { Button, Image, PlayIconCircleCentered } = require('stremio/common'); const StreamPlaceholder = require('./StreamPlaceholder'); const styles = require('./styles'); -const Stream = ({ className, addonName, title, thumbnail, progress, ...props }) => { +const Stream = ({ className, addonName, title, thumbnail, progress, deepLinks, ...props }) => { + const href = React.useMemo(() => { + return deepLinks ? + typeof deepLinks.player === 'string' ? + deepLinks.player + : + null + : + null; + }, [deepLinks]); const renderThumbnailFallback = React.useMemo(() => () => ( ), []); return ( -