diff --git a/src/backend/common/infrastructure/Atomic.ts b/src/backend/common/infrastructure/Atomic.ts index 719a0d6b..00001da6 100644 --- a/src/backend/common/infrastructure/Atomic.ts +++ b/src/backend/common/infrastructure/Atomic.ts @@ -243,6 +243,7 @@ export const NO_DEVICE = 'NoDevice'; export const NO_USER = 'SingleUser'; export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER]; +export const SINGLE_USER_PLATFORM_ID_STR = `${NO_DEVICE}-${NO_USER}`; export interface ScrobbledPlayObject { play: PlayObject diff --git a/src/backend/common/vendor/discord/DiscordIPCClient.ts b/src/backend/common/vendor/discord/DiscordIPCClient.ts index 19d940f9..5d28d223 100644 --- a/src/backend/common/vendor/discord/DiscordIPCClient.ts +++ b/src/backend/common/vendor/discord/DiscordIPCClient.ts @@ -11,7 +11,7 @@ import { removeUndefinedKeys, sleep } from "../../../utils.js"; import { playStateToActivityData } from "./DiscordUtils.js"; import { DiscordAbstractClient } from "./DiscordAbstractClient.js"; import dayjs from "dayjs"; -import { isPlayObject } from "../../../../core/Atomic.js"; +import { isPlayObject, SourcePlayerObj } from "../../../../core/Atomic.js"; import { mergeSimpleError, SimpleError } from "../../errors/MSErrors.js"; import { UpstreamError } from "../../errors/UpstreamError.js"; @@ -113,13 +113,13 @@ export class DiscordIPCClient extends DiscordAbstractClient { return true; } - async sendActivity(data?: SourceData | undefined) { + async sendActivity(data?: SourcePlayerObj | undefined) { if (data === undefined) { await this.sendClearActivity(); return; } const { activity: msActivity, artUrl } = playStateToActivityData(data); - const assets = await this.getArtAsset(data, artUrl, false); + const assets = await this.getArtAsset(data.play, artUrl, false); if (assets !== undefined) { const { assets: msAssets = {} diff --git a/src/backend/common/vendor/discord/DiscordUtils.ts b/src/backend/common/vendor/discord/DiscordUtils.ts index bf21c195..49109735 100644 --- a/src/backend/common/vendor/discord/DiscordUtils.ts +++ b/src/backend/common/vendor/discord/DiscordUtils.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { isPlayObject, PlayObject } from "../../../../core/Atomic.js"; +import { isPlayObject, PlayObject, SourcePlayerObj } from "../../../../core/Atomic.js"; import { asPlayerStateData, SourceData } from "../../infrastructure/Atomic.js"; import { GatewayActivity, GatewayOpcodes, PresenceUpdateStatus } from "discord.js"; import { capitalize } from "../../../../core/StringUtils.js"; @@ -8,24 +8,25 @@ import { ACTIVITY_TYPE, ActivityData, ActivityTypes, DiscordData, DiscordStrongD import { parseBool, removeUndefinedKeys } from "../../../utils.js"; import { parseArrayFromMaybeString, parseBoolOrArrayFromMaybeString } from "../../../utils/StringUtils.js"; -export const playStateToActivityData = (data: SourceData, opts: { useArt?: boolean } = {}): { activity: ActivityData, artUrl?: string } => { +export const playStateToActivityData = (data: SourcePlayerObj, opts: { useArt?: boolean } = {}): { activity: ActivityData, artUrl?: string } => { // unix timestamps in milliseconds let startTime: number, endTime: number; - let play: PlayObject; - if (isPlayObject(data)) { - play = data; - if (data.meta.trackProgressPosition !== undefined && play.data.duration !== undefined) { - startTime = dayjs().subtract(data.meta.trackProgressPosition, 's').unix() * 1000; - endTime = dayjs().add(data.data.duration - data.meta.trackProgressPosition, 's').unix() * 1000; - } else if (asPlayerStateData(data)) { - play = data.play; - if (data.position !== undefined && play.data.duration !== undefined) { - startTime = dayjs().subtract(data.position, 's').unix() * 1000; - endTime = dayjs().add(data.data.duration - data.position, 's').unix() * 1000; - } + let play: PlayObject = data.play; + + const position = data.position ?? data.play.meta?.trackProgressPosition; + if(position !== undefined && play.data.duration !== undefined) { + let realPosition = position; + if(data.playerLastUpdatedAt !== undefined) { + // if we know when player was last updated then we can forward-correct actual position + // by adding last known position + time since update + const lastUpdated = dayjs(data.playerLastUpdatedAt); + const sinceUpdate = dayjs().diff(lastUpdated, 's'); + realPosition = position + sinceUpdate; } + startTime = dayjs().subtract(realPosition, 's').unix() * 1000; + endTime = dayjs().add(play.data.duration - realPosition, 's').unix() * 1000; } let activityName = capitalize(play.meta?.musicService ?? play.meta?.mediaPlayerName ?? play.meta?.source ?? 'music') diff --git a/src/backend/common/vendor/discord/DiscordWSClient.ts b/src/backend/common/vendor/discord/DiscordWSClient.ts index 81ce9eb1..0ac5b671 100644 --- a/src/backend/common/vendor/discord/DiscordWSClient.ts +++ b/src/backend/common/vendor/discord/DiscordWSClient.ts @@ -8,7 +8,7 @@ import EventEmitter from "events"; import { randomInt } from "crypto"; import request from 'superagent'; import { AbstractApiOptions,SourceData } from "../../infrastructure/Atomic.js"; -import { isPlayObject } from "../../../../core/Atomic.js"; +import { isPlayObject, SourcePlayerObj } from "../../../../core/Atomic.js"; import dayjs, { Dayjs } from "dayjs"; import { getRoot } from "../../../ioc.js"; import { formatWebsocketClose, isCloseEvent, isErrorEvent, wsReadyStateToStr } from "../../../utils/NetworkUtils.js"; @@ -639,9 +639,9 @@ export class DiscordWSClient extends DiscordAbstractClient { } } - playStateToActivity = async (data: SourceData): Promise => { + playStateToActivity = async (data: SourcePlayerObj): Promise => { const {activity: msActivity, artUrl} = playStateToActivityData(data); - const assets = await this.getArtAsset(data, artUrl); + const assets = await this.getArtAsset(data.play, artUrl); if(assets !== undefined) { const { assets: msAssets = {} @@ -658,7 +658,7 @@ export class DiscordWSClient extends DiscordAbstractClient { return activity; } - sendActivity = async (data: SourceData | undefined) => { + sendActivity = async (data: SourcePlayerObj | undefined) => { if(data === undefined) { this.sendClearActivity(); return; diff --git a/src/backend/scrobblers/DiscordScrobbler.ts b/src/backend/scrobblers/DiscordScrobbler.ts index f3fbf4ae..d62fef36 100644 --- a/src/backend/scrobblers/DiscordScrobbler.ts +++ b/src/backend/scrobblers/DiscordScrobbler.ts @@ -1,7 +1,7 @@ import { Logger } from "@foxxmd/logging"; import EventEmitter from "events"; import { PlayObject, SourcePlayerObj } from "../../core/Atomic.js"; -import { CALCULATED_PLAYER_STATUSES, FormatPlayObjectOptions, REPORTED_PLAYER_STATUSES, ReportedPlayerStatus } from "../common/infrastructure/Atomic.js"; +import { CALCULATED_PLAYER_STATUSES, FormatPlayObjectOptions, REPORTED_PLAYER_STATUSES, ReportedPlayerStatus, SINGLE_USER_PLATFORM_ID_STR } from "../common/infrastructure/Atomic.js"; import { Notifiers } from "../notifier/Notifiers.js"; import AbstractScrobbleClient, { nowPlayingUpdateByPlayDuration } from "./AbstractScrobbleClient.js"; @@ -11,6 +11,7 @@ import { configToStrong } from "../common/vendor/discord/DiscordUtils.js"; import { DiscordIPCClient } from "../common/vendor/discord/DiscordIPCClient.js"; import { playStateToActivityData } from "../common/vendor/discord/DiscordUtils.js"; import { mergeSimpleError, SimpleError } from "../common/errors/MSErrors.js"; +import dayjs from "dayjs"; export default class DiscordScrobbler extends AbstractScrobbleClient { @@ -127,7 +128,18 @@ export default class DiscordScrobbler extends AbstractScrobbleClient { alreadyScrobbled = async (playObj: PlayObject, log = false) => true public playToClientPayload(playObj: PlayObject): any { - return playStateToActivityData(playObj).activity; + return playStateToActivityData({ + play: playObj, + platformId: SINGLE_USER_PLATFORM_ID_STR, + playerLastUpdatedAt: dayjs().toISOString(), + listenedDuration: 0, + status: { + reported: REPORTED_PLAYER_STATUSES.unknown, + calculated: CALCULATED_PLAYER_STATUSES.unknown, + stale: false, + orphaned: false + } + }).activity; } doScrobble = async (playObj: PlayObject) => { @@ -139,7 +151,7 @@ export default class DiscordScrobbler extends AbstractScrobbleClient { if([CALCULATED_PLAYER_STATUSES.stopped, CALCULATED_PLAYER_STATUSES.paused].includes(data.status.calculated as ReportedPlayerStatus)) { await this.api.sendActivity(undefined); } else { - await this.api.sendActivity(data.play); + await this.api.sendActivity(data); } } catch (e) { throw e; diff --git a/src/backend/sources/PlayerState/AbstractPlayerState.ts b/src/backend/sources/PlayerState/AbstractPlayerState.ts index 9c5ddf13..f533d0fe 100644 --- a/src/backend/sources/PlayerState/AbstractPlayerState.ts +++ b/src/backend/sources/PlayerState/AbstractPlayerState.ts @@ -435,6 +435,7 @@ export abstract class AbstractPlayerState { playLastUpdatedAt: this.playLastUpdatedAt !== undefined ? this.playLastUpdatedAt.toISOString() : undefined, playFirstSeenAt: this.playFirstSeenAt !== undefined ? this.playFirstSeenAt.toISOString() : undefined, playerLastUpdatedAt: this.stateLastUpdatedAt.toISOString(), + createdAt: dayjs().unix(), position: this.getPosition(), listenedDuration: this.getListenDuration(), status: { diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index 0183d2bf..cdd197c8 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -365,6 +365,7 @@ export interface SourcePlayerObj { playFirstSeenAt?: string, playLastUpdatedAt?: string, playerLastUpdatedAt: string + createdAt?: number position?: Second listenedDuration: Second nowPlayingMode?: boolean