diff --git a/src/backend/common/infrastructure/Atomic.ts b/src/backend/common/infrastructure/Atomic.ts index a819ab85..d51c5899 100644 --- a/src/backend/common/infrastructure/Atomic.ts +++ b/src/backend/common/infrastructure/Atomic.ts @@ -184,18 +184,6 @@ export type ExpressRequest = Request>; export type ExpressHandler = (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => Promise>> -export interface numberFormatOptions { - toFixed: number, - defaultVal?: any, - prefix?: string, - suffix?: string, - round?: { - type?: string, - enable: boolean, - indicate?: boolean, - } -} - export const DELIMITERS = [',','&','/','\\']; export const DELIMITERS_NO_AMP = [',','/','\\']; export const DELIMETERS_REGEX: RegExp = new RegExp(/[,&\/\\]/); diff --git a/src/backend/sources/AbstractSource.ts b/src/backend/sources/AbstractSource.ts index 2bfbbdfd..f0ef0a0a 100644 --- a/src/backend/sources/AbstractSource.ts +++ b/src/backend/sources/AbstractSource.ts @@ -24,7 +24,6 @@ import { TRANSFORM_HOOK } from "../common/infrastructure/Transform.js"; import TupleMap from "../common/TupleMap.js"; import { difference, - formatNumber, genGroupId, isDebugMode, playObjDataMatch, @@ -33,6 +32,7 @@ import { sortByNewestPlayDate, sortByOldestPlayDate, } from "../utils.js"; +import { formatNumber } from '../../core/DataUtils.js'; import { timeToHumanTimestamp } from "../utils/TimeUtils.js"; import { todayAwareFormat } from "../../core/TimeUtils.js"; import { getRoot } from '../ioc.js'; diff --git a/src/backend/sources/MemorySource.ts b/src/backend/sources/MemorySource.ts index 26b650b2..49deda64 100644 --- a/src/backend/sources/MemorySource.ts +++ b/src/backend/sources/MemorySource.ts @@ -13,7 +13,6 @@ import { import { SourceType, SourceConfig } from '../common/infrastructure/config/source/sources.js'; import { PollingOptions } from "../common/infrastructure/config/common.js"; import { - formatNumber, genGroupId, genGroupIdStr, getPlatformIdFromData, @@ -21,6 +20,7 @@ import { playObjDataMatch, thresholdResultSummary, } from "../utils.js"; +import { formatNumber } from '../../core/DataUtils.js'; import { timePassesScrobbleThreshold, timeToHumanTimestamp } from "../utils/TimeUtils.js"; import { PromisePool } from "@supercharge/promise-pool"; import AbstractSource from "./AbstractSource.js"; diff --git a/src/backend/sources/PlayerState/AbstractPlayerState.ts b/src/backend/sources/PlayerState/AbstractPlayerState.ts index ae75c651..eaaa424d 100644 --- a/src/backend/sources/PlayerState/AbstractPlayerState.ts +++ b/src/backend/sources/PlayerState/AbstractPlayerState.ts @@ -13,7 +13,8 @@ import { ReportedPlayerStatus, } from "../../common/infrastructure/Atomic.js"; import { PollingOptions } from "../../common/infrastructure/config/common.js"; -import { formatNumber, genGroupIdStr, playObjDataMatch, progressBar } from "../../utils.js"; +import { genGroupIdStr, playObjDataMatch, progressBar } from "../../utils.js"; +import { formatNumber } from '../../../core/DataUtils.js'; import { ListenProgress } from "./ListenProgress.js"; import { ListenRange, ListenRangePositional } from "./ListenRange.js"; import { closeToPlayEnd, closeToPlayStart, repeatDurationPlayed, timeToHumanTimestamp } from "../../utils/TimeUtils.js"; diff --git a/src/backend/sources/YTMusicSource.ts b/src/backend/sources/YTMusicSource.ts index c188f9a8..1fed9eda 100644 --- a/src/backend/sources/YTMusicSource.ts +++ b/src/backend/sources/YTMusicSource.ts @@ -6,7 +6,8 @@ import { YTMusicSourceConfig } from "../common/infrastructure/config/source/ytmu import { Innertube, UniversalCache, Parser, YTNodes, ApiResponse, IBrowseResponse, Log, SessionOptions } from 'youtubei.js'; import { GenerateAuthUrlOpts, OAuth2Client } from 'google-auth-library'; import {resolve} from 'path'; -import { formatNumber, isDebugMode, parseBool, sleep } from "../utils.js"; +import { isDebugMode, parseBool, sleep } from "../utils.js"; +import { formatNumber } from '../../core/DataUtils.js'; import { getPlaysDiff, humanReadableDiff, diff --git a/src/backend/utils.ts b/src/backend/utils.ts index 0f216c8a..46631bf3 100644 --- a/src/backend/utils.ts +++ b/src/backend/utils.ts @@ -11,7 +11,6 @@ import { asPlayerStateDataMaybePlay, NO_DEVICE, NO_USER, - numberFormatOptions, PlayerStateDataMaybePlay, PlayPlatformId, ProgressAwarePlayObject, @@ -436,45 +435,6 @@ export const progressBar = (value: number, maxValue: number, size: number) => { return bar; }; -export const formatNumber = (val: number | string, options?: numberFormatOptions) => { - const { - toFixed = 2, - defaultVal = null, - prefix = '', - suffix = '', - round, - } = options || {}; - let parsedVal = typeof val === 'number' ? val : Number.parseFloat(val); - if (Number.isNaN(parsedVal)) { - return defaultVal; - } - if(!Number.isFinite(val)) { - return 'Infinite'; - } - let prefixStr = prefix; - const {enable = false, indicate = true, type = 'round'} = round || {}; - if (enable && !Number.isInteger(parsedVal)) { - switch (type) { - case 'round': - parsedVal = Math.round(parsedVal); - break; - case 'ceil': - parsedVal = Math.ceil(parsedVal); - break; - case 'floor': - parsedVal = Math.floor(parsedVal); - } - if (indicate) { - prefixStr = `~${prefix}`; - } - } - const localeString = parsedVal.toLocaleString(undefined, { - minimumFractionDigits: toFixed, - maximumFractionDigits: toFixed, - }); - return `${prefixStr}${localeString}${suffix}`; -}; - export const durationToNormalizedTime = (dur: Duration): { hours: number, minutes: number, seconds: number } => { const totalSeconds = dur.asSeconds(); diff --git a/src/backend/utils/RequestUtils.ts b/src/backend/utils/RequestUtils.ts index 8ee0a3d9..0dbe5bab 100644 --- a/src/backend/utils/RequestUtils.ts +++ b/src/backend/utils/RequestUtils.ts @@ -11,7 +11,7 @@ import { loggerNoop } from "../common/logging.js"; import { findCauseByFunc } from "./ErrorUtils.js"; import { isSuperAgentResponseError } from "../common/errors/ErrorUtils.js"; import { isNodeNetworkException, NodeNetworkException } from "../common/errors/NodeErrors.js"; -import { formatNumber } from "../utils.js"; +import { formatNumber } from '../../core/DataUtils.js'; import { UpstreamError } from "../common/errors/UpstreamError.js"; // typings from Formidable are all nuts. diff --git a/src/backend/utils/TimeUtils.ts b/src/backend/utils/TimeUtils.ts index 6ce4c604..b602a839 100644 --- a/src/backend/utils/TimeUtils.ts +++ b/src/backend/utils/TimeUtils.ts @@ -29,7 +29,7 @@ import { ScrobbleThresholdResult, } from "../common/infrastructure/Atomic.js"; import { ScrobbleThresholds } from "../common/infrastructure/config/source/index.js"; -import { formatNumber } from "../utils.js"; +import { formatNumber } from '../../core/DataUtils.js'; //dayjs.extend(isToday); diff --git a/src/client/components/ActivityTimeline.tsx b/src/client/components/ActivityTimeline.tsx index 608daef3..633636c5 100644 --- a/src/client/components/ActivityTimeline.tsx +++ b/src/client/components/ActivityTimeline.tsx @@ -1,6 +1,6 @@ import { ComponentProps } from "react" import { Accordion, Timeline, Icon, Span, Stack, Heading, Card, Box } from '@chakra-ui/react'; -import { ErrorLike, JsonPlayObject, LifecycleStep, PlayActivity } from "../../core/Atomic"; +import { ErrorLike, JsonPlayObject, PlayActivity } from "../../core/Atomic"; import { PlayData } from "./PlayData"; import { ErrorAlert } from "./ErrorAlert"; import { IoMdCodeDownload } from "react-icons/io"; @@ -11,8 +11,8 @@ import { capitalize } from "../../core/StringUtils"; import { shortTodayAwareFormat, todayAwareFormat } from "../../core/TimeUtils"; import dayjs from "dayjs"; import { ChakraCodeBlockShort, ChakraPlainBlockShort } from "./CodeBlock"; -import { JsonDiffPatch } from "./JsonDiff"; -import { jdiff } from "../../core/DataUtils"; +import { TransformSteps } from "./TransformSteps"; +import { ScrobbleMatchResult } from "./ScrobbleMatchResult"; export interface ActivityDetailProps { @@ -32,7 +32,10 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { lifecycle: { input, original, - steps = [] + steps = [], + scrobble: { + match, + } = {} }, } = {} } = play; @@ -88,11 +91,24 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { Transforms - {/* - - - - */} + + + ) : null} + {match !== undefined ? ( + + + + + + + + + + + + Duplicate Match Check + + ) : null} @@ -112,100 +128,6 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { test */} - - - {/* - - - - - - - - - - - - - */} - - ) -} - -export interface LifeycleStepsTimelineProps { - steps: LifecycleStep[] - original: JsonPlayObject -} - -const TransformSteps = (props: LifeycleStepsTimelineProps) => { - const { - steps, - original - } = props; - - let currentPlay: JsonPlayObject | false = JSON.parse(JSON.stringify(original)); - - return ( - - {steps.map((x, index) => { - let err: Error; - const left = JSON.parse(JSON.stringify(currentPlay)); - if (currentPlay !== false) { - try { - currentPlay = jdiff.patch(currentPlay, x.patch) as JsonPlayObject; - } catch (e) { - err = new Error('Could not patch Play object', { cause: e }); - currentPlay = false; - } - } - return - - - - - - - - - - - {x.name} with {x.source} - - Diff - {err !== undefined ? : null} - - {left !== false && currentPlay !== false ? ( - - - - ) : } - Inputs - - {x.inputs.map((y) => { - return - })} - - - - })} - {currentPlay !== false ? ( - - - - - - - - - - - - Final Playafter all Transforms - - - - - ) : null} ) } \ No newline at end of file diff --git a/src/client/components/CodeBlock.tsx b/src/client/components/CodeBlock.tsx index 37c20b8a..000869a2 100644 --- a/src/client/components/CodeBlock.tsx +++ b/src/client/components/CodeBlock.tsx @@ -3,6 +3,7 @@ import { createShikiAdapter, CodeBlock, IconButton, ClientOnly, ScrollArea } fro import { useColorMode } from "./Color-Mode"; import { ComponentProps, PropsWithChildren, useMemo } from "react"; import { safeStringify } from "../../core/StringUtils"; +import { MarkOptional } from "ts-essentials"; const shikiAdapter = createShikiAdapter>({ async load() { @@ -101,7 +102,7 @@ export const ChakraCodeBlock = (props: ChakraCodeBlockProps) => { export const ChakraCodeBlockShort = (props: ChakraCodeBlockProps) => ; -export type ChakraPlainBlockProps = ChakraCodelessBlock & ChakraCodeBaseProps & {code?: string | object}; +export type ChakraPlainBlockProps = MarkOptional & ChakraCodeBaseProps & {code?: string | object}; export const ChakraPlainBlock = (props: ChakraPlainBlockProps) => { const { @@ -124,26 +125,37 @@ export const ChakraPlainBlock = (props: ChakraPlainBlockProps) => { return safeStringify(code); }, [code]); - return ( - + let header: JSX.Element | null; + if(props.title === undefined && maxLines === undefined) { + header = null; + } else { + header = ( - {props.title ?? ' '} + {props.title} - + {maxLines !== undefined ? ( + + ) : null} + ); + } + + return ( + + {header} - {props.children} + {props.children ?? } diff --git a/src/client/components/ScrobbleMatchResult.tsx b/src/client/components/ScrobbleMatchResult.tsx new file mode 100644 index 00000000..3dc8d95a --- /dev/null +++ b/src/client/components/ScrobbleMatchResult.tsx @@ -0,0 +1,59 @@ +import { ComponentProps } from "react" +import { Timeline, Icon, Span, Stack, Heading, Tabs, DataList } from '@chakra-ui/react'; +import { JsonPlayObject, LifecycleStep, PlayMatchResult } from "../../core/Atomic"; +import { PlayData } from "./PlayData"; +import { ErrorAlert } from "./ErrorAlert"; +import { BiWrench } from "react-icons/bi"; +import { IoMusicalNoteOutline } from "react-icons/io5"; +import { LuCheck, LuCircleX, LuX } from "react-icons/lu"; +import { ChakraCodeBlockShort, ChakraPlainBlock, ChakraPlainBlockShort } from "./CodeBlock"; +import { JsonDiffPatch } from "./JsonDiff"; +import { formatNumber, jdiff } from "../../core/DataUtils"; + +export interface ScrobbleMatchResultProps { + match: PlayMatchResult +} + +export const ScrobbleMatchResult = (props: ScrobbleMatchResultProps) => { + + const { + match: { + closestMatchedPlay + } = {}, + match + } = props; + + return ( + + + Results + Closest Match + + + + + Matched? + + {match.match ? : } + + + + Score + {formatNumber(match.score)} + + + Reason + {match.reason} + + + Breakdown + + + + + + + + + ) +} \ No newline at end of file diff --git a/src/client/components/TransformSteps.tsx b/src/client/components/TransformSteps.tsx new file mode 100644 index 00000000..50299748 --- /dev/null +++ b/src/client/components/TransformSteps.tsx @@ -0,0 +1,89 @@ +import { ComponentProps } from "react" +import { Timeline, Icon, Span, Stack, Heading } from '@chakra-ui/react'; +import { JsonPlayObject, LifecycleStep } from "../../core/Atomic"; +import { PlayData } from "./PlayData"; +import { ErrorAlert } from "./ErrorAlert"; +import { BiWrench } from "react-icons/bi"; +import { IoMusicalNoteOutline } from "react-icons/io5"; +import { ChakraCodeBlockShort, ChakraPlainBlockShort } from "./CodeBlock"; +import { JsonDiffPatch } from "./JsonDiff"; +import { jdiff } from "../../core/DataUtils"; + + +export interface LifeycleStepsTimelineProps { + steps: LifecycleStep[] + original: JsonPlayObject +} + +export const TransformSteps = (props: LifeycleStepsTimelineProps) => { + const { + steps, + original + } = props; + + let currentPlay: JsonPlayObject | false = JSON.parse(JSON.stringify(original)); + + return ( + + {steps.map((x, index) => { + let err: Error; + const left = JSON.parse(JSON.stringify(currentPlay)); + if (currentPlay !== false) { + try { + currentPlay = jdiff.patch(currentPlay, x.patch) as JsonPlayObject; + } catch (e) { + err = new Error('Could not patch Play object', { cause: e }); + currentPlay = false; + } + } + return + + + + + + + + + + + {x.name} with {x.source} + + Diff + {err !== undefined ? : null} + + {left !== false && currentPlay !== false ? ( + + + + ) : } + Inputs + + {x.inputs.map((y) => { + return + })} + + + + })} + {currentPlay !== false ? ( + + + + + + + + + + + + Final Playafter all Transforms + + + + + ) : null} + + ) +} \ No newline at end of file diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index f4081572..63dfe0c0 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -579,4 +579,15 @@ export type UnixTimestamp = number; export type Writeable = { -readonly [P in keyof T]: T[P] }; export const SHORT_CALENDAR_NOTZ_FORMAT = 'MMM D HH:mm:ss'; -export const SHORT_TODAY_NOTZ_FORMAT = 'HH:mm:ss'; \ No newline at end of file +export const SHORT_TODAY_NOTZ_FORMAT = 'HH:mm:ss'; +export interface numberFormatOptions { + toFixed: number; + defaultVal?: any; + prefix?: string; + suffix?: string; + round?: { + type?: string; + enable: boolean; + indicate?: boolean; + }; +} diff --git a/src/core/DataUtils.ts b/src/core/DataUtils.ts index 1c4b0bbe..c901529c 100644 --- a/src/core/DataUtils.ts +++ b/src/core/DataUtils.ts @@ -1,4 +1,5 @@ import { create as diffCreate } from "jsondiffpatch"; +import { numberFormatOptions } from './Atomic.js'; export const jdiff = diffCreate({ @@ -7,4 +8,38 @@ export const jdiff = diffCreate({ }, cloneDiffValues: true //omitRemovedValues: true -}); +});export const formatNumber = (val: number | string, options?: numberFormatOptions) => { + const { + toFixed = 2, defaultVal = null, prefix = '', suffix = '', round, + } = options || {}; + let parsedVal = typeof val === 'number' ? val : Number.parseFloat(val); + if (Number.isNaN(parsedVal)) { + return defaultVal; + } + if (!Number.isFinite(val)) { + return 'Infinite'; + } + let prefixStr = prefix; + const { enable = false, indicate = true, type = 'round' } = round || {}; + if (enable && !Number.isInteger(parsedVal)) { + switch (type) { + case 'round': + parsedVal = Math.round(parsedVal); + break; + case 'ceil': + parsedVal = Math.ceil(parsedVal); + break; + case 'floor': + parsedVal = Math.floor(parsedVal); + } + if (indicate) { + prefixStr = `~${prefix}`; + } + } + const localeString = parsedVal.toLocaleString(undefined, { + minimumFractionDigits: toFixed, + maximumFractionDigits: toFixed, + }); + return `${prefixStr}${localeString}${suffix}`; +}; +