PageSourceSearch

https://www.assajda.com/assets/js-ac530574/player.js

js assajda.com collected 2026-10-03 02:30:17 UTC 17,517 bytes, 483 lines download raw bytes

1
2import * as store from './store.js';
3import { $, $$, t, fmtTime, announce, toast, url, cfg } from './core.js';
4import { addHistory, toggleFav, syncFavButtons } from './library.js';
5import * as analytics from './analytics.js';
6const dock = document.getElementById('dock');
7const audio = dock ? $('[data-p="audio"]', dock) : null;
8const ui = {};
9if (dock) for (const n of $$('[data-p]', dock)) ui[n.dataset.p] = n;
10let queue = [];
11let index = -1;
12let queueId = '';
13let state = 'idle';
14let repeat = 'off';
15let rate = 1;
16let sleepAt = 0;
17let played = 0;
18let lastTick = 0;
19let beaconSent = false;
20let seeking = false;
21let lastSave = 0;
22let lastPosState = 0;
23let errorTimer = 0;
24let pendingResume = 0;
25const encodePath = (f) => String(f || '').split('/').map(encodeURIComponent).join('/');
26export function current() {
27  return index >= 0 ? queue[index] : null;
28}
29export function isActive() {
30  return !!audio && (state === 'playing' || state === 'loading');
31}
32function readRow(row, list) {
33  const d = row.dataset;
34  const L = (list && list.dataset) || {};
35  const kind = d.kind || L.kind || 'r';
36  const src = d.src || (L.base || '') + encodePath(d.f);
37  const link = row.querySelector('a[href]');
38  let title;
39  let sub = d.sub || L.sub || '';
40  if (row.classList.contains('track--reciter')) {
41    title = L.coll || '';
42  } else {
43    const pick = (sel) => row.querySelector(sel)?.textContent.trim() || '';
44    title = pick('.track__lat') || pick('.track__title') || pick('.track__ar') || pick('.track__link') || L.coll || '';
45  }
46  return {
47    k: kind,
48    id: Number(d.id),
49    src,
50    t: title,
51    s: sub,
52    a: row.classList.contains('track--reciter') ? '' : (L.coll || ''),
53    img: d.img !== undefined ? d.img : (L.img || ''),
54    u: link && !link.hasAttribute('data-a') ? link.getAttribute('href') : location.pathname,
55    d: Number(d.d) || 0,
56    n: Number(d.n) || 0,
57  };
58}
59function visibleRows(container) {
60  return $$('.track[data-id]', container).filter((r) => !r.hidden && !r.closest('[hidden]'));
61}
62export function playFromRow(row) {
63  const list = row.closest('.tracklist');
64  const it = readRow(row, list);
65  const cur = current();
66  if (cur && cur.k === it.k && cur.id === it.id && audio.src) {
67    toggle();
68    return;
69  }
70  const rows = visibleRows(list);
71  setQueue(rows.map((r) => readRow(r, r.closest('.tracklist'))), (list && list.dataset.queue) || '');
72  start(Math.max(0, rows.indexOf(row)));
73  if (list && list.dataset.queueSrc) extendQueue(list.dataset.queueSrc, it);
74}
75export function playList(target) {
76  const root = typeof target === 'string' ? $(target) : target;
77  if (!root) return;
78  const rows = visibleRows(root);
79  if (!rows.length) return;
80  setQueue(rows.map((r) => readRow(r, r.closest('.tracklist'))), root.id || 'list');
81  start(0);
82}
83function setQueue(items, id) {
84  queue = items.slice(0, 400);
85  queueId = id;
86}
87async function extendQueue(src, playing) {
88  try {
89    const res = await fetch(src, { headers: { Accept: 'application/json' } });
90    if (!res.ok) return;
91    const data = await res.json();
92    const items = data.items.map((x) => ({
93      k: 'r', id: x.id, src: data.base + encodePath(x.f), t: x.t, s: data.sub, a: data.title, img: data.img || '', u: x.u, d: x.d, n: x.n,
94    }));
95    const cur = current();
96    const i = items.findIndex((x) => cur && x.id === cur.id);
97    if (i >= 0 && cur && cur.id === playing.id) {
98      queue = items;
99      index = i;
100      queueId = data.queue;
101      updateButtons();
102      saveNow(true);
103    }
104  } catch {}
105}
106function start(i, from = null) {
107  const it = queue[i];
108  if (!it || !audio) return;
109  index = i;
110  clearTimeout(errorTimer);
111  hideMessage();
112  audio.src = it.src;
113  audio.defaultPlaybackRate = rate;
114  audio.playbackRate = rate;
115  const resume = from !== null ? from : resumePosition(it);
116  pendingResume = resume > 0 ? resume : 0;
117  if (pendingResume) {
118    try { audio.currentTime = pendingResume; } catch {}
119  }
120  setState('loading');
121  const p = audio.play();
122  if (p && p.catch) {
123    p.catch((err) => {
124      if (err && err.name === 'NotAllowedError') setState('paused');
125      else if (err && err.name !== 'AbortError') onError();
126    });
127  }
128  played = 0;
129  lastTick = 0;
130  beaconSent = false;
131  showDock();
132  render();
133  mediaSession(it);
134  saveNow(true);
135  addHistory({ k: it.k, id: it.id, title: it.t, sub: it.s, url: it.u, img: it.img });
136  analytics.event('Audio', 'play', `${it.t} — ${it.s}`);
137  announce(t('now_playing', { title: `${it.t} — ${it.s}` }));
138}
139export function toggle() {
140  if (!audio) return;
141  if (!audio.src || audio.src === location.href) {
142    if (index >= 0) start(index, pendingResume || null);
143    return;
144  }
145  if (audio.paused) {
146    const p = audio.play();
147    if (p && p.catch) p.catch(() => {});
148  } else {
149    audio.pause();
150  }
151}
152export function next(auto = false) {
153  if (index < queue.length - 1) start(index + 1, 0);
154  else if (repeat === 'all' && queue.length) start(0, 0);
155  else if (auto) setState('paused');
156}
157export function prev() {
158  if (audio && audio.currentTime > 3) {
159    audio.currentTime = 0;
160    return;
161  }
162  if (index > 0) start(index - 1, 0);
163}
164export function jump(sec) {
165  if (!audio || !audio.src) return;
166  const d = audio.duration || Infinity;
167  audio.currentTime = Math.max(0, Math.min(d - 0.5, audio.currentTime + sec));
168}
169export function seekPct(pct) {
170  if (audio && audio.duration) audio.currentTime = audio.duration * pct;
171}
172export function mute() {
173  if (audio) audio.muted = !audio.muted;
174}
175export function cycleRepeat() {
176  repeat = repeat === 'off' ? 'all' : repeat === 'all' ? 'one' : 'off';
177  savePrefs();
178  updateButtons();
179  toast(t('repeat_' + repeat));
180}
181export function changeRate(delta) {
182  const rates = [0.75, 1, 1.25, 1.5, 1.75, 2];
183  const i = Math.max(0, Math.min(rates.length - 1, rates.indexOf(rate) + delta));
184  setRate(rates[i]);
185}
186function setRate(r) {
187  rate = r;
188  if (audio) {
189    audio.playbackRate = r;
190    audio.defaultPlaybackRate = r;
191  }
192  if (ui.rate) ui.rate.value = String(r);
193  savePrefs();
194}
195function setState(s) {
196  state = s;
197  if (dock) dock.dataset.state = s;
198  if (ui.toggle) ui.toggle.setAttribute('aria-label', s === 'playing' || s === 'loading' ? t('pause') : t('play'));
199  if ('mediaSession' in navigator) navigator.mediaSession.playbackState = s === 'playing' ? 'playing' : 'paused';
200  syncRows(document);
201}
202export function syncRows(root) {
203  const cur = current();
204  for (const row of $$('.track[data-id]', root)) {
205    const kind = row.dataset.kind || row.closest('.tracklist')?.dataset.kind || 'r';
206    const on = !!cur && kind === cur.k && Number(row.dataset.id) === cur.id;
207    if (on) {
208      row.setAttribute('aria-current', 'true');
209      row.dataset.state = state;
210      const b = row.querySelector('[data-a="play"]');
211      if (b) b.setAttribute('aria-label', state === 'playing' ? t('pause') : t('play'));
212    } else if (row.hasAttribute('aria-current') || row.dataset.state) {
213      row.removeAttribute('aria-current');
214      delete row.dataset.state;
215    }
216  }
217  for (const b of $$('.play--xl', root)) {
218    const row = b.closest('.track');
219    b.dataset.state = row && row.dataset.state ? row.dataset.state : '';
220  }
221}
222function showDock() {
223  if (!dock || !dock.hidden) return;
224  dock.hidden = false;
225  document.documentElement.dataset.dock = '';
226  const skip = $('[data-skip-dock]');
227  if (skip) skip.hidden = false;
228}
229function hideDock() {
230  if (!dock) return;
231  dock.hidden = true;
232  delete document.documentElement.dataset.dock;
233  const skip = $('[data-skip-dock]');
234  if (skip) skip.hidden = true;
235}
236function render() {
237  const it = current();
238  if (!it || !dock) return;
239  ui.title.textContent = it.t;
240  ui.sub.textContent = it.a ? `${it.s} · ${it.a}` : it.s;
241  ui.link.href = it.u || '#';
242  ui.art.style.backgroundImage = it.img ? `url("${it.img}-96.webp")` : '';
243  ui.dur.textContent = it.d ? fmtTime(it.d) : '0:00';
244  ui.download.href = url(`/download/${it.k}${it.id}`);
245  ui.fav.dataset.entity = `${it.k === 's' ? 's' : 'r'}:${it.id}`;
246  ui.fav.dataset.name = `${it.t} — ${it.s}`;
247  ui.fav.dataset.url = it.u || '';
248  ui.fav.dataset.img = it.img || '';
249  syncFavButtons(dock);
250  updateButtons();
251  progress();
252}
253function updateButtons() {
254  if (!dock) return;
255  ui.prev.disabled = index <= 0 && !(audio && audio.currentTime > 3);
256  ui.next.disabled = index >= queue.length - 1 && repeat !== 'all';
257  ui.repeat.dataset.mode = repeat;
258  ui.repeat.setAttribute('aria-label', t('repeat_' + repeat));
259  ui.repeat.setAttribute('aria-pressed', repeat === 'off' ? 'false' : 'true');
260}
261function progress() {
262  if (!audio || seeking) return;
263  const it = current();
264  const d = audio.duration && isFinite(audio.duration) ? audio.duration : (it ? it.d : 0);
265  const cur = audio.currentTime || pendingResume || 0;
266  const pct = d ? Math.min(100, (cur / d) * 100) : 0;
267  ui.seek.value = String(Math.round(pct * 10));
268  ui.seek.style.setProperty('--p', pct + '%');
269  ui.seek.setAttribute('aria-valuetext', `${fmtTime(cur)} / ${fmtTime(d)}`);
270  ui.cur.textContent = fmtTime(cur);
271  if (d) ui.dur.textContent = fmtTime(d);
272  for (const row of $$('.track--single[aria-current="true"]')) {
273    const range = row.querySelector('[data-a="seek-inline"]');
274    const time = row.querySelector('[data-inline="cur"]');
275    if (range) {
276      range.value = String(Math.round(pct * 10));
277      range.style.setProperty('--p', pct + '%');
278    }
279    if (time) time.textContent = fmtTime(cur);
280  }
281}
282function mediaSession(it) {
283  if (!('mediaSession' in navigator) || typeof MediaMetadata === 'undefined') return;
284  const art = it.img
285    ? [96, 192, 384].map((w) => ({ src: new URL(`${it.img}-${w}.jpg`, location.href).href, sizes: `${w}x${Math.round(w * 1.2)}`, type: 'image/jpeg' }))
286    : [{ src: new URL(url('/assets/img/icon-512.png'), location.href).href, sizes: '512x512', type: 'image/png' }];
287  navigator.mediaSession.metadata = new MediaMetadata({ title: it.t, artist: it.s, album: it.a || 'Assajda', artwork: art });
288  const ms = navigator.mediaSession;
289  const set = (a, h) => { try { ms.setActionHandler(a, h); } catch {} };
290  set('play', () => toggle());
291  set('pause', () => audio.pause());
292  set('stop', () => audio.pause());
293  set('previoustrack', () => prev());
294  set('nexttrack', () => next());
295  set('seekbackward', (e) => jump(-(e.seekOffset || 10)));
296  set('seekforward', (e) => jump(e.seekOffset || 30));
297  set('seekto', (e) => { if (e.seekTime !== undefined) audio.currentTime = e.seekTime; });
298}
299function resumePosition(it) {
300  if (!it || it.d < 300) return 0;
301  const pos = store.get('resume', {})[it.k + it.id] || 0;
302  return pos > 30 && pos < it.d - 30 ? pos : 0;
303}
304function saveResume(it, pos) {
305  if (!it || it.d < 300) return;
306  const map = store.get('resume', {});
307  const key = it.k + it.id;
308  if (pos > 30 && pos < it.d - 30) map[key] = Math.round(pos);
309  else delete map[key];
310  const keys = Object.keys(map);
311  if (keys.length > 300) for (const k of keys.slice(0, keys.length - 300)) delete map[k];
312  store.set('resume', map);
313}
314function saveNow(force = false) {
315  const now = Date.now();
316  if (!force && now - lastSave < 5000) return;
317  lastSave = now;
318  const it = current();
319  if (!it) return;
320  const pos = audio ? audio.currentTime || 0 : 0;
321  saveResume(it, pos);
322  store.set('now', { q: queue.slice(0, 150), i: index, id: queueId, pos: Math.round(pos), ts: now });
323}
324function savePrefs() {
325  store.set('prefs', { rate, repeat });
326}
327function beacon(it) {
328  beaconSent = true;
329  const key = 'as:l:' + it.k + it.id;
330  try {
331    if (sessionStorage.getItem(key)) return;
332    sessionStorage.setItem(key, '1');
333  } catch {}
334  const body = new URLSearchParams({ t: it.k, id: String(it.id) });
335  if (navigator.sendBeacon) navigator.sendBeacon(url('/api/listen'), body);
336  else fetch(url('/api/listen'), { method: 'POST', body, keepalive: true }).catch(() => {});
337}
338function showMessage(msg) {
339  if (!ui.msg) return;
340  ui.msg.textContent = msg;
341  ui.msg.hidden = false;
342}
343function hideMessage() {
344  if (ui.msg) ui.msg.hidden = true;
345}
346function onError() {
347  const it = current();
348  setState('error');
349  showMessage(t('error_file'));
350  for (const row of $$('.track[data-id]')) if (it && Number(row.dataset.id) === it.id) row.dataset.state = 'error';
351  if (it) analytics.event('Audio', 'error', `${it.k}${it.id}`);
352  if (index < queue.length - 1) errorTimer = setTimeout(() => next(true), 3000);
353}
354export function init() {
355  if (!dock || !audio) return;
356  const prefs = store.get('prefs', {});
357  repeat = ['off', 'all', 'one'].includes(prefs.repeat) ? prefs.repeat : 'off';
358  setRate(Number(prefs.rate) || 1);
359  audio.addEventListener('play', () => setState('playing'));
360  audio.addEventListener('playing', () => { setState('playing'); hideMessage(); });
361  audio.addEventListener('pause', () => { if (state !== 'error') setState('paused'); saveNow(true); });
362  audio.addEventListener('waiting', () => setState('loading'));
363  audio.addEventListener('stalled', () => { if (state === 'loading') showMessage(t('slow')); });
364  audio.addEventListener('loadedmetadata', () => {
365    if (pendingResume && Math.abs(audio.currentTime - pendingResume) > 2) {
366      try { audio.currentTime = pendingResume; } catch {}
367    }
368    pendingResume = 0;
369    progress();
370  });
371  audio.addEventListener('error', () => { if (audio.src && audio.src !== location.href) onError(); });
372  audio.addEventListener('ended', () => {
373    const it = current();
374    if (it && !beaconSent) beacon(it);
375    if (it) {
376      saveResume(it, 0);
377      analytics.event('Audio', 'complete', `${it.t} — ${it.s}`);
378    }
379    if (sleepAt === -1) {
380      sleepAt = 0;
381      ui.sleep.value = '0';
382      setState('paused');
383      toast(t('sleep_done'));
384      return;
385    }
386    if (repeat === 'one') {
387      audio.currentTime = 0;
388      audio.play().catch(() => {});
389    } else {
390      next(true);
391    }
392  });
393  let raf = 0;
394  audio.addEventListener('timeupdate', () => {
395    const now = performance.now();
396    if (!audio.paused && lastTick) played += Math.min(1.5, (now - lastTick) / 1000);
397    lastTick = audio.paused ? 0 : now;
398    const it = current();
399    if (it && !beaconSent && played >= Math.min(30, (it.d || 60) * 0.5)) beacon(it);
400    if (sleepAt > 0 && Date.now() >= sleepAt) {
401      audio.pause();
402      sleepAt = 0;
403      ui.sleep.value = '0';
404      toast(t('sleep_done'));
405    }
406    saveNow();
407    if (!raf) raf = requestAnimationFrame(() => { raf = 0; progress(); });
408    if ('mediaSession' in navigator && navigator.mediaSession.setPositionState && now - lastPosState > 1000 && isFinite(audio.duration)) {
409      lastPosState = now;
410      try { navigator.mediaSession.setPositionState({ duration: audio.duration, playbackRate: audio.playbackRate, position: Math.min(audio.currentTime, audio.duration) }); } catch {}
411    }
412  });
413  ui.toggle.addEventListener('click', () => toggle());
414  ui.prev.addEventListener('click', () => prev());
415  ui.next.addEventListener('click', () => next());
416  ui.back.addEventListener('click', () => jump(-10));
417  ui.fwd.addEventListener('click', () => jump(30));
418  ui.repeat.addEventListener('click', () => cycleRepeat());
419  ui.rate.addEventListener('change', () => setRate(Number(ui.rate.value) || 1));
420  ui.sleep.addEventListener('change', () => {
421    const v = ui.sleep.value;
422    sleepAt = v === 'end' ? -1 : v === '0' ? 0 : Date.now() + Number(v) * 60000;
423    if (sleepAt) toast(v === 'end' ? t('sleep_end_set') : t('sleep_set', { n: v }));
424  });
425  ui.fav.addEventListener('click', () => toggleFav(ui.fav));
426  ui.close.addEventListener('click', () => {
427    audio.pause();
428    saveNow(true);
429    hideDock();
430    store.del('now');
431    index = -1;
432    queue = [];
433    syncRows(document);
434  });
435  ui.share.addEventListener('click', () => {
436    const it = current();
437    if (it) document.dispatchEvent(new CustomEvent('as:share', { detail: { url: new URL(it.u || location.pathname, location.href).href, title: `${it.t} — ${it.s}` } }));
438  });
439  ui.seek.addEventListener('input', () => {
440    seeking = true;
441    const pct = Number(ui.seek.value) / 1000;
442    ui.seek.style.setProperty('--p', pct * 100 + '%');
443    if (audio.duration) ui.cur.textContent = fmtTime(audio.duration * pct);
444  });
445  ui.seek.addEventListener('change', () => {
446    seeking = false;
447    seekPct(Number(ui.seek.value) / 1000);
448  });
449  ui.seek.addEventListener('keydown', (e) => {
450    if (e.key === 'PageUp' || e.key === 'PageDown') {
451      e.preventDefault();
452      jump(e.key === 'PageUp' ? 60 : -60);
453    }
454  });
455  addEventListener('pagehide', () => saveNow(true));
456  document.addEventListener('visibilitychange', () => { if (document.hidden) saveNow(true); });
457  if ('BroadcastChannel' in window) {
458    const bc = new BroadcastChannel('assajda-player');
459    audio.addEventListener('play', () => bc.postMessage('play'));
460    bc.onmessage = (e) => { if (e.data === 'play' && !audio.paused) audio.pause(); };
461  }
462  const now = store.get('now');
463  if (now && Array.isArray(now.q) && now.q[now.i]) {
464    for (const it of now.q) if (it && typeof it.t === 'string') it.t = it.t.replace(/^[0-9\u0660-\u0669]+\s*/, '');
465    queue = now.q;
466    index = now.i;
467    queueId = now.id || '';
468    pendingResume = now.pos || 0;
469    showDock();
470    setState('paused');
471    render();
472  }
473  updateButtons();
474}
475export function resume() {
476  if (index >= 0) start(index, pendingResume || null);
477}
478export function seekInline(range) {
479  const row = range.closest('.track');
480  const cur = current();
481  if (!row || !cur || Number(row.dataset.id) !== cur.id) return;
482  seekPct(Number(range.value) / 1000);
483}

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.