Support resume-able upload via tus (#186)
* Publish button: use spinner instead of "Publishing..." Looks better, plus the preview could take a while sometimes. * Refactor `doPublish`. No functional change This is to allow `doPublish` to accept a custom payload as an input (for resuming uploads), instead of always resolving it from the redux data. * Add doPublishResume * Support resume-able upload via tus ## Issue 38 Handle resumable file upload ## Notes Since we can't serialize a File object, we'll need to the user to re-select the file to resume. * Exclude "modified date" for Firefox/Android ## Issue It appears that the modification date of the Android file changes when selected, so that file was deemed "different" when trying to resume upload. ## Change Exclude modification date for now. Let's assume a smart user. * Move 'currentUploads' to 'publish' reducer `publish` is currently rehydrated, so we can ride on that and don't need to store the `currentUploads` in `localStorage` for persistence. This would allow us to store Markdown Post data too, as `localStorage` has a 5MB limit per app. We could have also made `webReducer` rehydrate, but in this repo, there is no need to split it to another reducer. It also makes more sense to be part of publish anyway (at least to me). This change is mostly moving items between files, with the exception of 1. An additional REHYDRATE in the publish reducer to clean up the tusUploader. 2. Not clearing `currentUploads` in CLEAR_PUBLISH. * Restore v1 code for livestream replay, etc. v2 (tus) does not handle `remote_url`, so the app still needs v1 for that. Since we'll still have v1 code, use v1 for previews as well.
This commit is contained in:
parent
b508fe8679
commit
cb6a044584
27 changed files with 814 additions and 325 deletions
|
@ -8,6 +8,7 @@ const config = {
|
|||
WEB_SERVER_PORT: process.env.WEB_SERVER_PORT,
|
||||
LBRY_WEB_API: process.env.LBRY_WEB_API, //api.na-backend.odysee.com',
|
||||
LBRY_WEB_PUBLISH_API: process.env.LBRY_WEB_PUBLISH_API,
|
||||
LBRY_WEB_PUBLISH_API_V2: process.env.LBRY_WEB_PUBLISH_API_V2,
|
||||
LBRY_API_URL: process.env.LBRY_API_URL, //api.lbry.com',
|
||||
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //cdn.lbryplayer.xyz',
|
||||
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
|
||||
|
|
|
@ -83,9 +83,6 @@ export const SYNC_APPLY_FAILED = 'SYNC_APPLY_FAILED';
|
|||
export const SYNC_APPLY_BAD_PASSWORD = 'SYNC_APPLY_BAD_PASSWORD';
|
||||
export const SYNC_RESET = 'SYNC_RESET';
|
||||
|
||||
// Lbry.tv
|
||||
export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS';
|
||||
|
||||
// User
|
||||
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
|
||||
export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED';
|
||||
|
|
|
@ -27,7 +27,6 @@ export {
|
|||
doResetSync,
|
||||
doSyncEncryptAndDecrypt,
|
||||
} from 'redux/actions/sync';
|
||||
export { doUpdateUploadProgress } from './redux/actions/web';
|
||||
|
||||
// reducers
|
||||
export { authReducer } from './redux/reducers/auth';
|
||||
|
@ -37,7 +36,6 @@ export { filteredReducer } from './redux/reducers/filtered';
|
|||
// export { homepageReducer } from './redux/reducers/homepage';
|
||||
export { statsReducer } from './redux/reducers/stats';
|
||||
export { syncReducer } from './redux/reducers/sync';
|
||||
export { webReducer } from './redux/reducers/web';
|
||||
|
||||
// selectors
|
||||
export { selectAuthToken, selectIsAuthenticating } from './redux/selectors/auth';
|
||||
|
@ -70,4 +68,3 @@ export {
|
|||
selectSyncApplyErrorMessage,
|
||||
selectSyncApplyPasswordError,
|
||||
} from './redux/selectors/sync';
|
||||
export { selectCurrentUploads, selectUploadCount } from './redux/selectors/web';
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
|
||||
export const doUpdateUploadProgress = (
|
||||
progress: string,
|
||||
params: { [key: string]: any },
|
||||
xhr: any
|
||||
) => (dispatch: Dispatch) =>
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_UPLOAD_PROGRESS,
|
||||
data: { progress, params, xhr },
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
|
||||
/*
|
||||
test mock:
|
||||
currentUploads: {
|
||||
'test#upload': {
|
||||
progress: 50,
|
||||
params: {
|
||||
name: 'steve',
|
||||
thumbnail_url: 'https://dev2.spee.ch/4/KMNtoSZ009fawGz59VG8PrID.jpeg',
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
|
||||
export type Params = {
|
||||
channel?: string,
|
||||
name: string,
|
||||
thumbnail_url: ?string,
|
||||
title: ?string,
|
||||
};
|
||||
|
||||
export type UploadItem = {
|
||||
progess: string,
|
||||
params: Params,
|
||||
xhr?: any,
|
||||
};
|
||||
|
||||
export type TvState = {
|
||||
currentUploads: { [key: string]: UploadItem },
|
||||
};
|
||||
|
||||
const reducers = {};
|
||||
|
||||
const defaultState: TvState = {
|
||||
currentUploads: {},
|
||||
};
|
||||
|
||||
reducers[ACTIONS.UPDATE_UPLOAD_PROGRESS] = (state: TvState, action) => {
|
||||
const { progress, params, xhr } = action.data;
|
||||
const key = params.channel ? `${params.name}#${params.channel}` : `${params.name}#anonymous`;
|
||||
let currentUploads;
|
||||
if (!progress) {
|
||||
currentUploads = Object.assign({}, state.currentUploads);
|
||||
Object.keys(currentUploads).forEach(k => {
|
||||
if (k === key) {
|
||||
delete currentUploads[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
currentUploads = Object.assign({}, state.currentUploads);
|
||||
currentUploads[key] = { progress, params, xhr };
|
||||
}
|
||||
return { ...state, currentUploads };
|
||||
};
|
||||
|
||||
export function webReducer(state: TvState = defaultState, action: any) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
return state;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
const selectState = (state) => state.web || {};
|
||||
|
||||
export const selectCurrentUploads = (state) => selectState(state).currentUploads;
|
||||
|
||||
export const selectUploadCount = createSelector(
|
||||
selectCurrentUploads,
|
||||
(currentUploads) => currentUploads && Object.keys(currentUploads).length
|
||||
);
|
21
flow-typed/publish.js
vendored
21
flow-typed/publish.js
vendored
|
@ -52,3 +52,24 @@ declare type PublishParams = {
|
|||
nsfw: boolean,
|
||||
tags: Array<Tag>,
|
||||
};
|
||||
|
||||
declare type TusUploader = any;
|
||||
|
||||
declare type FileUploadSdkParams = {
|
||||
file_path: string,
|
||||
name: ?string,
|
||||
preview?: boolean,
|
||||
remote_url?: string,
|
||||
thumbnail_url?: string,
|
||||
title?: string,
|
||||
};
|
||||
|
||||
declare type FileUploadItem = {
|
||||
params: FileUploadSdkParams,
|
||||
file: File,
|
||||
fileFingerprint: string,
|
||||
progress: string,
|
||||
status?: string,
|
||||
uploader?: TusUploader | XMLHttpRequest,
|
||||
resumable: boolean,
|
||||
};
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
"rss": "^1.2.2",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"tempy": "^0.6.0",
|
||||
"tus-js-client": "^2.3.0",
|
||||
"videojs-contrib-ads": "^6.9.0",
|
||||
"videojs-ima": "^1.11.0",
|
||||
"videojs-ima-player": "^0.5.6",
|
||||
|
|
|
@ -1291,6 +1291,16 @@
|
|||
"Select a file to upload": "Select a file to upload",
|
||||
"Select file to upload": "Select file to upload",
|
||||
"Url copied.": "Url copied.",
|
||||
"Failed to initiate upload (%err%)": "Failed to initiate upload (%err%)",
|
||||
"Invalid file": "Invalid file",
|
||||
"It appears to be a different or modified file.": "It appears to be a different or modified file.",
|
||||
"Please select the same file from the initial upload.": "Please select the same file from the initial upload.",
|
||||
"Cancel upload": "Cancel upload",
|
||||
"Cancel and remove the selected upload?": "Cancel and remove the selected upload?",
|
||||
"Select the file to resume upload...": "Select the file to resume upload...",
|
||||
"Stopped.": "Stopped.",
|
||||
"Resume": "Resume",
|
||||
"Retrying...": "Retrying...",
|
||||
"Uploading...": "Uploading...",
|
||||
"Creating...": "Creating...",
|
||||
"Use a URL": "Use a URL",
|
||||
|
@ -1430,7 +1440,6 @@
|
|||
"Log in to %SITE_NAME%": "Log in to %SITE_NAME%",
|
||||
"Log in": "Log in",
|
||||
"Not Yet": "Not Yet",
|
||||
"Preparing...": "Preparing...",
|
||||
"Confirm Upload": "Confirm Upload",
|
||||
"Confirm Edit": "Confirm Edit",
|
||||
"Create Livestream": "Create Livestream",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { hot } from 'react-hot-loader/root';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectUploadCount } from 'lbryinc';
|
||||
import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors/sync';
|
||||
import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user';
|
||||
import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
@ -22,6 +21,7 @@ import {
|
|||
selectActiveChannelClaim,
|
||||
selectIsReloadRequired,
|
||||
} from 'redux/selectors/app';
|
||||
import { selectUploadCount } from 'redux/selectors/publish';
|
||||
import { doGetWalletSyncPreference, doSetLanguage } from 'redux/actions/settings';
|
||||
import { doSyncLoop } from 'redux/actions/sync';
|
||||
import {
|
||||
|
|
|
@ -325,7 +325,7 @@ function PublishForm(props: Props) {
|
|||
submitLabel = __('Uploading...');
|
||||
}
|
||||
} else if (previewing) {
|
||||
submitLabel = __('Preparing...');
|
||||
submitLabel = <Spinner type="small" />;
|
||||
} else {
|
||||
if (isStillEditing) {
|
||||
submitLabel = __('Save');
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectCurrentUploads, selectUploadCount } from 'lbryinc';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { doPublishResume, doUpdateUploadRemove } from 'redux/actions/publish';
|
||||
import { selectCurrentUploads, selectUploadCount } from 'redux/selectors/publish';
|
||||
import WebUploadList from './view';
|
||||
|
||||
const select = state => ({
|
||||
const select = (state) => ({
|
||||
currentUploads: selectCurrentUploads(state),
|
||||
uploadCount: selectUploadCount(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(WebUploadList);
|
||||
const perform = {
|
||||
doPublishResume,
|
||||
doUpdateUploadRemove,
|
||||
doOpenModal,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(WebUploadList);
|
||||
|
|
|
@ -1,40 +1,155 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import FileSelector from 'component/common/file-selector';
|
||||
import Button from 'component/button';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import { serializeFileObj } from 'util/file';
|
||||
|
||||
type Props = {
|
||||
params: UpdatePublishFormData,
|
||||
progress: string,
|
||||
xhr?: () => void,
|
||||
uploadItem: FileUploadItem,
|
||||
doPublishResume: (any) => void,
|
||||
doUpdateUploadRemove: (any) => void,
|
||||
doOpenModal: (string, {}) => void,
|
||||
};
|
||||
|
||||
export default function WebUploadItem(props: Props) {
|
||||
const { params, progress, xhr } = props;
|
||||
const { uploadItem, doPublishResume, doUpdateUploadRemove, doOpenModal } = props;
|
||||
const { params, file, fileFingerprint, progress, status, resumable, uploader } = uploadItem;
|
||||
|
||||
const [showFileSelector, setShowFileSelector] = useState(false);
|
||||
|
||||
function handleFileChange(newFile: WebFile, clearName = true) {
|
||||
if (serializeFileObj(newFile) === fileFingerprint) {
|
||||
setShowFileSelector(false);
|
||||
doPublishResume({ ...params, file_path: newFile });
|
||||
} else {
|
||||
doOpenModal(MODALS.CONFIRM, {
|
||||
title: __('Invalid file'),
|
||||
subtitle: __('It appears to be a different or modified file.'),
|
||||
body: <p className="help--warning">{__('Please select the same file from the initial upload.')}</p>,
|
||||
onConfirm: (closeModal) => closeModal(),
|
||||
hideCancel: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
doOpenModal(MODALS.CONFIRM, {
|
||||
title: __('Cancel upload'),
|
||||
subtitle: __('Cancel and remove the selected upload?'),
|
||||
body: params.name ? <p className="empty">{`lbry://${params.name}`}</p> : undefined,
|
||||
onConfirm: (closeModal) => {
|
||||
if (uploader) {
|
||||
if (resumable) {
|
||||
// $FlowFixMe - couldn't resolve to TusUploader manually.
|
||||
uploader.abort(true); // TUS
|
||||
} else {
|
||||
uploader.abort(); // XHR
|
||||
}
|
||||
}
|
||||
doUpdateUploadRemove(params);
|
||||
closeModal();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resolveProgressStr() {
|
||||
if (!uploader) {
|
||||
return __('Stopped.');
|
||||
}
|
||||
|
||||
if (resumable) {
|
||||
if (status) {
|
||||
switch (status) {
|
||||
case 'retry':
|
||||
return __('Retrying...');
|
||||
case 'error':
|
||||
return __('Failed.');
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
} else {
|
||||
const progressInt = parseInt(progress);
|
||||
return progressInt === 100 ? __('Processing...') : __('Uploading...');
|
||||
}
|
||||
} else {
|
||||
return __('Uploading...');
|
||||
}
|
||||
}
|
||||
|
||||
function getRetryButton() {
|
||||
if (!resumable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uploader) {
|
||||
// Should still be uploading. Don't show.
|
||||
return null;
|
||||
} else {
|
||||
// Refreshed or connection broken.
|
||||
const isFileActive = file instanceof File;
|
||||
return (
|
||||
<Button
|
||||
label={isFileActive ? __('Resume') : __('Retry')}
|
||||
button="link"
|
||||
onClick={() => {
|
||||
if (isFileActive) {
|
||||
doPublishResume({ ...params, file_path: file });
|
||||
} else {
|
||||
setShowFileSelector(true);
|
||||
}
|
||||
}}
|
||||
disabled={showFileSelector}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getCancelButton() {
|
||||
return <Button label={__('Cancel')} button="link" onClick={handleCancel} />;
|
||||
}
|
||||
|
||||
function getFileSelector() {
|
||||
return (
|
||||
<div className="claim-preview--padded">
|
||||
<FileSelector
|
||||
label={__('File')}
|
||||
onFileChosen={handleFileChange}
|
||||
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||
accept={'video/mp4,video/x-m4v,video/*,audio/*'}
|
||||
placeholder={__('Select the file to resume upload...')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getProgressBar() {
|
||||
return (
|
||||
<>
|
||||
<div className="claim-upload__progress--label">lbry://{params.name}</div>
|
||||
<div className={'claim-upload__progress--outer card--inline'}>
|
||||
<div className={'claim-upload__progress--inner'} style={{ width: `${progress}%` }}>
|
||||
{resolveProgressStr()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={'claim-preview claim-preview--padded claim-preview--inactive card--inline'}>
|
||||
<li className={'web-upload-item claim-preview claim-preview--padded claim-preview--inactive card--inline'}>
|
||||
<FileThumbnail thumbnail={params.thumbnail_url} />
|
||||
<div className={'claim-preview-metadata'}>
|
||||
<div className="claim-preview-info">
|
||||
<div className="claim-preview__title">{params.title}</div>
|
||||
{xhr && (
|
||||
<div className="card__actions--inline">
|
||||
<Button
|
||||
button="link"
|
||||
onClick={() => {
|
||||
xhr.abort();
|
||||
}}
|
||||
label={__('Cancel')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2>lbry://{params.name}</h2>
|
||||
<div className={'claim-upload__progress--outer card--inline'}>
|
||||
<div className={'claim-upload__progress--inner'} style={{ width: `${progress}%` }}>
|
||||
Uploading...
|
||||
<div className="card__actions--inline">
|
||||
{getRetryButton()}
|
||||
{getCancelButton()}
|
||||
</div>
|
||||
</div>
|
||||
{showFileSelector && getFileSelector()}
|
||||
{!showFileSelector && getProgressBar()}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -3,19 +3,16 @@ import * as React from 'react';
|
|||
import Card from 'component/common/card';
|
||||
import WebUploadItem from './internal/web-upload-item';
|
||||
|
||||
export type UploadItem = {
|
||||
progess: string,
|
||||
params: UpdatePublishFormData,
|
||||
xhr?: { abort: () => void },
|
||||
};
|
||||
|
||||
type Props = {
|
||||
currentUploads: { [key: string]: UploadItem },
|
||||
currentUploads: { [key: string]: FileUploadItem },
|
||||
uploadCount: number,
|
||||
doPublishResume: (any) => void,
|
||||
doUpdateUploadRemove: (any) => void,
|
||||
doOpenModal: (string, {}) => void,
|
||||
};
|
||||
|
||||
export default function WebUploadList(props: Props) {
|
||||
const { currentUploads, uploadCount } = props;
|
||||
const { currentUploads, uploadCount, doPublishResume, doUpdateUploadRemove, doOpenModal } = props;
|
||||
|
||||
return (
|
||||
!!uploadCount && (
|
||||
|
@ -25,8 +22,16 @@ export default function WebUploadList(props: Props) {
|
|||
body={
|
||||
<section>
|
||||
{/* $FlowFixMe */}
|
||||
{Object.values(currentUploads).map(({ progress, params, xhr }) => (
|
||||
<WebUploadItem key={`upload${params.name}`} progress={progress} params={params} xhr={xhr} />
|
||||
{Object.values(currentUploads).map((uploadItem) => (
|
||||
<WebUploadItem
|
||||
// $FlowFixMe
|
||||
key={`upload${uploadItem.params.name}`}
|
||||
// $FlowFixMe
|
||||
uploadItem={uploadItem}
|
||||
doPublishResume={doPublishResume}
|
||||
doUpdateUploadRemove={doUpdateUploadRemove}
|
||||
doOpenModal={doOpenModal}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
}
|
||||
|
|
|
@ -331,6 +331,9 @@ export const PUBLISH_FAIL = 'PUBLISH_FAIL';
|
|||
export const CLEAR_PUBLISH_ERROR = 'CLEAR_PUBLISH_ERROR';
|
||||
export const REMOVE_PENDING_PUBLISH = 'REMOVE_PENDING_PUBLISH';
|
||||
export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT';
|
||||
export const UPDATE_UPLOAD_ADD = 'UPDATE_UPLOAD_ADD';
|
||||
export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS';
|
||||
export const UPDATE_UPLOAD_REMOVE = 'UPDATE_UPLOAD_REMOVE';
|
||||
|
||||
// Media
|
||||
export const MEDIA_PLAY = 'MEDIA_PLAY';
|
||||
|
@ -491,9 +494,6 @@ export const FETCH_SUB_COUNT_STARTED = 'FETCH_SUB_COUNT_STARTED';
|
|||
export const FETCH_SUB_COUNT_FAILED = 'FETCH_SUB_COUNT_FAILED';
|
||||
export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED';
|
||||
|
||||
// Lbry.tv
|
||||
export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS';
|
||||
|
||||
// User
|
||||
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
|
||||
export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED';
|
||||
|
|
|
@ -35,7 +35,7 @@ import {
|
|||
doAuthTokenRefresh,
|
||||
} from 'util/saved-passwords';
|
||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
||||
import { LBRY_WEB_API, DEFAULT_LANGUAGE, LBRY_API_URL, LBRY_WEB_PUBLISH_API } from 'config';
|
||||
import { LBRY_WEB_API, DEFAULT_LANGUAGE, LBRY_API_URL } from 'config';
|
||||
|
||||
// Import 3rd-party styles before ours for the current way we are code-splitting.
|
||||
import 'scss/third-party.scss';
|
||||
|
@ -69,7 +69,6 @@ sdkAPIHost = LBRY_WEB_API;
|
|||
|
||||
export const SDK_API_PATH = `${sdkAPIHost}/api/v1`;
|
||||
const proxyURL = `${SDK_API_PATH}/proxy`;
|
||||
const publishURL = LBRY_WEB_PUBLISH_API; // || `${SDK_API_PATH}/proxy`;
|
||||
|
||||
Lbry.setDaemonConnectionString(proxyURL);
|
||||
|
||||
|
@ -79,7 +78,6 @@ Lbry.setOverride(
|
|||
new Promise((resolve, reject) => {
|
||||
apiPublishCallViaWeb(
|
||||
apiCall,
|
||||
publishURL,
|
||||
Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(X_LBRY_AUTH_TOKEN)
|
||||
? Lbry.getApiRequestHeaders()[X_LBRY_AUTH_TOKEN]
|
||||
: '',
|
||||
|
|
|
@ -5,9 +5,9 @@ import {
|
|||
selectMyClaimsPageItemCount,
|
||||
selectFetchingMyClaimsPageError,
|
||||
} from 'redux/selectors/claims';
|
||||
import { selectUploadCount } from 'redux/selectors/publish';
|
||||
import { doFetchClaimListMine, doCheckPendingClaims } from 'redux/actions/claims';
|
||||
import { doClearPublish } from 'redux/actions/publish';
|
||||
import { selectUploadCount } from 'lbryinc';
|
||||
import FileListPublished from './view';
|
||||
import { withRouter } from 'react-router';
|
||||
import { MY_CLAIMS_PAGE_SIZE, PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import { connectRouter } from 'connected-react-router';
|
||||
import { costInfoReducer, blacklistReducer, filteredReducer, statsReducer, webReducer } from 'lbryinc';
|
||||
import { costInfoReducer, blacklistReducer, filteredReducer, statsReducer } from 'lbryinc';
|
||||
import { claimsReducer } from 'redux/reducers/claims';
|
||||
import { fileInfoReducer } from 'redux/reducers/file_info';
|
||||
import { walletReducer } from 'redux/reducers/wallet';
|
||||
|
@ -50,6 +50,5 @@ export default (history) =>
|
|||
user: userReducer,
|
||||
wallet: walletReducer,
|
||||
sync: syncReducer,
|
||||
web: webReducer,
|
||||
collections: collectionsReducer,
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import { batchActions } from 'util/batch-actions';
|
|||
import { doCheckPendingClaims } from 'redux/actions/claims';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
selectMyActiveClaims,
|
||||
selectMyClaims,
|
||||
selectMyChannelClaims,
|
||||
// selectMyClaimsWithoutChannels,
|
||||
|
@ -47,6 +48,147 @@ function resolveClaimTypeForAnalytics(claim) {
|
|||
}
|
||||
|
||||
export const NO_FILE = '---';
|
||||
|
||||
function resolvePublishPayload(publishData, myClaimForUri, myChannels, preview) {
|
||||
const {
|
||||
name,
|
||||
bid,
|
||||
filePath,
|
||||
description,
|
||||
language,
|
||||
releaseTimeEdited,
|
||||
// license,
|
||||
licenseUrl,
|
||||
useLBRYUploader,
|
||||
licenseType,
|
||||
otherLicenseDescription,
|
||||
thumbnail,
|
||||
channel,
|
||||
title,
|
||||
contentIsFree,
|
||||
fee,
|
||||
// uri,
|
||||
tags,
|
||||
// locations,
|
||||
optimize,
|
||||
isLivestreamPublish,
|
||||
remoteFileUrl,
|
||||
} = publishData;
|
||||
|
||||
// Handle scenario where we have a claim that has the same name as a channel we are publishing with.
|
||||
const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null;
|
||||
|
||||
let publishingLicense;
|
||||
switch (licenseType) {
|
||||
case COPYRIGHT:
|
||||
case OTHER:
|
||||
publishingLicense = otherLicenseDescription;
|
||||
break;
|
||||
default:
|
||||
publishingLicense = licenseType;
|
||||
}
|
||||
|
||||
// get the claim id from the channel name, we will use that instead
|
||||
const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null;
|
||||
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
|
||||
|
||||
const publishPayload: {
|
||||
name: ?string,
|
||||
bid: string,
|
||||
description?: string,
|
||||
channel_id?: string,
|
||||
file_path?: string,
|
||||
license_url?: string,
|
||||
license?: string,
|
||||
thumbnail_url?: string,
|
||||
release_time?: number,
|
||||
fee_currency?: string,
|
||||
fee_amount?: string,
|
||||
languages?: Array<string>,
|
||||
tags: Array<string>,
|
||||
locations?: Array<any>,
|
||||
blocking: boolean,
|
||||
optimize_file?: boolean,
|
||||
preview?: boolean,
|
||||
remote_url?: string,
|
||||
} = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
locations: [],
|
||||
bid: creditsToString(bid),
|
||||
languages: [language],
|
||||
tags: tags && tags.map((tag) => tag.name),
|
||||
thumbnail_url: thumbnail,
|
||||
blocking: true,
|
||||
preview: false,
|
||||
};
|
||||
|
||||
// Temporary solution to keep the same publish flow with the new tags api
|
||||
// Eventually we will allow users to enter their own tags on publish
|
||||
// `nsfw` will probably be removed
|
||||
if (remoteFileUrl) {
|
||||
publishPayload.remote_url = remoteFileUrl;
|
||||
}
|
||||
|
||||
if (publishingLicense) {
|
||||
publishPayload.license = publishingLicense;
|
||||
}
|
||||
|
||||
if (licenseUrl) {
|
||||
publishPayload.license_url = licenseUrl;
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
publishPayload.thumbnail_url = thumbnail;
|
||||
}
|
||||
|
||||
if (useLBRYUploader) {
|
||||
publishPayload.tags.push('lbry-first');
|
||||
}
|
||||
|
||||
// Set release time to curret date. On edits, keep original release/transaction time as release_time
|
||||
if (releaseTimeEdited) {
|
||||
publishPayload.release_time = releaseTimeEdited;
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) {
|
||||
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) {
|
||||
publishPayload.release_time = Number(myClaimForUriEditing.timestamp);
|
||||
} else {
|
||||
publishPayload.release_time = Number(Math.round(Date.now() / 1000));
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
publishPayload.channel_id = channelId;
|
||||
}
|
||||
|
||||
if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) {
|
||||
publishPayload.locations = myClaimForUriEditing.value.locations;
|
||||
}
|
||||
|
||||
if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) {
|
||||
publishPayload.fee_currency = fee.currency;
|
||||
publishPayload.fee_amount = creditsToString(fee.amount);
|
||||
}
|
||||
|
||||
if (optimize) {
|
||||
publishPayload.optimize_file = true;
|
||||
}
|
||||
|
||||
// Only pass file on new uploads, not metadata only edits.
|
||||
// The sdk will figure it out
|
||||
if (filePath && !isLivestreamPublish) {
|
||||
publishPayload.file_path = filePath;
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
publishPayload.preview = true;
|
||||
publishPayload.optimize_file = false;
|
||||
}
|
||||
|
||||
return publishPayload;
|
||||
}
|
||||
|
||||
export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
|
||||
const publishPreview = (previewResponse) => {
|
||||
dispatch(
|
||||
|
@ -148,6 +290,62 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat
|
|||
dispatch(doPublish(publishSuccess, publishFail));
|
||||
};
|
||||
|
||||
export const doPublishResume = (publishPayload: any) => (dispatch: Dispatch, getState: () => {}) => {
|
||||
const publishSuccess = (successResponse, lbryFirstError) => {
|
||||
const state = getState();
|
||||
const myClaimIds: Set<string> = selectMyActiveClaims(state);
|
||||
|
||||
const pendingClaim = successResponse.outputs[0];
|
||||
const { permanent_url: url } = pendingClaim;
|
||||
|
||||
analytics.apiLogPublish(pendingClaim);
|
||||
|
||||
// We have to fake a temp claim until the new pending one is returned by claim_list_mine
|
||||
// We can't rely on claim_list_mine because there might be some delay before the new claims are returned
|
||||
// Doing this allows us to show the pending claim immediately, it will get overwritten by the real one
|
||||
const isEdit = myClaimIds.has(pendingClaim.claim_id);
|
||||
|
||||
const actions = [];
|
||||
|
||||
actions.push({
|
||||
type: ACTIONS.PUBLISH_SUCCESS,
|
||||
data: {
|
||||
type: resolveClaimTypeForAnalytics(pendingClaim),
|
||||
},
|
||||
});
|
||||
|
||||
actions.push({
|
||||
type: ACTIONS.UPDATE_PENDING_CLAIMS,
|
||||
data: {
|
||||
claims: [pendingClaim],
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(batchActions(...actions));
|
||||
|
||||
dispatch(
|
||||
doOpenModal(MODALS.PUBLISH, {
|
||||
uri: url,
|
||||
isEdit,
|
||||
lbryFirstError,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(doCheckPendingClaims());
|
||||
};
|
||||
|
||||
const publishFail = (error) => {
|
||||
const actions = [];
|
||||
actions.push({
|
||||
type: ACTIONS.PUBLISH_FAIL,
|
||||
});
|
||||
actions.push(doError(error.message));
|
||||
dispatch(batchActions(...actions));
|
||||
};
|
||||
|
||||
dispatch(doPublish(publishSuccess, publishFail, false, publishPayload));
|
||||
};
|
||||
|
||||
export const doResetThumbnailStatus = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
|
@ -373,7 +571,7 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
|||
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||
};
|
||||
|
||||
export const doPublish = (success: Function, fail: Function, preview: Function) => (
|
||||
export const doPublish = (success: Function, fail: Function, preview: Function, payload: any) => (
|
||||
dispatch: Dispatch,
|
||||
getState: () => {}
|
||||
) => {
|
||||
|
@ -388,139 +586,9 @@ export const doPublish = (success: Function, fail: Function, preview: Function)
|
|||
// get redux publish form
|
||||
const publishData = selectPublishFormValues(state);
|
||||
|
||||
// destructure the data values
|
||||
const {
|
||||
name,
|
||||
bid,
|
||||
filePath,
|
||||
description,
|
||||
language,
|
||||
releaseTimeEdited,
|
||||
// license,
|
||||
licenseUrl,
|
||||
useLBRYUploader,
|
||||
licenseType,
|
||||
otherLicenseDescription,
|
||||
thumbnail,
|
||||
channel,
|
||||
title,
|
||||
contentIsFree,
|
||||
fee,
|
||||
// uri,
|
||||
tags,
|
||||
// locations,
|
||||
optimize,
|
||||
isLivestreamPublish,
|
||||
remoteFileUrl,
|
||||
} = publishData;
|
||||
|
||||
// Handle scenario where we have a claim that has the same name as a channel we are publishing with.
|
||||
const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null;
|
||||
|
||||
let publishingLicense;
|
||||
switch (licenseType) {
|
||||
case COPYRIGHT:
|
||||
case OTHER:
|
||||
publishingLicense = otherLicenseDescription;
|
||||
break;
|
||||
default:
|
||||
publishingLicense = licenseType;
|
||||
}
|
||||
|
||||
// get the claim id from the channel name, we will use that instead
|
||||
const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null;
|
||||
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
|
||||
|
||||
const publishPayload: {
|
||||
name: ?string,
|
||||
bid: string,
|
||||
description?: string,
|
||||
channel_id?: string,
|
||||
file_path?: string,
|
||||
license_url?: string,
|
||||
license?: string,
|
||||
thumbnail_url?: string,
|
||||
release_time?: number,
|
||||
fee_currency?: string,
|
||||
fee_amount?: string,
|
||||
languages?: Array<string>,
|
||||
tags: Array<string>,
|
||||
locations?: Array<any>,
|
||||
blocking: boolean,
|
||||
optimize_file?: boolean,
|
||||
preview?: boolean,
|
||||
remote_url?: string,
|
||||
} = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
locations: [],
|
||||
bid: creditsToString(bid),
|
||||
languages: [language],
|
||||
tags: tags && tags.map((tag) => tag.name),
|
||||
thumbnail_url: thumbnail,
|
||||
blocking: true,
|
||||
preview: false,
|
||||
};
|
||||
// Temporary solution to keep the same publish flow with the new tags api
|
||||
// Eventually we will allow users to enter their own tags on publish
|
||||
// `nsfw` will probably be removed
|
||||
if (remoteFileUrl) {
|
||||
publishPayload.remote_url = remoteFileUrl;
|
||||
}
|
||||
|
||||
if (publishingLicense) {
|
||||
publishPayload.license = publishingLicense;
|
||||
}
|
||||
|
||||
if (licenseUrl) {
|
||||
publishPayload.license_url = licenseUrl;
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
publishPayload.thumbnail_url = thumbnail;
|
||||
}
|
||||
|
||||
if (useLBRYUploader) {
|
||||
publishPayload.tags.push('lbry-first');
|
||||
}
|
||||
|
||||
// Set release time to curret date. On edits, keep original release/transaction time as release_time
|
||||
if (releaseTimeEdited) {
|
||||
publishPayload.release_time = releaseTimeEdited;
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) {
|
||||
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
||||
} else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) {
|
||||
publishPayload.release_time = Number(myClaimForUriEditing.timestamp);
|
||||
} else {
|
||||
publishPayload.release_time = Number(Math.round(Date.now() / 1000));
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
publishPayload.channel_id = channelId;
|
||||
}
|
||||
|
||||
if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) {
|
||||
publishPayload.locations = myClaimForUriEditing.value.locations;
|
||||
}
|
||||
|
||||
if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) {
|
||||
publishPayload.fee_currency = fee.currency;
|
||||
publishPayload.fee_amount = creditsToString(fee.amount);
|
||||
}
|
||||
|
||||
if (optimize) {
|
||||
publishPayload.optimize_file = true;
|
||||
}
|
||||
|
||||
// Only pass file on new uploads, not metadata only edits.
|
||||
// The sdk will figure it out
|
||||
if (filePath && !isLivestreamPublish) publishPayload.file_path = filePath;
|
||||
const publishPayload = payload || resolvePublishPayload(publishData, myClaimForUri, myChannels, preview);
|
||||
|
||||
if (preview) {
|
||||
publishPayload.preview = true;
|
||||
publishPayload.optimize_file = false;
|
||||
|
||||
return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => {
|
||||
return preview(previewResponse);
|
||||
}, fail);
|
||||
|
@ -620,3 +688,35 @@ export const doCheckReflectingFiles = () => (dispatch: Dispatch, getState: GetSt
|
|||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
export function doUpdateUploadAdd(
|
||||
file: File | string,
|
||||
params: { [key: string]: any },
|
||||
uploader: TusUploader | XMLHttpRequest
|
||||
) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_UPLOAD_ADD,
|
||||
data: { file, params, uploader },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const doUpdateUploadProgress = (props: {
|
||||
params: { [key: string]: any },
|
||||
progress?: string,
|
||||
status?: string,
|
||||
}) => (dispatch: Dispatch) =>
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_UPLOAD_PROGRESS,
|
||||
data: props,
|
||||
});
|
||||
|
||||
export function doUpdateUploadRemove(params: { [key: string]: any }) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_UPLOAD_REMOVE,
|
||||
data: { params },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
// @flow
|
||||
import { handleActions } from 'util/redux-utils';
|
||||
import { buildURI } from 'util/lbryURI';
|
||||
import { serializeFileObj } from 'util/file';
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||
import { CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||
|
||||
const getKeyFromParam = (params) => `${params.name}#${params.channel || 'anonymous'}`;
|
||||
|
||||
type PublishState = {
|
||||
editingURI: ?string,
|
||||
fileText: ?string,
|
||||
|
@ -38,6 +41,7 @@ type PublishState = {
|
|||
tags: Array<string>,
|
||||
optimize: boolean,
|
||||
useLBRYUploader: boolean,
|
||||
currentUploads: { [key: string]: FileUploadItem },
|
||||
};
|
||||
|
||||
const defaultState: PublishState = {
|
||||
|
@ -78,6 +82,7 @@ const defaultState: PublishState = {
|
|||
publishError: undefined,
|
||||
optimize: false,
|
||||
useLBRYUploader: false,
|
||||
currentUploads: {},
|
||||
};
|
||||
|
||||
export const publishReducer = handleActions(
|
||||
|
@ -96,6 +101,7 @@ export const publishReducer = handleActions(
|
|||
bid: state.bid,
|
||||
optimize: state.optimize,
|
||||
language: state.language,
|
||||
currentUploads: state.currentUploads,
|
||||
}),
|
||||
[ACTIONS.PUBLISH_START]: (state: PublishState): PublishState => ({
|
||||
...state,
|
||||
|
@ -127,8 +133,76 @@ export const publishReducer = handleActions(
|
|||
...publishData,
|
||||
editingURI: uri,
|
||||
uri: shortUri,
|
||||
currentUploads: state.currentUploads,
|
||||
};
|
||||
},
|
||||
[ACTIONS.UPDATE_UPLOAD_ADD]: (state: PublishState, action) => {
|
||||
const { file, params, uploader } = action.data;
|
||||
const key = getKeyFromParam(params);
|
||||
const currentUploads = Object.assign({}, state.currentUploads);
|
||||
|
||||
currentUploads[key] = {
|
||||
file,
|
||||
fileFingerprint: file ? serializeFileObj(file) : undefined, // TODO: get hash instead?
|
||||
progress: '0',
|
||||
params,
|
||||
uploader,
|
||||
resumable: !(uploader instanceof XMLHttpRequest),
|
||||
};
|
||||
|
||||
return { ...state, currentUploads };
|
||||
},
|
||||
[ACTIONS.UPDATE_UPLOAD_PROGRESS]: (state: PublishState, action) => {
|
||||
const { params, progress, status } = action.data;
|
||||
const key = getKeyFromParam(params);
|
||||
const currentUploads = Object.assign({}, state.currentUploads);
|
||||
|
||||
if (!currentUploads[key]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
currentUploads[key].progress = progress;
|
||||
delete currentUploads[key].status;
|
||||
} else if (status) {
|
||||
currentUploads[key].status = status;
|
||||
if (status === 'error') {
|
||||
delete currentUploads[key].uploader;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...state, currentUploads };
|
||||
},
|
||||
[ACTIONS.UPDATE_UPLOAD_REMOVE]: (state: PublishState, action) => {
|
||||
const { params } = action.data;
|
||||
const key = getKeyFromParam(params);
|
||||
const currentUploads = Object.assign({}, state.currentUploads);
|
||||
|
||||
delete currentUploads[key];
|
||||
|
||||
return { ...state, currentUploads };
|
||||
},
|
||||
[ACTIONS.REHYDRATE]: (state: PublishState, action) => {
|
||||
if (action && action.payload && action.payload.publish) {
|
||||
const newPublish = { ...action.payload.publish };
|
||||
|
||||
// Cleanup for 'publish::currentUploads'
|
||||
if (newPublish.currentUploads) {
|
||||
Object.keys(newPublish.currentUploads).forEach((key) => {
|
||||
const params = newPublish.currentUploads[key].params;
|
||||
if (!params || Object.keys(params).length === 0) {
|
||||
delete newPublish.currentUploads[key];
|
||||
} else {
|
||||
delete newPublish.currentUploads[key].uploader;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newPublish;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
},
|
||||
defaultState
|
||||
);
|
||||
|
|
|
@ -119,3 +119,10 @@ export const selectTakeOverAmount = createSelector(
|
|||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectCurrentUploads = (state) => selectState(state).currentUploads;
|
||||
|
||||
export const selectUploadCount = createSelector(
|
||||
selectCurrentUploads,
|
||||
(currentUploads) => currentUploads && Object.keys(currentUploads).length
|
||||
);
|
||||
|
|
|
@ -363,6 +363,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.claim-upload__progress--label {
|
||||
font-size: var(--font-small);
|
||||
color: var(--color-text-subtitle);
|
||||
}
|
||||
|
||||
.claim-upload__progress--outer {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -857,3 +862,17 @@
|
|||
margin-top: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
.web-upload-item.claim-preview {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
display: block;
|
||||
|
||||
.media__thumb {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.claim-preview-metadata {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
ui/util/file.js
Normal file
5
ui/util/file.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
|
||||
export function serializeFileObj(file: File) {
|
||||
return `${file.name}#${file.type}#${file.size}`;
|
||||
}
|
70
web/setup/publish-v1.js
Normal file
70
web/setup/publish-v1.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
|
||||
// https://api.na-backend.odysee.com/api/v1/proxy currently expects publish to
|
||||
// consist of a multipart/form-data POST request with:
|
||||
// - 'file' binary
|
||||
// - 'json_payload' publish params to be passed to the server's sdk.
|
||||
|
||||
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
||||
import { doUpdateUploadAdd, doUpdateUploadProgress, doUpdateUploadRemove } from '../../ui/redux/actions/publish';
|
||||
import { LBRY_WEB_PUBLISH_API } from 'config';
|
||||
|
||||
const ENDPOINT = LBRY_WEB_PUBLISH_API;
|
||||
const ENDPOINT_METHOD = 'publish';
|
||||
|
||||
export function makeUploadRequest(
|
||||
token: string,
|
||||
params: FileUploadSdkParams,
|
||||
file: File | string,
|
||||
isPreview?: boolean
|
||||
) {
|
||||
const { remote_url: remoteUrl } = params;
|
||||
|
||||
const body = new FormData();
|
||||
|
||||
if (file) {
|
||||
body.append('file', file);
|
||||
delete params['remote_url'];
|
||||
} else if (remoteUrl) {
|
||||
body.append('remote_url', remoteUrl);
|
||||
delete params['remote_url'];
|
||||
}
|
||||
|
||||
const jsonPayload = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: ENDPOINT_METHOD,
|
||||
params,
|
||||
id: new Date().getTime(),
|
||||
});
|
||||
|
||||
// no fileData? do the livestream remote publish
|
||||
body.append('json_payload', jsonPayload);
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', ENDPOINT);
|
||||
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
|
||||
xhr.responseType = 'json';
|
||||
xhr.upload.onprogress = (e) => {
|
||||
const percentage = ((e.loaded / e.total) * 100).toFixed(2);
|
||||
window.store.dispatch(doUpdateUploadProgress({ params, progress: percentage }));
|
||||
};
|
||||
xhr.onload = () => {
|
||||
window.store.dispatch(doUpdateUploadRemove(params));
|
||||
resolve(xhr);
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
window.store.dispatch(doUpdateUploadProgress({ params, status: 'error' }));
|
||||
reject(new Error(__('There was a problem with your upload. Please try again.')));
|
||||
};
|
||||
xhr.onabort = () => {
|
||||
window.store.dispatch(doUpdateUploadRemove(params));
|
||||
};
|
||||
|
||||
if (!isPreview) {
|
||||
window.store.dispatch(doUpdateUploadAdd(file, params, xhr));
|
||||
}
|
||||
|
||||
xhr.send(body);
|
||||
});
|
||||
}
|
100
web/setup/publish-v2.js
Normal file
100
web/setup/publish-v2.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
// @flow
|
||||
import * as tus from 'tus-js-client';
|
||||
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
||||
import { doUpdateUploadAdd, doUpdateUploadProgress, doUpdateUploadRemove } from '../../ui/redux/actions/publish';
|
||||
import { LBRY_WEB_PUBLISH_API_V2 } from 'config';
|
||||
|
||||
const RESUMABLE_ENDPOINT = LBRY_WEB_PUBLISH_API_V2;
|
||||
const RESUMABLE_ENDPOINT_METHOD = 'publish';
|
||||
const UPLOAD_CHUNK_SIZE_BYTE = 100000000;
|
||||
|
||||
export function makeResumableUploadRequest(
|
||||
token: string,
|
||||
params: FileUploadSdkParams,
|
||||
file: File | string,
|
||||
isPreview?: boolean
|
||||
) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
if (!RESUMABLE_ENDPOINT) {
|
||||
reject(new Error('Publish: endpoint undefined'));
|
||||
}
|
||||
|
||||
// @if NODE_ENV!='production'
|
||||
if (params.remote_url) {
|
||||
reject(new Error('Publish: v2 does not support remote_url'));
|
||||
}
|
||||
// @endif
|
||||
|
||||
const jsonPayload = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: RESUMABLE_ENDPOINT_METHOD,
|
||||
params,
|
||||
id: new Date().getTime(),
|
||||
});
|
||||
|
||||
const uploader = new tus.Upload(file, {
|
||||
endpoint: RESUMABLE_ENDPOINT,
|
||||
chunkSize: UPLOAD_CHUNK_SIZE_BYTE,
|
||||
retryDelays: [0, 3000, 3000],
|
||||
parallelUploads: 1,
|
||||
removeFingerprintOnSuccess: true,
|
||||
headers: { [X_LBRY_AUTH_TOKEN]: token },
|
||||
metadata: {
|
||||
filename: file instanceof File ? file.name : file,
|
||||
filetype: file instanceof File ? file.type : undefined,
|
||||
},
|
||||
onShouldRetry: (err, retryAttempt, options) => {
|
||||
window.store.dispatch(doUpdateUploadProgress({ params, status: 'retry' }));
|
||||
const FORBIDDEN_ERROR = 403;
|
||||
const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
|
||||
return status !== FORBIDDEN_ERROR;
|
||||
},
|
||||
onError: (error) => {
|
||||
window.store.dispatch(doUpdateUploadProgress({ params, status: 'error' }));
|
||||
reject(new Error(error));
|
||||
},
|
||||
onProgress: (bytesUploaded, bytesTotal) => {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
window.store.dispatch(doUpdateUploadProgress({ params, progress: percentage }));
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Notify lbrynet server
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `${uploader.url}/notify`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('Tus-Resumable', '1.0.0');
|
||||
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = () => {
|
||||
window.store.dispatch(doUpdateUploadRemove(params));
|
||||
resolve(xhr);
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
reject(new Error(__('There was a problem with your upload. Please try again.')));
|
||||
};
|
||||
xhr.onabort = () => {
|
||||
window.store.dispatch(doUpdateUploadRemove(params));
|
||||
};
|
||||
|
||||
xhr.send(jsonPayload);
|
||||
},
|
||||
});
|
||||
|
||||
uploader
|
||||
.findPreviousUploads()
|
||||
.then((previousUploads) => {
|
||||
if (previousUploads.length > 0) {
|
||||
uploader.resumeFromPreviousUpload(previousUploads[0]);
|
||||
}
|
||||
|
||||
if (!isPreview) {
|
||||
window.store.dispatch(doUpdateUploadAdd(file, params, uploader));
|
||||
}
|
||||
|
||||
uploader.start();
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(new Error(__('Failed to initiate upload (%err%)', { err })));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,22 +1,16 @@
|
|||
// @flow
|
||||
/*
|
||||
https://api.na-backend.odysee.com/api/v1/proxy currently expects publish to consist
|
||||
of a multipart/form-data POST request with:
|
||||
- 'file' binary
|
||||
- 'json_payload' collection of publish params to be passed to the server's sdk.
|
||||
*/
|
||||
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
||||
import { doUpdateUploadProgress } from 'lbryinc';
|
||||
import * as tus from 'tus-js-client';
|
||||
import { makeUploadRequest } from './publish-v1';
|
||||
import { makeResumableUploadRequest } from './publish-v2';
|
||||
|
||||
// A modified version of Lbry.apiCall that allows
|
||||
// to perform calling methods at arbitrary urls
|
||||
// and pass form file fields
|
||||
export default function apiPublishCallViaWeb(
|
||||
apiCall: (any, any, any, any) => any,
|
||||
connectionString: string,
|
||||
token: string,
|
||||
method: string,
|
||||
params: { file_path: string, preview: boolean, remote_url?: string }, // new param for remoteUrl
|
||||
params: FileUploadSdkParams,
|
||||
resolve: Function,
|
||||
reject: Function
|
||||
) {
|
||||
|
@ -26,7 +20,6 @@ export default function apiPublishCallViaWeb(
|
|||
return apiCall(method, params, resolve, reject);
|
||||
}
|
||||
|
||||
const counter = new Date().getTime();
|
||||
let fileField = filePath;
|
||||
|
||||
if (preview) {
|
||||
|
@ -37,53 +30,16 @@ export default function apiPublishCallViaWeb(
|
|||
|
||||
// Putting a dummy value here, the server is going to process the POSTed file
|
||||
// and set the file_path itself
|
||||
|
||||
const body = new FormData();
|
||||
if (fileField) {
|
||||
body.append('file', fileField);
|
||||
params.file_path = '__POST_FILE__';
|
||||
delete params['remote_url'];
|
||||
} else if (remoteUrl) {
|
||||
body.append('remote_url', remoteUrl);
|
||||
delete params['remote_url'];
|
||||
}
|
||||
|
||||
const jsonPayload = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
id: counter,
|
||||
});
|
||||
// no fileData? do the livestream remote publish
|
||||
body.append('json_payload', jsonPayload);
|
||||
const useV1 = remoteUrl || preview || !tus.isSupported;
|
||||
|
||||
function makeRequest(connectionString, method, token, body, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open(method, connectionString);
|
||||
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
|
||||
xhr.responseType = 'json';
|
||||
xhr.upload.onprogress = (e) => {
|
||||
let percentComplete = Math.ceil((e.loaded / e.total) * 100);
|
||||
window.store.dispatch(doUpdateUploadProgress(percentComplete, params, xhr));
|
||||
};
|
||||
xhr.onload = () => {
|
||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
||||
resolve(xhr);
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
||||
reject(new Error(__('There was a problem with your upload. Please try again.')));
|
||||
};
|
||||
// Note: both function signature (params) should match.
|
||||
const makeRequest = useV1 ? makeUploadRequest : makeResumableUploadRequest;
|
||||
|
||||
xhr.onabort = () => {
|
||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
||||
};
|
||||
xhr.send(body);
|
||||
});
|
||||
}
|
||||
|
||||
return makeRequest(connectionString, 'POST', token, body, params)
|
||||
return makeRequest(token, params, fileField, preview)
|
||||
.then((xhr) => {
|
||||
let error;
|
||||
if (xhr && xhr.response) {
|
||||
|
|
96
yarn.lock
96
yarn.lock
|
@ -3869,6 +3869,11 @@ buffer-fill@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
||||
|
||||
buffer-from@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0"
|
||||
integrity sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
|
@ -4634,6 +4639,14 @@ colors@~1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
||||
integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
|
||||
|
||||
combine-errors@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/combine-errors/-/combine-errors-3.0.3.tgz#f4df6740083e5703a3181110c2b10551f003da86"
|
||||
integrity sha1-9N9nQAg+VwOjGBEQwrEFUfAD2oY=
|
||||
dependencies:
|
||||
custom-error-instance "2.1.1"
|
||||
lodash.uniqby "4.5.0"
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
|
@ -5337,6 +5350,11 @@ currently-unhandled@^0.4.1:
|
|||
dependencies:
|
||||
array-find-index "^1.0.1"
|
||||
|
||||
custom-error-instance@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz#3cf6391487a6629a6247eb0ca0ce00081b7e361a"
|
||||
integrity sha1-PPY5FIemYppiR+sMoM4ACBt+Nho=
|
||||
|
||||
cyclist@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||
|
@ -9824,7 +9842,7 @@ jest@20.0.4:
|
|||
dependencies:
|
||||
jest-cli "^20.0.4"
|
||||
|
||||
js-base64@^2.1.9:
|
||||
js-base64@^2.1.9, js-base64@^2.6.1:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
|
||||
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
|
||||
|
@ -10375,11 +10393,48 @@ lodash-es@^4.17.14, lodash-es@^4.2.1:
|
|||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
|
||||
lodash._baseiteratee@~4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz#34a9b5543572727c3db2e78edae3c0e9e66bd102"
|
||||
integrity sha1-NKm1VDVycnw9sueO2uPA6eZr0QI=
|
||||
dependencies:
|
||||
lodash._stringtopath "~4.8.0"
|
||||
|
||||
lodash._basetostring@~4.12.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz#9327c9dc5158866b7fa4b9d42f4638e5766dd9df"
|
||||
integrity sha1-kyfJ3FFYhmt/pLnUL0Y45XZt2d8=
|
||||
|
||||
lodash._baseuniq@~4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
|
||||
integrity sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=
|
||||
dependencies:
|
||||
lodash._createset "~4.0.0"
|
||||
lodash._root "~3.0.0"
|
||||
|
||||
lodash._createset@~4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
|
||||
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
|
||||
|
||||
lodash._reinterpolate@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
||||
|
||||
lodash._root@~3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
|
||||
integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=
|
||||
|
||||
lodash._stringtopath@~4.8.0:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz#941bcf0e64266e5fc1d66fed0a6959544c576824"
|
||||
integrity sha1-lBvPDmQmbl/B1m/tCmlZVExXaCQ=
|
||||
dependencies:
|
||||
lodash._basetostring "~4.12.0"
|
||||
|
||||
lodash.camelcase@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
|
@ -10449,6 +10504,11 @@ lodash.templatesettings@^4.0.0:
|
|||
dependencies:
|
||||
lodash._reinterpolate "^3.0.0"
|
||||
|
||||
lodash.throttle@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
|
||||
|
||||
lodash.toarray@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
|
||||
|
@ -10457,6 +10517,14 @@ lodash.uniq@^4.5.0:
|
|||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
|
||||
lodash.uniqby@4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz#a3a17bbf62eeb6240f491846e97c1c4e2a5e1e21"
|
||||
integrity sha1-o6F7v2LutiQPSRhG6XwcTipeHiE=
|
||||
dependencies:
|
||||
lodash._baseiteratee "~4.7.0"
|
||||
lodash._baseuniq "~4.6.0"
|
||||
|
||||
lodash.unset@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed"
|
||||
|
@ -12980,6 +13048,14 @@ prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0,
|
|||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
proper-lockfile@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-2.0.1.tgz#159fb06193d32003f4b3691dd2ec1a634aa80d1d"
|
||||
integrity sha1-FZ+wYZPTIAP0s2kd0uwaY0qoDR0=
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
retry "^0.10.0"
|
||||
|
||||
property-information@^5.3.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
|
||||
|
@ -14245,6 +14321,11 @@ ret@~0.1.10:
|
|||
version "0.1.15"
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
|
||||
retry@^0.10.0:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
||||
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
|
||||
|
||||
retry@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
||||
|
@ -15896,6 +15977,19 @@ tunnel@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
||||
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
|
||||
|
||||
tus-js-client@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tus-js-client/-/tus-js-client-2.3.0.tgz#5d76145476cea46a4e7c045a0054637cddf8dc39"
|
||||
integrity sha512-I4cSwm6N5qxqCmBqenvutwSHe9ntf81lLrtf6BmLpG2v4wTl89atCQKqGgqvkodE6Lx+iKIjMbaXmfvStTg01g==
|
||||
dependencies:
|
||||
buffer-from "^0.1.1"
|
||||
combine-errors "^3.0.3"
|
||||
is-stream "^2.0.0"
|
||||
js-base64 "^2.6.1"
|
||||
lodash.throttle "^4.1.1"
|
||||
proper-lockfile "^2.0.1"
|
||||
url-parse "^1.4.3"
|
||||
|
||||
tween-functions@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
||||
|
|
Loading…
Reference in a new issue