mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
feat: Implement virtual states to make filtering play state easier
* Convert 'queued' and 'dead queued' play states to queue filters * Fix relation filter sql generation for count query
This commit is contained in:
@@ -3,9 +3,9 @@ import { DbConcrete, runTransaction } from "../drizzleUtils.js";
|
||||
import clone from 'clone';
|
||||
import { Traverse, TraverseContext } from 'neotraverse/modern';
|
||||
import { loggerNoop } from "../../../MaybeLogger.js";
|
||||
import { DateLike, DeepReplaceValue, ErrorLike, PlayObject, PlayState, REGEX_ISO8601_LOOSE, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT, TemporalAccuracy } from "../../../../../core/Atomic.js";
|
||||
import { DateLike, DeepReplaceValue, ErrorLike, PlayObject, PlayState, QueueName, REGEX_ISO8601_LOOSE, TA_CLOSE, TA_DEFAULT_ACCURACY, TA_EXACT, TemporalAccuracy } from "../../../../../core/Atomic.js";
|
||||
import { generateInputEntity, generatePlayEntity, PlayEntityOpts, hydratePlaySelect, PlayHydateOptions } from "../entityUtils.js";
|
||||
import { playInputs, plays, queueStates, relations } from "../schema/schema.js";
|
||||
import { playInputs, plays, queueStates, relations, schema } from "../schema/schema.js";
|
||||
import { PlayNew, PlaySelect, PlayInputNew, FindWhere, FindMany, QueueStateSelect, FindWith, PlaySelectWithQueueStates, WhereClause, PlayWith } from "../drizzleTypes.js";;
|
||||
import { MarkOptional, MarkRequired, PathValue } from "ts-essentials";
|
||||
import { genGroupIdStrFromPlay, removeEmptyArrays, removeUndefinedKeys } from "../../../../utils.js";
|
||||
@@ -23,7 +23,7 @@ import { SourceType } from "../../../infrastructure/config/source/sources.js";
|
||||
// https://github.com/drizzle-team/drizzle-orm/issues/695 may be useful for typing models with relations?
|
||||
|
||||
export interface QueueCriteria {
|
||||
queueName: string
|
||||
queueName: QueueName
|
||||
queueStatus: QueueStateSelect['queueStatus'][] | QueueStateSelect['queueStatus']
|
||||
}
|
||||
|
||||
@@ -252,8 +252,9 @@ export class DrizzlePlayRepository extends DrizzleBaseRepository<'plays'> {
|
||||
|
||||
|
||||
const where = buildPlayWhere({componentId: args.componentId ?? this.componentId, ...rest});
|
||||
// @ts-expect-error
|
||||
const filter = relationsFilterToSQL(plays, where);
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/issues/5218#issuecomment-3854900241
|
||||
const filter = relationsFilterToSQL(plays, where, relations.plays.relations, relations);
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/3119#discussioncomment-16379557
|
||||
const count = await this.db.$count(plays, filter);
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ export const getConfigByTableName = <T extends TableName>(name: T) => {
|
||||
}
|
||||
}
|
||||
|
||||
const schema = {playInputs, plays, components, componentMigrations, queueStates, jobs};
|
||||
export const schema = {playInputs, plays, components, componentMigrations, queueStates, jobs};
|
||||
|
||||
export type TSchema = typeof relations;
|
||||
export type Schema = typeof schema;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Badge, Separator, HStack } from "@chakra-ui/react";
|
||||
import React, { ComponentProps, useState, useCallback, useEffect } from "react";
|
||||
import React, { ComponentProps, useState, useCallback, useEffect, PropsWithChildren } from "react";
|
||||
import { COMPONENT_STATE, ComponentCommonApiJson, componentStateToFriendly, MsSseEvent, PlayApiCommon } from "../../core/Api";
|
||||
import { capitalize } from "../../core/StringUtils";
|
||||
import { capitalizeWords } from "../../core/StringUtils";
|
||||
import {useSSEContext, useSSEEvent} from "@flamefrontend/sse-runtime-react";
|
||||
import { Second } from "../../core/Atomic";
|
||||
import { useTimeout } from 'react-use-timeout';
|
||||
|
||||
export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: PlayApiCommon['state'], suffix?: React.JSX.Element, hasDeadQueue?: boolean }) => {
|
||||
export const PlayStateBadge = (props: PropsWithChildren<ComponentProps<typeof Badge>> & { state: PlayApiCommon['state'], suffix?: React.JSX.Element, hasDeadQueue?: boolean }) => {
|
||||
|
||||
const { state, suffix, ...rest } = props;
|
||||
const { state, suffix, children, ...rest } = props;
|
||||
|
||||
let badgeColor = undefined;
|
||||
let badgeText = capitalize(state);
|
||||
let badgeText = capitalizeWords(state);
|
||||
|
||||
switch (state) {
|
||||
case 'queued':
|
||||
@@ -22,6 +22,7 @@ export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: Pl
|
||||
badgeColor = 'green';
|
||||
break;
|
||||
case 'failed':
|
||||
case ('dead queued' as PlayApiCommon['state']):
|
||||
badgeColor = 'red';
|
||||
if(props.hasDeadQueue) {
|
||||
badgeText = 'Dead Queued';
|
||||
@@ -35,7 +36,7 @@ export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: Pl
|
||||
break;
|
||||
}
|
||||
|
||||
return <Badge variant="surface" colorPalette={badgeColor} {...rest}>{badgeText}{suffix}</Badge>
|
||||
return <Badge variant="surface" colorPalette={badgeColor} {...rest}>{children ?? badgeText}{suffix}</Badge>
|
||||
}
|
||||
|
||||
const DEFAULT_EXPIRES = 10000;
|
||||
|
||||
@@ -121,13 +121,15 @@ export const ListContainerFetchable = (props: { componentId: number, componentTy
|
||||
if ('componentId' in (payload.data as object) && (payload.data as Record<string, any>).componentId === props.componentId) {
|
||||
switch (payload.type) {
|
||||
case 'playInsert':
|
||||
const componentData = payload.data as MsSseEventPayload<PlayApiCommonDetailed>;
|
||||
{
|
||||
const componentData = payload.data as MsSseEventPayload<PlayApiCommonDetailed>;
|
||||
console.debug(`[Insert Check ${componentData.data.uid}] Recieved playInsert for Component ${componentId}, checking if Play can be inserted...`);
|
||||
if(playInWindow(componentData.data, query)) {
|
||||
queryClient.setQueryData(tanQueries.activities.list(componentId, query).queryKey, (old: InfiniteData<PaginatedResponse<PlayApiCommonDetailed>, unknown>) => {
|
||||
return insertInfinitePlay(componentData.data, old);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -189,7 +191,7 @@ const insertInfinitePlay = (data: PlayApiCommonDetailed, queryData: InfiniteData
|
||||
data: [...playData.slice(0, beforeIndex),
|
||||
{
|
||||
...data,
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error only used for inserts in this context
|
||||
isNew: true
|
||||
}
|
||||
, ...playData.slice(beforeIndex)]
|
||||
|
||||
@@ -4,8 +4,6 @@ import React, { ComponentProps, Fragment, useMemo, useCallback, useState } from
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import doy from 'dayjs/plugin/dayOfYear.js';
|
||||
import "./PlayList.scss";
|
||||
import { ToggleButtonVariant } from '../ToggleButton.js';
|
||||
import { capitalize } from '../../../core/StringUtils.js';
|
||||
import { PlayStateBadge } from '../Badges.js';
|
||||
import {
|
||||
DateFormatter,
|
||||
@@ -25,9 +23,10 @@ import { QueryPlaysOptsJson } from '../../../backend/common/database/drizzle/rep
|
||||
import { cardHeaderSeparator } from '../../utils/ComponentUtils.js';
|
||||
import { CompareDateBetween } from '../../../backend/common/database/drizzle/repositories/BaseRepository.js';
|
||||
import { CalendarButton, RefreshButton } from '../icons/ChakraIcons.js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { QueryPlaysOptsJsonRefreshable, tanQueries, useQueryWatcher } from '../../queries/index.js';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { PlayStateUI } from '../../../core/Api.js';
|
||||
import { capitalizeWords } from '../../../core/StringUtils.js';
|
||||
|
||||
const noop = (_) => null;
|
||||
|
||||
@@ -45,16 +44,16 @@ const SelectValue = () => {
|
||||
|
||||
interface PlayStateFilterProps {
|
||||
mode: ComponentType
|
||||
onChange?: (states: PlayState[]) => void
|
||||
onChange?: (states: PlayStateUI[]) => void
|
||||
}
|
||||
export const PlayStateFilter = (props: PlayStateFilterProps & {value?: PlayState[] | undefined}) => {
|
||||
export const PlayStateFilter = (props: PlayStateFilterProps & {value?: PlayStateUI[] | undefined}) => {
|
||||
const {
|
||||
mode,
|
||||
onChange = noop,
|
||||
value
|
||||
} = props;
|
||||
const availableStates = isComponentTypeSource(mode) ? PLAY_SOURCE_STATE : PLAY_CLIENT_STATE;
|
||||
const selectOptions = createListCollection({ items: availableStates.map(x => ({ label: capitalize(x), value: x })) });
|
||||
const availableStates = ['dead queued', ...(isComponentTypeSource(mode) ? PLAY_SOURCE_STATE : PLAY_CLIENT_STATE)];
|
||||
const selectOptions = createListCollection({ items: availableStates.map(x => ({ label: capitalizeWords(x), value: x })) });
|
||||
//const [enabledStates, setEnabledStates] = useState<PlayState[]>([]);
|
||||
// maxW="420px"
|
||||
return (
|
||||
@@ -273,7 +272,7 @@ export const ListFilters = (props: {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setState = useCallback((val: PlayState[]) => {
|
||||
const setState = useCallback((val: PlayStateUI[]) => {
|
||||
const {
|
||||
state,
|
||||
...rest
|
||||
@@ -283,7 +282,7 @@ export const ListFilters = (props: {
|
||||
...rest,
|
||||
state: val
|
||||
});
|
||||
}, [onchange, filters]);
|
||||
}, [onChange, filters]);
|
||||
const setDateRange = useCallback((val: [string, string]) => {
|
||||
const {
|
||||
playedAt,
|
||||
|
||||
@@ -6,10 +6,10 @@ import { QueryPlaysOpts, QueryPlaysOptsJson } from "../../backend/common/databas
|
||||
import qs from 'qs';
|
||||
import { baseUrl } from "../utils";
|
||||
import { PaginatedResponse } from "../../backend/common/database/drizzle/repositories/BaseRepository";
|
||||
import { ComponentsApiJson, PlayApiCommonDetailed } from "../../core/Api";
|
||||
import { SourcePlayerJson } from "../../core/Atomic";
|
||||
import { ComponentsApiJson, PlayApiCommonDetailed, PlayStateUI } from "../../core/Api";
|
||||
import { CLIENT_DEAD_QUEUE, CLIENT_INGRESS_QUEUE, isPlayState, SourcePlayerJson } from "../../core/Atomic";
|
||||
|
||||
export type QueryPlaysOptsJsonRefreshable = QueryPlaysOptsJson & {nonce?: string};
|
||||
export type QueryPlaysOptsJsonRefreshable = Omit<QueryPlaysOptsJson, 'state'> & {nonce?: string, state?: PlayStateUI[]};
|
||||
|
||||
const components = createQueryKeys('components', {
|
||||
list: () => ({
|
||||
@@ -32,11 +32,26 @@ const activities = createQueryKeys('activities', {
|
||||
queryFn: (ctx) => {
|
||||
const {
|
||||
nonce,
|
||||
state,
|
||||
...rest
|
||||
} = filters;
|
||||
const derived: QueryPlaysOptsJson = rest;
|
||||
if(state !== undefined) {
|
||||
derived.state = state.filter(x => isPlayState(x));
|
||||
|
||||
// remove 'dead queued' derived play state and replace with filter for queue = 'dead' & state = 'queued'
|
||||
if(state.includes('dead queued') && !rest.queues?.some(x => x.queueName === CLIENT_DEAD_QUEUE)) {
|
||||
derived.queues = [...(rest.queues ?? []), {queueName: CLIENT_DEAD_QUEUE, queueStatus: 'queued'}];
|
||||
}
|
||||
// remove 'queued' play state and replace with filter for queue = 'ingress' & state = 'queued'
|
||||
if(state.includes('queued') && !rest.queues?.some(x => x.queueName === CLIENT_INGRESS_QUEUE)) {
|
||||
derived.queues = [...(derived.queues ?? []), {queueName: CLIENT_INGRESS_QUEUE, queueStatus: 'queued'}];
|
||||
derived.state = derived.state.filter(x => x !== 'queued');
|
||||
}
|
||||
}
|
||||
return ky.get(`components/${componentId}/plays`, {
|
||||
baseUrl: baseUrl,
|
||||
searchParams: qs.stringify({...rest, offset: ctx.pageParam})
|
||||
searchParams: qs.stringify({...derived, offset: ctx.pageParam})
|
||||
}).json<PaginatedResponse<PlayApiCommonDetailed>>()
|
||||
}
|
||||
}),
|
||||
|
||||
+3
-1
@@ -151,4 +151,6 @@ export type MsSseEvent<T extends object = Record<string, any>> = {
|
||||
export type SortPlaysBy = 'played' | 'seen';
|
||||
export interface SortPlaysByProps {
|
||||
sortBy: SortPlaysBy
|
||||
}
|
||||
}
|
||||
|
||||
export type PlayStateUI = PlayState | 'dead queued';
|
||||
@@ -658,6 +658,7 @@ 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 const isPlayState = (val: string): val is PlayState => PLAY_STATES.includes(val);
|
||||
|
||||
|
||||
export type QueueStatus = 'queued' | 'completed' | 'failed';
|
||||
|
||||
@@ -191,9 +191,10 @@ export const slice = (str: string, index: number, count: number, add?: string):
|
||||
|
||||
return str.slice(0, index) + (add || "") + str.slice(index + count);
|
||||
}
|
||||
export const capitalize = (str: any) => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
|
||||
export const capitalizeWords = (str: string, delimiter = ' ') => str.split(delimiter).map(x => capitalize(x)).join(delimiter);
|
||||
|
||||
/**
|
||||
* Split a string-ish variable by a list of deliminators and return the first actually split array or default to returning the string as the first element.
|
||||
|
||||
Reference in New Issue
Block a user