music: backup play log from now-playing samples (origin nowplaying; 30 s or half the track, ignoring pauses and seeks); recently-played takes those rows over with its exact timestamp
All checks were successful
Deploy production / deploy (push) Successful in 1m9s
All checks were successful
Deploy production / deploy (push) Successful in 1m9s
This commit is contained in:
@@ -110,8 +110,9 @@ export const musicPlays = musicSchema.table(
|
||||
* usually when the track started. See the music.plays_deduped view. */
|
||||
playedAt: timestamptz('played_at').notNull(),
|
||||
source: text('source').notNull().default('spotify'),
|
||||
/** How the row got here: 'api' (poller), 'export' (Spotify streaming history),
|
||||
* 'import' (Last.fm and other backfills), 'manual'. */
|
||||
/** How the row got here: 'api' (poller, recently-played), 'nowplaying' (poller backup
|
||||
* log from now-playing samples, until recently-played confirms it), 'export' (Spotify
|
||||
* streaming history), 'import' (Last.fm and other backfills), 'manual'. */
|
||||
origin: text('origin').notNull().default('api'),
|
||||
/** Known for imports; null for API plays (Spotify only reports plays of 30 s or more). */
|
||||
msPlayed: integer('ms_played'),
|
||||
@@ -126,7 +127,10 @@ export const musicPlays = musicSchema.table(
|
||||
index('plays_source_played_idx').on(t.source, t.playedAt),
|
||||
index('plays_track_played_idx').on(t.trackId, t.playedAt),
|
||||
check('plays_source_check', sql`${t.source} in ('spotify', 'lastfm', 'youtube', 'local')`),
|
||||
check('plays_origin_check', sql`${t.origin} in ('api', 'export', 'import', 'manual')`)
|
||||
check(
|
||||
'plays_origin_check',
|
||||
sql`${t.origin} in ('api', 'export', 'import', 'manual', 'nowplaying')`
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -6,11 +6,19 @@ import {
|
||||
handlePollError,
|
||||
pollPlayer,
|
||||
pollRecent,
|
||||
recordBackupPlay,
|
||||
recordPoll,
|
||||
SpotifyError
|
||||
} from '#lib/music/collector.server.js';
|
||||
import { newCache } from '#lib/music/store.server.js';
|
||||
import { detectChange, nextDelay, toLive, type PlayerSample } from '#lib/music/now-playing.js';
|
||||
import {
|
||||
detectChange,
|
||||
newPlayTracker,
|
||||
nextDelay,
|
||||
toLive,
|
||||
trackPlay,
|
||||
type PlayerSample
|
||||
} from '#lib/music/now-playing.js';
|
||||
import { currentLive, liveSubscribers, publishLive } from './live';
|
||||
import { getLiveFromDb } from './stats';
|
||||
|
||||
@@ -25,7 +33,9 @@ import { getLiveFromDb } from './stats';
|
||||
// every minute, plus ~10 s after a track change so a finished track lands quickly.
|
||||
// Spotify rate-limits endpoints separately (recently-played can be blocked for hours while
|
||||
// currently-playing still answers), so a 429 on the play log only pauses the play log, for
|
||||
// the full Retry-After, and never slows the now-playing card down.
|
||||
// the full Retry-After, and never slows the now-playing card down. Meanwhile the backup log
|
||||
// (trackPlay → origin 'nowplaying') records finished tracks from the now-playing samples;
|
||||
// recently-played later takes those rows over with its exact timestamp.
|
||||
const LEASE_SECONDS = 20; // renewed every tick; another container takes over 20 s after we stop
|
||||
const RECENT_MS = 60_000;
|
||||
const CATCH_UP_MS = 10_000;
|
||||
@@ -48,6 +58,7 @@ let catchUps = 0;
|
||||
let recentBlockedUntil = 0;
|
||||
let recentError: string | null = null;
|
||||
let cache = newCache();
|
||||
let tracker = newPlayTracker();
|
||||
|
||||
export const spotifyCredentials = () => ({
|
||||
clientId: SPOTIFY_CLIENT_ID,
|
||||
@@ -81,6 +92,7 @@ export async function tick(): Promise<number> {
|
||||
const auth = await acquireLease(client, HOLDER, LEASE_SECONDS);
|
||||
if (!auth) {
|
||||
last = null;
|
||||
tracker = newPlayTracker();
|
||||
await followDatabase().catch(() => {});
|
||||
return 10_000;
|
||||
}
|
||||
@@ -97,6 +109,10 @@ export async function tick(): Promise<number> {
|
||||
catchUps = 2;
|
||||
}
|
||||
last = sample;
|
||||
// Backup play log: works even while recently-played is rate-limited.
|
||||
const tracked = trackPlay(tracker, sample);
|
||||
tracker = tracked.state;
|
||||
if (tracked.finished) await recordBackupPlay(client, tracked.finished);
|
||||
idleTicks = sample.isPlaying ? 0 : idleTicks + 1;
|
||||
if (Date.now() >= nextRecentAt && Date.now() >= recentBlockedUntil) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user