From 674fafd31af52793b734dca40e8f4ef39b651953 Mon Sep 17 00:00:00 2001 From: 6ea86b96 <6ea86b96@gmail.com> Date: Thu, 27 Apr 2017 00:08:26 +0700 Subject: [PATCH] Able to watch already downloaded videos --- ui/js/actions/content.js | 173 ++++++++++++++++++++++++++---- ui/js/actions/search.js | 2 +- ui/js/component/fileTile/index.js | 2 +- ui/js/component/fileTile/view.jsx | 4 +- ui/js/constants/action_types.js | 7 ++ ui/js/page/discover/view.jsx | 2 +- ui/js/reducers/content.js | 75 ++++++++++++- ui/js/selectors/content.js | 27 +++++ ui/js/store.js | 27 ++++- ui/js/util/batchActions.js | 9 ++ 10 files changed, 300 insertions(+), 28 deletions(-) create mode 100644 ui/js/util/batchActions.js diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index d29b40003..f26bd045a 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -4,34 +4,45 @@ import lbryio from 'lbryio' import { selectCurrentUri, } from 'selectors/app' +import { + selectBalance, +} from 'selectors/wallet' import { selectSearchTerm, + selectCurrentUriCostInfo, + selectCurrentUriFileInfo, } from 'selectors/content' import { selectCurrentResolvedUriClaimOutpoint, } from 'selectors/content' +import { + doOpenModal, +} from 'actions/app' +import batchActions from 'util/batchActions' -export function doResolveUri(dispatch, uri) { - dispatch({ - type: types.RESOLVE_URI_STARTED, - data: { uri } - }) - - lbry.resolve({uri: uri}).then((resolutionInfo) => { - const { - claim, - certificate, - } = resolutionInfo - +export function doResolveUri(uri) { + return function(dispatch, getState) { dispatch({ - type: types.RESOLVE_URI_COMPLETED, - data: { - uri, + type: types.RESOLVE_URI_STARTED, + data: { uri } + }) + + lbry.resolve({ uri }).then((resolutionInfo) => { + const { claim, certificate, - } + } = resolutionInfo + + dispatch({ + type: types.RESOLVE_URI_COMPLETED, + data: { + uri, + claim, + certificate, + } + }) }) - }) + } } export function doFetchCurrentUriFileInfo() { @@ -40,8 +51,6 @@ export function doFetchCurrentUriFileInfo() { const uri = selectCurrentUri(state) const outpoint = selectCurrentResolvedUriClaimOutpoint(state) - console.log(outpoint) - dispatch({ type: types.FETCH_FILE_INFO_STARTED, data: { @@ -50,7 +59,7 @@ export function doFetchCurrentUriFileInfo() { } }) - lbry.file_list({ outpoint }).then(fileInfo => { + lbry.file_list({ outpoint }).then(([fileInfo]) => { dispatch({ type: types.FETCH_FILE_INFO_COMPLETED, data: { @@ -126,7 +135,7 @@ export function doFetchFeaturedContent() { }) Object.keys(Uris).forEach((category) => { - Uris[category].forEach((uri) => doResolveUri(dispatch, uri)) + Uris[category].forEach((uri) => dispatch(doResolveUri(uri))) }) } @@ -161,3 +170,125 @@ export function doFetchCurrentUriCostInfo() { }) } } + +export function doUpdateLoadStatus(uri, outpoint) { + return function(dispatch, getState) { + const state = getState() + + lbry.file_list({ + outpoint: outpoint, + full_status: true, + }).then(([fileInfo]) => { + if(!fileInfo || fileInfo.written_bytes == 0) { + // download hasn't started yet + setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250) + } else if (fileInfo.completed) { + dispatch({ + type: types.DOWNLOADING_COMPLETED, + data: { + uri, + fileInfo, + } + }) + } else { + // ready to play + const { + total_bytes, + written_bytes, + } = fileInfo + const progress = (written_bytes / total_bytes) * 100 + + dispatch({ + type: types.DOWNLOADING_PROGRESSED, + data: { + uri, + fileInfo, + progress, + } + }) + setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250) + } + }) + } +} + +export function doPlayVideo(uri) { + return { + type: types.PLAY_VIDEO_STARTED, + data: { uri } + } +} + +export function doDownloadFile(uri, streamInfo) { + return function(dispatch, getState) { + const state = getState() + + dispatch({ + type: types.DOWNLOADING_STARTED, + data: { + uri, + } + }) + + lbryio.call('file', 'view', { + uri: uri, + outpoint: streamInfo.outpoint, + claimId: streamInfo.claim_id, + }).catch(() => {}) + dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint)) + } +} + +export function doLoadVideo() { + return function(dispatch, getState) { + const state = getState() + const uri = selectCurrentUri(state) + + dispatch({ + type: types.LOADING_VIDEO_STARTED, + data: { + uri + } + }) + + lbry.get({ uri }).then(streamInfo => { + if (streamInfo === null || typeof streamInfo !== 'object') { + dispatch({ + type: types.LOADING_VIDEO_FAILED, + data: { uri } + }) + dispatch(doOpenModal('timedOut')) + } else { + dispatch(doDownloadFile(uri, streamInfo)) + } + }) + } +} + +export function doWatchVideo() { + return function(dispatch, getState) { + const state = getState() + const uri = selectCurrentUri(state) + const balance = selectBalance(state) + const fileInfo = selectCurrentUriFileInfo(state) + const costInfo = selectCurrentUriCostInfo(state) + const { cost } = costInfo + + // TODO does > 0 mean the file is downloaded? We don't have the total_bytes + console.log(fileInfo) + console.log(fileInfo.written_bytes) + console.log('wtf') + if (fileInfo.written_bytes > 0) { + console.debug('this file is already downloaded') + dispatch(doPlayVideo(uri)) + } else { + if (cost > balance) { + dispatch(doOpenModal('notEnoughCredits')) + } else if (cost <= 0.01) { + dispatch(doLoadVideo()) + } else { + dispatch(doOpenModal('affirmPurchase')) + } + } + } +} diff --git a/ui/js/actions/search.js b/ui/js/actions/search.js index 154a52fd3..a44ec964f 100644 --- a/ui/js/actions/search.js +++ b/ui/js/actions/search.js @@ -32,7 +32,7 @@ export function doSearchContent(query) { contentName: result.name, claimId: result.channel_id || result.claim_id, }) - doResolveUri(dispatch, uri.split('://')[1]) + dispatch(doResolveUri(uri.split('://')[1])) }) dispatch({ diff --git a/ui/js/component/fileTile/index.js b/ui/js/component/fileTile/index.js index e24dade66..feccdb71b 100644 --- a/ui/js/component/fileTile/index.js +++ b/ui/js/component/fileTile/index.js @@ -8,7 +8,7 @@ import { import FileTile from './view' const select = (state) => ({ - resolvedUris: selectResolvedUris(state), + resolvedUris: (uri) => selectResolvedUris(state)[uri], }) const perform = (dispatch) => ({ diff --git a/ui/js/component/fileTile/view.jsx b/ui/js/component/fileTile/view.jsx index b80616dcc..50e506883 100644 --- a/ui/js/component/fileTile/view.jsx +++ b/ui/js/component/fileTile/view.jsx @@ -13,7 +13,9 @@ class FileTile extends React.Component { uri, claim, } = this.props - + const resolvedUri = this.props.resolvedUris(uri) || {} + const claimInfo = resolvedUri.claim + if(!claim) { if (displayStyle == 'card') { return diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 5ca93c0ca..568a85e70 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -47,6 +47,13 @@ export const FETCH_FILE_INFO_STARTED = 'FETCH_FILE_INFO_STARTED' export const FETCH_FILE_INFO_COMPLETED = 'FETCH_FILE_INFO_COMPLETED' export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED' export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED' +export const LOADING_VIDEO_STARTED = 'LOADING_VIDEO_STARTED' +export const LOADING_VIDEO_COMPLETED = 'LOADING_VIDEO_COMPLETED' +export const LOADING_VIDEO_FAILED = 'LOADING_VIDEO_FAILED' +export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED' +export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED' +export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED' +export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED' // Search export const SEARCH_STARTED = 'SEARCH_STARTED' diff --git a/ui/js/page/discover/view.jsx b/ui/js/page/discover/view.jsx index fc0eb8220..999af9604 100644 --- a/ui/js/page/discover/view.jsx +++ b/ui/js/page/discover/view.jsx @@ -62,4 +62,4 @@ let DiscoverPage = React.createClass({ } }) -export default DiscoverPage; +export default DiscoverPage; \ No newline at end of file diff --git a/ui/js/reducers/content.js b/ui/js/reducers/content.js index 0d420237d..720877177 100644 --- a/ui/js/reducers/content.js +++ b/ui/js/reducers/content.js @@ -129,7 +129,7 @@ reducers[types.FETCH_FILE_INFO_COMPLETED] = function(state, action) { const fileInfos = Object.assign({}, state.fileInfos) const byUri = Object.assign({}, fileInfos.byUri) - byUri[uri] = fileInfo + byUri[uri] = Object.assign({}, fileInfo) delete newFetchingFileInfos[uri] const newFileInfos = Object.assign({}, fileInfos, { @@ -178,6 +178,79 @@ reducers[types.FETCH_COST_INFO_COMPLETED] = function(state, action) { }) } +reducers[types.LOADING_VIDEO_STARTED] = function(state, action) { + const { + uri, + } = action.data + const newLoading = Object.assign({}, state.loading) + const newByUri = Object.assign({}, newLoading.byUri) + + newByUri[uri] = true + newLoading.byUri = newByUri + + return Object.assign({}, state, { + loading: newLoading, + }) +} + +reducers[types.LOADING_VIDEO_FAILED] = function(state, action) { + const { + uri, + } = action.data + const newLoading = Object.assign({}, state.loading) + const newByUri = Object.assign({}, newLoading.byUri) + + delete newByUri[uri] + newLoading.byUri = newByUri + + return Object.assign({}, state, { + loading: newLoading, + }) +} + +reducers[types.DOWNLOADING_STARTED] = function(state, action) { + const { + uri, + } = action.data + const newDownloading = Object.assign({}, state.downloading) + const newByUri = Object.assign({}, newDownloading.byUri) + + newByUri[uri] = true + newDownloading.byUri = newByUri + + return Object.assign({}, state, { + downloading: newDownloading, + }) +} + +reducers[types.DOWNLOADING_COMPLETED] = +reducers[types.DOWNLOADING_PROGRESSED] = function(state, action) { + const { + uri, + fileInfo, + } = action.data + const fileInfos = Object.assign({}, state.fileInfos) + const byUri = Object.assign({}, fileInfos.byUri) + + byUri[uri] = fileInfo + fileInfos.byUri = byUri + + return Object.assign({}, state, { + fileInfos: fileInfos, + }) +} + + +reducers[types.PLAY_VIDEO_STARTED] = function(state, action) { + const { + uri, + } = action.data + + return Object.assign({}, state, { + nowPlaying: uri, + }) +} + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/selectors/content.js b/ui/js/selectors/content.js index fa8559bce..274d98d3d 100644 --- a/ui/js/selectors/content.js +++ b/ui/js/selectors/content.js @@ -84,12 +84,23 @@ export const selectFetchingFileInfos = createSelector( (state) => state.fetchingFileInfos || {} ) +export const selectCurrentUriFileReadyToPlay = createSelector( + selectCurrentUriFileInfo, + (fileInfo) => (fileInfo || {}).written_bytes > 0 +) + export const selectIsFetchingCurrentUriFileInfo = createSelector( selectFetchingFileInfos, selectCurrentUri, (fetching, uri) => !!fetching[uri] ) +export const selectCurrentUriIsPlaying = createSelector( + _selectState, + selectCurrentUri, + (state, uri) => state.nowPlaying == uri +) + export const selectCostInfos = createSelector( _selectState, (state) => state.costInfos || {} @@ -185,6 +196,22 @@ export const selectPublishedContent = createSelector( (state) => state.publishedContent || {} ) +export const selectLoading = createSelector( + _selectState, + (state) => state.loading || {} +) + +export const selectLoadingByUri = createSelector( + selectLoading, + (loading) => loading.byUri || {} +) + +export const selectLoadingCurrentUri = createSelector( + selectLoadingByUri, + selectCurrentUri, + (byUri, uri) => byUri[uri] +) + export const shouldFetchPublishedContent = createSelector( selectDaemonReady, selectCurrentPage, diff --git a/ui/js/store.js b/ui/js/store.js index ac789a824..ffc6d9d88 100644 --- a/ui/js/store.js +++ b/ui/js/store.js @@ -19,6 +19,28 @@ function isNotFunction(object) { return !isFunction(object); } +function createBulkThunkMiddleware() { + return ({ dispatch, getState }) => next => (action) => { + if (action.type === 'BATCH_ACTIONS') { + action.actions.filter(isFunction).map(actionFn => + actionFn(dispatch, getState) + ) + } + return next(action) + } +} + +function enableBatching(reducer) { + return function batchingReducer(state, action) { + switch (action.type) { + case 'BATCH_ACTIONS': + return action.actions.filter(isNotFunction).reduce(batchingReducer, state) + default: + return reducer(state, action) + } + } +} + const reducers = redux.combineReducers({ app: appReducer, content: contentReducer, @@ -27,7 +49,8 @@ const reducers = redux.combineReducers({ wallet: walletReducer, }); -var middleware = [thunk] +const bulkThunk = createBulkThunkMiddleware() +const middleware = [thunk, bulkThunk] if (env === 'development') { const logger = createLogger({ @@ -40,6 +63,6 @@ const createStoreWithMiddleware = redux.compose( redux.applyMiddleware(...middleware) )(redux.createStore); -const reduxStore = createStoreWithMiddleware(reducers); +const reduxStore = createStoreWithMiddleware(enableBatching(reducers)); export default reduxStore; diff --git a/ui/js/util/batchActions.js b/ui/js/util/batchActions.js new file mode 100644 index 000000000..eedab1cc6 --- /dev/null +++ b/ui/js/util/batchActions.js @@ -0,0 +1,9 @@ +// https://github.com/reactjs/redux/issues/911 +function batchActions(...actions) { + return { + type: 'BATCH_ACTIONS', + actions: actions + }; +} + +export default batchActions