From 0be3154cbee844bb2a47a48627ce626a05068b8e Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 6 Oct 2021 08:34:21 +0800 Subject: [PATCH 1/7] Fix playlist strings --- static/app-strings.json | 3 +++ ui/component/collectionContentSidebar/view.jsx | 2 +- ui/component/collectionsListMine/view.jsx | 2 +- ui/page/collection/view.jsx | 4 +++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/static/app-strings.json b/static/app-strings.json index 100546ac1..d8b51bf2a 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1638,6 +1638,7 @@ "This link leads to an external website.": "This link leads to an external website.", "No Content Found": "No Content Found", "No Lists Found": "No Lists Found", + "No matching playlists": "No matching playlists", "You have no lists! Create one from any playable content.": "You have no lists! Create one from any playable content.", "Pick": "Pick", "You have unpublished lists! %pick% one and publish it!": "You have unpublished lists! %pick% one and publish it!", @@ -2058,6 +2059,8 @@ "MyAwesomeList": "MyAwesomeList", "My Awesome List": "My Awesome List", "This list has no items.": "This list has no items.", + "1 item": "1 item", + "%collectionCount% items": "%collectionCount% items", "Select File": "Select File", "File Selected": "File Selected", "Url": "Url", diff --git a/ui/component/collectionContentSidebar/view.jsx b/ui/component/collectionContentSidebar/view.jsx index 0e3573442..786c233c2 100644 --- a/ui/component/collectionContentSidebar/view.jsx +++ b/ui/component/collectionContentSidebar/view.jsx @@ -65,7 +65,7 @@ export default function CollectionContent(props: Props) { titleActions={
{/* TODO: BUTTON TO SAVE COLLECTION - Probably save/copy modal */} -
} body={ diff --git a/ui/component/collectionsListMine/view.jsx b/ui/component/collectionsListMine/view.jsx index c57273b38..3cbffa9b1 100644 --- a/ui/component/collectionsListMine/view.jsx +++ b/ui/component/collectionsListMine/view.jsx @@ -175,7 +175,7 @@ export default function CollectionsListMine(props: Props) { {filteredCollections && filteredCollections.length > 0 && filteredCollections.map((key) => )} - {!filteredCollections.length &&
{__('No matching collections')}
} + {!filteredCollections.length &&
{__('No matching playlists')}
} )} diff --git a/ui/page/collection/view.jsx b/ui/page/collection/view.jsx index 282233663..4befdc9d6 100644 --- a/ui/page/collection/view.jsx +++ b/ui/page/collection/view.jsx @@ -102,7 +102,9 @@ export default function CollectionPage(props: Props) { const subTitle = (
- {collectionCount} items + + {collectionCount === 1 ? __('1 item') : __('%collectionCount% items', { collectionCount })} + {uri && }
); From 95654955b1ef5c6ba55ada50f2419949c56cf10a Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 6 Oct 2021 11:41:52 +0800 Subject: [PATCH 2/7] Add sitemap to influence Sitelinks ## Issue Part of `7166 improve search metadata` ## Notes This is an experiment to influence the Sitelinks in our search results. Our current sitemap only consists of claims, so claims appear in Sitelinks more often. We (Julian) want categories to have higher priority, if possible. For now, the sitemap will be defined in Google Console instead of robots.txt. If it works, the file should be uploaded to sitemap.odysee.com, alongside the claim list sitemap. --- static/sitemap.txt | 18 ++++++++++++++++++ web/webpack.config.js | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 static/sitemap.txt diff --git a/static/sitemap.txt b/static/sitemap.txt new file mode 100644 index 000000000..bc0956c4a --- /dev/null +++ b/static/sitemap.txt @@ -0,0 +1,18 @@ +https://odysee.com +https://odysee.com/$/general +https://odysee.com/$/bighits +https://odysee.com/$/gaming +https://odysee.com/$/music +https://odysee.com/$/universe +https://odysee.com/$/tech +https://odysee.com/$/lab +https://odysee.com/$/movies +https://odysee.com/$/news +https://odysee.com/$/finance +https://odysee.com/$/wildwest +https://odysee.com/$/signup +https://odysee.com/$/signin +https://odysee.com/$/help +https://odysee.com/$/settings +https://odysee.com/@Odysee:8 +https://odysee.com/@OdyseeHelp:b \ No newline at end of file diff --git a/web/webpack.config.js b/web/webpack.config.js index f483c6217..45e2cc907 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -45,6 +45,11 @@ const copyWebpackCommands = [ to: `${DIST_ROOT}/robots.txt`, force: true, }, + { + from: `${STATIC_ROOT}/sitemap.txt`, + to: `${DIST_ROOT}/sitemap.txt`, + force: true, + }, { from: `${STATIC_ROOT}/img/favicon.png`, to: `${DIST_ROOT}/public/favicon.png`, From 401f7fec177756cc3a9191ec718a522e99be3b4c Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 6 Oct 2021 13:04:26 +0800 Subject: [PATCH 3/7] Revert "Add sitemap to influence Sitelinks" Seems like I messed up robots.txt? This reverts commit 95654955b1ef5c6ba55ada50f2419949c56cf10a. --- static/sitemap.txt | 18 ------------------ web/webpack.config.js | 5 ----- 2 files changed, 23 deletions(-) delete mode 100644 static/sitemap.txt diff --git a/static/sitemap.txt b/static/sitemap.txt deleted file mode 100644 index bc0956c4a..000000000 --- a/static/sitemap.txt +++ /dev/null @@ -1,18 +0,0 @@ -https://odysee.com -https://odysee.com/$/general -https://odysee.com/$/bighits -https://odysee.com/$/gaming -https://odysee.com/$/music -https://odysee.com/$/universe -https://odysee.com/$/tech -https://odysee.com/$/lab -https://odysee.com/$/movies -https://odysee.com/$/news -https://odysee.com/$/finance -https://odysee.com/$/wildwest -https://odysee.com/$/signup -https://odysee.com/$/signin -https://odysee.com/$/help -https://odysee.com/$/settings -https://odysee.com/@Odysee:8 -https://odysee.com/@OdyseeHelp:b \ No newline at end of file diff --git a/web/webpack.config.js b/web/webpack.config.js index 45e2cc907..f483c6217 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -45,11 +45,6 @@ const copyWebpackCommands = [ to: `${DIST_ROOT}/robots.txt`, force: true, }, - { - from: `${STATIC_ROOT}/sitemap.txt`, - to: `${DIST_ROOT}/sitemap.txt`, - force: true, - }, { from: `${STATIC_ROOT}/img/favicon.png`, to: `${DIST_ROOT}/public/favicon.png`, From 3a644d7bfcdf4dc8d35bdf79e65689dd5751fe38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Oct 2021 10:12:51 -0400 Subject: [PATCH 4/7] Bump url-parse from 1.5.1 to 1.5.3 (#7230) Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3) --- updated-dependencies: - dependency-name: url-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 96aee291f..154f85f0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16391,9 +16391,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.1.1, url-parse@^1.1.8, url-parse@^1.4.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" From 4bc4a965d9cfda25967fed24c88634c9d375af7e Mon Sep 17 00:00:00 2001 From: jessopb <36554050+jessopb@users.noreply.github.com> Date: Wed, 6 Oct 2021 10:13:37 -0400 Subject: [PATCH 5/7] fix notifications page on unauthed app (#7226) --- static/app-strings.json | 1 + ui/page/settingsNotifications/view.jsx | 42 ++++++++++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/static/app-strings.json b/static/app-strings.json index d8b51bf2a..5373770c1 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2183,5 +2183,6 @@ "Creator": "Creator", "From comments": "From comments", "From search": "From search", + "Manage tags": "Manage tags", "--end--": "--end--" } diff --git a/ui/page/settingsNotifications/view.jsx b/ui/page/settingsNotifications/view.jsx index 6d5053034..f086a9682 100644 --- a/ui/page/settingsNotifications/view.jsx +++ b/ui/page/settingsNotifications/view.jsx @@ -32,28 +32,30 @@ export default function NotificationSettingsPage(props: Props) { const lbryIoParams = verificationToken ? { auth_token: verificationToken } : undefined; React.useEffect(() => { - Lbryio.call('tag', 'list', lbryIoParams) - .then(setTags) - .catch((e) => { - setError(true); - }); + if (isAuthenticated) { + Lbryio.call('tag', 'list', lbryIoParams) + .then(setTags) + .catch((e) => { + setError(true); + }); - Lbryio.call('user_email', 'status', lbryIoParams) - .then((res) => { - const enabledEmails = - res.emails && - Object.keys(res.emails).reduce((acc, email) => { - const isEnabled = res.emails[email]; - return [...acc, { email, isEnabled }]; - }, []); + Lbryio.call('user_email', 'status', lbryIoParams) + .then((res) => { + const enabledEmails = + res.emails && + Object.keys(res.emails).reduce((acc, email) => { + const isEnabled = res.emails[email]; + return [...acc, { email, isEnabled }]; + }, []); - setTagMap(res.tags); - setEnabledEmails(enabledEmails); - }) - .catch((e) => { - setError(true); - }); - }, []); + setTagMap(res.tags); + setEnabledEmails(enabledEmails); + }) + .catch((e) => { + setError(true); + }); + } + }, [isAuthenticated]); function handleChangeTag(name, newIsEnabled) { const tagParams = newIsEnabled ? { add: name } : { remove: name }; From b44be392523fd5baabba1821dc28f48000dd0fa1 Mon Sep 17 00:00:00 2001 From: zeppi Date: Tue, 5 Oct 2021 16:57:47 -0400 Subject: [PATCH 6/7] move file actions from lbry-redux --- flow-typed/File.js | 78 ++++++++++++++ ui/component/claimPreview/index.js | 3 +- ui/component/claimPreviewTile/index.js | 2 +- ui/component/sideNavigation/index.js | 4 +- ui/constants/action_types.js | 7 ++ ui/redux/actions/content.js | 8 +- ui/redux/actions/file.js | 144 +++++++++++++++++++++++-- 7 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 flow-typed/File.js diff --git a/flow-typed/File.js b/flow-typed/File.js new file mode 100644 index 000000000..44dc4f398 --- /dev/null +++ b/flow-typed/File.js @@ -0,0 +1,78 @@ +// @flow + +declare type FileListItem = { + metadata: StreamMetadata, + added_on: number, + blobs_completed: number, + blobs_in_stream: number, + blobs_remaining: number, + channel_claim_id: string, + channel_name: string, + claim_id: string, + claim_name: string, + completed: false, + content_fee?: { txid: string }, + purchase_receipt?: { txid: string, amount: string }, + download_directory: string, + download_path: string, + file_name: string, + key: string, + mime_type: string, + nout: number, + outpoint: string, + points_paid: number, + protobuf: string, + reflector_progress: number, + sd_hash: string, + status: string, + stopped: false, + stream_hash: string, + stream_name: string, + streaming_url: string, + suggested_file_name: string, + total_bytes: number, + total_bytes_lower_bound: number, + is_fully_reflected: boolean, + // TODO: sdk plans to change `tx` + // It isn't currently used by the apps + tx: {}, + txid: string, + uploading_to_reflector: boolean, + written_bytes: number, +}; + +declare type FileState = { + failedPurchaseUris: Array, + purchasedUris: Array, +}; + +declare type PurchaseUriCompleted = { + type: ACTIONS.PURCHASE_URI_COMPLETED, + data: { + uri: string, + streamingUrl: string, + }, +}; + +declare type PurchaseUriFailed = { + type: ACTIONS.PURCHASE_URI_FAILED, + data: { + uri: string, + error: any, + }, +}; + +declare type PurchaseUriStarted = { + type: ACTIONS.PURCHASE_URI_STARTED, + data: { + uri: string, + streamingUrl: string, + }, +}; + +declare type DeletePurchasedUri = { + type: ACTIONS.CLEAR_PURCHASED_URI_SUCCESS, + data: { + uri: string, + }, +}; diff --git a/ui/component/claimPreview/index.js b/ui/component/claimPreview/index.js index 832004474..2dc96541d 100644 --- a/ui/component/claimPreview/index.js +++ b/ui/component/claimPreview/index.js @@ -6,7 +6,6 @@ import { makeSelectClaimIsMine, makeSelectClaimIsPending, makeSelectClaimIsNsfw, - doFileGet, makeSelectReflectingClaimForUri, makeSelectClaimWasPurchased, makeSelectStreamingUrlForUri, @@ -25,7 +24,7 @@ import { selectShowMatureContent } from 'redux/selectors/settings'; import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { selectModerationBlockList } from 'redux/selectors/comments'; - +import { doFileGet } from 'redux/actions/file'; import ClaimPreview from './view'; import formatMediaDuration from 'util/formatMediaDuration'; diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index 40b3355c0..59d506718 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -5,7 +5,6 @@ import { makeSelectIsUriResolving, makeSelectThumbnailForUri, makeSelectTitleForUri, - doFileGet, makeSelectChannelForClaimUri, makeSelectClaimIsNsfw, makeSelectClaimIsStreamPlaceholder, @@ -14,6 +13,7 @@ import { import { selectMutedChannels } from 'redux/selectors/blocked'; import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; +import { doFileGet } from 'redux/actions/file'; import { selectShowMatureContent } from 'redux/selectors/settings'; import ClaimPreviewTile from './view'; import formatMediaDuration from 'util/formatMediaDuration'; diff --git a/ui/component/sideNavigation/index.js b/ui/component/sideNavigation/index.js index 6eda01389..40a13ccc8 100644 --- a/ui/component/sideNavigation/index.js +++ b/ui/component/sideNavigation/index.js @@ -1,10 +1,12 @@ import { connect } from 'react-redux'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; -import { selectPurchaseUriSuccess, doClearPurchasedUriSuccess } from 'lbry-redux'; +import { selectPurchaseUriSuccess } from 'lbry-redux'; import { selectFollowedTags } from 'redux/selectors/tags'; import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user'; import { selectHomepageData, selectLanguage } from 'redux/selectors/settings'; import { doSignOut } from 'redux/actions/app'; +import { doClearPurchasedUriSuccess } from 'redux/actions/file'; + import { selectUnseenNotificationCount } from 'redux/selectors/notifications'; import SideNavigation from './view'; diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 7345e773f..4460028ed 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -120,6 +120,13 @@ export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED'; export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED'; export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'; export const FILE_DELETE = 'FILE_DELETE'; +export const FETCH_FILE_INFO_FAILED = 'FETCH_FILE_INFO_FAILED'; +export const DOWNLOADING_CANCELED = 'DOWNLOADING_CANCELED'; +export const SET_FILE_LIST_SORT = 'SET_FILE_LIST_SORT'; +export const PURCHASE_URI_STARTED = 'PURCHASE_URI_STARTED'; +export const PURCHASE_URI_COMPLETED = 'PURCHASE_URI_COMPLETED'; +export const PURCHASE_URI_FAILED = 'PURCHASE_URI_FAILED'; +export const CLEAR_PURCHASED_URI_SUCCESS = 'CLEAR_PURCHASED_URI_SUCCESS'; // Search export const SEARCH_START = 'SEARCH_START'; diff --git a/ui/redux/actions/content.js b/ui/redux/actions/content.js index 7e76e3962..1053b617a 100644 --- a/ui/redux/actions/content.js +++ b/ui/redux/actions/content.js @@ -10,7 +10,6 @@ import { SETTINGS, makeSelectFileInfoForUri, selectFileInfosByOutpoint, - doPurchaseUri, makeSelectUriIsStreamable, selectDownloadingByOutpoint, makeSelectClaimForUri, @@ -19,6 +18,7 @@ import { doToast, makeSelectUrlsForCollectionId, } from 'lbry-redux'; +import { doPurchaseUri } from 'redux/actions/file'; import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc'; import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings'; @@ -117,8 +117,8 @@ export function doSetPlayingUri({ uri: ?string, source?: string, commentId?: string, - pathname: string, - collectionId: string, + pathname?: string, + collectionId?: string, }) { return (dispatch: Dispatch) => { dispatch({ @@ -140,7 +140,7 @@ export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolea } } - dispatch(doPurchaseUri(uri, { costInfo: cost }, saveFile, onSuccess)); + dispatch(doPurchaseUri(uri, { cost }, saveFile, onSuccess)); }; } diff --git a/ui/redux/actions/file.js b/ui/redux/actions/file.js index 447fd4f20..73adf0bd4 100644 --- a/ui/redux/actions/file.js +++ b/ui/redux/actions/file.js @@ -1,3 +1,4 @@ +// @flow import * as ACTIONS from 'constants/action_types'; // @if TARGET='app' import { shell } from 'electron'; @@ -7,22 +8,28 @@ import { batchActions, doAbandonClaim, makeSelectFileInfoForUri, + selectDownloadingByOutpoint, + makeSelectStreamingUrlForUri, makeSelectClaimForUri, + selectBalance, ABANDON_STATES, } from 'lbry-redux'; import { doHideModal } from 'redux/actions/app'; import { goBack } from 'connected-react-router'; import { doSetPlayingUri } from 'redux/actions/content'; import { selectPlayingUri } from 'redux/selectors/content'; +import { doToast } from 'redux/actions/notifications'; +type Dispatch = (action: any) => any; +type GetState = () => { file: FileState }; -export function doOpenFileInFolder(path) { +export function doOpenFileInFolder(path: string) { return () => { shell.showItemInFolder(path); }; } -export function doOpenFileInShell(path) { - return (dispatch) => { +export function doOpenFileInShell(path: string) { + return (dispatch: Dispatch) => { const success = shell.openPath(path); if (!success) { dispatch(doOpenFileInFolder(path)); @@ -30,8 +37,8 @@ export function doOpenFileInShell(path) { }; } -export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim, cb) { - return (dispatch) => { +export function doDeleteFile(outpoint: string, deleteFromComputer?: boolean, abandonClaim?: boolean, cb: any) { + return (dispatch: Dispatch) => { if (abandonClaim) { const [txid, nout] = outpoint.split(':'); dispatch(doAbandonClaim(txid, Number(nout), cb)); @@ -53,8 +60,13 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim, cb) { }; } -export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim, doGoBack) { - return (dispatch, getState) => { +export function doDeleteFileAndMaybeGoBack( + uri: string, + deleteFromComputer?: boolean, + abandonClaim?: boolean, + doGoBack: (any) => void +) { + return (dispatch: Dispatch, getState: GetState) => { const state = getState(); const playingUri = selectPlayingUri(state); const { outpoint } = makeSelectFileInfoForUri(uri)(state) || ''; @@ -88,3 +100,121 @@ export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim dispatch(batchActions(...actions)); }; } + +export function doFileGet(uri: string, saveFile: boolean = true, onSuccess?: (GetResponse) => any) { + return (dispatch: Dispatch, getState: () => any) => { + const state = getState(); + const { nout, txid } = makeSelectClaimForUri(uri)(state); + const outpoint = `${txid}:${nout}`; + + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_STARTED, + data: { + outpoint, + }, + }); + + // set save_file argument to True to save the file (old behaviour) + Lbry.get({ uri, save_file: saveFile }) + .then((streamInfo: GetResponse) => { + const timeout = streamInfo === null || typeof streamInfo !== 'object' || streamInfo.error === 'Timeout'; + if (timeout) { + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_FAILED, + data: { outpoint }, + }); + + dispatch(doToast({ message: `File timeout for uri ${uri}`, isError: true })); + } else { + if (streamInfo.purchase_receipt || streamInfo.content_fee) { + dispatch({ + type: ACTIONS.PURCHASE_URI_COMPLETED, + data: { uri, purchaseReceipt: streamInfo.purchase_receipt || streamInfo.content_fee }, + }); + } + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_COMPLETED, + data: { + fileInfo: streamInfo, + outpoint: outpoint, + }, + }); + + if (onSuccess) { + onSuccess(streamInfo); + } + } + }) + .catch((error) => { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error }, + }); + + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_FAILED, + data: { outpoint }, + }); + + dispatch( + doToast({ + message: `Failed to view ${uri}, please try again. If this problem persists, visit https://lbry.com/faq/support for support.`, + isError: true, + }) + ); + }); + }; +} + +export function doPurchaseUri( + uri: string, + costInfo: { cost: number }, + saveFile: boolean = true, + onSuccess?: (GetResponse) => any +) { + return (dispatch: Dispatch, getState: GetState) => { + dispatch({ + type: ACTIONS.PURCHASE_URI_STARTED, + data: { uri }, + }); + + const state = getState(); + const balance = selectBalance(state); + const fileInfo = makeSelectFileInfoForUri(uri)(state); + const downloadingByOutpoint = selectDownloadingByOutpoint(state); + const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; + const alreadyStreaming = makeSelectStreamingUrlForUri(uri)(state); + + if (!saveFile && (alreadyDownloading || alreadyStreaming)) { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error: `Already fetching uri: ${uri}` }, + }); + + if (onSuccess) { + onSuccess(fileInfo); + } + + return; + } + + const { cost } = costInfo; + if (parseFloat(cost) > balance) { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error: 'Insufficient credits' }, + }); + + Promise.resolve(); + return; + } + + dispatch(doFileGet(uri, saveFile, onSuccess)); + }; +} + +export function doClearPurchasedUriSuccess() { + return { + type: ACTIONS.CLEAR_PURCHASED_URI_SUCCESS, + }; +} From e3791aefdce67098fbb366843278df0d13f5224f Mon Sep 17 00:00:00 2001 From: mayeaux Date: Wed, 6 Oct 2021 21:59:33 +0300 Subject: [PATCH 7/7] Send video bitrate and user bandwidth to Watchman (#7145) * adding functionality to detect user download speed * calculating bandwidth speed more intelligently * saving download speed and updating it every 30s * all the functionality should be done needs testing * fix linting * use a 1mb file for calculating bandwidth * add optional chaining plugin to babel and get bitrate from texttrack * allow optional chaining for flow * ignore flow error * disable bandwidth checking functionality * fix flow error --- .flowconfig | 3 ++ package.json | 1 + ui/analytics.js | 24 ++++++++++++--- ui/component/viewers/videoViewer/view.jsx | 10 ++++++- ui/redux/actions/app.js | 1 + ui/util/detect-user-bandwidth.js | 20 +++++++++++++ web/webpack.config.js | 2 +- yarn.lock | 36 ++++++++++++++++++++++- 8 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 ui/util/detect-user-bandwidth.js diff --git a/.flowconfig b/.flowconfig index 2c6b8839f..0ae16f24e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -43,6 +43,8 @@ module.name_mapper='^web\/page\(.*\)$' -> '/web/page\1' module.name_mapper='^homepage\(.*\)$' -> '/ui/util/homepage\1' module.name_mapper='^scss\/component\(.*\)$' -> '/ui/scss/component/\1' +esproposal.optional_chaining=enable + ; Extensions module.file_ext=.js module.file_ext=.jsx @@ -51,4 +53,5 @@ module.file_ext=.css module.file_ext=.scss + [strict] diff --git a/package.json b/package.json index f1545afa3..6826b3296 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-proposal-object-rest-spread": "^7.6.2", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-transform-flow-strip-types": "^7.2.3", "@babel/plugin-transform-runtime": "^7.4.3", diff --git a/ui/analytics.js b/ui/analytics.js index 495535dcc..feae9a64a 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -10,6 +10,15 @@ import ElectronCookies from '@exponent/electron-cookies'; import { generateInitialUrl } from 'util/url'; // @endif import { MATOMO_ID, MATOMO_URL } from 'config'; +// import getConnectionSpeed from 'util/detect-user-bandwidth'; + +// let userDownloadBandwidthInBitsPerSecond; +// async function getUserBandwidth() { +// userDownloadBandwidthInBitsPerSecond = await getConnectionSpeed(); +// } + +// get user bandwidth every minute, starting after an initial one minute wait +// setInterval(getUserBandwidth, 1000 * 60); const isProduction = process.env.NODE_ENV === 'production'; const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev'); @@ -40,7 +49,7 @@ type Analytics = { tagFollowEvent: (string, boolean, ?string) => void, playerLoadedEvent: (?boolean) => void, playerStartedEvent: (?boolean) => void, - videoStartEvent: (string, number, string, number, string, any) => void, + videoStartEvent: (string, number, string, number, string, any, number) => void, videoIsPlaying: (boolean, any) => void, videoBufferEvent: ( StreamClaim, @@ -111,7 +120,7 @@ function getDeviceType() { // variables initialized for watchman let amountOfBufferEvents = 0; let amountOfBufferTimeInMS = 0; -let videoType, userId, claimUrl, playerPoweredBy, videoPlayer; +let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond; let lastSentTime; // calculate data for backend, send them, and reset buffer data for next interval @@ -130,6 +139,9 @@ async function sendAndResetWatchmanData() { let protocol; if (videoType === 'application/x-mpegURL') { protocol = 'hls'; + // get bandwidth if it exists from the texttrack (so it's accurate if user changes quality) + // $FlowFixMe + bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth; } else { protocol = 'stb'; } @@ -152,6 +164,9 @@ async function sendAndResetWatchmanData() { user_id: userId.toString(), position: Math.round(positionInVideo), rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100), + bitrate: bitrateAsBitsPerSecond, + bandwidth: undefined, + // ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated }; // post to watchman @@ -202,7 +217,7 @@ async function sendWatchmanData(body) { } const analytics: Analytics = { - // receive buffer events from tracking plugin and jklj + // receive buffer events from tracking plugin and save buffer amounts and times for backend call videoBufferEvent: async (claim, data) => { amountOfBufferEvents = amountOfBufferEvents + 1; amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration; @@ -240,7 +255,7 @@ const analytics: Analytics = { startWatchmanIntervalIfNotRunning(); } }, - videoStartEvent: (claimId, duration, poweredBy, passedUserId, canonicalUrl, passedPlayer) => { + videoStartEvent: (claimId, duration, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => { // populate values for watchman when video starts userId = passedUserId; claimUrl = canonicalUrl; @@ -248,6 +263,7 @@ const analytics: Analytics = { videoType = passedPlayer.currentSource().type; videoPlayer = passedPlayer; + bitrateAsBitsPerSecond = videoBitrate; sendPromMetric('time_to_start', duration); sendMatomoEvent('Media', 'TimeToStart', claimId, duration); diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index efe534dd1..5619065f9 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -168,9 +168,17 @@ function VideoViewer(props: Props) { } analytics.playerStartedEvent(embedded); + // convert bytes to bits, and then divide by seconds + const contentInBits = Number(claim.value.source.size) * 8; + const durationInSeconds = claim.value.video && claim.value.video.duration; + let bitrateAsBitsPerSecond; + if (durationInSeconds) { + bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds); + } + fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => { let playerPoweredBy = response.headers.get('x-powered-by') || ''; - analytics.videoStartEvent(claimId, timeToStart, playerPoweredBy, userId, claim.canonical_url, this); + analytics.videoStartEvent(claimId, timeToStart, playerPoweredBy, userId, claim.canonical_url, this, bitrateAsBitsPerSecond); }); doAnalyticsView(uri, timeToStart).then(() => { diff --git a/ui/redux/actions/app.js b/ui/redux/actions/app.js index 67a00c4a0..cb0b81332 100644 --- a/ui/redux/actions/app.js +++ b/ui/redux/actions/app.js @@ -505,6 +505,7 @@ export function doAnalyticsBuffer(uri, bufferData) { const fileSizeInBits = fileSize * 8; const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds); const userId = user && user.id.toString(); + // if there's a logged in user, send buffer event data to watchman if (userId) { analytics.videoBufferEvent(claim, { timeAtBuffer, diff --git a/ui/util/detect-user-bandwidth.js b/ui/util/detect-user-bandwidth.js new file mode 100644 index 000000000..fd3f0d187 --- /dev/null +++ b/ui/util/detect-user-bandwidth.js @@ -0,0 +1,20 @@ +const imageAddr = 'https://upload.wikimedia.org/wikipedia/commons/b/b9/Pizigani_1367_Chart_1MB.jpg'; +const downloadSize = 1093957; // this must match with the image above + +let startTime, endTime; +async function measureConnectionSpeed() { + startTime = (new Date()).getTime(); + const cacheBuster = '?nnn=' + startTime; + + const download = new Image(); + download.src = imageAddr + cacheBuster; + // this returns when the image is finished downloading + await download.decode(); + endTime = (new Date()).getTime(); + const duration = (endTime - startTime) / 1000; + const bitsLoaded = downloadSize * 8; + const speedBps = (bitsLoaded / duration).toFixed(2); + return Math.round(Number(speedBps)); +} + +module.exports = measureConnectionSpeed; diff --git a/web/webpack.config.js b/web/webpack.config.js index f483c6217..120747802 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -149,7 +149,7 @@ const webConfig = { test: /\.jsx?$/, options: { presets: ['@babel/env', '@babel/react', '@babel/flow'], - plugins: ['@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'], + plugins: ['@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'], }, }, { diff --git a/yarn.lock b/yarn.lock index 154f85f0e..22e0a95f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -329,6 +329,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== +"@babel/helper-plugin-utils@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" + integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== + "@babel/helper-remap-async-to-generator@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" @@ -390,6 +395,13 @@ dependencies: "@babel/types" "^7.12.1" +"@babel/helper-skip-transparent-expression-wrappers@^7.14.5": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz#707dbdba1f4ad0fa34f9114fc8197aec7d5da2eb" + integrity sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-split-export-declaration@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" @@ -426,6 +438,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== +"@babel/helper-validator-identifier@^7.14.9": + version "7.15.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + "@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" @@ -590,6 +607,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" "@babel/plugin-syntax-optional-chaining" "^7.8.0" +"@babel/plugin-proposal-optional-chaining@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz#fa83651e60a360e3f13797eef00b8d519695b603" + integrity sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-proposal-private-methods@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389" @@ -694,7 +720,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.8.0": +"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== @@ -1230,6 +1256,14 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.15.4": + version "7.15.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f" + integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== + dependencies: + "@babel/helper-validator-identifier" "^7.14.9" + to-fast-properties "^2.0.0" + "@datapunt/matomo-tracker-js@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@datapunt/matomo-tracker-js/-/matomo-tracker-js-0.1.4.tgz#1226f0964d2c062bf9392e9c2fd89838262b10df"