recsys v0.2 (#6977)
* recsys wip better logging fix floating player popout playing uri bug with recsys lint add empty entries to create use beacon; fire on visibilitychange cleanup, not record recs if not seen ifweb recsys beacon recsys handle embeds, cleanup use history.listen to trigger events fix recsys embed bug bugfix more default data cleaner cleaner * remove tentative * disable recsys debug logging
This commit is contained in:
parent
64cbd4ae8d
commit
049fb2878e
13 changed files with 340 additions and 238 deletions
|
@ -31,6 +31,7 @@ module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/ui/modal\1'
|
|||
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/ui/app\1'
|
||||
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/ui/native\1'
|
||||
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/ui/analytics\1'
|
||||
module.name_mapper='^recsys\(.*\)$' -> '<PROJECT_ROOT>/ui/recsys\1'
|
||||
module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/ui/rewards\1'
|
||||
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1'
|
||||
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1'
|
||||
|
|
|
@ -27,7 +27,6 @@ type Props = {
|
|||
onScrollBottom?: (any) => void,
|
||||
page?: number,
|
||||
pageSize?: number,
|
||||
id?: string,
|
||||
// If using the default header, this is a unique ID needed to persist the state of the filter setting
|
||||
persistedStorageKey?: string,
|
||||
showHiddenByUser: boolean,
|
||||
|
@ -47,7 +46,7 @@ type Props = {
|
|||
searchOptions?: any,
|
||||
collectionId?: string,
|
||||
showNoSourceClaims?: boolean,
|
||||
onClick?: (e: any, index?: number) => void,
|
||||
onClick?: (e: any, claim?: ?Claim, index?: number) => void,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
|
@ -56,7 +55,6 @@ export default function ClaimList(props: Props) {
|
|||
uris,
|
||||
headerAltControls,
|
||||
loading,
|
||||
id,
|
||||
persistedStorageKey,
|
||||
empty,
|
||||
defaultSort,
|
||||
|
@ -110,9 +108,9 @@ export default function ClaimList(props: Props) {
|
|||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||
}
|
||||
|
||||
function handleClaimClicked(e, index) {
|
||||
function handleClaimClicked(e, claim, index) {
|
||||
if (onClick) {
|
||||
onClick(e, index);
|
||||
onClick(e, claim, index);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,7 +198,6 @@ export default function ClaimList(props: Props) {
|
|||
{injectedItem && index === 4 && <li>{injectedItem}</li>}
|
||||
<ClaimPreview
|
||||
uri={uri}
|
||||
containerId={id}
|
||||
indexInContainer={index}
|
||||
type={type}
|
||||
active={activeUri && uri === activeUri}
|
||||
|
@ -220,7 +217,7 @@ export default function ClaimList(props: Props) {
|
|||
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
|
||||
}}
|
||||
live={resolveLive(index)}
|
||||
onClick={handleClaimClicked}
|
||||
onClick={(e, claim, index) => handleClaimClicked(e, claim, index)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
|
|
@ -583,7 +583,6 @@ function ClaimListDiscover(props: Props) {
|
|||
)}
|
||||
<ClaimList
|
||||
tileLayout
|
||||
id={mainSearchKey}
|
||||
loading={loading}
|
||||
uris={finalUris}
|
||||
onScrollBottom={handleScrollBottom}
|
||||
|
@ -617,7 +616,6 @@ function ClaimListDiscover(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
<ClaimList
|
||||
id={mainSearchKey}
|
||||
type={type}
|
||||
loading={loading}
|
||||
uris={finalUris}
|
||||
|
|
|
@ -29,7 +29,6 @@ import ClaimPreviewNoContent from './claim-preview-no-content';
|
|||
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { CONTAINER_ID } from 'constants/navigation';
|
||||
|
||||
const AbandonedChannelPreview = lazyImport(() =>
|
||||
import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */)
|
||||
|
@ -67,7 +66,7 @@ type Props = {
|
|||
actions: boolean | Node | string | number,
|
||||
properties: boolean | Node | string | number | ((Claim) => Node),
|
||||
empty?: Node,
|
||||
onClick?: (e: any, index?: number) => any,
|
||||
onClick?: (e: any, claim?: ?Claim, index?: number) => any,
|
||||
streamingUrl: ?string,
|
||||
getFile: (string) => void,
|
||||
customShouldHide?: (Claim) => boolean,
|
||||
|
@ -90,7 +89,6 @@ type Props = {
|
|||
disableNavigation?: boolean,
|
||||
mediaDuration?: string,
|
||||
date?: any,
|
||||
containerId?: string, // ID or name of the container (e.g. ClaimList, HOC, etc.) that this is in.
|
||||
indexInContainer?: number, // The index order of this component within 'containerId'.
|
||||
channelSubCount?: number,
|
||||
};
|
||||
|
@ -154,7 +152,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
isCollectionMine,
|
||||
collectionUris,
|
||||
disableNavigation,
|
||||
containerId,
|
||||
indexInContainer,
|
||||
channelSubCount,
|
||||
} = props;
|
||||
|
@ -223,7 +220,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
|
||||
const handleNavLinkClick = (e) => {
|
||||
if (onClick) {
|
||||
onClick(e, indexInContainer);
|
||||
onClick(e, claim, indexInContainer); // not sure indexInContainer is used for anything.
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
@ -232,10 +229,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
to: {
|
||||
pathname: navigateUrl,
|
||||
search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '',
|
||||
state: containerId ? { [CONTAINER_ID]: containerId } : undefined,
|
||||
},
|
||||
onClick: (e) => handleNavLinkClick(e),
|
||||
onAuxClick: (e) => handleNavLinkClick(e),
|
||||
onClick: handleNavLinkClick,
|
||||
onAuxClick: handleNavLinkClick,
|
||||
};
|
||||
|
||||
// do not block abandoned and nsfw claims if showUserBlocked is passed
|
||||
|
@ -281,14 +277,13 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
|
||||
function handleOnClick(e) {
|
||||
if (onClick) {
|
||||
onClick(e, indexInContainer);
|
||||
onClick(e, claim, indexInContainer);
|
||||
}
|
||||
|
||||
if (claim && !pending && !disableNavigation) {
|
||||
history.push({
|
||||
pathname: navigateUrl,
|
||||
search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '',
|
||||
state: containerId ? { [CONTAINER_ID]: containerId } : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,19 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|||
import { makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
|
||||
import RecommendedContent from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
mature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||
recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state),
|
||||
nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state),
|
||||
isSearching: selectIsSearching(state),
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||
const { claim_id: claimId } = claim;
|
||||
return {
|
||||
mature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||
recommendedContentUris: makeSelectRecommendedContentForUri(props.uri)(state),
|
||||
nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state),
|
||||
isSearching: selectIsSearching(state),
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
claim,
|
||||
claimId,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)),
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// @flow
|
||||
import { SHOW_ADS } from 'config';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import ClaimList from 'component/claimList';
|
||||
import ClaimListDiscover from 'component/claimListDiscover';
|
||||
import Ads from 'web/component/ads';
|
||||
|
@ -10,14 +8,14 @@ import Card from 'component/common/card';
|
|||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
import { CONTAINER_ID } from 'constants/navigation';
|
||||
import RecSys from 'recsys';
|
||||
|
||||
const VIEW_ALL_RELATED = 'view_all_related';
|
||||
const VIEW_MORE_FROM = 'view_more_from';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
recommendedContent: Array<string>,
|
||||
recommendedContentUris: Array<string>,
|
||||
nextRecommendedUri: string,
|
||||
isSearching: boolean,
|
||||
doFetchRecommendedContent: (string, boolean) => void,
|
||||
|
@ -25,7 +23,7 @@ type Props = {
|
|||
isAuthenticated: boolean,
|
||||
claim: ?StreamClaim,
|
||||
doRecommendationUpdate: (claimId: string, urls: Array<string>, id: string, parentId: string) => void,
|
||||
doRecommendationClicked: (claimId: string, index: number) => void,
|
||||
claimId: string,
|
||||
};
|
||||
|
||||
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||
|
@ -33,23 +31,20 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
|||
uri,
|
||||
doFetchRecommendedContent,
|
||||
mature,
|
||||
recommendedContent,
|
||||
recommendedContentUris,
|
||||
nextRecommendedUri,
|
||||
isSearching,
|
||||
isAuthenticated,
|
||||
claim,
|
||||
doRecommendationUpdate,
|
||||
doRecommendationClicked,
|
||||
claimId,
|
||||
} = props;
|
||||
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
|
||||
const [recommendationId, setRecommendationId] = React.useState('');
|
||||
const [recommendationUrls, setRecommendationUrls] = React.useState();
|
||||
const history = useHistory();
|
||||
const signingChannel = claim && claim.signing_channel;
|
||||
const channelName = signingChannel ? signingChannel.name : null;
|
||||
const isMobile = useIsMobile();
|
||||
const isMedium = useIsMediumScreen();
|
||||
|
||||
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
|
||||
React.useEffect(() => {
|
||||
function moveAutoplayNextItemToTop(recommendedContent) {
|
||||
let newList = recommendedContent;
|
||||
|
@ -72,33 +67,27 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContent);
|
||||
const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContentUris);
|
||||
|
||||
if (claim && !listEq(recommendationUrls, newRecommendationUrls)) {
|
||||
const parentId = (history.location.state && history.location.state[CONTAINER_ID]) || '';
|
||||
const id = uuidv4();
|
||||
setRecommendationId(id);
|
||||
setRecommendationUrls(newRecommendationUrls);
|
||||
|
||||
doRecommendationUpdate(claim.claim_id, newRecommendationUrls, id, parentId);
|
||||
}
|
||||
}, [
|
||||
recommendedContent,
|
||||
nextRecommendedUri,
|
||||
recommendationUrls,
|
||||
setRecommendationUrls,
|
||||
claim,
|
||||
doRecommendationUpdate,
|
||||
history.location.state,
|
||||
]);
|
||||
}, [recommendedContentUris, nextRecommendedUri, recommendationUrls, setRecommendationUrls, claim]);
|
||||
|
||||
React.useEffect(() => {
|
||||
doFetchRecommendedContent(uri, mature);
|
||||
}, [uri, mature, doFetchRecommendedContent]);
|
||||
|
||||
function handleRecommendationClicked(e: any, index: number) {
|
||||
React.useEffect(() => {
|
||||
// Right now we only want to record the recs if they actually saw them.
|
||||
if (recommendationUrls && recommendationUrls.length && nextRecommendedUri && viewMode === VIEW_ALL_RELATED) {
|
||||
onRecommendationsLoaded(claimId, recommendationUrls);
|
||||
}
|
||||
}, [recommendationUrls, onRecommendationsLoaded, claimId, nextRecommendedUri, viewMode]);
|
||||
|
||||
function handleRecommendationClicked(e, clickedClaim, index: number) {
|
||||
if (claim) {
|
||||
doRecommendationClicked(claim.claim_id, index);
|
||||
onRecommendationClicked(claim.claim_id, clickedClaim.claim_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,7 +122,6 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
|||
<div>
|
||||
{viewMode === VIEW_ALL_RELATED && (
|
||||
<ClaimList
|
||||
id={recommendationId}
|
||||
type="small"
|
||||
loading={isSearching}
|
||||
uris={recommendationUrls}
|
||||
|
@ -177,8 +165,8 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
|||
a.isAuthenticated !== b.isAuthenticated ||
|
||||
a.isSearching !== b.isSearching ||
|
||||
a.mature !== b.mature ||
|
||||
(a.recommendedContent && !b.recommendedContent) ||
|
||||
(!a.recommendedContent && b.recommendedContent) ||
|
||||
(a.recommendedContentUris && !b.recommendedContentUris) ||
|
||||
(!a.recommendedContentUris && b.recommendedContentUris) ||
|
||||
(a.claim && !b.claim) ||
|
||||
(!a.claim && b.claim)
|
||||
) {
|
||||
|
@ -189,14 +177,14 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (a.recommendedContent && b.recommendedContent) {
|
||||
if (a.recommendedContent.length !== b.recommendedContent.length) {
|
||||
if (a.recommendedContentUris && b.recommendedContentUris) {
|
||||
if (a.recommendedContentUris.length !== b.recommendedContentUris.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i = a.recommendedContent.length;
|
||||
let i = a.recommendedContentUris.length;
|
||||
while (i--) {
|
||||
if (a.recommendedContent[i] !== b.recommendedContent[i]) {
|
||||
if (a.recommendedContentUris[i] !== b.recommendedContentUris[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +1,20 @@
|
|||
// Created by xander on 6/21/2021
|
||||
import videojs from 'video.js';
|
||||
import {
|
||||
makeSelectRecommendationId,
|
||||
makeSelectRecommendationParentId,
|
||||
makeSelectRecommendedClaimIds,
|
||||
makeSelectRecommendationClicks,
|
||||
} from 'redux/selectors/content';
|
||||
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
|
||||
|
||||
import RecSys from 'recsys';
|
||||
const VERSION = '0.0.1';
|
||||
|
||||
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
|
||||
const recsysId = 'lighthouse-v0';
|
||||
|
||||
/* RecSys */
|
||||
const RecsysData = {
|
||||
const PlayerEvent = {
|
||||
event: {
|
||||
start: 0,
|
||||
start: 0, // event types
|
||||
stop: 1,
|
||||
scrub: 2,
|
||||
speed: 3,
|
||||
},
|
||||
};
|
||||
|
||||
function createRecsys(claimId, userId, events, loadedAt, isEmbed) {
|
||||
const pageLoadedAt = loadedAt;
|
||||
const pageExitedAt = Date.now();
|
||||
|
||||
if (window.store) {
|
||||
const state = window.store.getState();
|
||||
|
||||
return {
|
||||
uuid: makeSelectRecommendationId(claimId)(state),
|
||||
parentUuid: makeSelectRecommendationParentId(claimId)(state),
|
||||
uid: userId,
|
||||
claimId: claimId,
|
||||
pageLoadedAt: pageLoadedAt,
|
||||
pageExitedAt: pageExitedAt,
|
||||
recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId,
|
||||
recClaimIds: makeSelectRecommendedClaimIds(claimId)(state),
|
||||
recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state),
|
||||
events: events,
|
||||
isEmbed: isEmbed,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function newRecsysEvent(eventType, offset, arg) {
|
||||
function newRecsysPlayerEvent(eventType, offset, arg) {
|
||||
if (arg) {
|
||||
return {
|
||||
event: eventType,
|
||||
|
@ -63,30 +29,11 @@ function newRecsysEvent(eventType, offset, arg) {
|
|||
}
|
||||
}
|
||||
|
||||
function sendRecsysEvents(recsys) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' }, // application/json
|
||||
body: JSON.stringify(recsys),
|
||||
};
|
||||
|
||||
try {
|
||||
fetch(recsysEndpoint, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
// console.log(`Recsys response data:`, data);
|
||||
});
|
||||
} catch (error) {
|
||||
// console.error(`Recsys Error`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
endpoint: recsysEndpoint,
|
||||
recsysId: recsysId,
|
||||
videoId: null,
|
||||
userId: 0,
|
||||
debug: false,
|
||||
embedded: false,
|
||||
};
|
||||
|
||||
const Component = videojs.getComponent('Component');
|
||||
|
@ -98,16 +45,13 @@ class RecsysPlugin extends Component {
|
|||
|
||||
// Plugin started
|
||||
if (options.debug) {
|
||||
this.log(`Created recsys plugin for: videoId:${options.videoId}, userId:${options.userId}`);
|
||||
this.log(`Created recsys plugin for: videoId:${options.videoId}`);
|
||||
}
|
||||
|
||||
// To help with debugging, we'll add a global vjs object with the video js player
|
||||
window.vjs = player;
|
||||
|
||||
this.player = player;
|
||||
|
||||
this.recsysEvents = [];
|
||||
this.loadedAt = Date.now();
|
||||
this.lastTimeUpdate = null;
|
||||
this.currentTimeUpdate = null;
|
||||
this.inPause = false;
|
||||
|
@ -124,57 +68,37 @@ class RecsysPlugin extends Component {
|
|||
player.on('dispose', (event) => this.onDispose(event));
|
||||
}
|
||||
|
||||
addRecsysEvent(recsysEvent) {
|
||||
// For now, don't do client-side preprocessing. I think there
|
||||
// are browser inconsistencies and preprocessing loses too much info.
|
||||
this.recsysEvents.push(recsysEvent);
|
||||
}
|
||||
|
||||
getRecsysEvents() {
|
||||
return this.recsysEvents;
|
||||
}
|
||||
|
||||
sendRecsysEvents() {
|
||||
const event = createRecsys(
|
||||
this.options_.videoId,
|
||||
this.options_.userId,
|
||||
this.getRecsysEvents(),
|
||||
this.loadedAt,
|
||||
false
|
||||
);
|
||||
|
||||
if (event) {
|
||||
sendRecsysEvents(event);
|
||||
}
|
||||
}
|
||||
|
||||
onPlay(event) {
|
||||
const recsysEvent = newRecsysEvent(RecsysData.event.start, this.player.currentTime());
|
||||
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.start, this.player.currentTime());
|
||||
this.log('onPlay', recsysEvent);
|
||||
this.addRecsysEvent(recsysEvent);
|
||||
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent, this.options_.embedded);
|
||||
|
||||
this.inPause = false;
|
||||
this.lastTimeUpdate = recsysEvent.offset;
|
||||
}
|
||||
|
||||
onPause(event) {
|
||||
const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime());
|
||||
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime());
|
||||
this.log('onPause', recsysEvent);
|
||||
this.addRecsysEvent(recsysEvent);
|
||||
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||
|
||||
this.inPause = true;
|
||||
}
|
||||
|
||||
onEnded(event) {
|
||||
const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime());
|
||||
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime());
|
||||
this.log('onEnded', recsysEvent);
|
||||
this.addRecsysEvent(recsysEvent);
|
||||
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||
}
|
||||
|
||||
onRateChange(event) {
|
||||
const recsysEvent = newRecsysEvent(RecsysData.event.speed, this.player.currentTime(), this.player.playbackRate());
|
||||
const recsysEvent = newRecsysPlayerEvent(
|
||||
PlayerEvent.event.speed,
|
||||
this.player.currentTime(),
|
||||
this.player.playbackRate()
|
||||
);
|
||||
this.log('onRateChange', recsysEvent);
|
||||
this.addRecsysEvent(recsysEvent);
|
||||
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||
}
|
||||
|
||||
onTimeUpdate(event) {
|
||||
|
@ -212,19 +136,19 @@ class RecsysPlugin extends Component {
|
|||
|
||||
if (fromTime !== curTime) {
|
||||
// This removes duplicates that aren't useful.
|
||||
const recsysEvent = newRecsysEvent(RecsysData.event.scrub, fromTime, curTime);
|
||||
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.scrub, fromTime, curTime);
|
||||
this.log('onSeeked', recsysEvent);
|
||||
this.addRecsysEvent(recsysEvent);
|
||||
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||
}
|
||||
}
|
||||
|
||||
onDispose(event) {
|
||||
this.sendRecsysEvents();
|
||||
RecSys.onPlayerDispose(this.options_.videoId, this.options_.embedded);
|
||||
}
|
||||
|
||||
log(...args) {
|
||||
if (this.options_.debug) {
|
||||
// console.log(`Recsys Debug:`, JSON.stringify(args));
|
||||
console.log(`Recsys Player Debug:`, JSON.stringify(args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ type Props = {
|
|||
startMuted: boolean,
|
||||
autoplay: boolean,
|
||||
autoplaySetting: boolean,
|
||||
embedded: boolean,
|
||||
toggleVideoTheaterMode: () => void,
|
||||
adUrl: ?string,
|
||||
claimId: ?string,
|
||||
|
@ -194,6 +195,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
const {
|
||||
autoplay,
|
||||
autoplaySetting,
|
||||
embedded,
|
||||
startMuted,
|
||||
source,
|
||||
sourceType,
|
||||
|
@ -590,6 +592,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
player.recsys({
|
||||
videoId: claimId,
|
||||
userId: userId,
|
||||
embedded: embedded,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ function VideoViewer(props: Props) {
|
|||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [ended, setEnded] = useState(false);
|
||||
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
||||
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
||||
const [isEndedEmbed, setIsEndedEmbed] = useState(false);
|
||||
const vjsCallbackDataRef: any = React.useRef();
|
||||
const previousUri = usePrevious(uri);
|
||||
const embedded = useContext(EmbedContext);
|
||||
|
@ -142,7 +142,7 @@ function VideoViewer(props: Props) {
|
|||
useEffect(() => {
|
||||
if (uri && previousUri && uri !== previousUri) {
|
||||
setShowAutoplayCountdown(false);
|
||||
setIsEndededEmbed(false);
|
||||
setIsEndedEmbed(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [uri, previousUri]);
|
||||
|
@ -236,7 +236,7 @@ function VideoViewer(props: Props) {
|
|||
}
|
||||
|
||||
if (embedded) {
|
||||
setIsEndededEmbed(true);
|
||||
setIsEndedEmbed(true);
|
||||
} else if (!collectionId && autoplayNext) {
|
||||
setShowAutoplayCountdown(true);
|
||||
} else if (collectionId) {
|
||||
|
@ -247,7 +247,7 @@ function VideoViewer(props: Props) {
|
|||
}
|
||||
}, [
|
||||
embedded,
|
||||
setIsEndededEmbed,
|
||||
setIsEndedEmbed,
|
||||
autoplayMedia,
|
||||
setShowAutoplayCountdown,
|
||||
adUrl,
|
||||
|
@ -264,7 +264,7 @@ function VideoViewer(props: Props) {
|
|||
setIsLoading(false);
|
||||
setIsPlaying(true);
|
||||
setShowAutoplayCountdown(false);
|
||||
setIsEndededEmbed(false);
|
||||
setIsEndedEmbed(false);
|
||||
setReplay(false);
|
||||
setDoNavigate(false);
|
||||
analytics.videoIsPlaying(true, player);
|
||||
|
@ -391,7 +391,7 @@ function VideoViewer(props: Props) {
|
|||
<div
|
||||
className={classnames('file-viewer', {
|
||||
'file-viewer--is-playing': isPlaying,
|
||||
'file-viewer--ended-embed': isEndededEmbed,
|
||||
'file-viewer--ended-embed': isEndedEmbed,
|
||||
})}
|
||||
onContextMenu={stopContextMenu}
|
||||
>
|
||||
|
@ -403,8 +403,8 @@ function VideoViewer(props: Props) {
|
|||
doReplay={() => setReplay(true)}
|
||||
/>
|
||||
)}
|
||||
{isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
||||
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||
{isEndedEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
||||
{embedded && !isEndedEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
||||
{isLoading && <LoadingScreen status={__('Loading')} />}
|
||||
|
||||
|
@ -458,6 +458,7 @@ function VideoViewer(props: Props) {
|
|||
videoTheaterMode={videoTheaterMode}
|
||||
playNext={doPlayNext}
|
||||
playPrevious={doPlayPrevious}
|
||||
embedded={embedded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
254
ui/recsys.js
Normal file
254
ui/recsys.js
Normal file
|
@ -0,0 +1,254 @@
|
|||
import { selectUser } from 'redux/selectors/user';
|
||||
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
|
||||
import { v4 as Uuidv4 } from 'uuid';
|
||||
import { parseURI, SETTINGS, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { selectPlayingUri, selectPrimaryUri } from 'redux/selectors/content';
|
||||
import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings';
|
||||
import { history } from './store';
|
||||
|
||||
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
|
||||
const recsysId = 'lighthouse-v0';
|
||||
|
||||
const getClaimIdsFromUris = (uris) => {
|
||||
return uris
|
||||
? uris.map((uri) => {
|
||||
try {
|
||||
const { claimId } = parseURI(uri);
|
||||
return claimId;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
: [];
|
||||
};
|
||||
|
||||
const recsys = {
|
||||
entries: {},
|
||||
debug: false,
|
||||
/**
|
||||
* Provides for creating, updating, and sending Clickstream data object Entries.
|
||||
* Entries are Created either when recommendedContent loads, or when recommendedContent is clicked.
|
||||
* If recommended content is clicked, An Entry with parentUuid is created.
|
||||
* On page load, find an empty entry with your claimId, or create a new entry and record to it.
|
||||
* The entry will be populated with the following:
|
||||
* - parentUuid // optional
|
||||
* - Uuid
|
||||
* - claimId
|
||||
* - recommendedClaims [] // optionally empty
|
||||
* - playerEvents [] // optionally empty
|
||||
* - recommendedClaimsIndexClicked [] // optionally empty
|
||||
* - UserId
|
||||
* - pageLoadedAt
|
||||
* - isEmbed
|
||||
* - pageExitedAt
|
||||
* - recsysId // optional
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function: onClickedRecommended()
|
||||
* Called when RecommendedContent was clicked.
|
||||
* Adds index of clicked recommendation to parent entry
|
||||
* Adds new Entry with parentUuid for destination page
|
||||
* @param parentClaimId: string,
|
||||
* @param newClaimId: string,
|
||||
*/
|
||||
onClickedRecommended: function (parentClaimId, newClaimId) {
|
||||
const parentEntry = recsys.entries[parentClaimId] ? recsys.entries[parentClaimId] : null;
|
||||
const parentUuid = parentEntry['uuid'];
|
||||
const parentRecommendedClaims = parentEntry['recClaimIds'] || [];
|
||||
const parentClickedIndexes = parentEntry['recClickedVideoIdx'] || [];
|
||||
const indexClicked = parentRecommendedClaims.indexOf(newClaimId);
|
||||
|
||||
if (parentUuid) {
|
||||
recsys.createRecsysEntry(newClaimId, parentUuid);
|
||||
}
|
||||
parentClickedIndexes.push(indexClicked);
|
||||
recsys.log('onClickedRecommended', { parentClaimId, newClaimId });
|
||||
},
|
||||
|
||||
/**
|
||||
* Page was loaded. Get or Create entry and populate it with default data, plus recommended content, recsysId, etc.
|
||||
* Called from recommendedContent component
|
||||
*/
|
||||
onRecsLoaded: function (claimId, uris) {
|
||||
if (window.store) {
|
||||
const state = window.store.getState();
|
||||
if (!recsys.entries[claimId]) {
|
||||
recsys.createRecsysEntry(claimId);
|
||||
}
|
||||
const claimIds = getClaimIdsFromUris(uris);
|
||||
recsys.entries[claimId]['recsysId'] = makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId;
|
||||
recsys.entries[claimId]['pageLoadedAt'] = Date.now();
|
||||
recsys.entries[claimId]['recClaimIds'] = claimIds;
|
||||
}
|
||||
recsys.log('onRecsLoaded', claimId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an Entry with optional parentUuid
|
||||
* @param: claimId: string
|
||||
* @param: parentUuid: string (optional)
|
||||
*/
|
||||
createRecsysEntry: function (claimId, parentUuid) {
|
||||
if (window.store && claimId) {
|
||||
const state = window.store.getState();
|
||||
const { id: userId } = selectUser(state);
|
||||
if (parentUuid) {
|
||||
// Make a stub entry that will be filled out on page load
|
||||
recsys.entries[claimId] = {
|
||||
uuid: Uuidv4(),
|
||||
parentUuid: parentUuid,
|
||||
uid: userId || null, // selectUser
|
||||
claimId: claimId,
|
||||
recClickedVideoIdx: [],
|
||||
pageLoadedAt: Date.now(),
|
||||
events: [],
|
||||
};
|
||||
} else {
|
||||
recsys.entries[claimId] = {
|
||||
uuid: Uuidv4(),
|
||||
uid: userId, // selectUser
|
||||
claimId: claimId,
|
||||
pageLoadedAt: Date.now(),
|
||||
recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId,
|
||||
recClaimIds: [],
|
||||
recClickedVideoIdx: [],
|
||||
events: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
recsys.log('createRecsysEntry', claimId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send event for claimId
|
||||
* @param claimId
|
||||
* @param isTentative
|
||||
*/
|
||||
sendRecsysEntry: function (claimId, isTentative) {
|
||||
const shareTelemetry =
|
||||
IS_WEB || (window && window.store && selectDaemonSettings(window.store.getState()).share_usage_data);
|
||||
|
||||
if (recsys.entries[claimId] && shareTelemetry) {
|
||||
const data = JSON.stringify(recsys.entries[claimId]);
|
||||
try {
|
||||
navigator.sendBeacon(recsysEndpoint, data);
|
||||
if (!isTentative) {
|
||||
delete recsys.entries[claimId];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('no beacon for you', error);
|
||||
}
|
||||
}
|
||||
recsys.log('sendRecsysEntry', claimId);
|
||||
},
|
||||
|
||||
/**
|
||||
* A player event fired. Get the Entry for the claimId, and add the events
|
||||
* @param claimId
|
||||
* @param event
|
||||
*/
|
||||
onRecsysPlayerEvent: function (claimId, event, isEmbedded) {
|
||||
if (!recsys.entries[claimId]) {
|
||||
recsys.createRecsysEntry(claimId);
|
||||
// do something to show it's floating or autoplay
|
||||
}
|
||||
if (isEmbedded) {
|
||||
recsys.entries[claimId]['isEmbed'] = true;
|
||||
}
|
||||
recsys.entries[claimId].events.push(event);
|
||||
recsys.log('onRecsysPlayerEvent', claimId);
|
||||
},
|
||||
log: function (callName, claimId) {
|
||||
if (recsys.debug) {
|
||||
console.log(`Call: ***${callName}***, ClaimId: ${claimId}, Recsys Entries`, Object.assign({}, recsys.entries));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Player closed. Check to see if primaryUri = playingUri
|
||||
* if so, send the Entry.
|
||||
*/
|
||||
onPlayerDispose: function (claimId, isEmbedded) {
|
||||
if (window.store) {
|
||||
const state = window.store.getState();
|
||||
const playingUri = selectPlayingUri(state);
|
||||
const primaryUri = selectPrimaryUri(state);
|
||||
const onFilePage = playingUri === primaryUri;
|
||||
if (!onFilePage || isEmbedded) {
|
||||
if (isEmbedded) {
|
||||
recsys.entries[claimId]['isEmbed'] = true;
|
||||
}
|
||||
recsys.sendRecsysEntry(claimId);
|
||||
}
|
||||
}
|
||||
recsys.log('PlayerDispose', claimId);
|
||||
},
|
||||
|
||||
// /**
|
||||
// * File page unmount or change event
|
||||
// * Check to see if playingUri, floatingEnabled, primaryUri === playingUri
|
||||
// * If not, send the Entry.
|
||||
// * If floating enabled, leaving file page will pop out player, leading to
|
||||
// * more events until player is disposed. Don't send unless floatingPlayer playingUri
|
||||
// */
|
||||
// onLeaveFilePage: function (primaryUri) {
|
||||
// if (window.store) {
|
||||
// const state = window.store.getState();
|
||||
// const claim = makeSelectClaimForUri(primaryUri)(state);
|
||||
// const claimId = claim ? claim.claim_id : null;
|
||||
// const playingUri = selectPlayingUri(state);
|
||||
// const actualPlayingUri = playingUri && playingUri.uri;
|
||||
// // const primaryUri = selectPrimaryUri(state);
|
||||
// const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state);
|
||||
// // When leaving page, if floating player is enabled, play will continue.
|
||||
// if (claimId) {
|
||||
// recsys.entries[claimId]['pageExitedAt'] = Date.now();
|
||||
// }
|
||||
// const shouldSend =
|
||||
// (claimId && floatingPlayer && actualPlayingUri && actualPlayingUri !== primaryUri) || !floatingPlayer || !actualPlayingUri;
|
||||
// if (shouldSend) {
|
||||
// recsys.sendRecsysEntry(claimId);
|
||||
// }
|
||||
// recsys.log('LeaveFile', claimId);
|
||||
// }
|
||||
// },
|
||||
|
||||
/**
|
||||
* Navigate event
|
||||
* Send all claimIds that aren't currently playing.
|
||||
*/
|
||||
onNavigate: function () {
|
||||
if (window.store) {
|
||||
const state = window.store.getState();
|
||||
const playingUri = selectPlayingUri(state);
|
||||
const actualPlayingUri = playingUri && playingUri.uri;
|
||||
const claim = makeSelectClaimForUri(actualPlayingUri)(state);
|
||||
const playingClaimId = claim ? claim.claim_id : null;
|
||||
// const primaryUri = selectPrimaryUri(state);
|
||||
const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state);
|
||||
// When leaving page, if floating player is enabled, play will continue.
|
||||
Object.keys(recsys.entries).forEach((claimId) => {
|
||||
const shouldSkip = recsys.entries[claimId].parentUuid && !recsys.entries[claimId].recClaimIds;
|
||||
if (!shouldSkip && ((claimId !== playingClaimId && floatingPlayer) || !floatingPlayer)) {
|
||||
recsys.entries[claimId]['pageExitedAt'] = Date.now();
|
||||
recsys.sendRecsysEntry(claimId);
|
||||
}
|
||||
recsys.log('OnNavigate', claimId);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
// @if TARGET='web'
|
||||
document.addEventListener('visibilitychange', function logData() {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
Object.keys(recsys.entries).map((claimId) => recsys.sendRecsysEntry(claimId, true));
|
||||
}
|
||||
});
|
||||
// @endif
|
||||
|
||||
history.listen(() => {
|
||||
recsys.onNavigate();
|
||||
});
|
||||
|
||||
export default recsys;
|
|
@ -116,43 +116,6 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
|
|||
|
||||
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [] });
|
||||
|
||||
reducers[ACTIONS.RECOMMENDATION_UPDATED] = (state, action) => {
|
||||
const { claimId, urls, id, parentId } = action.data;
|
||||
const recommendationId = Object.assign({}, state.recommendationId);
|
||||
const recommendationParentId = Object.assign({}, state.recommendationParentId);
|
||||
const recommendationUrls = Object.assign({}, state.recommendationUrls);
|
||||
const recommendationClicks = Object.assign({}, state.recommendationClicks);
|
||||
|
||||
if (urls && urls.length > 0) {
|
||||
recommendationId[claimId] = id;
|
||||
recommendationParentId[claimId] = parentId;
|
||||
recommendationUrls[claimId] = urls;
|
||||
recommendationClicks[claimId] = [];
|
||||
} else {
|
||||
delete recommendationId[claimId];
|
||||
delete recommendationParentId[claimId];
|
||||
delete recommendationUrls[claimId];
|
||||
delete recommendationClicks[claimId];
|
||||
}
|
||||
|
||||
return { ...state, recommendationId, recommendationParentId, recommendationUrls, recommendationClicks };
|
||||
};
|
||||
|
||||
reducers[ACTIONS.RECOMMENDATION_CLICKED] = (state, action) => {
|
||||
const { claimId, index } = action.data;
|
||||
const recommendationClicks = Object.assign({}, state.recommendationClicks);
|
||||
|
||||
if (state.recommendationUrls[claimId] && index >= 0 && index < state.recommendationUrls[claimId].length) {
|
||||
if (recommendationClicks[claimId]) {
|
||||
recommendationClicks[claimId].push(index);
|
||||
} else {
|
||||
recommendationClicks[claimId] = [index];
|
||||
}
|
||||
}
|
||||
|
||||
return { ...state, recommendationClicks };
|
||||
};
|
||||
|
||||
// reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
||||
// return {
|
||||
// ...state,
|
||||
|
|
|
@ -3,7 +3,6 @@ import { createSelector } from 'reselect';
|
|||
import {
|
||||
makeSelectClaimForUri,
|
||||
selectClaimsByUri,
|
||||
makeSelectClaimsInChannelForCurrentPageState,
|
||||
makeSelectClaimIsNsfw,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectMediaTypeForUri,
|
||||
|
@ -11,7 +10,6 @@ import {
|
|||
parseURI,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectFileNameForUri,
|
||||
selectClaimIdsByUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
|
@ -155,18 +153,6 @@ export const selectRecentHistory = createSelector(selectHistory, (history) => {
|
|||
return history.slice(0, RECENT_HISTORY_AMOUNT);
|
||||
});
|
||||
|
||||
export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string) =>
|
||||
createSelector(makeSelectClaimsInChannelForCurrentPageState(channel), (channelClaims) => {
|
||||
if (uris) return uris;
|
||||
|
||||
if (channelClaims) {
|
||||
const CATEGORY_LIST_SIZE = 10;
|
||||
return channelClaims.slice(0, CATEGORY_LIST_SIZE).map(({ name, claim_id: claimId }) => `${name}#${claimId}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export const makeSelectShouldObscurePreview = (uri: string) =>
|
||||
createSelector(selectShowMatureContent, makeSelectClaimIsNsfw(uri), (showMatureContent, isClaimMature) => {
|
||||
return isClaimMature && !showMatureContent;
|
||||
|
@ -247,21 +233,3 @@ export const makeSelectInsufficientCreditsForUri = (uri: string) =>
|
|||
return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance;
|
||||
}
|
||||
);
|
||||
|
||||
export const makeSelectRecommendationId = (claimId: string) =>
|
||||
createSelector(selectState, (state) => state.recommendationId[claimId]);
|
||||
|
||||
export const makeSelectRecommendationParentId = (claimId: string) =>
|
||||
createSelector(selectState, (state) => state.recommendationParentId[claimId]);
|
||||
|
||||
export const makeSelectRecommendedClaimIds = (claimId: string) =>
|
||||
createSelector(selectState, selectClaimIdsByUri, (state, claimIdsByUri) => {
|
||||
const recommendationUrls = state.recommendationUrls[claimId];
|
||||
if (recommendationUrls) {
|
||||
return recommendationUrls.map((url) => claimIdsByUri[url]);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export const makeSelectRecommendationClicks = (claimId: string) =>
|
||||
createSelector(selectState, (state) => state.recommendationClicks[claimId]);
|
||||
|
|
|
@ -98,7 +98,7 @@ export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
|||
createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => {
|
||||
// TODO: DRY this out.
|
||||
let poweredBy;
|
||||
if (claim) {
|
||||
if (claim && claimId) {
|
||||
const isMature = isClaimNsfw(claim);
|
||||
const { title } = claim.value;
|
||||
|
||||
|
@ -204,3 +204,8 @@ export const makeSelectIsResolvingWinningUri = (query: string = '') => {
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const makeSelectUrlForClaimId = (claimId: string) =>
|
||||
createSelector(
|
||||
makeSelectClaimForClaimId(claimId), (claim) => claim ? claim.canonical_url || claim.permanent_url : null
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue