1(function(){ 2 'use strict'; 3 4 const VERSION = '20260722_single_binding_5'; 5 if (window.__swUnifiedListenPresence) return; 6 window.__swUnifiedListenPresence = VERSION; 7 const ENDPOINT = '/minus_listen_presence.php'; 8 const HEARTBEAT_MS = 8000; 9 const MIN_SEND_GAP_MS = 2000; 10 const MANUAL_ACTIVE_MS = 12000; 11 12 let activeAudio = null; 13 let activeCtx = null; 14 let heartbeatTimer = 0; 15 let manualActiveUntil = 0; 16 let lastSent = { key: '', at: 0 }; 17 let lastPlaySignalAt = 0; 18 let presenceActive = false; 19 let cachedJob = ''; 20 21 function now(){ return Date.now(); } 22 23 function sourcePage(){ 24 return /minusa\.php$/i.test(String(location.pathname || '')) ? 'minusa.php' : 'index.php'; 25 } 26 27 function cleanJob(v){ 28 v = String(v || '').toLowerCase().trim(); 29 const m = v.match(/[a-f0-9]{32}/); 30 return m ? m[0] : ''; 31 } 32 33 function pageJob(){ 34 if (cachedJob && /^[a-f0-9]{32}$/.test(cachedJob)) return cachedJob; 35 36 try { 37 const u = new URL(location.href); 38 cachedJob = cleanJob(u.searchParams.get('job') || u.searchParams.get('id') || ''); 39 if (cachedJob) return cachedJob; 40 } catch (_) {} 41 42 const selectors = [ 43 '[href*="job="]', 44 '[src*="jobs_minus/"]', 45 '[style*="jobs_minus/"]', 46 '[data-sw-clean-href*="job="]', 47 '[data-audio-src*="jobs_minus/"]', 48 '[data-audio-src*="job="]' 49 ]; 50 51 for (const sel of selectors) { 52 const nodes = document.querySelectorAll(sel); 53 for (const el of nodes) { 54 const blob = [ 55 el.getAttribute('href'), 56 el.getAttribute('src'), 57 el.getAttribute('style'), 58 el.getAttribute('data-sw-clean-href'), 59 el.getAttribute('data-audio-src') 60 ].filter(Boolean).join(' '); 61 cachedJob = cleanJob(blob); 62 if (cachedJob) return cachedJob; 63 } 64 } 65 66 return ''; 67 } 68 69 function jobFromContext(ctx){ 70 const srcJob = cleanJob(sourceFrom(ctx || {})); 71 if (srcJob) return srcJob; 72 73 const audio = ctx && ctx.audio; 74 const btn = ctx && ctx.button; 75 const nodes = [audio, btn]; 76 for (const node of nodes) { 77 if (!node || !node.getAttribute) continue; 78 const blob = [ 79 node.id || '', 80 node.getAttribute('data-job') || '', 81 node.getAttribute('data-job-id') || '', 82 node.getAttribute('data-audio-src') || '', 83 node.getAttribute('href') || '', 84 node.getAttribute('src') || '' 85 ].join(' '); 86 const directJob = cleanJob(blob); 87 if (directJob) return directJob; 88 } 89 90 const root = (btn || audio) && (btn || audio).closest 91 ? (btn || audio).closest('[data-job],[data-job-id],.swPlanetTrack,.swTrack,.swResult,.swModal,.modal,[data-planet-players],[data-audio-root]') 92 : null; 93 if (root) { 94 const rootJob = cleanJob([ 95 root.getAttribute && root.getAttribute('data-job'), 96 root.getAttribute && root.getAttribute('data-job-id'), 97 root.innerHTML || '' 98 ].filter(Boolean).join(' ')); 99 if (rootJob) return rootJob; 100 } 101 102 return pageJob(); 103 } 104 105 function findJob(ctx){ 106 return jobFromContext(ctx || {}) || pageJob(); 107 } 108 109 function sessionId(job){ 110 const key = 'sw_listen_session_' + (job || 'unknown'); 111 try { 112 let v = sessionStorage.getItem(key) || ''; 113 if (!v) { 114 v = 's' + Math.random().toString(36).slice(2) + Date.now().toString(36); 115 sessionStorage.setItem(key, v); 116 } 117 return v; 118 } catch (_) { 119 return 's' + Math.random().toString(36).slice(2) + Date.now().toString(36); 120 } 121 } 122 123 function normKind(v){ 124 v = String(v || '').toLowerCase(); 125 if (/sourceaudio|before|source|original|input/.test(v)) return 'source'; 126 if (/resultaudio|after|result|processed|output/.test(v)) return 'result'; 127 if (/vocal|vocals|voice|голоÑ|вокал/.test(v)) return 'vocals'; 128 if (/minus|instr|instrumental|инÑÑÑ|минÑÑ/.test(v)) return 'instrumental'; 129 return 'mix'; 130 } 131 132 function kindFrom(el, fallback){ 133 const parts = []; 134 if (fallback) parts.push(fallback); 135 if (el) { 136 parts.push(el.id || ''); 137 parts.push(el.className || ''); 138 parts.push(el.textContent || ''); 139 if (el.getAttribute) { 140 parts.push(el.getAttribute('data-kind') || ''); 141 parts.push(el.getAttribute('data-listen-kind') || ''); 142 parts.push(el.getAttribute('data-audio-toggle') || ''); 143 parts.push(el.getAttribute('aria-label') || ''); 144 parts.push(el.getAttribute('href') || ''); 145 parts.push(el.getAttribute('src') || ''); 146 } 147 if (el.currentSrc || el.src) parts.push(el.currentSrc || el.src); 148 } 149 return normKind(parts.join(' ')); 150 } 151 152 function isAudio(el){ 153 return !!(el && String(el.tagName || '').toLowerCase() === 'audio'); 154 } 155 156 function resolveAudioFromButton(btn){ 157 if (!btn) return null; 158 159 const id = String(btn.getAttribute('data-audio-toggle') || '').trim(); 160 if (id) { 161 const byId = document.getElementById(id); 162 if (isAudio(byId)) return byId; 163 const byAttr = document.querySelector(`audio[data-audio-id="${CSS.escape(id)}"], audio[data-audio-for="${CSS.escape(id)}"]`); 164 if (byAttr) return byAttr; 165 } 166 167 const saasId = String(btn.getAttribute('data-saas-audio') || '').trim(); 168 if (saasId) { 169 const bySaasId = document.getElementById(saasId); 170 if (isAudio(bySaasId)) return bySaasId; 171 const bySaasAttr = document.querySelector(`audio[data-audio-id="${CSS.escape(saasId)}"], audio[data-audio-for="${CSS.escape(saasId)}"]`); 172 if (bySaasAttr) return bySaasAttr; 173 } 174 175 const root = btn.closest('.swPlanetTrack,.swTrack,.swResult,.swModal,.modal,[data-
175planet-players],[data-audio-root]') || document; 176 const localAudio = root.querySelector ? root.querySelector('audio') : null; 177 if (localAudio) return localAudio; 178 179 return Array.from(document.querySelectorAll('audio')).find(isPlaying) || null; 180 } 181 182 function sourceFrom(ctx){ 183 const audio = ctx && ctx.audio; 184 if (audio) { 185 const src = String(audio.currentSrc || audio.src || audio.getAttribute('src') || '').trim(); 186 if (src) return src; 187 } 188 189 const btn = ctx && ctx.button; 190 if (btn) { 191 const root = btn.closest('.swPlanetTrack,.swTrack,.swResult') || document; 192 const a = root.querySelector && root.querySelector('a[href*="minus_download.php"][href*="job="]'); 193 if (a) return String(a.getAttribute('href') || ''); 194 const img = root.querySelector && root.querySelector('img[src*="jobs_minus/"]'); 195 if (img) return String(img.getAttribute('src') || ''); 196 const wave = root.querySelector && root.querySelector('[style*="jobs_minus/"]'); 197 if (wave) return String(wave.getAttribute('style') || ''); 198 } 199 200 return ''; 201 } 202 203 function isPlaying(audio){ 204 return !!(audio && !audio.paused && !audio.ended && audio.readyState >= 1); 205 } 206 207 function buttonLooksPressed(btn){ 208 if (!btn) return false; 209 const ap = String(btn.getAttribute('aria-pressed') || '').toLowerCase(); 210 if (ap === 'true') return true; 211 if (btn.classList && ( 212 btn.classList.contains('is-playing') || 213 btn.classList.contains('playing') || 214 btn.classList.contains('active') || 215 btn.classList.contains('is-active') 216 )) return true; 217 return false; 218 } 219 220 function makePayload(action, ctx, active){ 221 const audio = ctx && ctx.audio ? ctx.audio : activeAudio; 222 const btn = ctx && ctx.button ? ctx.button : null; 223 const source = sourceFrom(ctx || {audio: audio, button: btn}); 224 const job = jobFromContext(ctx || {audio: audio, button: btn}); 225 if (!job) return null; 226 227 const kind = (ctx && ctx.kind) || kindFrom(audio || btn, ''); 228 const current = audio ? Number(audio.currentTime || 0) : Number((ctx && ctx.currentTime) || 0); 229 const duration = audio && isFinite(audio.duration) ? Number(audio.duration || 0) : Number((ctx && ctx.duration) || 0); 230 231 return new URLSearchParams({ 232 job: job, 233 job_id: job, 234 session_id: sessionId(job), 235 action: action, 236 active: active ? '1' : '0', 237 track_kind: kind, 238 source_url: source, 239 current_time_sec: String(Math.max(0, current).toFixed(3)), 240 duration_sec: String(Math.max(0, duration).toFixed(3)), 241 card_key: String((audio && audio.id) || (btn && btn.id) || ''), 242 button_id: String((btn && btn.id) || ''), 243 audio_id: String((audio && audio.id) || ''), 244 source_page: sourcePage(), 245 v: VERSION, 246 t: String(Date.now()) 247 }); 248 } 249 250 function send(action, ctx, active, beacon, force){ 251 const p = makePayload(action, ctx, active); 252 if (!p) return; 253 254 const key = [ 255 action, 256 p.get('job'), 257 p.get('track_kind'), 258 p.get('active'), 259 Math.floor(Number(p.get('current_time_sec') || 0)) 260 ].join('|'); 261 262 if (!force && key === lastSent.key && now() - lastSent.at < MIN_SEND_GAP_MS) return; 263 lastSent = { key: key, at: now() }; 264 265 if (beacon && navigator.sendBeacon) { 266 try { 267 const blob = new Blob([p.toString()], {type:'application/x-www-form-urlencoded;charset=UTF-8'}); 268 navigator.sendBeacon(ENDPOINT, blob); 269 return; 270 } catch (_) {} 271 } 272 273 try { 274 fetch(ENDPOINT, { 275 method: 'POST', 276 credentials: 'same-origin', 277 cache: 'no-store', 278 keepalive: !!beacon, 279 headers: {'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'}, 280 body: p.toString() 281 }).catch(function(){}); 282 } catch (_) {} 283 } 284 285 function startTimer(){ 286 if (!heartbeatTimer) heartbeatTimer = setInterval(heartbeat, HEARTBEAT_MS); 287 } 288 289 function markPlay(ctx){ 290 if (!ctx) ctx = {}; 291 if (ctx.audio) activeAudio = ctx.audio; 292 activeCtx = ctx; 293 manualActiveUntil = now() + MANUAL_ACTIVE_MS; 294 295 document.documentElement.dataset.swListenPresence = 'active'; 296 document.documentElement.dataset.swListenPresenceJob = jobFromContext(ctx) || pageJob() || ''; 297 298 const signalAt = now(); 299 if (!presenceActive || signalAt - lastPlaySignalAt >= 1500) { 300 send('play', ctx, true, false, false); 301 lastPlaySignalAt = signalAt; 302 } 303 presenceActive = true; 304 startTimer(); 305 window.setTimeout(function(){ 306 if (!presenceActive || activeCtx !== ctx) return; 307 const audio = ctx && ctx.audio ? ctx.audio : activeAudio; 308 if (audio && isPlaying(audio)) return; 309 markStop('pause', ctx); 310 }, MANUAL_ACTIVE_MS + 900); 311 } 312 313 function markStop(action, ctx){ 314 if (!presenceActive) return; 315 send(action || 'pause', ctx || activeCtx || {audio: activeAudio}, false, true, true); 316 presenceActive = false;
317 manualActiveUntil = 0; 318 } 319 320 function heartbeat(){ 321 const playing = activeAudio && isPlaying(activeAudio); 322 if (playing) { 323 send('heartbeat', {audio: activeAudio, kind: kindFrom(activeAudio)}, true, false, false); 324 return; 325 } 326 327 if (activeCtx && now() < manualActiveUntil) { 328 const btn = activeCtx.button || null; 329 const shouldKeep = !btn || buttonLooksPressed(btn) || (now() < activeCtx.keepUntil); 330 if (shouldKeep) { 331 send('heartbeat', activeCtx, true, false, false); 332 } 333 } 334 } 335 336 function bindAudio(audio){ 337 if (!audio || audio.dataset.swFinanceListenBound === VERSION) return; 338 audio.dataset.swFinanceListenBound = VERSION; 339 340 audio.addEventListener('play', function(){ 341 markPlay({audio: audio, kind: kindFrom(audio)}); 342 }, {passive:true}); 343 344 audio.addEventListener('playing', function(){ 345 markPlay({audio: audio, kind: kindFrom(audio)}); 346 }, {passive:true}); 347 348 audio.addEventListener('timeupdate', function(){ 349 if (isPlaying(audio)) activeAudio = audio; 350 }, {passive:true}); 351 352 audio.addEventListener('pause', function(){ 353 if (activeAudio === audio) markStop('pause', {audio: audio, kind: kindFrom(audio)}); 354 }, {passive:true}); 355 356 audio.addEventListener('ended', function(){ 357 if (activeAudio === audio) markStop('ended', {audio: audio, kind: kindFrom(audio)}); 358 }, {passive:true}); 359 360 audio.addEventListener('error', function(){ 361 if (activeAudio === audio) markStop('hidden', {audio: audio, kind: kindFrom(audio)}); 362 }, {passive:true}); 363 } 364 365 function bindButton(btn){ 366 if (!btn || btn.dataset.swFinanceListenBtnBound === VERSION) return; 367 btn.dataset.swFinanceListenBtnBound = VERSION; 368 369 function prime(){ 370 const audio = resolveAudioFromButton(btn); 371 const kind = kindFrom(audio || btn); 372 btn.dataset.swListenWasActive = (buttonLooksPressed(btn) || !!(audio && !audio.paused && !audio.ended)) ? '1' : '0'; 373 const ctx = { 374 button: btn, 375 audio: audio, 376 kind: kind, 377 keepUntil: now() + MANUAL_ACTIVE_MS 378 }; 379 if (audio) activeAudio = audio; 380 activeCtx = ctx; 381 } 382 383 function fire(){ 384 const audio = resolveAudioFromButton(btn); 385 const kind = kindFrom(audio || btn); 386 const ctx = { 387 button: btn, 388 audio: audio, 389 kind: kind, 390 keepUntil: now() + MANUAL_ACTIVE_MS 391 }; 392 const wasActive = btn.dataset.swListenWasActive === '1'; 393 delete btn.dataset.swListenWasActive; 394 if (wasActive && (!audio || audio.paused || audio.ended || !buttonLooksPressed(btn))) { 395 markStop('pause', ctx); 396 return; 397 } 398 markPlay(ctx); 399 400 setTimeout(function(){ 401 const a2 = resolveAudioFromButton(btn); 402 if (a2 && isPlaying(a2) && !presenceActive) { 403 markPlay({button: btn, audio: a2, kind: kindFrom(a2 || btn), keepUntil: now() + MANUAL_ACTIVE_MS}); 404 } 405 }, 180); 406 } 407 408 ['pointerdown','mousedown','touchstart'].forEach(function(type){ 409 btn.addEventListener(type, prime, {capture:true, passive:true}); 410 }); 411 412 btn.addEventListener('click', function(){ 413 setTimeout(fire, 40); 414 }, {capture:true, passive:true}); 415 } 416 417 function bind(){ 418 pageJob(); 419 document.querySelectorAll('audio').forEach(bindAudio); 420 document.querySelectorAll('#minusPlay,#vocalPlay,[data-audio-toggle],[data-saas-audio],.swPlay,.swPlanetPlay,.play,.play-btn').forEach(bindButton); 421 } 422 423 if (document.readyState === 'loading') { 424 document.addEventListener('DOMContentLoaded', bind, {once:true}); 425 } else { 426 bind(); 427 } 428 429 window.addEventListener('load', bind, {once:true}); 430 431 try { 432 new MutationObserver(function(){ 433 setTimeout(bind, 80); 434 }).observe(document.documentElement, {childList:true, subtree:true, attributes:true, attributeFilter:['src','data-audio-toggle','data-audio-for']}); 435 } catch (_) {} 436 437 document.addEventListener('visibilitychange', function(){ 438 if (document.visibilityState === 'hidden') { 439 if (activeAudio && isPlaying(activeAudio)) { 440 send('heartbeat', {audio: activeAudio, kind: kindFrom(activeAudio)}, true, true, true); 441 } else if (activeCtx) { 442 send('hidden', activeCtx, false, true, true); 443 } 444 } else { 445 bind(); 446 heartbeat(); 447 } 448 }, {passive:true}); 449 450 function leave(){ 451 if (presenceActive && (activeAudio || activeCtx)) { 452 send('leave', activeCtx || {audio: activeAudio}, false, true, true); 453 presenceActive = false;
454 } 455 } 456 457 window.addEventListener('pagehide', leave, {capture:true}); 458 window.addEventListener('beforeunload', leave, {capture:true}); 459 460 window.swListenPresence = { 461 version: VERSION, 462 endpoint: ENDPOINT, 463 findJob: findJob, 464 bind: bind, 465 heartbeat: heartbeat, 466 markPlay: markPlay 467 }; 468})();
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.