1class AudioAnalytics { 2 constructor(postId, ajaxUrl, nonce) { 3 this.postId = postId; 4 this.ajaxUrl = ajaxUrl; 5 this.nonce = nonce; 6 7 const sessionIdKey = `audio_session_id_${postId}`; 8 let sessionId = localStorage.getItem(sessionIdKey); 9 10 if (!sessionId) { 11 sessionId = this.generateSessionId(); 12 localStorage.setItem(sessionIdKey, sessionId); 13 } 14 15 this.sessionId = sessionId; 16 this.sessionIdKey = sessionIdKey; 17 this.storageKey = `audio_session_${postId}_${this.sessionId}`; 18 19 this.startTime = Date.now(); 20 this.lastUpdate = Date.now(); 21 this.listenTime = 0; 22 this.furthestPosition = 0; 23 this.playCount = 0; 24 this.pauseCount = 0; 25 this.seekCount = 0; 26 this.currentSpeed = 1.0; 27 this.completed = false; 28 this.milestones = new Set(); 29 30 this.heartbeatInterval = null; 31 this.isPlaying = false; 32 this.lastSync = Date.now(); 33 34 this.loadSession(); 35 36 this.setupCleanupHandlers(); 37 } 38 39 generateSessionId() { 40 return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, () => 41 ((Math.random() * 16) | 0).toString(16) 42 ); 43 } 44 45 loadSession() { 46 try { 47 const saved = localStorage.getItem(this.storageKey); 48 if (saved) { 49 const data = JSON.parse(saved); 50 51 if (data.completed) { 52 localStorage.removeItem(this.storageKey); 53 localStorage.removeItem(this.sessionIdKey); 54 return; 55 } 56 57 this.listenTime = data.listenTime || 0; 58 this.furthestPosition = data.furthestPosition || 0; 59 this.playCount = data.playCount || 0; 60 this.pauseCount = data.pauseCount || 0; 61 this.seekCount = data.seekCount || 0; 62 this.currentSpeed = data.currentSpeed || 1.0; 63 this.completed = data.completed || false; 64 this.milestones = new Set(data.milestones || []); 65 } 66 } catch (e) { 67 } 68 } 69 70 saveSession() { 71 try { 72 localStorage.setItem(this.storageKey, JSON.stringify({ 73 sessionId: this.sessionId, 74 listenTime: this.listenTime, 75 furthestPosition: this.furthestPosition, 76 playCount: this.playCount, 77 pauseCount: this.pauseCount, 78 seekCount: this.seekCount, 79 currentSpeed: this.currentSpeed, 80 completed: this.completed, 81 milestones: Array.from(this.milestones), 82 lastSync: Date.now() 83 })); 84 } catch (e) { 85 } 86 } 87 88 onPlay() { 89 if (!this.isPlaying) { 90 this.playCount++; 91 this.isPlaying = true; 92 this.lastUpdate = Date.now(); 93 94 if (this.playCount === 1 && typeof gtag !== 'undefined') { 95 gtag('event', 'audio_play', { 96 event_category: 'Audio Player', 97 post_id: this.postId 98 }); 99 } 100 101 this.startHeartbeat(); 102 } 103 } 104 105 onPause() { 106 if (this.isPlaying) { 107 this.pauseCount++; 108 this.isPlaying = false; 109 this.updateListenTime(); 110 this.stopHeartbeat(); 111 112 this.syncToServer(); 113 } 114 } 115 116 onEnded() { 117 this.isPlaying = false; 118 this.updateListenTime(); 119 this.stopHeartbeat(); 120 this.syncToServer(); 121 } 122 123 onTimeUpdate(currentTime, duration) { 124 if (!this.isPlaying || !duration || duration <= 0) return; 125 126 this.updateListenTime(); 127 128 const position = (currentTime / duration) * 100; 129 if (position > this.furthestPosition) { 130 this.furthestPosition = position; 131 } 132 133 this.checkMilestones(position); 134 135 if (position >= 95 && !this.completed) { 136 this.completed = true; 137 if (typeof gtag !== 'undefined') { 138 gtag('event', 'audio_complete', { 139 event_category: 'Audio Player', 140 post_id: this.postId, 141 listen_duration: Math.round(this.listenTime) 142 }); 143 } 144 } 145 } 146 147 updateListenTime() { 148 const now = Date.now(); 149 const delta = (now - this.lastUpdate) / 1000; 150 151 if (delta > 0 && delta < 5) { 152 this.listenTime += delta; 153 } 154 this.lastUpdate = now; 155 this.saveSession(); 156 } 157
158 checkMilestones(position) { 159 [25, 50, 75].forEach(milestone => { 160 if (position >= milestone && !this.milestones.has(milestone)) { 161 this.milestones.add(milestone); 162 163 if (typeof gtag !== 'undefined') { 164 gtag('event', 'audio_progress', { 165 event_category: 'Audio Player', 166 post_id: this.postId, 167 progress_percent: milestone 168 }); 169 } 170 } 171 }); 172 } 173 174 onSeek() { 175 this.seekCount++; 176 this.saveSession(); 177 } 178 179 onSpeedChange(speed) { 180 this.currentSpeed = speed; 181 this.saveSession(); 182 } 183 184 startHeartbeat() { 185 this.heartbeatInterval = setInterval(() => { 186 if (this.isPlaying) { 187 this.syncToServer(); 188 } 189 }, 30000); 190 } 191 192 stopHeartbeat() { 193 if (this.heartbeatInterval) { 194 clearInterval(this.heartbeatInterval); 195 this.heartbeatInterval = null; 196 } 197 } 198 199 syncToServer(forceSync = false) { 200 if (this.playCount === 0 || (this.listenTime === 0 && this.furthestPosition === 0)) { 201 return; 202 } 203 204 const now = Date.now(); 205 206 if (!forceSync && now - this.lastSync < 5000) { 207 return; 208 } 209 this.lastSync = now; 210 211 const data = new URLSearchParams({ 212 action: 'sync_audio_session', 213 nonce: this.nonce, 214 session_id: this.sessionId, 215 post_id: this.postId, 216 listen_time: Math.round(this.listenTime), 217 furthest_position: this.furthestPosition.toFixed(2), 218 completed: this.completed ? '1' : '0', 219 play_count: this.playCount, 220 pause_count: this.pauseCount, 221 seek_count: this.seekCount, 222 final_speed: this.currentSpeed 223 }); 224 225 if (navigator.sendBeacon) { 226 navigator.sendBeacon(this.ajaxUrl, data); 227 } else { 228 jQuery.post(this.ajaxUrl, data); 229 } 230 } 231 232 setupCleanupHandlers() { 233 document.addEventListener('visibilitychange', () => { 234 if (document.hidden && this.isPlaying) { 235 this.updateListenTime(); 236 this.syncToServer(); 237 } 238 }); 239 240 window.addEventListener('beforeunload', () => { 241 this.onBeforeUnload(); 242 }); 243 } 244 245 onBeforeUnload() { 246 if (this.isPlaying) { 247 this.updateListenTime(); 248 } 249 250 this.syncToServer(true); 251 } 252 253 getResumePosition() { 254 if (!this.completed && this.furthestPosition > 5 && this.furthestPosition < 95) { 255 return this.furthestPosition; 256 } 257 return 0; 258 } 259} 260
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.