-
-
- {this.props.file ? (
-
-
+ {isUpdate && fileExt === 'mp4' ? (
+ Video updates are currently disabled. This feature will be available soon. You can edit metadata.
+ ) : (
+
+
+ )}
);
}
diff --git a/client/src/containers/PublishDetails/index.js b/client/src/containers/PublishDetails/index.js
index 7b880782..8c8eede0 100644
--- a/client/src/containers/PublishDetails/index.js
+++ b/client/src/containers/PublishDetails/index.js
@@ -1,16 +1,21 @@
import { connect } from 'react-redux';
-import { clearFile, startPublish } from '../../actions/publish';
+import { clearFile, startPublish, abandonClaim } from '../../actions/publish';
+import { selectAsset } from '../../selectors/show';
import View from './view';
-const mapStateToProps = ({ channel, publish }) => {
+const mapStateToProps = ({ show, publish }) => {
return {
file: publish.file,
+ isUpdate: publish.isUpdate,
+ hasChanged: publish.hasChanged,
+ asset: selectAsset(show),
};
};
const mapDispatchToProps = {
clearFile,
startPublish,
+ abandonClaim,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);
diff --git a/client/src/containers/PublishDetails/view.jsx b/client/src/containers/PublishDetails/view.jsx
index ac55e226..95a79edd 100644
--- a/client/src/containers/PublishDetails/view.jsx
+++ b/client/src/containers/PublishDetails/view.jsx
@@ -1,35 +1,76 @@
import React from 'react';
-import { withRouter } from 'react-router-dom';
+import {Link, withRouter} from 'react-router-dom';
import PublishUrlInput from '@containers/PublishUrlInput';
import PublishThumbnailInput from '@containers/PublishThumbnailInput';
import PublishMetadataInputs from '@containers/PublishMetadataInputs';
import ChannelSelect from '@containers/ChannelSelect';
import Row from '@components/Row';
+import Label from '@components/Label';
+import RowLabeled from '@components/RowLabeled';
import ButtonPrimaryJumbo from '@components/ButtonPrimaryJumbo';
import ButtonTertiary from '@components/ButtonTertiary';
+import ButtonSecondary from '@components/ButtonSecondary';
import SpaceAround from '@components/SpaceAround';
import PublishFinePrint from '@components/PublishFinePrint';
+import { SAVE } from '../../constants/confirmation_messages';
class PublishDetails extends React.Component {
constructor (props) {
super(props);
this.onPublishSubmit = this.onPublishSubmit.bind(this);
+ this.abandonClaim = this.abandonClaim.bind(this);
+ this.onCancel = this.onCancel.bind(this);
}
onPublishSubmit () {
this.props.startPublish(this.props.history);
}
+ abandonClaim () {
+ const {asset, history} = this.props;
+ if (asset) {
+ const {claimData} = asset;
+ this.props.abandonClaim({claimData, history});
+ }
+ }
+ onCancel () {
+ const { isUpdate, hasChanged, clearFile, history } = this.props;
+ if (isUpdate || !hasChanged) {
+ history.push('/');
+ } else {
+ if (confirm(SAVE)) {
+ clearFile();
+ }
+ }
+ }
render () {
+ const {file, isUpdate, asset} = this.props;
return (
-
-
-
+ {isUpdate ? (asset && (
+
+
+ }
+ content={
+
+ {asset.claimData.channelName}
+
+ }
+ />
+
+ )) : (
+
+
+
+
-
-
-
+
+
+
+
+ )}
- { this.props.file.type === 'video/mp4' && (
+ { file && file.type === 'video/mp4' && (
@@ -41,16 +82,27 @@ class PublishDetails extends React.Component {
+ {isUpdate && (
+
+
+
+
+
+ )}
+
diff --git a/client/src/containers/PublishMetadataInputs/index.js b/client/src/containers/PublishMetadataInputs/index.js
index b00b6f09..1c5b462a 100644
--- a/client/src/containers/PublishMetadataInputs/index.js
+++ b/client/src/containers/PublishMetadataInputs/index.js
@@ -8,6 +8,7 @@ const mapStateToProps = ({ publish }) => {
description : publish.metadata.description,
license : publish.metadata.license,
nsfw : publish.metadata.nsfw,
+ isUpdate : publish.isUpdate,
};
};
diff --git a/client/src/containers/PublishMetadataInputs/view.jsx b/client/src/containers/PublishMetadataInputs/view.jsx
index 10d8a793..d5bfb3d9 100644
--- a/client/src/containers/PublishMetadataInputs/view.jsx
+++ b/client/src/containers/PublishMetadataInputs/view.jsx
@@ -26,27 +26,30 @@ class PublishMetadataInputs extends React.Component {
this.props.onMetadataChange(name, selectedOption);
}
render () {
+ const { showMetadataInputs, description, isUpdate, nsfw } = this.props;
return (
- {this.props.showMetadataInputs && (
+ {(showMetadataInputs || isUpdate) && (
)}
-
+ {!isUpdate && (
+
+ )}
);
}
diff --git a/client/src/containers/PublishStatus/view.jsx b/client/src/containers/PublishStatus/view.jsx
index 71c04e7e..73212fba 100644
--- a/client/src/containers/PublishStatus/view.jsx
+++ b/client/src/containers/PublishStatus/view.jsx
@@ -12,7 +12,7 @@ class PublishStatus extends React.Component {
{status === publishStates.LOAD_START &&
- le is loading to server
+ File is loading to server
0%
@@ -42,7 +42,7 @@ class PublishStatus extends React.Component {
}
- {status === publishStates.SUCCESS &&
+ {status === publishStates.SUCCEEDED &&
Your publish is complete! You are being redirected to it now.
@@ -71,6 +71,13 @@ class PublishStatus extends React.Component {
}
+ {status === publishStates.ABANDONING &&
+
+
+ Your claim is being abandoned.
+
+
+ }
);
}
diff --git a/client/src/containers/PublishTool/index.js b/client/src/containers/PublishTool/index.js
index 9258d560..86a4b6b3 100644
--- a/client/src/containers/PublishTool/index.js
+++ b/client/src/containers/PublishTool/index.js
@@ -1,11 +1,22 @@
import {connect} from 'react-redux';
import View from './view';
+import {selectAsset} from "../../selectors/show";
+import {buildURI} from "../../utils/buildURI";
-const mapStateToProps = ({ publish }) => {
+const mapStateToProps = props => {
+ const { show, publish } = props;
+ const asset = selectAsset(show);
+ let uri;
+ if (asset) {
+ uri = `lbry://${buildURI(asset)}`;
+ }
return {
disabled: publish.disabled,
- file : publish.file,
- status : publish.status.status,
+ file: publish.file,
+ status: publish.status.status,
+ isUpdate: publish.isUpdate,
+ hasChanged: publish.hasChanged,
+ uri,
};
};
diff --git a/client/src/containers/PublishTool/view.jsx b/client/src/containers/PublishTool/view.jsx
index b628911a..86e4c2b2 100644
--- a/client/src/containers/PublishTool/view.jsx
+++ b/client/src/containers/PublishTool/view.jsx
@@ -1,23 +1,34 @@
import React from 'react';
+import { withRouter, Prompt } from 'react-router';
import Dropzone from '@containers/Dropzone';
import PublishPreview from '@components/PublishPreview';
import PublishStatus from '@containers/PublishStatus';
import PublishDisabledMessage from '@containers/PublishDisabledMessage';
+import { SAVE } from '../../constants/confirmation_messages';
class PublishTool extends React.Component {
render () {
- if (this.props.disabled) {
+ const {disabled, file, isUpdate, hasChanged, uri, status, location: currentLocation} = this.props;
+ if (disabled) {
return (
);
} else {
- if (this.props.file) {
- if (this.props.status) {
+ if (file || isUpdate) {
+ if (status) {
return (
);
} else {
- return
;
+ return (
+
+ location.pathname === currentLocation.pathname ? false : SAVE}
+ />
+
+
+ );
}
}
return
;
@@ -25,4 +36,4 @@ class PublishTool extends React.Component {
}
};
-export default PublishTool;
+export default withRouter(PublishTool);
diff --git a/client/src/pages/AboutPage/index.jsx b/client/src/pages/AboutPage/index.jsx
index 887e17f9..621f27b7 100644
--- a/client/src/pages/AboutPage/index.jsx
+++ b/client/src/pages/AboutPage/index.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { withRouter } from 'react-router';
import PageLayout from '@components/PageLayout';
import HorizontalSplit from '@components/HorizontalSplit';
import AboutSpeechOverview from '@components/AboutSpeechOverview';
@@ -20,4 +21,4 @@ class AboutPage extends React.Component {
}
}
-export default AboutPage;
+export default withRouter(AboutPage);
diff --git a/client/src/pages/EditPage/index.js b/client/src/pages/EditPage/index.js
new file mode 100644
index 00000000..d7ca7b25
--- /dev/null
+++ b/client/src/pages/EditPage/index.js
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux';
+import { setUpdateTrue, setHasChanged, updateMetadata, clearFile } from '../../actions/publish';
+import { onHandleShowPageUri } from '../../actions/show';
+import { selectAsset } from '../../selectors/show';
+import View from './view';
+
+const mapStateToProps = (props) => {
+ const { show } = props;
+ return {
+ asset : selectAsset(show),
+ myChannel: props.channel.loggedInChannel.name,
+ isUpdate : props.publish.isUpdate,
+ };
+};
+
+const mapDispatchToProps = {
+ updateMetadata,
+ onHandleShowPageUri,
+ setUpdateTrue,
+ setHasChanged,
+ clearFile,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(View);
diff --git a/client/src/pages/EditPage/view.jsx b/client/src/pages/EditPage/view.jsx
new file mode 100644
index 00000000..60b07900
--- /dev/null
+++ b/client/src/pages/EditPage/view.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import PageLayout from '@components/PageLayout';
+import { Redirect } from 'react-router-dom';
+import PublishTool from '@containers/PublishTool';
+
+class EditPage extends React.Component {
+ componentDidMount () {
+ const {asset, match, onHandleShowPageUri, setUpdateTrue, setHasChanged, updateMetadata} = this.props;
+ onHandleShowPageUri(match.params);
+ setUpdateTrue();
+ if (asset) {
+ ['title', 'description', 'license', 'nsfw'].forEach(meta => updateMetadata(meta, asset.claimData[meta]));
+ }
+ setHasChanged(false);
+ }
+ componentWillUnmount () {
+ this.props.clearFile();
+ }
+ render () {
+ const { myChannel, asset } = this.props;
+ // redirect if user does not own this claim
+ if (
+ !myChannel || (
+ asset &&
+ asset.claimsData &&
+ asset.claimsData.channelName &&
+ asset.claimsData.channelName !== myChannel
+ )
+ ) {
+ return (
);
+ }
+ return (
+
+
+
+ );
+ }
+};
+
+export default EditPage;
diff --git a/client/src/pages/HomePage/index.jsx b/client/src/pages/HomePage/index.js
similarity index 75%
rename from client/src/pages/HomePage/index.jsx
rename to client/src/pages/HomePage/index.js
index bdff3cab..4695ef62 100644
--- a/client/src/pages/HomePage/index.jsx
+++ b/client/src/pages/HomePage/index.js
@@ -1,17 +1,20 @@
import { connect } from 'react-redux';
import { onHandleShowHomepage } from '../../actions/show';
+import { clearFile } from '../../actions/publish';
import View from './view';
-const mapStateToProps = ({ show, site, channel }) => {
+const mapStateToProps = ({ show, site, channel, publish }) => {
return {
error : show.request.error,
requestType: show.request.type,
homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,
+ isUpdate : publish.isUpdate,
};
};
const mapDispatchToProps = {
onHandleShowHomepage,
+ clearFile,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);
diff --git a/client/src/pages/HomePage/view.jsx b/client/src/pages/HomePage/view.jsx
index 491d00a1..36b8a78e 100644
--- a/client/src/pages/HomePage/view.jsx
+++ b/client/src/pages/HomePage/view.jsx
@@ -4,16 +4,9 @@ import PublishTool from '@containers/PublishTool';
import ContentPageWrapper from '@pages/ContentPageWrapper';
class HomePage extends React.Component {
- componentDidMount () {
- this.props.onHandleShowHomepage(this.props.match.params);
+ componentWillUnmount () {
+ this.props.clearFile();
}
-
- componentWillReceiveProps (nextProps) {
- if (nextProps.match.params !== this.props.match.params) {
- this.props.onHandleShowHomepage(nextProps.match.params);
- }
- }
-
render () {
const { homeChannel } = this.props;
return homeChannel ? (
diff --git a/client/src/pages/ShowAssetDetails/index.js b/client/src/pages/ShowAssetDetails/index.js
index 0af0073c..cabb5046 100644
--- a/client/src/pages/ShowAssetDetails/index.js
+++ b/client/src/pages/ShowAssetDetails/index.js
@@ -1,20 +1,10 @@
import { connect } from 'react-redux';
+import { selectAsset } from '../../selectors/show';
import View from './view';
const mapStateToProps = ({ show }) => {
- // select request info
- const requestId = show.request.id;
- // select asset info
- let asset;
- const request = show.requestList[requestId] || null;
- const assetList = show.assetList;
- if (request && assetList) {
- const assetKey = request.key; // note: just store this in the request
- asset = assetList[assetKey] || null;
- };
- // return props
return {
- asset,
+ asset: selectAsset(show),
};
};
diff --git a/client/src/pages/ShowAssetDetails/view.jsx b/client/src/pages/ShowAssetDetails/view.jsx
index 98c09372..ed26c436 100644
--- a/client/src/pages/ShowAssetDetails/view.jsx
+++ b/client/src/pages/ShowAssetDetails/view.jsx
@@ -1,6 +1,5 @@
import React from 'react';
import PageLayout from '@components/PageLayout';
-
import HorizontalSplit from '@components/HorizontalSplit';
import AssetTitle from '@containers/AssetTitle';
import AssetDisplay from '@containers/AssetDisplay';
diff --git a/client/src/reducers/publish.js b/client/src/reducers/publish.js
index 29a31d7d..3282a722 100644
--- a/client/src/reducers/publish.js
+++ b/client/src/reducers/publish.js
@@ -41,6 +41,8 @@ const initialState = {
license : '',
nsfw : false,
},
+ isUpdate: false,
+ hasChanged: false,
thumbnail: null,
thumbnailChannel,
thumbnailChannelId,
@@ -49,8 +51,9 @@ const initialState = {
export default function (state = initialState, action) {
switch (action.type) {
case actions.FILE_SELECTED:
- return Object.assign({}, initialState, { // note: clears to initial state
+ return Object.assign({}, state.isUpdate ? state : initialState, { // note: clears to initial state
file: action.data,
+ hasChanged: true,
});
case actions.FILE_CLEAR:
return initialState;
@@ -59,14 +62,17 @@ export default function (state = initialState, action) {
metadata: Object.assign({}, state.metadata, {
[action.data.name]: action.data.value,
}),
+ hasChanged: true,
});
case actions.CLAIM_UPDATE:
return Object.assign({}, state, {
claim: action.data,
+ hasChanged: true,
});
case actions.SET_PUBLISH_IN_CHANNEL:
return Object.assign({}, state, {
publishInChannel: action.channel,
+ hasChanged: true,
});
case actions.PUBLISH_STATUS_UPDATE:
return Object.assign({}, state, {
@@ -83,13 +89,26 @@ export default function (state = initialState, action) {
selectedChannel: action.data,
});
case actions.TOGGLE_METADATA_INPUTS:
- return Object.assign({}, state, {
+ return {
+ ...state,
showMetadataInputs: action.data,
- });
+ };
case actions.THUMBNAIL_NEW:
- return Object.assign({}, state, {
+ return {
+ ...state,
thumbnail: action.data,
- });
+ hasChanged: true,
+ };
+ case actions.SET_UPDATE_TRUE:
+ return {
+ ...state,
+ isUpdate: true,
+ };
+ case actions.SET_HAS_CHANGED:
+ return {
+ ...state,
+ hasChanged: action.data,
+ };
default:
return state;
}
diff --git a/client/src/reducers/show.js b/client/src/reducers/show.js
index 91177370..a93da58b 100644
--- a/client/src/reducers/show.js
+++ b/client/src/reducers/show.js
@@ -65,6 +65,43 @@ export default function (state = initialState, action) {
},
}),
});
+ case actions.ASSET_REMOVE:
+ const claim = action.data;
+ const newAssetList = state.assetList;
+ delete newAssetList[`a#${claim.name}#${claim.claimId}`];
+
+ const channelId = `c#${claim.channelName}#${claim.certificateId}`;
+ const channelClaims = state.channelList[channelId].claimsData.claims;
+ const newClaimsData = channelClaims.filter(c => c.claimId !== claim.claimId);
+
+ return {
+ ...state,
+ assetList : newAssetList,
+ channelList: {
+ ...state.channelList,
+ [channelId]: {
+ ...state.channelList[channelId],
+ claimsData: {
+ ...state.channelList[channelId].claimsData,
+ claims: newClaimsData,
+ },
+ },
+ },
+ };
+ case actions.ASSET_UPDATE_CLAIMDATA:
+ return {
+ ...state,
+ assetList: {
+ ...state.assetList,
+ [action.data.id]: {
+ ...state.assetList[action.data.id],
+ claimData: {
+ ...state.assetList[action.data.id].claimData,
+ ...action.data.claimData,
+ },
+ },
+ },
+ };
// channel data
case actions.CHANNEL_ADD:
return Object.assign({}, state, {
@@ -77,7 +114,7 @@ export default function (state = initialState, action) {
},
}),
});
- case actions.CHANNEL_CLAIMS_UPDATE_SUCCESS:
+ case actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED:
return Object.assign({}, state, {
channelList: Object.assign({}, state.channelList, {
[action.data.channelListId]: Object.assign({}, state.channelList[action.data.channelListId], {
diff --git a/client/src/sagas/abandon.js b/client/src/sagas/abandon.js
new file mode 100644
index 00000000..db290274
--- /dev/null
+++ b/client/src/sagas/abandon.js
@@ -0,0 +1,30 @@
+import { call, put, takeLatest } from 'redux-saga/effects';
+import * as actions from '../constants/publish_action_types';
+import * as publishStates from '../constants/publish_claim_states';
+import { updatePublishStatus, clearFile } from '../actions/publish';
+import { removeAsset } from '../actions/show';
+import { doAbandonClaim } from '../api/assetApi';
+
+function * abandonClaim (action) {
+ const { claimData, history } = action.data;
+ const { claimId } = claimData;
+
+ const confirm = window.confirm('Are you sure you want to abandon this claim? This action cannot be undone.');
+ if (!confirm) return;
+
+ yield put(updatePublishStatus(publishStates.ABANDONING, 'Your claim is being abandoned...'));
+
+ try {
+ yield call(doAbandonClaim, claimId);
+ } catch (error) {
+ return console.log('abandon error:', error.message);
+ }
+
+ yield put(clearFile());
+ yield put(removeAsset(claimData));
+ return history.push('/');
+}
+
+export function * watchAbandonClaim () {
+ yield takeLatest(actions.ABANDON_CLAIM, abandonClaim);
+};
diff --git a/client/src/sagas/publish.js b/client/src/sagas/publish.js
index 62157eca..7e61cda7 100644
--- a/client/src/sagas/publish.js
+++ b/client/src/sagas/publish.js
@@ -5,37 +5,57 @@ import { updateError, updatePublishStatus, clearFile } from '../actions/publish'
import { selectPublishState } from '../selectors/publish';
import { selectChannelState } from '../selectors/channel';
import { selectSiteState } from '../selectors/site';
+import { selectShowState, selectAsset } from '../selectors/show';
import { validateChannelSelection, validateNoPublishErrors } from '../utils/validate';
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from '../utils/publish';
import { makePublishRequestChannel } from '../channels/publish';
function * publishFile (action) {
const { history } = action.data;
- const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, error: publishToolErrors } = yield select(selectPublishState);
+ const publishState = yield select(selectPublishState);
+ const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, isUpdate, error: publishToolErrors } = publishState;
const { loggedInChannel } = yield select(selectChannelState);
const { host } = yield select(selectSiteState);
+
+ let show, asset;
+ if (isUpdate) {
+ show = yield select(selectShowState);
+ asset = selectAsset(show);
+ }
// validate the channel selection
try {
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
} catch (error) {
return yield put(updateError('channel', error.message));
- };
+ }
// validate publish parameters
try {
validateNoPublishErrors(publishToolErrors);
} catch (error) {
return console.log('publish error:', error.message);
}
+
+ let publishMetadata, publishFormData, publishChannel;
// create metadata
- let publishMetadata = createPublishMetadata(claim, file, metadata, publishInChannel, selectedChannel);
+ publishMetadata = createPublishMetadata(
+ isUpdate ? asset.name : claim,
+ isUpdate ? {type: asset.claimData.contentType} : file,
+ metadata,
+ publishInChannel,
+ selectedChannel
+ );
+ if (isUpdate) {
+ publishMetadata['channelName'] = asset.claimData.channelName;
+ }
if (thumbnail) {
// add thumbnail to publish metadata
- publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
+ publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
}
// create form data for main publish
- const publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
+ publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
// make the publish request
- const publishChannel = yield call(makePublishRequestChannel, publishFormData);
+ publishChannel = yield call(makePublishRequestChannel, publishFormData, isUpdate);
+
while (true) {
const {loadStart, progress, load, success, error: publishError} = yield take(publishChannel);
if (publishError) {
@@ -43,7 +63,21 @@ function * publishFile (action) {
}
if (success) {
yield put(clearFile());
- return history.push(`/${success.data.claimId}/${success.data.name}`);
+ if (isUpdate) {
+ yield put({
+ type: 'ASSET_UPDATE_CLAIMDATA',
+ data: {
+ id : `a#${success.data.name}#${success.data.claimId}`,
+ claimData: success.data.claimData,
+ },
+ });
+ }
+ if (success.data.claimId) {
+ return history.push(success.data.pushTo);
+ } else {
+ // this returns to the homepage, needs work
+ return yield put(updatePublishStatus(publishStates.FAILED, 'ERROR'));
+ }
}
if (loadStart) {
yield put(updatePublishStatus(publishStates.LOAD_START, null));
@@ -55,7 +89,7 @@ function * publishFile (action) {
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
}
}
-};
+}
export function * watchPublishStart () {
yield takeLatest(actions.PUBLISH_START, publishFile);
diff --git a/client/src/sagas/rootSaga.js b/client/src/sagas/rootSaga.js
index 05a5c471..39481bca 100644
--- a/client/src/sagas/rootSaga.js
+++ b/client/src/sagas/rootSaga.js
@@ -10,6 +10,7 @@ import { watchUpdateChannelAvailability } from './updateChannelAvailability';
import { watchChannelCreate } from './createChannel';
import { watchChannelLoginCheck } from './checkForLoggedInChannel';
import { watchChannelLogout } from './logoutChannel';
+import { watchAbandonClaim } from './abandon';
export function * rootSaga () {
yield all([
@@ -27,5 +28,6 @@ export function * rootSaga () {
watchChannelLoginCheck(),
watchChannelLogout(),
watchUpdateAssetViews(),
+ watchAbandonClaim(),
]);
}
diff --git a/client/src/selectors/show.js b/client/src/selectors/show.js
index b3b5ba92..d7358e8e 100644
--- a/client/src/selectors/show.js
+++ b/client/src/selectors/show.js
@@ -1,7 +1,13 @@
-export const selectAsset = (show) => {
- const request = show.requestList[show.request.id];
- const assetKey = request.key;
- return show.assetList[assetKey];
+export const selectAsset = show => {
+ const requestId = show.request.id;
+ let asset;
+ const request = show.requestList[requestId] || null;
+ const assetList = show.assetList;
+ if (request && assetList) {
+ const assetKey = request.key; // note: just store this in the request
+ asset = assetList[assetKey] || null;
+ }
+ return asset;
};
export const selectShowState = (state) => {
diff --git a/client/src/utils/buildURI.js b/client/src/utils/buildURI.js
new file mode 100644
index 00000000..b6fb599c
--- /dev/null
+++ b/client/src/utils/buildURI.js
@@ -0,0 +1,10 @@
+export const buildURI = asset => {
+ let channelName, certificateId, name, claimId;
+ if (asset.claimData) {
+ ({ channelName, certificateId, name, claimId } = asset.claimData);
+ }
+ if (channelName) {
+ return `${channelName}:${certificateId}/${name}`;
+ }
+ return `${claimId}/${name}`;
+};
diff --git a/client/src/utils/publish.js b/client/src/utils/publish.js
index ecce371f..00ac2240 100644
--- a/client/src/utils/publish.js
+++ b/client/src/utils/publish.js
@@ -16,7 +16,9 @@ export const createPublishMetadata = (claim, { type }, { title, description, lic
export const createPublishFormData = (file, thumbnail, metadata) => {
let fd = new FormData();
// append file
- fd.append('file', file);
+ if (file) {
+ fd.append('file', file);
+ }
// append thumbnail
if (thumbnail) {
fd.append('thumbnail', thumbnail);
@@ -31,5 +33,5 @@ export const createPublishFormData = (file, thumbnail, metadata) => {
};
export const createThumbnailUrl = (channel, channelId, claim, host) => {
- return `${host}/${channel}:${channelId}/${claim}-thumb.png`;
+ return `${host}/${channel}:${channelId}/${claim}-thumb.jpg`;
};
diff --git a/server/chainquery/bundle.js b/server/chainquery/bundle.js
index 2b51ddb8..e97ae49a 100644
--- a/server/chainquery/bundle.js
+++ b/server/chainquery/bundle.js
@@ -850,7 +850,7 @@ var claimQueries = (db, table, sequelize) => ({
});
},
- getShortClaimIdFromLongClaimId: async (claimId, claimName) => {
+ getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {
logger$1.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
return await table.findAll({
where: { name: claimName },
@@ -860,7 +860,12 @@ var claimQueries = (db, table, sequelize) => ({
throw new Error('No claim(s) found with that claim name');
}
- return returnShortId(result, claimId);
+ let list = result.map(claim => claim.dataValues);
+ if (pendingClaim) {
+ list = list.concat(pendingClaim);
+ }
+
+ return returnShortId(list, claimId);
});
},
@@ -981,6 +986,24 @@ var claimQueries = (db, table, sequelize) => ({
});
},
+ resolveClaimInChannel: async (claimName, channelId) => {
+ logger$1.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);
+ return table.findAll({
+ where: {
+ name: claimName,
+ publisher_id: channelId,
+ },
+ }).then(claimArray => {
+ if (claimArray.length === 0) {
+ return null;
+ } else if (claimArray.length !== 1) {
+ logger$1.warn(`more than one record matches ${claimName} in ${channelId}`);
+ }
+
+ return claimArray[0];
+ });
+ },
+
getOutpoint: async (name, claimId) => {
logger$1.debug(`finding outpoint for ${name}#${claimId}`);
diff --git a/server/chainquery/queries/claimQueries.js b/server/chainquery/queries/claimQueries.js
index d8deb21c..185959ae 100644
--- a/server/chainquery/queries/claimQueries.js
+++ b/server/chainquery/queries/claimQueries.js
@@ -49,7 +49,7 @@ export default (db, table, sequelize) => ({
});
},
- getShortClaimIdFromLongClaimId: async (claimId, claimName) => {
+ getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {
logger.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
return await table.findAll({
where: { name: claimName },
@@ -59,7 +59,12 @@ export default (db, table, sequelize) => ({
throw new Error('No claim(s) found with that claim name');
}
- return returnShortId(result, claimId);
+ let list = result.map(claim => claim.dataValues);
+ if (pendingClaim) {
+ list = list.concat(pendingClaim);
+ }
+
+ return returnShortId(list, claimId);
});
},
@@ -180,6 +185,24 @@ export default (db, table, sequelize) => ({
});
},
+ resolveClaimInChannel: async (claimName, channelId) => {
+ logger.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);
+ return table.findAll({
+ where: {
+ name: claimName,
+ publisher_id: channelId,
+ },
+ }).then(claimArray => {
+ if (claimArray.length === 0) {
+ return null;
+ } else if (claimArray.length !== 1) {
+ logger.warn(`more than one record matches ${claimName} in ${channelId}`);
+ }
+
+ return claimArray[0];
+ });
+ },
+
getOutpoint: async (name, claimId) => {
logger.debug(`finding outpoint for ${name}#${claimId}`);
diff --git a/server/controllers/api/channel/claims/getChannelClaims.js b/server/controllers/api/channel/claims/getChannelClaims.js
index d1ff3b96..90a9c2c7 100644
--- a/server/controllers/api/channel/claims/getChannelClaims.js
+++ b/server/controllers/api/channel/claims/getChannelClaims.js
@@ -5,7 +5,11 @@ const { returnPaginatedChannelClaims } = require('./channelPagination.js');
const getChannelClaims = async (channelName, channelShortId, page) => {
const channelId = await chainquery.claim.queries.getLongClaimId(channelName, channelShortId);
- const channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelId);
+
+ let channelClaims;
+ if (channelId) {
+ channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelId);
+ }
const processingChannelClaims = channelClaims ? channelClaims.map((claim) => getClaimData(claim)) : [];
const processedChannelClaims = await Promise.all(processingChannelClaims);
diff --git a/server/controllers/api/claim/abandon/index.js b/server/controllers/api/claim/abandon/index.js
new file mode 100644
index 00000000..08a6505f
--- /dev/null
+++ b/server/controllers/api/claim/abandon/index.js
@@ -0,0 +1,44 @@
+const logger = require('winston');
+const db = require('server/models');
+const { abandonClaim } = require('server/lbrynet');
+const deleteFile = require('../publish/deleteFile.js');
+const authenticateUser = require('../publish/authentication.js');
+
+/*
+ route to abandon a claim through the daemon
+*/
+
+const claimAbandon = async (req, res) => {
+ const {claimId} = req.body;
+ const {user} = req;
+ try {
+ const [channel, claim] = await Promise.all([
+ authenticateUser(user.channelName, null, null, user),
+ db.Claim.findOne({where: {claimId}}),
+ ]);
+
+ if (!claim) throw new Error('That channel does not exist');
+ if (!channel.channelName) throw new Error('You don\'t own this channel');
+
+ await abandonClaim({claimId});
+ const file = await db.File.findOne({where: {claimId}});
+ await Promise.all([
+ deleteFile(file.filePath),
+ db.File.destroy({where: {claimId}}),
+ db.Claim.destroy({where: {claimId}}),
+ ]);
+ logger.debug(`Claim abandoned: ${claimId}`);
+ res.status(200).json({
+ success: true,
+ message: `Claim with id ${claimId} abandonded`,
+ });
+ } catch (error) {
+ logger.error('abandon claim error:', error);
+ res.status(400).json({
+ success: false,
+ message: error.message,
+ });
+ }
+};
+
+module.exports = claimAbandon;
diff --git a/server/controllers/api/claim/data/index.js b/server/controllers/api/claim/data/index.js
index 6efe4e83..95146c37 100644
--- a/server/controllers/api/claim/data/index.js
+++ b/server/controllers/api/claim/data/index.js
@@ -1,8 +1,8 @@
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
const getClaimData = require('server/utils/getClaimData');
+const fetchClaimData = require('server/utils/fetchClaimData');
const chainquery = require('chainquery');
const db = require('server/models');
-
/*
route to return data for a claim
@@ -10,16 +10,9 @@ const db = require('server/models');
*/
const claimData = async ({ ip, originalUrl, body, params }, res) => {
- const claimName = params.claimName;
- let claimId = params.claimId;
- if (claimId === 'none') claimId = null;
try {
- let resolvedClaim = await chainquery.claim.queries.resolveClaim(claimName, claimId).catch(() => {});
-
- if(!resolvedClaim) {
- resolvedClaim = await db.Claim.resolveClaim(claimName, claimId);
- }
+ const resolvedClaim = await fetchClaimData(params);
if (!resolvedClaim) {
return res.status(404).json({
diff --git a/server/controllers/api/claim/publish/createPublishParams.js b/server/controllers/api/claim/publish/createPublishParams.js
index 242f4312..364e831a 100644
--- a/server/controllers/api/claim/publish/createPublishParams.js
+++ b/server/controllers/api/claim/publish/createPublishParams.js
@@ -11,7 +11,7 @@ const createPublishParams = (filePath, name, title, description, license, nsfw,
}
// provide default for license
if (license === null || license.trim() === '') {
- license = ' '; // default to empty string
+ license = ''; // default to empty string
}
// create the basic publish params
const publishParams = {
diff --git a/server/controllers/api/claim/publish/index.js b/server/controllers/api/claim/publish/index.js
index f51d4055..cee6c2bc 100644
--- a/server/controllers/api/claim/publish/index.js
+++ b/server/controllers/api/claim/publish/index.js
@@ -17,6 +17,9 @@ const parsePublishApiRequestBody = require('./parsePublishApiRequestBody.js');
const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
const authenticateUser = require('./authentication.js');
+const chainquery = require('chainquery');
+const createCanonicalLink = require('../../../../../utils/createCanonicalLink');
+
const CLAIM_TAKEN = 'CLAIM_TAKEN';
const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL';
@@ -42,7 +45,25 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
});
}
// define variables
- let channelName, channelId, channelPassword, description, fileName, filePath, fileExtension, fileType, gaStartTime, license, name, nsfw, thumbnail, thumbnailFileName, thumbnailFilePath, thumbnailFileType, title;
+ let channelName,
+ channelId,
+ channelPassword,
+ description,
+ fileName,
+ filePath,
+ fileExtension,
+ fileType,
+ gaStartTime,
+ license,
+ name,
+ nsfw,
+ thumbnail,
+ thumbnailFileName,
+ thumbnailFilePath,
+ thumbnailFileType,
+ title,
+ claimData,
+ claimId;
// record the start time of the request
gaStartTime = Date.now();
// validate the body and files of the request
@@ -64,6 +85,7 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
};
throw error;
}
+
return Promise.all([
checkClaimAvailability(name),
createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName, channelClaimId),
@@ -83,19 +105,40 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);
}
// publish the asset
- return publish(publishParams, fileName, fileType);
+ return publish(publishParams, fileName, fileType, filePath);
})
- .then(result => {
+ .then(publishResults => {
+ logger.info('Publish success >', publishResults);
+ claimData = publishResults;
+ ({claimId} = claimData);
+
+ if (channelName) {
+ return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimData.certificateId, channelName);
+ } else {
+ return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimId, name, claimData).catch(error => {
+ return claimId.slice(0, 1);
+ });
+ }
+ })
+ .then(shortId => {
+ let canonicalUrl;
+ if (channelName) {
+ canonicalUrl = createCanonicalLink({ asset: { ...claimData, channelShortId: shortId } });
+ } else {
+ canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId } })
+ }
+
res.status(200).json({
success: true,
message: 'publish completed successfully',
data : {
name,
- claimId : result.claim_id,
- url : `${host}/${result.claim_id}/${name}`, // for backwards compatability with app
- showUrl : `${host}/${result.claim_id}/${name}`,
- serveUrl: `${host}/${result.claim_id}/${name}${fileExtension}`,
- lbryTx : result,
+ claimId,
+ url : `${host}${canonicalUrl}`, // for backwards compatability with app
+ showUrl : `${host}${canonicalUrl}`,
+ serveUrl: `${host}${canonicalUrl}${fileExtension}`,
+ pushTo : canonicalUrl,
+ claimData,
},
});
// record the publish end time and send to google analytics
diff --git a/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js b/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js
index 3e99ad20..6e1f3409 100644
--- a/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js
+++ b/server/controllers/api/claim/publish/parsePublishApiRequestFiles.js
@@ -1,9 +1,19 @@
const path = require('path');
const validateFileTypeAndSize = require('./validateFileTypeAndSize.js');
-const parsePublishApiRequestFiles = ({file, thumbnail}) => {
+const parsePublishApiRequestFiles = ({file, thumbnail}, isUpdate) => {
// make sure a file was provided
if (!file) {
+ if (isUpdate) {
+ if (thumbnail) {
+ const obj = {};
+ obj.thumbnailFileName = thumbnail.name;
+ obj.thumbnailFilePath = thumbnail.path;
+ obj.thumbnailFileType = thumbnail.type;
+ return obj;
+ }
+ return {};
+ }
throw new Error('no file with key of [file] found in request');
}
if (!file.path) {
@@ -28,18 +38,24 @@ const parsePublishApiRequestFiles = ({file, thumbnail}) => {
if (/'/.test(file.name)) {
throw new Error('apostrophes are not allowed in the file name');
}
+
// validate the file
- validateFileTypeAndSize(file);
+ if (file) validateFileTypeAndSize(file);
// return results
- return {
- fileName : file.name,
- filePath : file.path,
- fileExtension : path.extname(file.path),
- fileType : file.type,
- thumbnailFileName: (thumbnail ? thumbnail.name : null),
- thumbnailFilePath: (thumbnail ? thumbnail.path : null),
- thumbnailFileType: (thumbnail ? thumbnail.type : null),
+ const obj = {
+ fileName : file.name,
+ filePath : file.path,
+ fileExtension: path.extname(file.path),
+ fileType : file.type,
};
+
+ if (thumbnail) {
+ obj.thumbnailFileName = thumbnail.name;
+ obj.thumbnailFilePath = thumbnail.path;
+ obj.thumbnailFileType = thumbnail.type;
+ }
+
+ return obj;
};
module.exports = parsePublishApiRequestFiles;
diff --git a/server/controllers/api/claim/publish/publish.js b/server/controllers/api/claim/publish/publish.js
index 21de83bd..b0589ece 100644
--- a/server/controllers/api/claim/publish/publish.js
+++ b/server/controllers/api/claim/publish/publish.js
@@ -1,81 +1,72 @@
const logger = require('winston');
-const { publishClaim } = require('../../../../lbrynet');
const db = require('../../../../models');
+const { publishClaim } = require('../../../../lbrynet');
const { createFileRecordDataAfterPublish } = require('../../../../models/utils/createFileRecordData.js');
const { createClaimRecordDataAfterPublish } = require('../../../../models/utils/createClaimRecordData.js');
const deleteFile = require('./deleteFile.js');
-const publish = (publishParams, fileName, fileType) => {
- return new Promise((resolve, reject) => {
- let publishResults, certificateId, channelName;
- // publish the file
- return publishClaim(publishParams)
- .then(result => {
- logger.info(`Successfully published ${publishParams.name} ${fileName}`, result);
+const publish = async (publishParams, fileName, fileType) => {
+ let publishResults;
+ let channel;
+ let fileRecord;
+ let newFile = Boolean(publishParams.file_path);
- // Support new daemon, TODO: remove
- publishResults = result.output && result.output.claim_id ? result.output : result;
-
- // get the channel information
- if (publishParams.channel_name) {
- logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
- return db.Channel.findOne({
- where: {
- channelName: publishParams.channel_name,
- },
- });
- } else {
- logger.debug('this claim was not published in a channel');
- return null;
- }
- })
- .then(channel => {
- // set channel information
- certificateId = null;
- channelName = null;
- if (channel) {
- certificateId = channel.channelClaimId;
- channelName = channel.channelName;
- }
- logger.debug(`certificateId: ${certificateId}`);
- })
- .then(() => {
- return Promise.all([
- createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults),
- createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults),
- ]);
- })
- .then(([fileRecord, claimRecord]) => {
- // upsert the records
- const {name} = publishParams;
- const {claim_id: claimId} = publishResults;
- const upsertCriteria = {
- name,
- claimId,
- };
- return Promise.all([
- db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
- db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
- ]);
- })
- .then(([file, claim]) => {
- logger.debug('File and Claim records successfully created');
- return Promise.all([
- file.setClaim(claim),
- claim.setFile(file),
- ]);
- })
- .then(() => {
- logger.debug('File and Claim records successfully associated');
- // resolve the promise with the result from lbryApi publishClaim;
- resolve(publishResults);
- })
- .catch(error => {
- logger.error('PUBLISH ERROR', error);
- deleteFile(publishParams.file_path); // delete the local file
- reject(error);
+ try {
+ publishResults = await publishClaim(publishParams);
+ logger.info(`Successfully published ${publishParams.name} ${fileName}`, publishResults);
+ const outpoint = `${publishResults.output.txid}:${publishResults.output.nout}`;
+ // get the channel information
+ if (publishParams.channel_name) {
+ logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
+ channel = await db.Channel.findOne({
+ where: {
+ channelName: publishParams.channel_name,
+ },
});
- });
+ } else {
+ channel = null;
+ }
+ const certificateId = channel ? channel.channelClaimId : null;
+ const channelName = channel ? channel.channelName : null;
+
+ const claimRecord = await createClaimRecordDataAfterPublish(certificateId, channelName, fileName, fileType, publishParams, publishResults);
+ const {claimId} = claimRecord;
+ const upsertCriteria = {name: publishParams.name, claimId};
+ if (newFile) {
+ // this is the problem
+ //
+ fileRecord = await createFileRecordDataAfterPublish(fileName, fileType, publishParams, publishResults);
+ } else {
+ fileRecord = await db.File.findOne({where: {claimId}}).then(result => result.dataValues);
+ }
+
+ const [file, claim] = await Promise.all([
+ db.upsert(db.File, fileRecord, upsertCriteria, 'File'),
+ db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim'),
+ ]);
+ logger.info(`File and Claim records successfully created (${publishParams.name})`);
+
+ await Promise.all([
+ file.setClaim(claim),
+ claim.setFile(file),
+ ]);
+ logger.info(`File and Claim records successfully associated (${publishParams.name})`);
+
+ return Object.assign({}, claimRecord, {outpoint});
+ } catch (err) {
+ // parse daemon response when err is a string
+ // this needs work
+ logger.info('publish/publish err:', err);
+ const error = typeof err === 'string' ? JSON.parse(err) : err;
+ if (publishParams.file_path) {
+ await deleteFile(publishParams.file_path);
+ }
+ const message = error.error && error.error.message ? error.error.message : 'Unknown publish error';
+ return {
+ error: true,
+ message,
+ };
+ }
};
module.exports = publish;
diff --git a/server/controllers/api/claim/update/index.js b/server/controllers/api/claim/update/index.js
new file mode 100644
index 00000000..c1df4fb8
--- /dev/null
+++ b/server/controllers/api/claim/update/index.js
@@ -0,0 +1,199 @@
+const logger = require('winston');
+const db = require('server/models');
+const { details, publishing: { disabled, disabledMessage, primaryClaimAddress } } = require('@config/siteConfig');
+const { resolveUri } = require('server/lbrynet');
+const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');
+const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
+const publish = require('../publish/publish.js');
+const parsePublishApiRequestBody = require('../publish/parsePublishApiRequestBody');
+const parsePublishApiRequestFiles = require('../publish/parsePublishApiRequestFiles.js');
+const authenticateUser = require('../publish/authentication.js');
+const createThumbnailPublishParams = require('../publish/createThumbnailPublishParams.js');
+const chainquery = require('chainquery');
+const createCanonicalLink = require('../../../../../utils/createCanonicalLink');
+
+/*
+ route to update a claim through the daemon
+*/
+
+const updateMetadata = ({nsfw, license, title, description}) => {
+ const update = {};
+ if (nsfw) update['nsfw'] = nsfw;
+ if (license) update['license'] = license;
+ if (title) update['title'] = title;
+ if (description) update['description'] = description;
+ return update;
+};
+
+const rando = () => {
+ let text = '';
+ const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ for (let i = 0; i < 6; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
+ return text;
+};
+
+const claimUpdate = ({ body, files, headers, ip, originalUrl, user, tor }, res) => {
+ // logging
+ logger.info('Claim update request:', {
+ ip,
+ headers,
+ body,
+ files,
+ user,
+ });
+
+ // check for disabled publishing
+ if (disabled) {
+ return res.status(503).json({
+ success: false,
+ message: disabledMessage,
+ });
+ }
+
+ // define variables
+ let channelName,
+ channelId,
+ channelPassword,
+ description,
+ fileName,
+ filePath,
+ fileType,
+ gaStartTime,
+ thumbnail,
+ fileExtension,
+ license,
+ name,
+ nsfw,
+ thumbnailFileName,
+ thumbnailFilePath,
+ thumbnailFileType,
+ title,
+ claimRecord,
+ metadata,
+ publishResult,
+ thumbnailUpdate = false;
+ // record the start time of the request
+ gaStartTime = Date.now();
+
+ try {
+ ({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body));
+ ({fileName, filePath, fileExtension, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType} = parsePublishApiRequestFiles(files, true));
+ ({channelName, channelId, channelPassword} = body);
+ } catch (error) {
+ return res.status(400).json({success: false, message: error.message});
+ }
+
+ // check channel authorization
+ authenticateUser(channelName, channelId, channelPassword, user)
+ .then(({ channelName, channelClaimId }) => {
+ if (!channelId) {
+ channelId = channelClaimId;
+ }
+ return chainquery.claim.queries.resolveClaimInChannel(name, channelClaimId).then(claim => claim.dataValues);
+ })
+ .then(claim => {
+ claimRecord = claim;
+ if (claimRecord.content_type === 'video/mp4' && files.file) {
+ thumbnailUpdate = true;
+ }
+
+ if (!files.file || thumbnailUpdate) {
+ return Promise.all([
+ db.File.findOne({ where: { name, claimId: claim.claim_id } }),
+ resolveUri(`${claim.name}#${claim.claim_id}`),
+ ]);
+ }
+
+ return [null, null];
+ })
+ .then(([fileResult, resolution]) => {
+
+ metadata = Object.assign({}, {
+ title : claimRecord.title,
+ description: claimRecord.description,
+ nsfw : claimRecord.nsfw,
+ license : claimRecord.license,
+ language : 'en',
+ author : details.title,
+ }, updateMetadata({title, description, nsfw, license}));
+ const publishParams = {
+ name,
+ bid : '0.01',
+ claim_address: primaryClaimAddress,
+ channel_name : channelName,
+ channel_id : channelId,
+ metadata,
+ };
+
+ if (files.file) {
+ if (thumbnailUpdate) {
+ // publish new thumbnail
+ const newThumbnailName = `${name}-${rando()}`;
+ const newThumbnailParams = createThumbnailPublishParams(filePath, newThumbnailName, license, nsfw);
+ newThumbnailParams['file_path'] = filePath;
+ publish(newThumbnailParams, fileName, fileType);
+
+ publishParams['sources'] = resolution.claim.value.stream.source;
+ publishParams['thumbnail'] = `${details.host}/${newThumbnailParams.channel_name}:${newThumbnailParams.channel_id}/${newThumbnailName}-thumb.jpg`;
+ } else {
+ publishParams['file_path'] = filePath;
+ }
+ } else {
+ fileName = fileResult.fileName;
+ fileType = fileResult.fileType;
+ publishParams['sources'] = resolution.claim.value.stream.source;
+ publishParams['thumbnail'] = claimRecord.thumbnail_url;
+ }
+
+ const fp = files && files.file && files.file.path ? files.file.path : undefined;
+ return publish(publishParams, fileName, fileType, fp);
+ })
+ .then(result => {
+ publishResult = result;
+
+ if (channelName) {
+ return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.certificateId, channelName);
+ } else {
+ return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.claimId, name, result).catch(error => {
+ return result.claimId.slice(0, 1);
+ });
+ }
+ })
+ .then(shortId => {
+ let canonicalUrl;
+ if (channelName) {
+ canonicalUrl = createCanonicalLink({ asset: { ...publishResult, channelShortId: shortId } });
+ } else {
+ canonicalUrl = createCanonicalLink({ asset: { ...publishResult, shortId } })
+ }
+
+ if (publishResult.error) {
+ res.status(400).json({
+ success: false,
+ message: publishResult.message,
+ });
+ }
+
+ const {claimId} = publishResult;
+ res.status(200).json({
+ success: true,
+ message: 'update successful',
+ data : {
+ name,
+ claimId,
+ url : `${details.host}${canonicalUrl}`, // for backwards compatability with app
+ showUrl : `${details.host}${canonicalUrl}`,
+ serveUrl: `${details.host}${canonicalUrl}${fileExtension}`,
+ pushTo : canonicalUrl,
+ claimData: publishResult,
+ },
+ });
+ // record the publish end time and send to google analytics
+ sendGATimingEvent('end-to-end', 'update', fileType, gaStartTime, Date.now());
+ })
+ .catch(error => {
+ handleErrorResponse(originalUrl, ip, error, res);
+ });
+};
+
+module.exports = claimUpdate;
diff --git a/server/lbrynet/index.js b/server/lbrynet/index.js
index 1b914682..3b4eafcd 100644
--- a/server/lbrynet/index.js
+++ b/server/lbrynet/index.js
@@ -2,6 +2,7 @@ const axios = require('axios');
const logger = require('winston');
const { apiHost, apiPort, getTimeout } = require('@config/lbryConfig');
const lbrynetUri = 'http://' + apiHost + ':' + apiPort;
+const db = require('../models');
const { chooseGaLbrynetPublishLabel, sendGATimingEvent } = require('../utils/googleAnalytics.js');
const handleLbrynetResponse = require('./utils/handleLbrynetResponse.js');
const { publishing } = require('@config/siteConfig');
@@ -46,6 +47,21 @@ module.exports = {
});
});
},
+ async abandonClaim ({claimId}) {
+ logger.debug(`lbryApi >> Abandon claim "${claimId}"`);
+ const gaStartTime = Date.now();
+ try {
+ const abandon = await axios.post(lbrynetUri, {
+ method: 'claim_abandon',
+ params: { claim_id: claimId },
+ });
+ sendGATimingEvent('lbrynet', 'abandonClaim', 'ABANDON_CLAIM', gaStartTime, Date.now());
+ return abandon.data;
+ } catch (error) {
+ logger.error(error);
+ return error;
+ }
+ },
getClaimList (claimName) {
logger.debug(`lbryApi >> Getting claim_list for "${claimName}"`);
const gaStartTime = Date.now();
@@ -75,7 +91,13 @@ module.exports = {
})
.then(({ data }) => {
sendGATimingEvent('lbrynet', 'resolveUri', 'RESOLVE', gaStartTime, Date.now());
- if (data.result[uri].error) { // check for errors
+ if (Object.keys(data.result).length === 0 && data.result.constructor === Object) {
+ // workaround for daemon returning empty result object
+ // https://github.com/lbryio/lbry/issues/1485
+ db.Claim.findOne({ where: { claimId: uri.split('#')[1] } })
+ .then(() => reject('This claim has not yet been confirmed on the LBRY blockchain'))
+ .catch(() => reject(`Claim ${uri} does not exist`));
+ } else if (data.result[uri].error) { // check for errors
reject(data.result[uri].error);
} else { // if no errors, resolve
resolve(data.result[uri]);
diff --git a/server/models/utils/createFileRecordData.js b/server/models/utils/createFileRecordData.js
index aa0802cf..fdcb9ce0 100644
--- a/server/models/utils/createFileRecordData.js
+++ b/server/models/utils/createFileRecordData.js
@@ -28,7 +28,7 @@ async function createFileRecordDataAfterGet (resolveResult, getResult) {
filePath,
fileType,
};
-};
+}
async function createFileRecordDataAfterPublish (fileName, fileType, publishParams, publishResults) {
const {
diff --git a/server/routes/api/index.js b/server/routes/api/index.js
index 71b673eb..26706bd5 100644
--- a/server/routes/api/index.js
+++ b/server/routes/api/index.js
@@ -13,6 +13,8 @@ const claimGet = require('../../controllers/api/claim/get');
const claimList = require('../../controllers/api/claim/list');
const claimLongId = require('../../controllers/api/claim/longId');
const claimPublish = require('../../controllers/api/claim/publish');
+const claimAbandon = require('../../controllers/api/claim/abandon');
+const claimUpdate = require('../../controllers/api/claim/update');
const claimResolve = require('../../controllers/api/claim/resolve');
const claimShortId = require('../../controllers/api/claim/shortId');
const claimViews = require('../../controllers/api/claim/views');
@@ -29,12 +31,10 @@ const getOEmbedData = require('../../controllers/api/oEmbed');
module.exports = {
// homepage routes
'/api/homepage/data/channels': { controller: [ torCheckMiddleware, channelData ] },
-
// channel routes
'/api/channel/availability/:name': { controller: [ torCheckMiddleware, channelAvailability ] },
'/api/channel/short-id/:longId/:name': { controller: [ torCheckMiddleware, channelShortId ] },
'/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
- '/api/channel/data/:channelName/:channelClaimId': { controller: [ torCheckMiddleware, channelData ] },
'/api/channel/claims/:channelName/:channelClaimId/:page': { controller: [ torCheckMiddleware, channelClaims ] },
// sepcial routes
@@ -47,6 +47,8 @@ module.exports = {
'/api/claim/list/:name': { controller: [ torCheckMiddleware, claimList ] },
'/api/claim/long-id': { method: 'post', controller: [ torCheckMiddleware, claimLongId ] }, // note: should be a 'get'
'/api/claim/publish': { method: 'post', controller: [ torCheckMiddleware, autoblockPublishMiddleware, multipartMiddleware, autoblockPublishBodyMiddleware, claimPublish ] },
+ '/api/claim/update': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimUpdate ] },
+ '/api/claim/abandon': { method: 'post', controller: [ torCheckMiddleware, multipartMiddleware, claimAbandon ] },
'/api/claim/resolve/:name/:claimId': { controller: [ torCheckMiddleware, claimResolve ] },
'/api/claim/short-id/:longId/:name': { controller: [ torCheckMiddleware, claimShortId ] },
'/api/claim/views/:claimId': { controller: [ torCheckMiddleware, claimViews ] },
diff --git a/server/routes/pages/index.js b/server/routes/pages/index.js
index fc742b42..858a1898 100644
--- a/server/routes/pages/index.js
+++ b/server/routes/pages/index.js
@@ -15,6 +15,7 @@ module.exports = {
'/trending': { controller: redirect('/popular') },
'/popular': { controller: handlePageRequest },
'/new': { controller: handlePageRequest },
+ '/edit/:claimId': { controller: handlePageRequest },
'/multisite': { controller: handlePageRequest },
'/video-embed/:name/:claimId/:config?': { controller: handleVideoEmbedRequest }, // for twitter
};
diff --git a/server/utils/fetchClaimData.js b/server/utils/fetchClaimData.js
new file mode 100644
index 00000000..aeaec8f3
--- /dev/null
+++ b/server/utils/fetchClaimData.js
@@ -0,0 +1,25 @@
+const chainquery = require('chainquery');
+const db = require('server/models');
+
+const fetchClaimData = async (params) => {
+ let { claimId, claimName: name } = params;
+ if (claimId === 'none') claimId = null;
+
+ const [cq, local] = await Promise.all([
+ chainquery.claim.queries.resolveClaim(name, claimId).then(res => res.dataValues).catch(() => {}),
+ db.Claim.resolveClaim(name, claimId).catch(() => {}),
+ ]);
+
+ if (!cq && !local) {
+ return null;
+ }
+ if (cq && cq.name === name && !local) {
+ return cq;
+ }
+ if (local && local.name === name && !cq) {
+ return local;
+ }
+ return local.updatedAt > cq.modified_at ? local : cq;
+};
+
+module.exports = fetchClaimData;
diff --git a/server/utils/getClaimData.js b/server/utils/getClaimData.js
index f247a4d3..2e77a4ee 100644
--- a/server/utils/getClaimData.js
+++ b/server/utils/getClaimData.js
@@ -25,7 +25,7 @@ module.exports = async (data) => {
claimId: data.claim_id || data.claimId,
fileExt: data.generated_extension || data.fileExt,
description: data.description,
- thumbnail: data.generated_thumbnail || data.thumbnail,
+ thumbnail: data.generated_thumbnail || data.thumbnail_url || data.thumbnail,
outpoint: data.transaction_hash_id || data.outpoint,
host,
})