1
- import { PlayObject , TemporalPlayComparison } from "../../core/Atomic" ;
2
- import { lowGranularitySources } from "../common/infrastructure/Atomic" ;
1
+ import {
2
+ PlayObject ,
3
+ TA_CLOSE ,
4
+ TA_EXACT ,
5
+ TA_FUZZY ,
6
+ TA_NONE ,
7
+ TemporalAccuracy ,
8
+ TemporalPlayComparison
9
+ } from "../../core/Atomic" ;
10
+ import {
11
+ DEFAULT_SCROBBLE_DURATION_THRESHOLD ,
12
+ DEFAULT_SCROBBLE_PERCENT_THRESHOLD ,
13
+ lowGranularitySources ,
14
+ ScrobbleThresholdResult
15
+ } from "../common/infrastructure/Atomic" ;
3
16
import { formatNumber } from "../utils" ;
4
-
5
- export const isPlayTemporallyClose = ( existingPlay : PlayObject , candidatePlay : PlayObject , options : {
6
- diffThreshold ?: number ,
7
- fuzzyDuration ?: boolean ,
8
- useListRanges ?: boolean
9
- } = { } ) : boolean => {
10
- return comparePlayTemporally ( existingPlay , candidatePlay , options ) . close ;
11
- }
17
+ import { ScrobbleThresholds } from "../common/infrastructure/config/source" ;
18
+ import { capitalize } from "../../core/StringUtils" ;
12
19
13
20
export const temporalPlayComparisonSummary = ( data : TemporalPlayComparison , existingPlay ?: PlayObject , candidatePlay ?: PlayObject ) => {
14
21
const parts : string [ ] = [ ] ;
@@ -19,7 +26,7 @@ export const temporalPlayComparisonSummary = (data: TemporalPlayComparison, exis
19
26
parts . push ( `Existing: ${ existingPlay . data . playDate . toISOString ( ) } - Candidate: ${ candidatePlay . data . playDate . toISOString ( ) } ` ) ;
20
27
}
21
28
}
22
- parts . push ( `Close : ${ data . close ? 'YES' : 'NO' } ` ) ;
29
+ parts . push ( `Temporal Sameness : ${ capitalize ( temporalAccuracyToString ( data . match ) ) } ` ) ;
23
30
if ( data . date !== undefined ) {
24
31
parts . push ( `Play Diff: ${ formatNumber ( data . date . diff , { toFixed : 0 } ) } s (Needed <${ data . date . threshold } s)` )
25
32
}
@@ -33,10 +40,10 @@ export const temporalPlayComparisonSummary = (data: TemporalPlayComparison, exis
33
40
if ( data . range === false ) {
34
41
parts . push ( 'Candidate not played during Existing tracked listening' ) ;
35
42
} else {
36
- parts . push ( `Candidate played during tracked listening range from existing: ${ data . range [ 0 ] . timestamp . format ( 'HH:mm:ssZ' ) } => ${ data . range [ 1 ] . timestamp . format ( 'HH:mm:ssZ' ) } ` ) ;
43
+ parts . push ( `Candidate played during tracked listening range from Existing ${ data . range [ 0 ] . timestamp . format ( 'HH:mm:ssZ' ) } => ${ data . range [ 1 ] . timestamp . format ( 'HH:mm:ssZ' ) } ` ) ;
37
44
}
38
45
} else {
39
- parts . push ( 'One or both Plays did not have have tracked listening to compare ' ) ;
46
+ parts . push ( 'Range Comparison N/A ' ) ;
40
47
}
41
48
return parts . join ( ' | ' ) ;
42
49
}
@@ -47,7 +54,7 @@ export const comparePlayTemporally = (existingPlay: PlayObject, candidatePlay: P
47
54
} = { } ) : TemporalPlayComparison => {
48
55
49
56
const result : TemporalPlayComparison = {
50
- close : false
57
+ match : TA_NONE
51
58
} ;
52
59
53
60
const {
@@ -94,8 +101,10 @@ export const comparePlayTemporally = (existingPlay: PlayObject, candidatePlay: P
94
101
diff : scrobblePlayDiff
95
102
} ;
96
103
97
- if ( scrobblePlayDiff <= playDiffThreshold ) {
98
- result . close = true ;
104
+ if ( scrobblePlayDiff <= 1 ) {
105
+ result . match = TA_EXACT ;
106
+ } else if ( scrobblePlayDiff <= playDiffThreshold ) {
107
+ result . match = TA_CLOSE ;
99
108
}
100
109
101
110
if ( useListRanges && existingRanges !== undefined ) {
@@ -105,7 +114,9 @@ export const comparePlayTemporally = (existingPlay: PlayObject, candidatePlay: P
105
114
for ( const range of existingRanges ) {
106
115
if ( newPlayDate . isBetween ( range . start . timestamp , range . end . timestamp ) ) {
107
116
result . range = range ;
108
- result . close = true ;
117
+ if ( ! temporalAccuracyIsAtLeast ( TA_CLOSE , result . match ) ) {
118
+ result . match = TA_CLOSE ;
119
+ }
109
120
break ;
110
121
}
111
122
}
@@ -116,21 +127,73 @@ export const comparePlayTemporally = (existingPlay: PlayObject, candidatePlay: P
116
127
117
128
// if the source has a duration its possible one play was scrobbled at the beginning of the track and the other at the end
118
129
// so check if the duration matches the diff between the two play dates
119
- if ( result . close === false && referenceDuration !== undefined && fuzzyDuration ) {
130
+ if ( result . match === TA_NONE && referenceDuration !== undefined ) {
120
131
result . date . fuzzyDurationDiff = Math . abs ( scrobblePlayDiff - referenceDuration ) ;
121
132
if ( result . date . fuzzyDurationDiff < 10 ) { // TODO use finer comparison for this?
122
- result . close = true ;
133
+ result . match = TA_FUZZY ;
123
134
}
124
135
}
125
136
// if the source has listened duration (maloja) it may differ from actual track duration
126
137
// and its possible (spotify) the candidate play date is set at the end of this duration
127
138
// so check if there is a close match between candidate play date and source + listened for
128
- if ( result . close === false && referenceListenedFor !== undefined && fuzzyDuration ) {
139
+ if ( result . match === TA_NONE && referenceListenedFor !== undefined && fuzzyDuration ) {
129
140
result . date . fuzzyListenedDiff = Math . abs ( scrobblePlayDiff - referenceListenedFor ) ;
130
141
if ( result . date . fuzzyListenedDiff < 10 ) { // TODO use finer comparison for this?
131
- result . close = true ;
142
+ result . match = TA_FUZZY
132
143
}
133
144
}
134
145
135
146
return result ;
136
147
}
148
+ export const timePassesScrobbleThreshold = ( thresholds : ScrobbleThresholds , secondsTracked : number , playDuration ?: number ) : ScrobbleThresholdResult => {
149
+ let durationPasses = undefined ,
150
+ durationThreshold : number | null = thresholds . duration ?? DEFAULT_SCROBBLE_DURATION_THRESHOLD ,
151
+ percentPasses = undefined ,
152
+ percentThreshold : number | null = thresholds . percent ?? DEFAULT_SCROBBLE_PERCENT_THRESHOLD ,
153
+ percent : number | undefined ;
154
+
155
+ if ( percentThreshold !== null && playDuration !== undefined && playDuration !== 0 ) {
156
+ percent = Math . round ( ( ( secondsTracked / playDuration ) * 100 ) ) ;
157
+ percentPasses = percent >= percentThreshold ;
158
+ }
159
+ if ( durationThreshold !== null || percentPasses === undefined ) {
160
+ durationPasses = secondsTracked >= durationThreshold ;
161
+ }
162
+
163
+ return {
164
+ passes : ( durationPasses ?? false ) || ( percentPasses ?? false ) ,
165
+ duration : {
166
+ passes : durationPasses ,
167
+ threshold : durationThreshold ,
168
+ value : secondsTracked
169
+ } ,
170
+ percent : {
171
+ passes : percentPasses ,
172
+ value : percent ,
173
+ threshold : percentThreshold
174
+ }
175
+ }
176
+ }
177
+
178
+ export const temporalAccuracyIsAtLeast = ( expected : TemporalAccuracy , found : TemporalAccuracy ) : boolean => {
179
+ if ( typeof expected === 'number' ) {
180
+ if ( typeof found === 'number' ) {
181
+ return found <= expected ;
182
+ }
183
+ return false ;
184
+ }
185
+ return found === false ;
186
+ }
187
+
188
+ export const temporalAccuracyToString = ( acc : TemporalAccuracy ) : string => {
189
+ switch ( acc ) {
190
+ case 1 :
191
+ return 'exact' ;
192
+ case 2 :
193
+ return 'close' ;
194
+ case 3 :
195
+ return 'fuzzy' ;
196
+ case false :
197
+ return 'no correlation' ;
198
+ }
199
+ }
0 commit comments