PageSourceSearch

https://suno-wave.ru/assets/js/sw-minusa-listen-presence.js?v=20260722_single_binding_5

js suno-wave.ru collected 2026-10-09 03:41:01 UTC 15,463 bytes, 468 lines download raw bytes

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.