mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Some progress towards recent page
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import {
|
||||
buildTrackString,
|
||||
capitalize, isPlayTemporallyClose,
|
||||
mergeArr,
|
||||
playObjDataMatch, setIntersection,
|
||||
truncateStringToLength
|
||||
playObjDataMatch, setIntersection
|
||||
} from "../utils.js";
|
||||
import {
|
||||
ClientType, FormatPlayObjectOptions,
|
||||
@@ -19,6 +17,7 @@ import {CommonClientConfig} from "../common/infrastructure/config/client/index.j
|
||||
import {ClientConfig} from "../common/infrastructure/config/client/clients.js";
|
||||
import {Notifiers} from "../notifier/Notifiers.js";
|
||||
import {FixedSizeList} from 'fixed-size-list';
|
||||
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
export default abstract class AbstractScrobbleClient {
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import AbstractScrobbleClient from "./AbstractScrobbleClient.js";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
buildTrackString, capitalize,
|
||||
capitalize,
|
||||
playObjDataMatch, removeUndefinedKeys,
|
||||
setIntersection, sleep,
|
||||
sortByOldestPlayDate,
|
||||
truncateStringToLength,
|
||||
|
||||
} from "../utils.js";
|
||||
import LastfmApiClient from "../apis/LastfmApiClient.js";
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import {LastfmClientConfig} from "../common/infrastructure/config/client/lastfm.
|
||||
import {TrackScrobbleResponse, UserGetRecentTracksResponse} from "lastfm-node-client";
|
||||
import {Notifiers} from "../notifier/Notifiers.js";
|
||||
import {Logger} from '@foxxmd/winston';
|
||||
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
export default class LastfmScrobbler extends AbstractScrobbleClient {
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import dayjs from 'dayjs';
|
||||
|
||||
import AbstractScrobbleClient from "./AbstractScrobbleClient.js";
|
||||
import {
|
||||
buildTrackString, capitalize,
|
||||
capitalize,
|
||||
playObjDataMatch, removeUndefinedKeys,
|
||||
setIntersection, sleep,
|
||||
sortByOldestPlayDate,
|
||||
truncateStringToLength,
|
||||
|
||||
} from "../utils.js";
|
||||
import LastfmApiClient from "../apis/LastfmApiClient.js";
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import {Notifiers} from "../notifier/Notifiers.js";
|
||||
import {Logger} from '@foxxmd/winston';
|
||||
import {ListenBrainzClientConfig} from "../common/infrastructure/config/client/listenbrainz.js";
|
||||
import {ListenbrainzApiClient} from "../apis/ListenbrainzApiClient.js";
|
||||
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
export default class ListenbrainzScrobbler extends AbstractScrobbleClient {
|
||||
|
||||
|
||||
@@ -3,12 +3,10 @@ import request from 'superagent';
|
||||
import dayjs from 'dayjs';
|
||||
import compareVersions from 'compare-versions';
|
||||
import {
|
||||
buildTrackString,
|
||||
playObjDataMatch,
|
||||
setIntersection,
|
||||
sleep,
|
||||
sortByOldestPlayDate,
|
||||
truncateStringToLength,
|
||||
parseRetryAfterSecsFromObj, capitalize, isPlayTemporallyClose
|
||||
} from "../utils.js";
|
||||
import {
|
||||
@@ -26,6 +24,7 @@ import {
|
||||
MalojaScrobbleV2RequestData,
|
||||
MalojaScrobbleV3RequestData, MalojaScrobbleV3ResponseData, MalojaV2ScrobbleData, MalojaV3ScrobbleData
|
||||
} from "../apis/maloja/interfaces";
|
||||
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
const feat = ["ft.", "ft", "feat.", "feat", "featuring", "Ft.", "Ft", "Feat.", "Feat", "Featuring"];
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import {
|
||||
buildTrackString,
|
||||
createAjvFactory,
|
||||
mergeArr,
|
||||
playObjDataMatch,
|
||||
@@ -23,6 +22,7 @@ import winston from '@foxxmd/winston';
|
||||
import ListenbrainzScrobbler from "./ListenbrainzScrobbler.js";
|
||||
import {ListenBrainzClientConfig} from "../common/infrastructure/config/client/listenbrainz.js";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import {buildTrackString} from "../utils/StringUtils.js";
|
||||
|
||||
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ class TupleMap<X,Y,Z> {
|
||||
return this.map.values();
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: Z, key: [X, Y], map: Map<[X, Y], Z>) => void, thisArg?: any): void {
|
||||
forEach(callbackfn: (value: Z, key: [X, Y], map: Map<string, Z>) => void, thisArg?: any): void {
|
||||
this.map.forEach((value, key) => {
|
||||
callbackfn.call(thisArg, value, JSON.parse(key), this);
|
||||
callbackfn.call(thisArg, value, JSON.parse(key), this.map);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,13 +173,14 @@ export const NO_USER = 'SingleUser';
|
||||
|
||||
export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER];
|
||||
|
||||
export interface TrackStringOptions {
|
||||
export interface TrackStringOptions<T = string> {
|
||||
include?: ('time' | 'artist' | 'track' | 'timeFromNow' | 'trackId')[]
|
||||
transformers?: {
|
||||
artists?: (a: string[]) => string
|
||||
track?: (t: string) => string
|
||||
time?: (t: Dayjs) => string
|
||||
timeFromNow?: (t: Dayjs) => string
|
||||
artists?: (a: string[]) => T | string
|
||||
track?: (t: string, hasExistingParts?: boolean) => T | string
|
||||
time?: (t: Dayjs) => T | string
|
||||
timeFromNow?: (t: Dayjs) => T | string
|
||||
reducer?: (arr: (T|string)[]) => T //(acc: T, curr: T | string) => T
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import winston, {format, Logger} from '@foxxmd/winston';
|
||||
import {DuplexTransport} from "winston-duplex";
|
||||
import {asLogOptions, LogConfig, LogInfo, LogLevel, LogOptions} from "./infrastructure/Atomic.js";
|
||||
import process from "process";
|
||||
import {fileOrDirectoryIsWriteable, parseBool, truncateStringToLength} from "../utils.js";
|
||||
import {fileOrDirectoryIsWriteable, parseBool} from "../utils.js";
|
||||
import {ErrorWithCause, stackWithCauses} from "pony-cause";
|
||||
import {NullTransport} from 'winston-null';
|
||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
@@ -12,6 +12,7 @@ import dayjs from "dayjs";
|
||||
import stringify from 'safe-stable-stringify';
|
||||
import {SPLAT, LEVEL, MESSAGE} from 'triple-beam';
|
||||
import {Symbol} from "typescript-json-schema";
|
||||
import {truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
const {combine, printf, timestamp, label, splat, errors} = format;
|
||||
|
||||
|
||||
+3
-4
@@ -13,13 +13,11 @@ import session from 'express-session';
|
||||
import {createSession} from 'better-sse';
|
||||
|
||||
import {
|
||||
buildTrackString,
|
||||
capitalize,
|
||||
longestString, mergeArr, parseBool,
|
||||
mergeArr, parseBool,
|
||||
readJson,
|
||||
remoteHostIdentifiers,
|
||||
sleep,
|
||||
truncateStringToLength
|
||||
sleep
|
||||
} from "./utils.js";
|
||||
import {makeClientCheckMiddle, makeSourceCheckMiddle} from "./server/middleware.js";
|
||||
import TautulliSource from "./sources/TautulliSource.js";
|
||||
@@ -49,6 +47,7 @@ import {formatLogToHtml, getLogger, isLogLineMinLevel} from "./common/logging.js
|
||||
import {MESSAGE} from "triple-beam";
|
||||
import {setupApi} from "./server/api.js";
|
||||
import {Transform} from "stream";
|
||||
import {buildTrackString, longestString, truncateStringToLength} from "./utils/StringUtils.js";
|
||||
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import {ExpressWithAsync} from "@awaitjs/express";
|
||||
import {getRoot} from "../ioc.js";
|
||||
import {buildTrackString, capitalize, longestString, truncateStringToLength} from "../utils.js";
|
||||
import {capitalize} from "../utils.js";
|
||||
import {makeClientCheckMiddle, makeSourceCheckMiddle} from "./middleware.js";
|
||||
import AbstractSource from "../sources/AbstractSource.js";
|
||||
import {ClientStatusData, PlayObject, SourceStatusData, TrackStringOptions} from "../common/infrastructure/Atomic.js";
|
||||
import {buildTrackString, longestString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import {
|
||||
buildTrackString,
|
||||
capitalize, isPlayTemporallyClose, genGroupId,
|
||||
genGroupIdStrFromPlay, mergeArr,
|
||||
playObjDataMatch, pollingBackoff,
|
||||
@@ -20,6 +19,7 @@ import {SourceConfig} from "../common/infrastructure/config/source/sources.js";
|
||||
import {EventEmitter} from "events";
|
||||
import {FixedSizeList} from "fixed-size-list";
|
||||
import TupleMap from "../common/TupleMap.js";
|
||||
import {buildTrackString} from "../utils/StringUtils.js";
|
||||
|
||||
export interface RecentlyPlayedOptions {
|
||||
limit?: number
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import MemorySource from "./MemorySource.js";
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import {
|
||||
buildTrackString, isPlayTemporallyClose,
|
||||
isPlayTemporallyClose,
|
||||
combinePartsToString, parseBool,
|
||||
parseDurationFromTimestamp,
|
||||
playObjDataMatch,
|
||||
truncateStringToLength, comparePlayTemporally, temporalPlayComparisonSummary, doubleReturnNewline
|
||||
comparePlayTemporally, temporalPlayComparisonSummary, doubleReturnNewline
|
||||
} from "../utils.js";
|
||||
import {JellySourceConfig} from "../common/infrastructure/config/source/jellyfin.js";
|
||||
import {FormatPlayObjectOptions, InternalConfig, PlayObject, PlayPlatformId} from "../common/infrastructure/Atomic.js";
|
||||
@@ -13,6 +13,7 @@ import EventEmitter from "events";
|
||||
import {PlayerStateOptions} from "./PlayerState/AbstractPlayerState.js";
|
||||
import {Logger} from "@foxxmd/winston";
|
||||
import {JellyfinPlayerState} from "./PlayerState/JellyfinPlayerState.js";
|
||||
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
const shortDeviceId = truncateStringToLength(10, '');
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import AbstractSource from "./AbstractSource.js";
|
||||
import {
|
||||
playObjDataMatch,
|
||||
sortByOldestPlayDate,
|
||||
buildTrackString,
|
||||
toProgressAwarePlayObject,
|
||||
getProgress,
|
||||
genGroupIdStrFromPlay,
|
||||
@@ -25,6 +24,7 @@ import TupleMap from "../common/TupleMap.js";
|
||||
import {AbstractPlayerState, PlayerStateOptions} from "./PlayerState/AbstractPlayerState.js";
|
||||
import {GenericPlayerState} from "./PlayerState/GenericPlayerState.js";
|
||||
import {Logger} from "@foxxmd/winston";
|
||||
import {buildTrackString} from "../utils/StringUtils.js";
|
||||
|
||||
export default class MemorySource extends AbstractSource {
|
||||
/*
|
||||
|
||||
@@ -16,8 +16,9 @@ import pEvent from 'p-event';
|
||||
import winston from '@foxxmd/winston';
|
||||
import {RecentlyPlayedOptions} from "./AbstractSource.js";
|
||||
import request from "superagent";
|
||||
import {buildTrackString, removeDuplicates} from "../utils.js";
|
||||
import {removeDuplicates} from "../utils.js";
|
||||
import {SubsonicSource} from "./SubsonicSource.js";
|
||||
import {buildTrackString} from "../utils/StringUtils.js";
|
||||
|
||||
export class MopidySource extends MemorySource {
|
||||
declare config: MopidySourceConfig;
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
ReportedPlayerStatus
|
||||
} from "../../common/infrastructure/Atomic.js";
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import {buildTrackString, formatNumber, genGroupIdStr, playObjDataMatch, progressBar} from "../../utils.js";
|
||||
import {formatNumber, genGroupIdStr, playObjDataMatch, progressBar} from "../../utils.js";
|
||||
import {Logger} from "@foxxmd/winston";
|
||||
import {ListenProgress} from "./ListenProgress.js";
|
||||
import {buildTrackString} from "../../utils/StringUtils.js";
|
||||
|
||||
export interface PlayerStateIntervals {
|
||||
staleInterval?: number
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
buildTrackString,
|
||||
combinePartsToString,
|
||||
mergeArr,
|
||||
truncateStringToLength
|
||||
mergeArr
|
||||
} from "../utils.js";
|
||||
import AbstractSource from "./AbstractSource.js";
|
||||
import formidable from 'formidable';
|
||||
@@ -12,6 +10,7 @@ import {PlexSourceConfig} from "../common/infrastructure/config/source/plex.js";
|
||||
import {FormatPlayObjectOptions, InternalConfig, PlayObject, SourceType} from "../common/infrastructure/Atomic.js";
|
||||
import EventEmitter from "events";
|
||||
import winston from '@foxxmd/winston';
|
||||
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
const shortDeviceId = truncateStringToLength(10, '');
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
||||
import {
|
||||
readJson,
|
||||
writeFile,
|
||||
sortByOldestPlayDate, sleep, parseRetryAfterSecsFromObj, buildTrackString, truncateStringToLength, combinePartsToString,
|
||||
sortByOldestPlayDate, sleep, parseRetryAfterSecsFromObj, combinePartsToString,
|
||||
} from "../utils.js";
|
||||
import SpotifyWebApi from "spotify-web-api-node";
|
||||
import AbstractSource, {RecentlyPlayedOptions} from "./AbstractSource.js";
|
||||
@@ -23,6 +23,7 @@ import AlbumObjectSimplified = SpotifyApi.AlbumObjectSimplified;
|
||||
import UserDevice = SpotifyApi.UserDevice;
|
||||
import MemorySource from "./MemorySource.js";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
const scopes = ['user-read-recently-played', 'user-read-currently-playing', 'user-read-playback-state', 'user-read-playback-position'];
|
||||
const state = 'random';
|
||||
|
||||
@@ -2,12 +2,13 @@ import request from 'superagent';
|
||||
import * as crypto from 'crypto';
|
||||
import dayjs from "dayjs";
|
||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
|
||||
import {buildTrackString, parseRetryAfterSecsFromObj, removeDuplicates, sleep} from "../utils.js";
|
||||
import {parseRetryAfterSecsFromObj, removeDuplicates, sleep} from "../utils.js";
|
||||
import MemorySource from "./MemorySource.js";
|
||||
import {SubSonicSourceConfig} from "../common/infrastructure/config/source/subsonic.js";
|
||||
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
|
||||
import {RecentlyPlayedOptions} from "./AbstractSource.js";
|
||||
import EventEmitter from "events";
|
||||
import {buildTrackString} from "../utils/StringUtils.js";
|
||||
|
||||
dayjs.extend(isSameOrAfter);
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import dayjs from "dayjs";
|
||||
import PlexSource from "./PlexSource.js";
|
||||
import {TautulliSourceConfig} from "../common/infrastructure/config/source/tautulli.js";
|
||||
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
|
||||
import {combinePartsToString, truncateStringToLength} from "../utils.js";
|
||||
import {combinePartsToString} from "../utils.js";
|
||||
import EventEmitter from "events";
|
||||
import {Request} from "express";
|
||||
import {truncateStringToLength} from "../utils/StringUtils.js";
|
||||
|
||||
const shortDeviceId = truncateStringToLength(10, '');
|
||||
|
||||
|
||||
+17
-80
@@ -1,32 +1,33 @@
|
||||
import {promises, constants, accessSync} from "fs";
|
||||
import dayjs, {Dayjs} from 'dayjs';
|
||||
import {accessSync, constants, promises} from "fs";
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import {Logger} from '@foxxmd/winston';
|
||||
import JSON5 from 'json5';
|
||||
import { TimeoutError, WebapiError } from "spotify-web-api-node/src/response-error.js";
|
||||
import {TimeoutError, WebapiError} from "spotify-web-api-node/src/response-error.js";
|
||||
import Ajv, {Schema} from 'ajv';
|
||||
import {
|
||||
asPlayerStateData,
|
||||
DEFAULT_SCROBBLE_DURATION_THRESHOLD, ListenRange,
|
||||
lowGranularitySources, NO_DEVICE, NO_USER, numberFormatOptions, PlayerStateData,
|
||||
PlayObject, PlayPlatformId, ProgressAwarePlayObject, RegExResult,
|
||||
RemoteIdentityParts, ScrobbleThresholdResult,
|
||||
TrackStringOptions
|
||||
DEFAULT_SCROBBLE_DURATION_THRESHOLD,
|
||||
ListenRange,
|
||||
lowGranularitySources,
|
||||
NO_DEVICE,
|
||||
NO_USER,
|
||||
numberFormatOptions,
|
||||
PlayerStateData,
|
||||
PlayObject,
|
||||
PlayPlatformId,
|
||||
ProgressAwarePlayObject,
|
||||
RegExResult,
|
||||
RemoteIdentityParts,
|
||||
ScrobbleThresholdResult
|
||||
} from "./common/infrastructure/Atomic.js";
|
||||
import {Request} from "express";
|
||||
import pathUtil from "path";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import backoffStrategies from '@kenyip/backoff-strategies';
|
||||
import {ScrobbleThresholds} from "./common/infrastructure/config/source/index.js";
|
||||
import {
|
||||
oneLineTrim,
|
||||
replaceResultTransformer,
|
||||
stripIndentTransformer,
|
||||
TemplateTag,
|
||||
trimResultTransformer
|
||||
} from 'common-tags';
|
||||
import {replaceResultTransformer, stripIndentTransformer, TemplateTag, trimResultTransformer} from 'common-tags';
|
||||
import is from "@sindresorhus/is";
|
||||
import date = is.date;
|
||||
import {Duration} from "dayjs/plugin/duration.js";
|
||||
|
||||
dayjs.extend(utc);
|
||||
@@ -83,70 +84,6 @@ export function sleep(ms: any) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const longestString = (strings: any) => strings.reduce((acc: any, curr: any) => curr.length > acc ? curr.length : acc, 0);
|
||||
export const truncateStringArrToLength = (length: any, truncStr = '...') => {
|
||||
const truncater = truncateStringToLength(length, truncStr);
|
||||
return (strings: any) => strings.map(truncater);
|
||||
}
|
||||
export const truncateStringToLength = (length: any, truncStr = '...') => (val: any = '') => {
|
||||
if(val === null) {
|
||||
return '';
|
||||
}
|
||||
const str = typeof val !== 'string' ? val.toString() : val;
|
||||
return str.length > length ? `${str.slice(0, length)}${truncStr}` : str;
|
||||
}
|
||||
|
||||
const defaultTransformer = (input: any) => input;
|
||||
|
||||
export const buildTrackString = (playObj: PlayObject, options: TrackStringOptions = {}) => {
|
||||
const {
|
||||
include = ['time', 'artist', 'track'],
|
||||
transformers: {
|
||||
artists: artistsFunc = (a: string[]) => a.join(' / '),
|
||||
track: trackFunc = defaultTransformer,
|
||||
time: timeFunc = (t: Dayjs | undefined) => t === undefined ? 'N/A' : t.local().format(),
|
||||
timeFromNow = (t: Dayjs | undefined) => t === undefined ? undefined : t.local().fromNow(),
|
||||
} = {}
|
||||
} = options;
|
||||
const {
|
||||
data: {
|
||||
artists,
|
||||
album,
|
||||
track,
|
||||
playDate
|
||||
} = {},
|
||||
meta: {
|
||||
trackId
|
||||
} = {},
|
||||
} = playObj;
|
||||
|
||||
const strParts = [];
|
||||
if(include.includes('trackId') && trackId !== undefined) {
|
||||
strParts.push(`(${trackId})`);
|
||||
}
|
||||
if(include.includes('artist')) {
|
||||
strParts.push(`${artistsFunc(artists)}`)
|
||||
}
|
||||
if(include.includes('track')) {
|
||||
if(strParts.length > 0) {
|
||||
strParts.push(`- ${trackFunc(track)}`);
|
||||
} else {
|
||||
strParts.push(`${trackFunc(track)}`);
|
||||
}
|
||||
}
|
||||
if (include.includes('time')) {
|
||||
strParts.push(`@ ${timeFunc(playDate)}`);
|
||||
}
|
||||
if (include.includes('timeFromNow')) {
|
||||
const tfn = timeFromNow(playDate);
|
||||
if(tfn !== undefined) {
|
||||
strParts.push(`(${tfn})`)
|
||||
}
|
||||
|
||||
}
|
||||
return strParts.join(' ');
|
||||
}
|
||||
|
||||
// sorts playObj formatted objects by playDate in ascending (oldest first) order
|
||||
export const sortByOldestPlayDate = (a: PlayObject, b: PlayObject) => {
|
||||
const {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import {PlayObject, TrackStringOptions} from "../common/infrastructure/Atomic.js";
|
||||
import React, {ReactElement} from "react";
|
||||
import utc from "dayjs/plugin/utc.js";
|
||||
import isBetween from "dayjs/plugin/isBetween.js";
|
||||
import relativeTime from "dayjs/plugin/relativeTime.js";
|
||||
import duration from "dayjs/plugin/duration.js";
|
||||
import timezone from "dayjs/plugin/timezone.js";
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export const longestString = (strings: any) => strings.reduce((acc: any, curr: any) => curr.length > acc ? curr.length : acc, 0);
|
||||
export const truncateStringArrToLength = (length: any, truncStr = '...') => {
|
||||
const truncater = truncateStringToLength(length, truncStr);
|
||||
return (strings: any) => strings.map(truncater);
|
||||
}
|
||||
export const truncateStringToLength = (length: any, truncStr = '...') => (val: any = '') => {
|
||||
if (val === null) {
|
||||
return '';
|
||||
}
|
||||
const str = typeof val !== 'string' ? val.toString() : val;
|
||||
return str.length > length ? `${str.slice(0, length)}${truncStr}` : str;
|
||||
}
|
||||
export const defaultTrackTransformer = (input: any, hasExistingParts: boolean = false) => hasExistingParts ? `- ${input}` : input;
|
||||
export const defaultReducer = (acc, curr) => `${acc} ${curr}`;
|
||||
export const defaultArtistFunc = (a: string[]) => a.join(' / ');
|
||||
export const defaultTimeFunc = (t: Dayjs | undefined) => t === undefined ? '@ N/A' : `@ ${t.local().format()}`;
|
||||
export const defaultTimeFromNowFunc = (t: Dayjs | undefined) => t === undefined ? undefined : `(${t.local().fromNow()})`;
|
||||
export const defaultBuildTrackStringTransformers = {
|
||||
artists: defaultArtistFunc,
|
||||
track: defaultTrackTransformer,
|
||||
time: defaultTimeFunc,
|
||||
timeFromNow: defaultTimeFromNowFunc,
|
||||
}
|
||||
export const buildTrackString = <T = string>(playObj: PlayObject, options: TrackStringOptions<T> = {}): T => {
|
||||
const {
|
||||
include = ['time', 'artist', 'track'],
|
||||
transformers: {
|
||||
artists: artistsFunc = defaultBuildTrackStringTransformers.artists,
|
||||
track: trackFunc = defaultBuildTrackStringTransformers.track,
|
||||
time: timeFunc = defaultBuildTrackStringTransformers.time,
|
||||
timeFromNow = defaultBuildTrackStringTransformers.timeFromNow,
|
||||
reducer = arr => arr.join(' ') // (acc, curr) => `${acc} ${curr}`
|
||||
} = {},
|
||||
} = options;
|
||||
const {
|
||||
data: {
|
||||
artists,
|
||||
album,
|
||||
track,
|
||||
playDate
|
||||
} = {},
|
||||
meta: {
|
||||
trackId
|
||||
} = {},
|
||||
} = playObj;
|
||||
|
||||
const strParts: (T | string)[] = [];
|
||||
if (include.includes('trackId') && trackId !== undefined) {
|
||||
strParts.push(`(${trackId})`);
|
||||
}
|
||||
if (include.includes('artist')) {
|
||||
strParts.push(artistsFunc(artists))
|
||||
}
|
||||
if (include.includes('track')) {
|
||||
strParts.push(trackFunc(track, strParts.length > 0));
|
||||
}
|
||||
if (include.includes('time')) {
|
||||
strParts.push(timeFunc(playDate));
|
||||
}
|
||||
if (include.includes('timeFromNow')) {
|
||||
const tfn = timeFromNow(playDate);
|
||||
if (tfn !== undefined) {
|
||||
strParts.push(tfn)
|
||||
}
|
||||
|
||||
}
|
||||
// @ts-ignore
|
||||
return reducer(strParts); //strParts.join(' ');
|
||||
}
|
||||
export const buildTrackStringReactOptions: TrackStringOptions<ReactElement> = {
|
||||
transformers: {
|
||||
...defaultBuildTrackStringTransformers,
|
||||
reducer: arr => {
|
||||
const allFrags = arr.map(x => typeof x === 'string' ? React.createElement("span", null, x) : x);
|
||||
return React.createElement("span", null, allFrags);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
"ky": "^0.33.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.9.5",
|
||||
|
||||
+19
-4
@@ -1,11 +1,27 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import './App.css';
|
||||
import StatusSection from "./status/StatusSection";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import LogsSection from "./logs/LogsSection";
|
||||
const queryClient = new QueryClient()
|
||||
import Dashboard from "./dashboard/dashboard";
|
||||
import RecentPage from "./recent/RecentPage";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Dashboard />,
|
||||
},
|
||||
{
|
||||
path: "/recent",
|
||||
element: <RecentPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -24,8 +40,7 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="container mx-auto">
|
||||
<StatusSection/>
|
||||
<LogsSection/>
|
||||
<RouterProvider router={router}/>
|
||||
</div>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, {Fragment, PropsWithChildren, ReactElement} from 'react';
|
||||
import SkeletonTitle from "./skeleton/SkeletonTitle.js";
|
||||
import SkeletonParagraph from "./skeleton/SkeletonParagraph.js";
|
||||
import StatusCardSkeleton, {StatusCardSkeletonData} from "./statusCard/StatusCardSkeleton.js";
|
||||
import {PlayData, PlayObject, TrackStringOptions} from "../../../src/common/infrastructure/Atomic.js";
|
||||
|
||||
import {buildTrackString, buildTrackStringReactOptions} from "../../../src/utils/StringUtils.js";
|
||||
|
||||
interface PlayObjectProps {
|
||||
data: PlayObject
|
||||
artistLength?: number
|
||||
trackLength?: number
|
||||
buildOptions?: TrackStringOptions<ReactElement> & {includeWeb?: boolean}
|
||||
}
|
||||
const PlayDisplay = (props: PlayObjectProps) => {
|
||||
const {
|
||||
data,
|
||||
artistLength,
|
||||
trackLength,
|
||||
buildOptions = {},
|
||||
} = props || {};
|
||||
|
||||
const {
|
||||
transformers,
|
||||
includeWeb,
|
||||
...restBuild
|
||||
} = buildOptions;
|
||||
|
||||
const bOpts: TrackStringOptions<ReactElement> = {
|
||||
...restBuild,
|
||||
// @ts-ignore
|
||||
transformers: {
|
||||
...buildTrackStringReactOptions.transformers,
|
||||
...transformers,
|
||||
track: (t, hasExistingParts) => includeWeb ? <>{hasExistingParts ? '- ' : ''}<a>${t}</a></> : <>{hasExistingParts ? '- ' : ''}t</>
|
||||
}
|
||||
}
|
||||
|
||||
return buildTrackString<ReactElement>(data, bOpts);
|
||||
}
|
||||
export default PlayDisplay;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import StatusSection from "../status/StatusSection";
|
||||
import LogsSection from "../logs/LogsSection";
|
||||
|
||||
const dashboard = () => {
|
||||
return (
|
||||
<>
|
||||
<StatusSection/>
|
||||
<LogsSection/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default dashboard;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, {Fragment, useState} from 'react';
|
||||
import {
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import ky from "ky";
|
||||
import {PlayObject} from "../../../src/common/infrastructure/Atomic";
|
||||
import PlayDisplay from "../components/PlayDisplay";
|
||||
|
||||
const recent = () => {
|
||||
const {isLoading, isSuccess, isError, data, error} = useQuery({
|
||||
queryKey: ['recent?name=default&type=spotify'], queryFn: async () => {
|
||||
return await ky.get('/api/recent?name=default&type=spotify').json() as PlayObject[]
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid ">
|
||||
<div className="shadow-md rounded my-6 bg-gray-500 text-white">
|
||||
<div className="space-x-4 p-6 md:px-10 md:py-6 leading-6 font-semibold bg-gray-700 text-white">
|
||||
<h2>Recently Played
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-6 md:px-10 md:py-6">
|
||||
<ul>{data.map(x => <li key={x.data.track}><PlayDisplay data={x}/></li>)}</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default recent;
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
|
||||
@@ -1738,6 +1738,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@react-nano/use-event-source/-/use-event-source-0.13.0.tgz#38469b006e093c10e3cbfa0039cde59b404ae30b"
|
||||
integrity sha512-RzeoLepE/EnZTUkdIuZc1Odoswcfsu9bcTaoSqFhWkJ7j41T8COxcHatUHytJMiKKr+J7hpuAFoDGATshU98nQ==
|
||||
|
||||
"@remix-run/router@1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
|
||||
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
|
||||
|
||||
"@rollup/plugin-babel@^5.2.0":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
||||
@@ -9091,6 +9096,21 @@ react-refresh@^0.11.0:
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
|
||||
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
|
||||
|
||||
react-router-dom@^6.15.0:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.15.0.tgz#6da7db61e56797266fbbef0d5e324d6ac443ee40"
|
||||
integrity sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.8.0"
|
||||
react-router "6.15.0"
|
||||
|
||||
react-router@6.15.0:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
|
||||
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.8.0"
|
||||
|
||||
react-scripts@5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"
|
||||
|
||||
Reference in New Issue
Block a user