refactor: Re-organize server/client structure so everything exists on same port

Used https://spin.atomicobject.com/2020/08/17/cra-express-share-code as a base for refactoring
This commit is contained in:
FoxxMD
2023-08-16 14:47:21 -04:00
parent e2b472cf44
commit 7cb215be52
125 changed files with 960 additions and 831 deletions
+1
View File
@@ -17,3 +17,4 @@ flatpak/generated-sources.json
flatpak/.flatpak-builder
docsite/build
docsite/node_modules
build
+2
View File
@@ -127,3 +127,5 @@ src/**/**.js.map
*.bak
.flatpak-builder
flatpak/generated-sources.json
build
+48 -10
View File
@@ -4,11 +4,6 @@
"description": "scrobble plays from multiple sources to multiple clients",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "concurrently -c \"auto\" --names \"server,client\" \"npm:server\" \"npm:client\"",
"server": "node src/index.js",
"client": "node start-client.js",
"schema": "npm run -s schema-aio & npm run -s schema-source & npm run -s schema-client & npm run -s schema-aiosource & npm run -s schema-aioclient",
"schema-client": "typescript-json-schema tsconfig.json ClientConfig --out src/common/schema/client.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-source": "typescript-json-schema tsconfig.json SourceConfig --out src/common/schema/source.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
@@ -17,7 +12,17 @@
"schema-aioclient": "typescript-json-schema tsconfig.json AIOClientConfig --out src/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"typedoc": "typedoc",
"postinstall": "patch-package",
"circular": "madge --circular --extensions ts src/index.ts"
"circular": "madge --circular --extensions ts src/index.ts",
"test": "react-scripts test",
"eject": "react-scripts eject",
"dev": "concurrently -p name -c \"yellow,magenta,blue\" -n \"webpack-server,nodemon-server,CRA\" \"yarn dev:server:webpack\" \"yarn dev:server:nodemon\" \"yarn dev:client\"",
"dev:client": "wait-on -l tcp:9079 && react-scripts start",
"dev:server": "concurrently -p name -c \"yellow,magenta\" -n \"webpack-server,nodemon-server\" \"yarn dev:server:webpack\" \"yarn dev:server:nodemon\"",
"dev:server:webpack": "webpack --config webpack.config.server.js --watch",
"dev:server:nodemon": "rm -f build/server.js && wait-on -l build/server.js && nodemon build/server.js",
"build": "yarn build:client && yarn build:server",
"build:client": "react-scripts build",
"build:server": "webpack --config webpack.config.server.js"
},
"exports": {
".": {
@@ -38,15 +43,15 @@
"bugs": {
"url": "https://github.com/FoxxMD/multi-scrobbler/issues"
},
"homepage": "https://github.com/FoxxMD/multi-scrobbler#readme",
"private": true,
"workspaces": [
"web"
],
"homepage": ".",
"proxy": "http://localhost:9079",
"dependencies": {
"@awaitjs/express": "^0.6.3",
"@foxxmd/winston": "3.3.31",
"@kenyip/backoff-strategies": "^1.0.4",
"@react-nano/use-event-source": "^0.13.0",
"@tanstack/react-query": "^4.32.1",
"ajv": "^7.2.4",
"better-sse": "^0.8.0",
"body-parser": "^1.19.0",
@@ -66,19 +71,32 @@
"iti": "^0.6.0",
"json5": "^2.2.3",
"kodi-api": "^0.2.1",
"ky": "^0.33.3",
"lastfm-node-client": "^2.2.0",
"mopidy": "^1.3.0",
"nodemon": "^3.0.1",
"normalize-url": "^6.1.0",
"ntfy": "1.0.5",
"p-event": "^4.2.0",
"passport": "^0.6.0",
"passport-deezer": "^0.2.0",
"pony-cause": "^1.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"safe-stable-stringify": "^1.1.1",
"socket.io": "^4.6.1",
"spotify-web-api-node": "^5.0.2",
"superagent": "^8.0.9",
"tailwindcss": "^3.3.3",
"triple-beam": "^1.3.0",
"ts-loader": "^9.4.4",
"typescript": "^4.9.5",
"wait-on": "^7.0.1",
"web-vitals": "^2.1.4",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0",
"winston-daily-rotate-file": "^4.5.0",
"winston-duplex": "0.1.3",
"winston-null": "^2.0.0",
@@ -87,14 +105,22 @@
"youtube-music-ts-api": "github:FoxxMD/youtube-music-ts-api#libraryHistoryImprovements-dist"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@tsconfig/node18": "^1.0.1",
"@types/common-tags": "^1.8.1",
"@types/concat-stream": "^2.0.0",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/formidable": "^2.0.5",
"@types/jest": "^27.5.2",
"@types/node": "^18.0.0",
"@types/passport": "^1.0.12",
"@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7",
"@types/react-window": "^1.8.5",
"@types/spotify-web-api-node": "^5.0.7",
"@types/superagent": "^4.1.16",
"@types/triple-beam": "^1.3.2",
@@ -107,6 +133,18 @@
"typescript": "^4.9.5",
"typescript-json-schema": "~0.55"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"overrides": {
"spotify-web-api-node": {
"superagent": "$superagent"

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

+3 -3
View File
@@ -10,17 +10,17 @@ import './App.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
import Dashboard from "./dashboard/dashboard";
import RecentPage from "./recent/RecentPage";
//import RecentPage from "./recent/RecentPage";
const router = createBrowserRouter([
{
path: "/",
element: <Dashboard />,
},
{
/* {
path: "/recent",
element: <RecentPage />,
},
},*/
]);
function App() {
@@ -1,13 +1,10 @@
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";
import {JsonPlayObject, TrackStringOptions} from "../../core/Atomic.js";
import {buildTrackString, buildTrackStringReactOptions} from "../../core/StringUtils.js";
interface PlayObjectProps {
data: PlayObject
data: JsonPlayObject
artistLength?: number
trackLength?: number
buildOptions?: TrackStringOptions<ReactElement> & {includeWeb?: boolean}
@@ -1,7 +1,7 @@
import React, {Fragment} from 'react';
import {ClientStatusData} from "../../../../src/common/infrastructure/Atomic";
import StatusCardSkeleton, {StatusCardSkeletonData} from "./StatusCardSkeleton";
import SkeletonParagraph from "../skeleton/SkeletonParagraph";
import {ClientStatusData} from "../../../core/Atomic";
export interface ClientStatusCardData extends StatusCardSkeletonData {
loading?: boolean
@@ -1,7 +1,7 @@
import React, {Fragment} from 'react';
import {ClientStatusData, SourceStatusData} from "../../../../src/common/infrastructure/Atomic";
import StatusCardSkeleton, {StatusCardSkeletonData} from "./StatusCardSkeleton";
import SkeletonParagraph from "../skeleton/SkeletonParagraph";
import {SourceStatusData} from "../../../core/Atomic";
export interface SourceStatusCardData extends StatusCardSkeletonData {
loading?: boolean
@@ -1,5 +1,4 @@
import React, {PropsWithChildren} from 'react';
import {ClientStatusData, SourceStatusData} from "../../../../src/common/infrastructure/Atomic";
import SkeletonTitle from "../skeleton/SkeletonTitle";
import SkeletonParagraph from "../skeleton/SkeletonParagraph";

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -3,13 +3,13 @@ import {
useQuery,
} from '@tanstack/react-query'
import ky from "ky";
import {PlayObject} from "../../../src/common/infrastructure/Atomic";
import PlayDisplay from "../components/PlayDisplay";
import {JsonPlayObject} from "../../core/Atomic.js";
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 await ky.get('/api/recent?name=default&type=spotify').json() as JsonPlayObject[]
}
});
@@ -4,9 +4,9 @@ import {
} from '@tanstack/react-query'
import ky from "ky";
import StatusCardSkeleton from "../components/statusCard/StatusCardSkeleton";
import {ClientStatusData, SourceStatusData} from "../../../src/common/infrastructure/Atomic";
import SourceStatusCard from "../components/statusCard/SourceStatusCard";
import ClientStatusCard from "../components/statusCard/ClientStatusCard";
import {ClientStatusData, SourceStatusData} from "../../core/Atomic";
const StatusSection = () => {
const {isLoading, isSuccess, isError, data, error} = useQuery({
+125
View File
@@ -0,0 +1,125 @@
import {Dayjs} from "dayjs";
export interface SourceStatusData {
status: string;
type: "spotify" | "plex" | "tautulli" | "subsonic" | "jellyfin" | "lastfm" | "deezer" | "ytmusic" | "mpris" | "mopidy" | "listenbrainz" | "jriver" | "kodi";
display: string;
tracksDiscovered: number;
name: string;
canPoll: boolean;
hasAuth: boolean;
hasAuthInteraction: boolean;
authed: boolean;
}
export interface ClientStatusData {
status: string;
type: "maloja" | "lastfm" | "listenbrainz";
display: string;
tracksDiscovered: number;
name: string;
hasAuth: boolean;
}
export interface TrackStringOptions<T = string> {
include?: ('time' | 'artist' | 'track' | 'timeFromNow' | 'trackId')[]
transformers?: {
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
}
}
export interface PlayProgress {
timestamp: Dayjs
position?: number
positionPercent?: number
}
export type ListenRange = [PlayProgress, PlayProgress]
export interface TrackData {
artists?: string[]
album?: string
track?: string
/**
* The length of the track, in seconds
* */
duration?: number
meta?: {
brainz?: {
artist?: string
albumArtist?: string
album?: string
track?: string
releaseGroup?: string
}
}
}
export interface PlayData extends TrackData {
/**
* The date the track was played at
* */
playDate?: Dayjs | string
/** Number of seconds the track was listened to */
listenedFor?: number
listenRanges?: ListenRange[]
}
export interface PlayMeta {
source?: string
/**
* Specifies from what facet/data from the source this play was parsed from IE history, now playing, etc...
* */
parsedFrom?: string
/**
* Unique ID for this track, given by the Source
* */
trackId?: string
/**
* Atomic ID for this instance of played tracked IE a unique ID for "this track played at this time"
* */
playId?: string
newFromSource?: boolean
url?: {
web: string
[key: string]: string
}
user?: string
mediaType?: string
server?: string
library?: string
/**
* The position the "player" is at in the track at the time the play was reported, in seconds
* */
trackProgressPosition?: number
/**
* A unique identifier for the device playing this track
* */
deviceId?: string
[key: string]: any
}
export interface AmbPlayObject {
data: PlayData,
meta: PlayMeta
}
export interface PlayObject extends AmbPlayObject {
data: ObjectPlayData,
}
export interface JsonPlayObject extends AmbPlayObject {
playDate?: string
}
export interface ObjectPlayData extends PlayData {
playDate?: Dayjs
}
@@ -1,11 +1,11 @@
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";
import {AmbPlayObject, TrackStringOptions} from "./Atomic.js";
dayjs.extend(utc)
dayjs.extend(isBetween);
@@ -36,7 +36,7 @@ export const defaultBuildTrackStringTransformers = {
time: defaultTimeFunc,
timeFromNow: defaultTimeFromNowFunc,
}
export const buildTrackString = <T = string>(playObj: PlayObject, options: TrackStringOptions<T> = {}): T => {
export const buildTrackString = <T = string>(playObj: AmbPlayObject, options: TrackStringOptions<T> = {}): T => {
const {
include = ['time', 'artist', 'track'],
transformers: {
@@ -59,6 +59,8 @@ export const buildTrackString = <T = string>(playObj: PlayObject, options: Track
} = {},
} = playObj;
const pd = typeof playDate === 'string' ? dayjs(playDate) : playDate;
const strParts: (T | string)[] = [];
if (include.includes('trackId') && trackId !== undefined) {
strParts.push(`(${trackId})`);
@@ -70,10 +72,10 @@ export const buildTrackString = <T = string>(playObj: PlayObject, options: Track
strParts.push(trackFunc(track, strParts.length > 0));
}
if (include.includes('time')) {
strParts.push(timeFunc(playDate));
strParts.push(timeFunc(pd));
}
if (include.includes('timeFromNow')) {
const tfn = timeFromNow(playDate);
const tfn = timeFromNow(pd);
if (tfn !== undefined) {
strParts.push(tfn)
}
+3 -3
View File
@@ -1,8 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './client/index.css';
import App from './client/App';
import reportWebVitals from './client/reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
+2 -2
View File
@@ -3,8 +3,8 @@ import {getRoot} from "../ioc.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";
import {ClientStatusData, PlayObject, SourceStatusData, TrackStringOptions} from "../../core/Atomic.js";
import {buildTrackString, longestString, truncateStringToLength} from "../../core/StringUtils.js";
@@ -1,7 +1,8 @@
import {capitalize, mergeArr} from "../utils.js";
import {Logger} from '@foxxmd/winston';
import {FormatPlayObjectOptions, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions} from "../common/infrastructure/Atomic.js";
import winston from '@foxxmd/winston';
import {PlayObject} from "../../core/Atomic.js";
export default abstract class AbstractApiClient {
name: string;
@@ -5,8 +5,9 @@ import { KodiClient } from 'kodi-api'
import normalizeUrl from "normalize-url";
import {URL} from "url";
import {RecentlyPlayedOptions} from "../sources/AbstractSource.js";
import {FormatPlayObjectOptions, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions} from "../common/infrastructure/Atomic.js";
import dayjs from "dayjs";
import {PlayObject} from "../../core/Atomic.js";
interface KodiDuration {
hours: number
@@ -2,8 +2,9 @@ import LastFm, {AuthGetSessionResponse, TrackObject, UserGetInfoResponse} from "
import AbstractApiClient from "./AbstractApiClient.js";
import dayjs from "dayjs";
import {readJson, sleep, writeFile} from "../utils.js";
import {FormatPlayObjectOptions, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions} from "../common/infrastructure/Atomic.js";
import {LastfmData} from "../common/infrastructure/config/client/lastfm.js";
import {PlayObject} from "../../core/Atomic.js";
const badErrors = [
'api key suspended',
@@ -1,7 +1,7 @@
import AbstractApiClient from "./AbstractApiClient.js";
import request, {Request} from 'superagent';
import {ListenBrainzClientData} from "../common/infrastructure/config/client/listenbrainz.js";
import {FormatPlayObjectOptions, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions} from "../common/infrastructure/Atomic.js";
import dayjs from "dayjs";
import {
containsDelimiters, findDelimiters,
@@ -11,6 +11,7 @@ import {
unique,
uniqueNormalizedStrArr
} from "../utils.js";
import {PlayObject} from "../../core/Atomic.js";
export interface ArtistMBIDMapping {
@@ -10,14 +10,15 @@ import {
INITIALIZING,
InitState,
NOT_INITIALIZED,
PlayObject, ScrobbledPlayObject, TrackStringOptions
ScrobbledPlayObject
} from "../common/infrastructure/Atomic.js";
import winston, {Logger} from '@foxxmd/winston';
import {CommonClientConfig} from "../common/infrastructure/config/client/index.js";
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";
import {PlayObject, TrackStringOptions} from "../../core/Atomic.js";
import {buildTrackString, truncateStringToLength} from "../../core/StringUtils.js";
export default abstract class AbstractScrobbleClient {
@@ -12,14 +12,14 @@ import LastfmApiClient from "../apis/LastfmApiClient.js";
import {
FormatPlayObjectOptions,
INITIALIZING,
PlayObject, ScrobbledPlayObject,
TrackStringOptions
ScrobbledPlayObject
} from "../common/infrastructure/Atomic.js";
import {LastfmClientConfig} from "../common/infrastructure/config/client/lastfm.js";
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";
import {PlayObject, TrackStringOptions} from "../../core/Atomic.js";
import {buildTrackString} from "../../core/StringUtils.js";
export default class LastfmScrobbler extends AbstractScrobbleClient {
@@ -11,15 +11,14 @@ import {
import LastfmApiClient from "../apis/LastfmApiClient.js";
import {
FormatPlayObjectOptions,
INITIALIZING,
PlayObject,
TrackStringOptions
INITIALIZING
} from "../common/infrastructure/Atomic.js";
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";
import {PlayObject, TrackStringOptions} from "../../core/Atomic.js";
import {buildTrackString} from "../../core/StringUtils.js";
export default class ListenbrainzScrobbler extends AbstractScrobbleClient {
@@ -11,9 +11,7 @@ import {
} from "../utils.js";
import {
FormatPlayObjectOptions,
INITIALIZING,
PlayObject,
TrackStringOptions
INITIALIZING
} from "../common/infrastructure/Atomic.js";
import {MalojaClientConfig} from "../common/infrastructure/config/client/maloja.js";
import {Notifiers} from "../notifier/Notifiers.js";
@@ -24,7 +22,8 @@ import {
MalojaScrobbleV2RequestData,
MalojaScrobbleV3RequestData, MalojaScrobbleV3ResponseData, MalojaV2ScrobbleData, MalojaV3ScrobbleData
} from "../apis/maloja/interfaces";
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
import {PlayObject, TrackStringOptions} from "../../core/Atomic.js";
import {buildTrackString} from "../../core/StringUtils.js";
const feat = ["ft.", "ft", "feat.", "feat", "featuring", "Ft.", "Ft", "Feat.", "Feat", "Featuring"];
@@ -8,7 +8,7 @@ import {
} from "../utils.js";
import MalojaScrobbler from "./MalojaScrobbler.js";
import LastfmScrobbler from "./LastfmScrobbler.js";
import {clientTypes, ConfigMeta, PlayObject} from "../common/infrastructure/Atomic.js";
import {clientTypes, ConfigMeta} from "../common/infrastructure/Atomic.js";
import {AIOConfig} from "../common/infrastructure/config/aioConfig.js";
import * as aioSchema from '../common/schema/aio-client.json';
import * as clientSchema from '../common/schema/client.json';
@@ -22,7 +22,8 @@ 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";
import {PlayObject} from "../../core/Atomic.js";
import {buildTrackString} from "../../core/StringUtils.js";
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
@@ -4,5 +4,5 @@ import * as path from 'path';
//const __filename = fileURLToPath(import.meta.url);
//const __dirname = path.dirname(__filename);
export const projectDir = path.resolve(__dirname, '../../');
export const projectDir = process.cwd(); //path.resolve(__dirname, '../../../');
export const configDir: string = path.resolve(projectDir, './config');
@@ -3,9 +3,9 @@ import {FixedSizeList} from 'fixed-size-list';
import {MESSAGE} from 'triple-beam';
import {Logger} from '@foxxmd/winston';
import TupleMap from "../TupleMap.js";
import is from "@sindresorhus/is";
import {Request, Response} from "express";
import {NextFunction, ParamsDictionary, Query} from "express-serve-static-core";
import {PlayMeta, PlayObject} from "../../../core/Atomic.js";
export type SourceType = 'spotify' | 'plex' | 'tautulli' | 'subsonic' | 'jellyfin' | 'lastfm' | 'deezer' | 'ytmusic' | 'mpris' | 'mopidy' | 'listenbrainz' | 'jriver' | 'kodi';
export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic', 'mpris', 'mopidy', 'listenbrainz', 'jriver', 'kodi'];
@@ -69,85 +69,6 @@ export const asPlayerStateData = (obj: object): obj is PlayerStateData => {
return 'platformId' in obj && 'play' in obj;
}
export interface PlayProgress {
timestamp: Dayjs
position?: number
positionPercent?: number
}
export type ListenRange = [PlayProgress, PlayProgress]
export interface TrackData {
artists?: string[]
album?: string
track?: string
/**
* The length of the track, in seconds
* */
duration?: number
meta?: {
brainz?: {
artist?: string
albumArtist?: string
album?: string
track?: string
releaseGroup?: string
}
}
}
export interface PlayData extends TrackData {
/**
* The date the track was played at
* */
playDate?: Dayjs
/** Number of seconds the track was listened to */
listenedFor?: number
listenRanges?: ListenRange[]
}
export interface PlayMeta {
source?: string
/**
* Specifies from what facet/data from the source this play was parsed from IE history, now playing, etc...
* */
parsedFrom?: string
/**
* Unique ID for this track, given by the Source
* */
trackId?: string
/**
* Atomic ID for this instance of played tracked IE a unique ID for "this track played at this time"
* */
playId?: string
newFromSource?: boolean
url?: {
web: string
[key: string]: string
}
user?: string
mediaType?: string
server?: string
library?: string
/**
* The position the "player" is at in the track at the time the play was reported, in seconds
* */
trackProgressPosition?: number
/**
* A unique identifier for the device playing this track
* */
deviceId?: string
[key: string]: any
}
export interface PlayObject {
data: PlayData,
meta: PlayMeta
}
export interface FormatPlayObjectOptions {
newFromSource?: boolean
parsedFrom?: string
@@ -173,17 +94,6 @@ export const NO_USER = 'SingleUser';
export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER];
export interface TrackStringOptions<T = string> {
include?: ('time' | 'artist' | 'track' | 'timeFromNow' | 'trackId')[]
transformers?: {
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
}
}
export interface ScrobbledPlayObject {
play: PlayObject
scrobble: PlayObject
@@ -282,27 +192,6 @@ export type ExpressRequest = Request<ParamsDictionary, any, any, Query, Record<s
export type ExpressResponse = Response<any, Record<string, any>>;
export type ExpressHandler = (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>
export interface SourceStatusData {
status: string;
type: "spotify" | "plex" | "tautulli" | "subsonic" | "jellyfin" | "lastfm" | "deezer" | "ytmusic" | "mpris" | "mopidy" | "listenbrainz" | "jriver" | "kodi";
display: string;
tracksDiscovered: number;
name: string;
canPoll: boolean;
hasAuth: boolean;
hasAuthInteraction: boolean;
authed: boolean;
}
export interface ClientStatusData {
status: string;
type: "maloja" | "lastfm" | "listenbrainz";
display: string;
tracksDiscovered: number;
name: string;
hasAuth: boolean;
}
export interface numberFormatOptions {
toFixed: number,
defaultVal?: any,
@@ -11,8 +11,6 @@ import DailyRotateFile from 'winston-daily-rotate-file';
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;
+19 -6
View File
@@ -19,7 +19,7 @@ import {
remoteHostIdentifiers,
sleep
} from "./utils.js";
import {makeClientCheckMiddle, makeSourceCheckMiddle} from "./server/middleware.js";
import {makeClientCheckMiddle, makeSourceCheckMiddle} from "./api/middleware.js";
import TautulliSource from "./sources/TautulliSource.js";
import PlexSource, {plexRequestMiddle} from "./sources/PlexSource.js";
import JellyfinSource from "./sources/JellyfinSource.js";
@@ -33,9 +33,7 @@ import AbstractSource from "./sources/AbstractSource.js";
import {
ExpressHandler,
LogInfo,
LogLevel,
PlayObject,
TrackStringOptions
LogLevel
} from "./common/infrastructure/Atomic.js";
import SpotifySource from "./sources/SpotifySource.js";
import {JellyfinNotifier} from "./sources/ingressNotifiers/JellyfinNotifier.js";
@@ -45,9 +43,12 @@ import {AIOConfig} from "./common/infrastructure/config/aioConfig.js";
import {getRoot} from "./ioc.js";
import {formatLogToHtml, getLogger, isLogLineMinLevel} from "./common/logging.js";
import {MESSAGE} from "triple-beam";
import {setupApi} from "./server/api.js";
import {setupApi} from "./api/api.js";
import {Transform} from "stream";
import {buildTrackString, longestString, truncateStringToLength} from "./utils/StringUtils.js";
import {PlayObject, TrackStringOptions} from "../core/Atomic.js";
import {buildTrackString, longestString, truncateStringToLength} from "../core/StringUtils.js";
const buildDir = path.join(process.cwd() + "/build");
dayjs.extend(utc)
@@ -67,6 +68,13 @@ let io: Server | undefined = undefined;
app.use(router);
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
app.use(express.static(buildDir));
app.use(session({secret: 'keyboard cat', resave: false, saveUninitialized: false}));
app.use(passport.initialize());
@@ -85,6 +93,7 @@ initLogger.stream().on('log', (log: LogInfo) => {
let logger: Logger;
const f = projectDir;
const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`);
@@ -649,6 +658,10 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`)
logger.info(`Some sources are not ready, open ${localUrl} to continue`);
}
app.get("/*", function (req, res) {
res.sendFile(path.join(buildDir, "index.html"));
});
app.set('views', path.resolve(projectDir, 'src/views'));
app.set('view engine', 'ejs');
logger.info(`Server started at ${localUrl}`);
View File
@@ -11,7 +11,7 @@ import {
GroupedFixedPlays,
GroupedPlays,
InternalConfig, NO_DEVICE, NO_USER,
PlayObject, PlayPlatformId, PlayUserId, ProgressAwarePlayObject, SINGLE_USER_PLATFORM_ID,
PlayPlatformId, PlayUserId, ProgressAwarePlayObject, SINGLE_USER_PLATFORM_ID,
SourceType
} from "../common/infrastructure/Atomic.js";
import {Logger} from '@foxxmd/winston';
@@ -19,7 +19,8 @@ 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";
import {PlayObject} from "../../core/Atomic.js";
import {buildTrackString} from "../../core/StringUtils.js";
export interface RecentlyPlayedOptions {
limit?: number
@@ -4,8 +4,9 @@ import {Strategy as DeezerStrategy} from 'passport-deezer';
import AbstractSource, {RecentlyPlayedOptions} from "./AbstractSource.js";
import dayjs from "dayjs";
import {DeezerSourceConfig} from "../common/infrastructure/config/source/deezer.js";
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import EventEmitter from "events";
import {PlayObject} from "../../core/Atomic.js";
export default class DeezerSource extends AbstractSource {
workingCredsPath;
@@ -1,5 +1,5 @@
import MemorySource from "./MemorySource.js";
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import dayjs from "dayjs";
import {URL} from "url";
import normalizeUrl from 'normalize-url';
@@ -7,6 +7,7 @@ import {EventEmitter} from "events";
import {RecentlyPlayedOptions} from "./AbstractSource.js";
import {JRiverSourceConfig} from "../common/infrastructure/config/source/jriver.js";
import {Info, JRiverApiClient, PLAYER_STATE} from "../apis/JRiverApiClient.js";
import {PlayObject} from "../../core/Atomic.js";
export class JRiverSource extends MemorySource {
declare config: JRiverSourceConfig;
@@ -8,12 +8,13 @@ import {
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";
import {FormatPlayObjectOptions, InternalConfig, PlayPlatformId} from "../common/infrastructure/Atomic.js";
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";
import {PlayObject} from "../../core/Atomic.js";
import {buildTrackString, truncateStringToLength} from "../../core/StringUtils.js";
const shortDeviceId = truncateStringToLength(10, '');
@@ -1,9 +1,10 @@
import MemorySource from "./MemorySource.js";
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import {EventEmitter} from "events";
import {RecentlyPlayedOptions} from "./AbstractSource.js";
import {KodiSourceConfig} from "../common/infrastructure/config/source/kodi.js";
import {KodiApiClient} from "../apis/KodiApiClient.js";
import {PlayObject} from "../../core/Atomic.js";
export class KodiSource extends MemorySource {
declare config: KodiSourceConfig;
@@ -2,9 +2,10 @@ import AbstractSource, {RecentlyPlayedOptions} from "./AbstractSource.js";
import LastfmApiClient from "../apis/LastfmApiClient.js";
import {sortByOldestPlayDate} from "../utils.js";
import {LastfmClientConfig} from "../common/infrastructure/config/client/lastfm.js";
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import {UserGetRecentTracksResponse} from "lastfm-node-client";
import EventEmitter from "events";
import {PlayObject} from "../../core/Atomic.js";
export default class LastfmSource extends AbstractSource {
@@ -7,12 +7,13 @@ import {
PlaybackStatus, PlayerInfo,
PROPERTIES_IFACE
} from "../common/infrastructure/config/source/mpris.js";
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import MemorySource from "./MemorySource.js";
import {RecentlyPlayedOptions} from "./AbstractSource.js";
import {removeDuplicates} from "../utils.js";
import EventEmitter from "events";
import {ErrorWithCause} from "pony-cause";
import {PlayObject} from "../../core/Atomic.js";
export class MPRISSource extends MemorySource {
@@ -16,7 +16,7 @@ import {
asPlayerStateData,
DeviceId,
GroupedPlays, PlayerStateData,
PlayObject, PlayPlatformId, PlayUserId,
PlayPlatformId, PlayUserId,
ProgressAwarePlayObject,
ScrobbleThresholdResult
} from "../common/infrastructure/Atomic.js";
@@ -24,7 +24,8 @@ 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";
import {PlayObject} from "../../core/Atomic.js";
import {buildTrackString} from "../../core/StringUtils.js";
export default class MemorySource extends AbstractSource {
/*
@@ -4,7 +4,6 @@ import {
FormatPlayObjectOptions,
InternalConfig,
PlayerStateData,
PlayObject,
SINGLE_USER_PLATFORM_ID
} from "../common/infrastructure/Atomic.js";
import dayjs from "dayjs";
@@ -15,10 +14,8 @@ import {EventEmitter} from "events";
import pEvent from 'p-event';
import winston from '@foxxmd/winston';
import {RecentlyPlayedOptions} from "./AbstractSource.js";
import request from "superagent";
import {removeDuplicates} from "../utils.js";
import {SubsonicSource} from "./SubsonicSource.js";
import {buildTrackString} from "../utils/StringUtils.js";
import {PlayObject} from "../../core/Atomic.js";
import {buildTrackString} from "../../core/StringUtils.js";
export class MopidySource extends MemorySource {
declare config: MopidySourceConfig;
@@ -1,8 +1,6 @@
import {
CALCULATED_PLAYER_STATUSES,
CalculatedPlayerStatus,
ListenRange, PlayData,
PlayObject,
PlayPlatformId,
REPORTED_PLAYER_STATUSES,
ReportedPlayerStatus
@@ -11,7 +9,8 @@ import dayjs, {Dayjs} from "dayjs";
import {formatNumber, genGroupIdStr, playObjDataMatch, progressBar} from "../../utils.js";
import {Logger} from "@foxxmd/winston";
import {ListenProgress} from "./ListenProgress.js";
import {buildTrackString} from "../../utils/StringUtils.js";
import {ListenRange, PlayData, PlayObject} from "../../../core/Atomic.js";
import {buildTrackString} from "../../../core/StringUtils.js";
export interface PlayerStateIntervals {
staleInterval?: number
@@ -1,7 +1,8 @@
import {GenericPlayerState} from "./GenericPlayerState.js";
import {Logger} from "@foxxmd/winston";
import {PlayObject, PlayPlatformId, ReportedPlayerStatus} from "../../common/infrastructure/Atomic.js";
import {PlayPlatformId, ReportedPlayerStatus} from "../../common/infrastructure/Atomic.js";
import {PlayerStateOptions} from "./AbstractPlayerState.js";
import {PlayObject} from "../../../core/Atomic.js";
export class JellyfinPlayerState extends GenericPlayerState {
constructor(logger: Logger, platformId: PlayPlatformId, opts?: PlayerStateOptions) {
@@ -1,5 +1,6 @@
import dayjs, {Dayjs} from "dayjs";
import {PlayProgress} from "../../common/infrastructure/Atomic.js";
import {PlayProgress} from "../../../core/Atomic.js";
export class ListenProgress implements PlayProgress {
@@ -7,10 +7,11 @@ import AbstractSource from "./AbstractSource.js";
import formidable from 'formidable';
import concatStream from 'concat-stream';
import {PlexSourceConfig} from "../common/infrastructure/config/source/plex.js";
import {FormatPlayObjectOptions, InternalConfig, PlayObject, SourceType} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions, InternalConfig, SourceType} from "../common/infrastructure/Atomic.js";
import EventEmitter from "events";
import winston from '@foxxmd/winston';
import {buildTrackString, truncateStringToLength} from "../utils/StringUtils.js";
import {PlayObject} from "../../core/Atomic.js";
import {truncateStringToLength} from "../../core/StringUtils.js";
const shortDeviceId = truncateStringToLength(10, '');
@@ -12,7 +12,7 @@ import {
InternalConfig,
NO_USER,
PlayerStateData,
PlayObject, ReportedPlayerStatus, SourceData
ReportedPlayerStatus, SourceData
} from "../common/infrastructure/Atomic.js";
import PlayHistoryObject = SpotifyApi.PlayHistoryObject;
import EventEmitter from "events";
@@ -23,7 +23,8 @@ 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";
import {PlayObject} from "../../core/Atomic.js";
import {buildTrackString, truncateStringToLength} from "../../core/StringUtils.js";
const scopes = ['user-read-recently-played', 'user-read-currently-playing', 'user-read-playback-state', 'user-read-playback-position'];
const state = 'random';
@@ -5,10 +5,10 @@ import isSameOrAfter from "dayjs/plugin/isSameOrAfter.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 {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import {RecentlyPlayedOptions} from "./AbstractSource.js";
import EventEmitter from "events";
import {buildTrackString} from "../utils/StringUtils.js";
import {PlayObject} from "../../core/Atomic.js";
dayjs.extend(isSameOrAfter);
@@ -1,11 +1,12 @@
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 {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import {combinePartsToString} from "../utils.js";
import EventEmitter from "events";
import {Request} from "express";
import {truncateStringToLength} from "../utils/StringUtils.js";
import {PlayObject} from "../../core/Atomic.js";
import {truncateStringToLength} from "../../core/StringUtils.js";
const shortDeviceId = truncateStringToLength(10, '');
@@ -1,13 +1,14 @@
import YouTubeMusic from "youtube-music-ts-api";
import AbstractSource, {RecentlyPlayedOptions} from "./AbstractSource.js";
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
import {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.js";
import {IYouTubeMusicAuthenticated} from "youtube-music-ts-api/interfaces-primary";
import dayjs from "dayjs";
import {parseDurationFromTimestamp, playObjDataMatch} from "../utils.js";
import {IPlaylistDetail, ITrackDetail} from "youtube-music-ts-api/interfaces-supplementary";
import {YTMusicSourceConfig} from "../common/infrastructure/config/source/ytmusic.js";
import EventEmitter from "events";
import {PlayObject} from "../../core/Atomic.js";
export default class YTMusicSource extends AbstractSource {
apiInstance?: IYouTubeMusicAuthenticated

Some files were not shown because too many files have changed in this diff Show More