diff --git a/src/backend/common/database/drizzle/schema/schema.ts b/src/backend/common/database/drizzle/schema/schema.ts
index 240bc270..4e8a1509 100644
--- a/src/backend/common/database/drizzle/schema/schema.ts
+++ b/src/backend/common/database/drizzle/schema/schema.ts
@@ -7,6 +7,7 @@ import { asPlayCheap } from "../../../../../core/PlayMarshalUtils.js";
import { ExternalMetadataTerm, PlayTransformPartsConfig, SearchAndReplaceTerm } from "../../../infrastructure/Transform.js";
import { JobRangeCount, JobRangeTime } from "../../../infrastructure/Job.js";
import { serializeError, deserializeError } from "serialize-error";
+import { generatePlayUid } from "../../../../../core/StringUtils.js";
const DayjsTimestamp = customType<
{
@@ -67,7 +68,7 @@ const ErrorLikeJson = customType<
export const plays = sqliteTable("plays", {
id: integer().primaryKey(),
- uid: text({ length: 30 }).notNull().unique().$defaultFn(() => nanoid(20)),
+ uid: text({ length: 30 }).notNull().unique().$defaultFn(() => generatePlayUid()),
componentId: integer().references(() => components.id, {onDelete: 'cascade', onUpdate: 'cascade'}),
error: ErrorLikeJson('error'),
playedAt: DayjsTimestamp('playedAt'),
diff --git a/src/client/components/ActivityDetail.tsx b/src/client/components/ActivityDetail.tsx
index 88ecb7ca..4df590e5 100644
--- a/src/client/components/ActivityDetail.tsx
+++ b/src/client/components/ActivityDetail.tsx
@@ -6,9 +6,10 @@ import { ErrorAlert } from "./ErrorAlert";
import { AiOutlineExclamationCircle } from "react-icons/ai";
import { ActivityTimeline } from "./ActivityTimeline";
import { ExpandCollapse } from "./ExpandCollapse";
+import { PlayApiCommon, PlayApiCommonDetailed } from "../../core/Api";
export interface ActivityDetailProps {
- activity: PlayActivity
+ activity: PlayApiCommonDetailed
}
export const ActivityDetails = (props: ActivityDetailProps) => {
@@ -16,15 +17,9 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
activity,
activity: {
error,
- play: {
- meta: {
- lifecycle: {
- original,
- scrobble
- } = {},
- lifecycle
- } = {},
- } = {}
+ input: {
+ play: original,
+ }
}
} = props;
@@ -69,7 +64,7 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
-
+
diff --git a/src/client/components/ActivityTimeline.tsx b/src/client/components/ActivityTimeline.tsx
index 5e64f56a..f59bb638 100644
--- a/src/client/components/ActivityTimeline.tsx
+++ b/src/client/components/ActivityTimeline.tsx
@@ -19,16 +19,20 @@ import { ExpandCollapse } from "./ExpandCollapse";
import { MSCollapsible } from "./MSCollapsible";
import { TimelineErrorIcon } from "./timeline/TimelineIcon";
import { Muted } from "./Typography";
+import { PlayApiCommonDetailed } from "../../core/Api";
export interface ActivityDetailProps {
- play: JsonPlayObject
+ activity: PlayApiCommonDetailed
collapsibleOpen?: boolean
}
export const ActivityTimeline = (props: ActivityDetailProps) => {
const {
- play,
+ activity:{
+ play,
+ input
+ } = {},
collapsibleOpen
} = props;
const {
@@ -38,8 +42,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
meta: {
source,
lifecycle: {
- input,
- original,
+ // input,
+ // original,
steps = [],
scrobble: {
match,
@@ -51,6 +55,10 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
},
} = {}
} = play;
+ const {
+ play: original,
+ data: ogInput
+ } = input || {};
let scrobbleSummary: React.JSX.Element,
scrobbleIconProps: Record = {
@@ -98,7 +106,7 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
-
+
diff --git a/src/client/components/playActivity/PlayList.tsx b/src/client/components/playActivity/PlayList.tsx
index 7b7a2c14..1f23bcd5 100644
--- a/src/client/components/playActivity/PlayList.tsx
+++ b/src/client/components/playActivity/PlayList.tsx
@@ -12,11 +12,12 @@ import { GroupedVirtuoso } from 'react-virtuoso'
import { ActivityDetails } from '../ActivityDetail.js';
import { sortByNewestPlayDate, sortByNewestSeenDate } from '../../../core/PlayUtils.js';
import "./PlayList.scss";
+import { PlayApiCommon } from '../../../core/Api.js';
dayjs.extend(doy);
export interface ActivityLogProps {
- data: PlayActivity[]
+ data: PlayApiCommon[]
sortBy?: 'played' | 'seen'
render?: 'virtCollapse' | 'virtAccordian' | 'accordian'
}
@@ -27,11 +28,11 @@ interface GroupInfo {
}
interface GroupData {
- plays: PlayActivity[]
+ plays: PlayApiCommon[]
date: Dayjs
}
-const generateGroupInfo = (data: PlayActivity[]): GroupInfo[] => {
+const generateGroupInfo = (data: PlayApiCommon[]): GroupInfo[] => {
const groupsReduced = data.reduce((acc: { groups: GroupInfo[], active?: GroupInfo }, curr, index) => {
const date = dayjs(curr.play.data.playDate);
@@ -48,7 +49,7 @@ const generateGroupInfo = (data: PlayActivity[]): GroupInfo[] => {
return groupsReduced.groups.concat(groupsReduced.active);
}
-const generateGroupPlays = (data: PlayActivity[]): GroupData[] => {
+const generateGroupPlays = (data: PlayApiCommon[]): GroupData[] => {
const groupsReduced = data.reduce((acc: { groups: GroupData[], active?: GroupData }, curr, index) => {
const date = dayjs(curr.play.data.playDate);
@@ -91,7 +92,7 @@ export const PlayList = (props: ActivityLogProps) => {
}
}
-const VirtualizedCollapse = (props: { data: PlayActivity[] }) => {
+const VirtualizedCollapse = (props: { data: PlayApiCommon[] }) => {
const {
data,
} = props;
@@ -180,7 +181,7 @@ const VirtualizedCollapse = (props: { data: PlayActivity[] }) => {
paddingInline: "var(--chakra-spacing-4)"
}} justify="flex-start" alignItems="flex-end">
- {activity.status === 'error' ?
+ {activity.state === 'failed' ?
: null}
@@ -200,7 +201,7 @@ const VirtualizedCollapse = (props: { data: PlayActivity[] }) => {
);
}
-const VirtualizedAccordian = (props: { data: PlayActivity[] }) => {
+const VirtualizedAccordian = (props: { data: PlayApiCommon[] }) => {
const {
data,
} = props;
@@ -290,7 +291,7 @@ const VirtualizedAccordian = (props: { data: PlayActivity[] }) => {
);
}
-const PlainAccordian = (props: { data: PlayActivity[], sortBy: 'played' | 'seen' }) => {
+const PlainAccordian = (props: { data: PlayApiCommon[], sortBy: 'played' | 'seen' }) => {
const {
data = [],
sortBy
@@ -343,7 +344,7 @@ const PlainAccordian = (props: { data: PlayActivity[], sortBy: 'played' | 'seen'
paddingInline: "var(--accordion-padding-x)"
}} justify="flex-start" alignItems="flex-end">
- {activity.status === 'error' ?
+ {activity.state === 'failed' ?
: null}
@@ -365,23 +366,30 @@ const PlainAccordian = (props: { data: PlayActivity[], sortBy: 'played' | 'seen'
);
}
-const StatusBadge = (props: ComponentProps & { data: PlayActivity }) => {
+const StatusBadge = (props: ComponentProps & { data: PlayApiCommon }) => {
const { data, ...rest } = props;
let badgeColor = undefined,
- badgeText = capitalize(data.status);
+ badgeText = capitalize(data.state);
- switch (data.status) {
+ switch (data.state) {
case 'queued':
badgeColor = 'gray';
break;
case 'scrobbled':
+ case 'discovered':
badgeColor = 'green';
break;
- case 'error':
+ case 'failed':
badgeColor = 'red';
break;
+ case 'discarded':
+ badgeColor = 'grey';
+ break;
+ case 'duped':
+ badgeColor = 'orange';
+ break;
}
return {badgeText}
diff --git a/src/core/Api.ts b/src/core/Api.ts
index b7353789..6f395d45 100644
--- a/src/core/Api.ts
+++ b/src/core/Api.ts
@@ -1,26 +1,26 @@
import { ErrorLike, JsonPlayObject, PlayState } from "./Atomic.js"
-export type PlayApiCommon = {
+export interface PlayApiCommon {
uid: string
componentId: number
state: PlayState
play: JsonPlayObject
compacted: boolean
playedAt: string
- seentAt: string
+ seenAt: string
updatedAt: string
parentUid?: string
// TODO add parent source type/name?
}
-export type PlayInputApi = {
+export interface PlayInputApi {
id: number
- data: object
- play: JsonPlayObject
+ data?: object
+ play?: JsonPlayObject
createdAt: string
}
-export type QueueStateApi = {
+export interface QueueStateApi {
id: number
queueName: string
queueState: string
@@ -30,8 +30,8 @@ export type QueueStateApi = {
updatedAt: string
}
-export type PlayApiCommonDetailed = PlayApiCommon & {
- error: ErrorLike
- input: PlayInputApi
+export interface PlayApiCommonDetailed extends PlayApiCommon {
+ error?: ErrorLike
+ input?: PlayInputApi
queueStates: QueueStateApi[]
}
\ No newline at end of file
diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts
index 0e8f96d6..67aef9c3 100644
--- a/src/core/Atomic.ts
+++ b/src/core/Atomic.ts
@@ -366,7 +366,7 @@ export interface AmbPlayObject {
id?: number
uid?: string
data: PlayData,
- meta: PlayMetaLifecycleless
+ meta: PlayMetaLifecycleless
original?: PlayOriginal
}
@@ -614,8 +614,10 @@ export const REGEX_ISO8601_LOOSE = new RegExp(/\d{4}-[01]\d-[0-3]\dT/);
*/
export const REGEX_ISO8601_WELLKNOWN = new RegExp(/dayjs-(\d{4}-[01]\d-[0-3]\dT.*)/);
-export const CLIENT_INGRESS_QUEUE = 'ingress';
-export const CLIENT_DEAD_QUEUE = 'dead';
+export const CLIENT_INGRESS_QUEUE: QueueName = 'ingress';
+export const CLIENT_DEAD_QUEUE: QueueName = 'dead';
+export type QueueName = 'ingress' | 'dead';
+export const QUEUE_NAMES = [CLIENT_INGRESS_QUEUE, CLIENT_DEAD_QUEUE];
/**
* Useful TS type-only utility for testing type equality
@@ -639,3 +641,10 @@ export type PlayClientState = PlayStateCommon | 'duped' | 'scrobbled';
export const PLAY_CLIENT_STATE = [...PLAY_STATE_COMMON, 'duped', 'scrobbled'];
export type PlayState = PlaySourceState | PlayClientState;
export const PLAY_STATES = Array.from(new Set(...PLAY_CLIENT_STATE, ...PLAY_SOURCE_STATE));
+
+
+export type QueueStatus = 'queued' | 'completed' | 'failed';
+export const QUEUE_STATUS_QUEUED: QueueStatus = 'queued';
+export const QUEUE_STATUS_COMPLETED: QueueStatus = 'completed';
+export const QUEUE_STATUS_FAILED: QueueStatus = 'failed';
+export const QUEUE_STATUSES: QueueStatus[] = [QUEUE_STATUS_COMPLETED, QUEUE_STATUS_FAILED, QUEUE_STATUS_QUEUED];
\ No newline at end of file
diff --git a/src/core/PlayMarshalUtils.ts b/src/core/PlayMarshalUtils.ts
index e7f5a3fe..271a2503 100644
--- a/src/core/PlayMarshalUtils.ts
+++ b/src/core/PlayMarshalUtils.ts
@@ -2,7 +2,7 @@ import clone from 'clone';
import dayjs from 'dayjs';
import { Traverse, TraverseContext } from 'neotraverse/modern';
import { ListenRange } from '../backend/sources/PlayerState/ListenRange.js';
-import { AmbPlayObject, JsonPlayObject, PlayObject, PlayProgressAmb, REGEX_ISO8601_LOOSE } from './Atomic.js';
+import { AmbPlayObject, DateLike, JsonPlayObject, PlayObject, PlayProgressAmb, REGEX_ISO8601_LOOSE } from './Atomic.js';
import { ListenProgressPositional, ListenProgressTS } from '../backend/sources/PlayerState/ListenProgress.js';
interface BlockPath { key: string, parent: string };
@@ -39,7 +39,7 @@ export const shouldBlock = (ctx: TraverseContext): boolean => {
});
};
-export const asJsonPlayObject = (play: AmbPlayObject): JsonPlayObject => {
+export const asJsonPlayObject = (play: AmbPlayObject): JsonPlayObject => {
const cloned = clone(play);
new Traverse(cloned).forEach((ctx, x) => {
if (shouldBlock(ctx)) {
diff --git a/src/core/StringUtils.ts b/src/core/StringUtils.ts
index 9ad8593e..b07bb4fe 100644
--- a/src/core/StringUtils.ts
+++ b/src/core/StringUtils.ts
@@ -17,6 +17,7 @@ import {
import { DELIMETERS_REGEX, DELIMITERS } from "../backend/common/infrastructure/Atomic.js";
import { parseRegexSingle } from "@foxxmd/regex-buddy-core";
import { removeUndefinedKeys } from "../backend/utils.js";
+import { nanoid } from "nanoid";
dayjs.extend(utc)
dayjs.extend(isBetween);
@@ -331,4 +332,6 @@ export const artistNameToCredit = (val: string | undefined | Partial a.name;
-export const artistCreditsToNames = (a: ArtistCredit[]): string[] => a.map((x) => x.name);
\ No newline at end of file
+export const artistCreditsToNames = (a: ArtistCredit[]): string[] => a.map((x) => x.name);
+
+export const generatePlayUid = () => nanoid(20);
\ No newline at end of file
diff --git a/src/core/tests/utils/apiFixtures.ts b/src/core/tests/utils/apiFixtures.ts
new file mode 100644
index 00000000..57cc12ea
--- /dev/null
+++ b/src/core/tests/utils/apiFixtures.ts
@@ -0,0 +1,99 @@
+import { faker } from "@faker-js/faker";
+import { PlayApiCommon, PlayApiCommonDetailed, PlayInputApi, QueueStateApi } from "../../Api.js";
+import { CLIENT_INGRESS_QUEUE, JsonPlayObject, PlayObject, PlayState, QUEUE_STATUSES } from "../../Atomic.js";
+import { generatePlay } from "../../PlayTestUtils.js";
+import { generatePlayInput, randomPlayState } from "./fixtures.js";
+import { asJsonPlayObject } from "../../PlayMarshalUtils.js";
+import { generatePlayUid } from "../../StringUtils.js";
+import dayjs from "dayjs";
+import { ErrorLike } from "serialize-error";
+
+export const generatePlayApiCommon = (commonData: Partial & {play?: JsonPlayObject | PlayObject } = {}, ...playOpts: Parameters): PlayApiCommon => {
+ let play: JsonPlayObject | PlayObject;
+ const {
+ play: cPlay,
+ ...rest
+ } = commonData;
+ if(cPlay !== undefined) {
+ play = cPlay
+ } else {
+ play = generatePlay(...playOpts);
+ }
+
+ const {
+ playedAt = typeof play.data.playDate === 'string' ? play.data.playDate : play.data.playDate.toISOString(),
+ seenAt = playedAt,
+ updatedAt = seenAt,
+ compacted = false,
+ state = randomPlayState(),
+ componentId = faker.number.int({min: 1, max: 10}),
+ uid = generatePlayUid()
+ } = commonData;
+
+ return {
+ play: asJsonPlayObject(play),
+ ...rest,
+ playedAt,
+ seenAt,
+ updatedAt,
+ compacted,
+ state,
+ componentId,
+ uid
+ }
+}
+
+export const generatePlayInputApi = (inputData: Partial = {}, ...args: Parameters): PlayInputApi => {
+ const res = generatePlayInput(...args);
+ let createdAt: string = dayjs().toISOString();
+ if(res.play?.data?.playDate !== undefined) {
+ if(typeof res.play?.data?.playDate === 'string') {
+ createdAt = res.play?.data?.playDate;
+ } else {
+ createdAt = res.play?.data?.playDate.toISOString();
+ }
+ }
+ return {
+ id: faker.number.int({min: 1, max: 100}),
+ createdAt,
+ data: res.data,
+ play: res.play !== undefined ? asJsonPlayObject(res.play) : undefined,
+ ...inputData,
+ }
+}
+
+export const generateQueueStateApi = (data: Partial): QueueStateApi => {
+ const cAt = faker.date.recent().toISOString();
+ return {
+ id: faker.number.int({min: 1, max: 100}),
+ queueName: CLIENT_INGRESS_QUEUE,
+ queueState: faker.helpers.arrayElement(QUEUE_STATUSES),
+ createdAt: cAt,
+ updatedAt: cAt,
+ retries: 0,
+ ...data
+ }
+}
+
+export const generatePlayApiCommonDetailed = (opts: {
+ playOpts?: Parameters,
+ inputOpts?: Parameters,
+ queueOpts?: Parameters
+} = {}, error?: ErrorLike): PlayApiCommonDetailed => {
+ const {
+ playOpts = [],
+ inputOpts = [],
+ queueOpts = [],
+ } = opts;
+
+ const playCommon = generatePlayApiCommon(...playOpts);
+ const inputRes = generatePlayInputApi(...inputOpts);
+ const queueRes = generateQueueStateApi(queueOpts[0]);
+
+ return {
+ ...playCommon,
+ input: inputRes,
+ queueStates: [queueRes],
+ error
+ }
+}
\ No newline at end of file
diff --git a/src/core/tests/utils/fixtures.ts b/src/core/tests/utils/fixtures.ts
index 139042a9..50b2bf57 100644
--- a/src/core/tests/utils/fixtures.ts
+++ b/src/core/tests/utils/fixtures.ts
@@ -1,6 +1,6 @@
import { Traverse, TraverseContext } from 'neotraverse/modern';
import { faker } from '@faker-js/faker';
-import { LifecycleInput, LifecycleStep, ObjectPlayData, PlayMeta, PlayObject, ScrobbleResult } from '../../Atomic.js';
+import { AmbPlayObject, DateLike, LifecycleInput, LifecycleStep, ObjectPlayData, PLAY_STATES, PlayMeta, PlayObject, PlayOriginal, PlayState, ScrobbleResult } from '../../Atomic.js';
import { MarkOptional } from 'ts-essentials';
import { generateBrainz, generateMbid, generatePlay, GeneratePlayOpts, generatePlays } from '../../PlayTestUtils.js';
import { lifecyclelessInvariantTransform } from '../../PlayUtils.js';
@@ -10,6 +10,7 @@ import { existingScrobble } from '../../../backend/utils/PlayComparisonUtils.js'
import { UpstreamError } from '../../../backend/common/errors/UpstreamError.js';
import { playToListenPayload } from '../../../backend/common/vendor/listenbrainz/lzUtils.js';
import { mergeSimpleError, SimpleError, SkipTransformStageError, StagePrerequisiteError } from '../../../backend/common/errors/MSErrors.js';
+import { Dayjs } from 'dayjs';
export interface ScrobbleMatchOptions {
match?: boolean
@@ -98,6 +99,13 @@ export const generatePlayWithLifecycle = (opts: GeneratePlayWithLifecycleOptions
return lplay;
}
+export const generatePlayInput = (play?: AmbPlayObject, data?: object | false): PlayOriginal => {
+ return {
+ play,
+ data: data ?? generateRandomObj(2)
+ }
+}
+
export const playWithLifecycleScrobble = async (play: PlayObject, opts: ScrobbleMatchOptions = {}): Promise => {
const {
match = false,
@@ -282,4 +290,6 @@ export const generateRandomObj = (depth: number = 0, opt: RandomObjOptions = {})
}
}
return tgrt
-}
\ No newline at end of file
+}
+
+export const randomPlayState = () => faker.helpers.arrayElement(PLAY_STATES) as PlayState;
\ No newline at end of file
diff --git a/src/stories/ActivityTimeline.stories.tsx b/src/stories/ActivityTimeline.stories.tsx
index 185b23b8..46e82759 100644
--- a/src/stories/ActivityTimeline.stories.tsx
+++ b/src/stories/ActivityTimeline.stories.tsx
@@ -7,6 +7,7 @@ import { ActivityTimeline } from "../client/components/ActivityTimeline";
import {Provider} from "../client/components/Provider";
import { generateJsonPlays } from "../core/PlayTestUtils.js";
import { ErrorLike, JsonPlayObject, PlayLifecycle } from "../core/Atomic.js";
+import { generatePlayApiCommonDetailed } from "../core/tests/utils/apiFixtures.js";
import { examplePlay, lastfmErrorExample } from "./storyUtils.js";
import { generatePlayWithLifecycle, playWithLifecycleScrobble } from "../core/tests/utils/fixtures.js";
import { asJsonPlayObject } from '../core/PlayMarshalUtils.js';
@@ -23,9 +24,9 @@ const meta = preview.meta({
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
args: {
- play: examplePlay()
+ activity: generatePlayApiCommonDetailed()
},
- render: function Render(args, { loaded: { play } }) { return () },
+ render: function Render(args, { loaded: { activity } }) { return () },
decorators: [
(Story) => (),
]
@@ -43,8 +44,12 @@ export const ActivityTimelineStory = meta.story({
postCompare: 1,
}
}
- )))
- return {play: scrobbleError};
+ )));
+
+ return {activity: generatePlayApiCommonDetailed({
+ playOpts: [{play: scrobbleError}],
+ inputOpts: [{play: scrobbleError}]
+ })};
}
],
});