@@ -197,21 +197,12 @@ export class TransferJob {
197197 } = resp ;
198198
199199 if ( pageInfo && ! totalPagesKnown ) {
200- const totalPages = parseInt ( pageInfo . totalPages , 10 ) ;
201- const total = parseInt ( pageInfo . total , 10 ) ;
202-
203- // If playCount is specified, cap the total at playCount
204- if ( this . playCount ) {
205- this . progress . total = Math . min ( this . playCount , total ) ;
206- // Calculate how many pages we'll actually need
207- this . progress . totalPages = Math . ceil ( this . progress . total / this . PAGE_SIZE ) ;
208- } else {
209- this . progress . total = total ;
210- this . progress . totalPages = totalPages ;
211- }
200+ const apiTotal = parseInt ( pageInfo . total , 10 ) ;
201+ this . progress . total = this . playCount ? Math . min ( this . playCount , apiTotal ) : apiTotal ;
202+ this . progress . totalPages = Math . ceil ( this . progress . total / this . PAGE_SIZE ) ;
212203
213204 totalPagesKnown = true ;
214- this . logger . info ( `Total pages in source: ${ totalPages } , Total plays in source: ${ total } , Will transfer: ${ this . progress . total } , Expected pages: ${ this . progress . totalPages } ` ) ;
205+ this . logger . info ( `Total plays in source: ${ apiTotal } , Will transfer: ${ this . progress . total } , Expected pages: ${ this . progress . totalPages } ` ) ;
215206 }
216207
217208 if ( rawTracks . length === 0 ) {
@@ -313,6 +304,8 @@ export class TransferJob {
313304 await this . processPlaysWithSlidingWindow ( sortedPlays , client ) ;
314305 }
315306
307+ private timeRangeScrobbles : PlayObject [ ] = [ ] ;
308+
316309 private async processPlaysWithSlidingWindow ( plays : PlayObject [ ] , client : AbstractScrobbleClient ) : Promise < void > {
317310 if ( plays . length === 0 ) {
318311 return ;
@@ -321,11 +314,33 @@ export class TransferJob {
321314 const oldest = plays [ 0 ] . data . playDate ;
322315 const newest = plays [ plays . length - 1 ] . data . playDate ;
323316
317+ // Fetch scrobbles for the specific time range being processed
324318 if ( oldest && newest ) {
325- this . logger . debug ( `Refreshing client scrobbles for time range: ${ oldest . format ( ) } to ${ newest . format ( ) } ` ) ;
326- await client . refreshScrobbles ( this . SLIDING_WINDOW_SIZE ) ;
319+ this . logger . debug ( `Fetching client scrobbles for time range: ${ oldest . format ( ) } to ${ newest . format ( ) } ` ) ;
320+
321+ // Check if client supports time-range fetching
322+ if ( 'getScrobblesForTimeRange' in client && typeof ( client as any ) . getScrobblesForTimeRange === 'function' ) {
323+ try {
324+ // Add a buffer before/after to catch nearby scrobbles
325+ const bufferHours = 24 ;
326+ const fromDate = oldest . subtract ( bufferHours , 'hours' ) ;
327+ const toDate = newest . add ( bufferHours , 'hours' ) ;
328+
329+ this . timeRangeScrobbles = await ( client as any ) . getScrobblesForTimeRange ( fromDate , toDate , this . SLIDING_WINDOW_SIZE ) ;
330+ this . logger . debug ( `Fetched ${ this . timeRangeScrobbles . length } scrobbles from ${ this . clientName } for duplicate detection` ) ;
331+ } catch ( e ) {
332+ this . logger . error ( `Error fetching scrobbles for time range: ${ e . message } ` ) ;
333+ throw e ;
334+ }
335+ } else {
336+ // Fallback to regular refresh (will only work for recent scrobbles)
337+ this . logger . warn ( 'Client does not support time-range fetching, falling back to regular refresh (may not detect duplicates for old scrobbles)' ) ;
338+ await client . refreshScrobbles ( this . SLIDING_WINDOW_SIZE ) ;
339+ this . timeRangeScrobbles = [ ] ;
340+ }
327341 } else {
328342 await client . refreshScrobbles ( this . SLIDING_WINDOW_SIZE ) ;
343+ this . timeRangeScrobbles = [ ] ;
329344 }
330345
331346 for ( let i = 0 ; i < plays . length ; i ++ ) {
@@ -358,6 +373,37 @@ export class TransferJob {
358373 this . progress . currentTrack = undefined ;
359374 }
360375
376+ private isAlreadyScrobbledInTimeRange ( play : PlayObject ) : boolean {
377+ if ( this . timeRangeScrobbles . length === 0 ) {
378+ return false ;
379+ }
380+
381+ const playDate = play . data . playDate ;
382+ if ( ! playDate ) {
383+ return false ;
384+ }
385+
386+ // Check for matching scrobble (same track, artist, and similar timestamp)
387+ return this . timeRangeScrobbles . some ( scrobbled => {
388+ const scrobbledDate = scrobbled . data . playDate ;
389+ if ( ! scrobbledDate ) {
390+ return false ;
391+ }
392+
393+ // Check if timestamps are within 30 seconds of each other
394+ const timeDiffSeconds = Math . abs ( playDate . diff ( scrobbledDate , 'second' ) ) ;
395+ if ( timeDiffSeconds > 30 ) {
396+ return false ;
397+ }
398+
399+ // Check if track and artists match
400+ const trackMatch = play . data . track ?. toLowerCase ( ) === scrobbled . data . track ?. toLowerCase ( ) ;
401+ const artistsMatch = play . data . artists ?. join ( ',' ) . toLowerCase ( ) === scrobbled . data . artists ?. join ( ',' ) . toLowerCase ( ) ;
402+
403+ return trackMatch && artistsMatch ;
404+ } ) ;
405+ }
406+
361407 private async processPlay ( play : PlayObject , client : AbstractScrobbleClient ) : Promise < void > {
362408 const transformedPlay = client . transformPlay ( play , TRANSFORM_HOOK . preCompare ) ;
363409
@@ -366,9 +412,17 @@ export class TransferJob {
366412 // 2. timeFrameIsValid is designed to prevent re-scrobbling during normal operation
367413 // 3. We have our own duplicate detection via time-range fetching
368414
415+ // Check against our time-range-specific scrobbles (the actual duplicate detection)
416+ if ( this . isAlreadyScrobbledInTimeRange ( transformedPlay ) ) {
417+ this . logger . verbose ( `DUPLICATE (time-range): ${ buildTrackString ( play ) } - found in ${ this . clientName } 's ${ this . timeRangeScrobbles . length } scrobbles` ) ;
418+ this . progress . duplicates ++ ;
419+ return ;
420+ }
421+
422+ // Check using the client's built-in method (for recently added scrobbles from this transfer)
369423 const alreadyScrobbled = await client . alreadyScrobbled ( transformedPlay ) ;
370424 if ( alreadyScrobbled ) {
371- this . logger . debug ( `Skipping ${ buildTrackString ( play ) } : already scrobbled `) ;
425+ this . logger . verbose ( `DUPLICATE (recent): ${ buildTrackString ( play ) } - found in ${ this . clientName } 's recent scrobbles `) ;
372426 this . progress . duplicates ++ ;
373427 return ;
374428 }
0 commit comments