anihub-tv 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- Instalação alternativa pelo npm: no TizenBrew, escolha Adicionar módulo → npm e digite `anihub-tv@0.3.2`. Esta distribuição não depende do acesso público ao repositório GitHub.
1
+ Instalação alternativa pelo npm: no TizenBrew, escolha Adicionar módulo → npm e digite `anihub-tv@0.3.3`. Esta distribuição não depende do acesso público ao repositório GitHub.
2
2
 
3
- # AniHub — 0.3.2
3
+ # AniHub — 0.3.3
4
4
 
5
5
  Cliente independente PT-BR para Samsung Tizen 3.0 / Chromium 47, com **AniStream como fonte principal após conectar a conta**. Animes Brasil e TopAnimes continuam disponíveis como alternativas. Código novo, separado de Cinefy e StremioK.
6
6
 
@@ -53,7 +53,7 @@ Para instalar esta versão:
53
53
 
54
54
  1. TizenBrew → Gerenciador de módulos → Adicionar módulo GitHub.
55
55
  2. Use `srwayne15-bot/AniHub@main` no campo GitHub, sem `gh/`. O app aparecerá como **AniHub**.
56
- 3. Confira **v0.3.2** na lateral. O manifesto inicia `service/index.js` no serviço Node do TizenBrew.
56
+ 3. Confira **v0.3.3** na lateral. O manifesto inicia `service/index.js` no serviço Node do TizenBrew.
57
57
  4. Feche e reabra TizenBrew após alterações no serviço. O CDN pode demorar a atualizar arquivos de @main.
58
58
  5. Vincule a conta nessa TV por Quick Connect. A sessão do computador não acompanha o pacote.
59
59
 
@@ -79,7 +79,7 @@ Ferramentas de teste: Acorn e jsdom nas versões de package.json. A TV não inst
79
79
 
80
80
  Edite os arquivos de origem do serviço e execute `node tools/build-service.cjs`: o resultado reúne todos os helpers em `service/index.js`, conforme o carregamento do TizenBrew. Esse arquivo gerado precisa acompanhar a publicação.
81
81
 
82
- **73 verificações automatizadas aprovadas**: 51 gerais e 25 AniStream. Elas cobrem ES5, identidade de episódios, quotas, autenticação simulada, persistência da sessão, HTTP401/403, preferência de idiomas, Quick Connect, proteção de destinos, URLs opacas, conversão e deduplicação de legendas e espera pelo carregamento antes de reproduzir. Rede e mídia são simuladas nessas suítes.
82
+ **73 verificações automatizadas aprovadas**: 54 gerais e 25 AniStream. Elas cobrem ES5, identidade de episódios, quotas, autenticação simulada, persistência da sessão, HTTP401/403, preferência de idiomas, Quick Connect, proteção de destinos, URLs opacas, conversão e deduplicação de legendas e espera pelo carregamento antes de reproduzir. Rede e mídia são simuladas nessas suítes.
83
83
 
84
84
  A autenticação real pelo site, catálogo, restauração de sessão e reprodução 1920×1080 com legenda portuguesa visível foram verificados no Opera do computador. Evidências em `evidence/`; os detalhes e limites constam em [ANISTREAM-0.3.0.md](docs/ANISTREAM-0.3.0.md).
85
85
 
@@ -1,8 +1,8 @@
1
- Instalação alternativa pelo npm: no TizenBrew, escolha Adicionar módulo → npm e digite `anihub-tv@0.3.2`. Esta distribuição não depende do acesso público ao repositório GitHub.
1
+ Instalação alternativa pelo npm: no TizenBrew, escolha Adicionar módulo → npm e digite `anihub-tv@0.3.3`. Esta distribuição não depende do acesso público ao repositório GitHub.
2
2
 
3
3
  # Primeiro teste do AniHub na TV
4
4
 
5
- Versão inicial para este teste: **0.3.2**. Aparelho alvo: Samsung UN55MU7500, Tizen 3.0. Ainda não há resultado de teste físico registrado.
5
+ Versão para o próximo teste: **0.3.3**. Aparelho alvo: Samsung UN55MU7500, Tizen 3.0. Ainda não há resultado de teste físico registrado.
6
6
 
7
7
  ## Instalação
8
8
 
@@ -41,3 +41,12 @@ Os 76 testes automatizados e a reprodução real no computador não substituem e
41
41
 
42
42
  ## Correção 0.3.2
43
43
  A foto do teste físico de 0.3.1 mostrou HTML sem CSS aplicado, com controles do player expostos. A causa do carregamento externo não foi isolada na TV. A nova entrada tv.html incorpora CSS e scripts, eliminando essas requisições na abertura. Há diagnóstico de erro de início sem dados de conta. Os testes simulados validam interface, player oculto e ES5 sem rede de assets; a correção ainda precisa de novo teste físico. Atualize o módulo para anihub-tv@0.3.2 e feche/reabra o TizenBrew. Nenhuma limpeza de armazenamento é necessária.
44
+
45
+ ## Ajustes 0.3.3 — controle e carregamento
46
+ O usuário confirmou a abertura e a vinculação na TV com 0.3.2. Relatou teclado abrindo sozinho e vídeo parando para carregar; no PC o vídeo flui normalmente. A causa completa dos engasgos ainda não foi medida no aparelho.
47
+
48
+ Campos de texto agora ficam fora da navegação por setas. Use Digitar busca/usuário/senha para abrir o teclado; Voltar/Concluído devolve o foco ao botão. Quick Connect recebe o foco inicial na conta desconectada. As letras do teclado do app continuam funcionando pelas setas.
49
+
50
+ HLS acumula alvo de 8 segundos no início e 12 após esgotar a reserva. A espera é limitada a 12 segundos para acomodar players nativos que limitam downloads quando pausados. Pausa manual e saída cancelam a espera; legenda continua obrigatória. No fallback MSE, a reserva configurada passa a 45–60 segundos, com meta de memória de 48 MiB e descarte após 10 segundos já assistidos. HLS nativo controla sua própria reserva. As conexões HTTPS de mídia agora usam keep-alive, quatro sockets e dois ociosos, mantendo TLS e a qualidade da fonte.
51
+
52
+ 79 verificações automatizadas passaram, incluindo foco, teclado explícito, reserva/esgotamento, pausa, fim do vídeo, encerramento e compartilhamento do agente HTTPS. Navegação por setas/Enter e saída por Escape conferidas no navegador do computador. Melhoria de streaming ainda depende do teste físico da versão 0.3.3. Testar o mesmo episódio por cinco minutos e anotar se o texto Preparando reprodução aparece repetidamente.
package/index.html CHANGED
@@ -1,9 +1,9 @@
1
1
  <!doctype html>
2
- <html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#10121c"><title>AniHub</title><link rel="stylesheet" href="css/app.css?v=0.3.2"></head>
2
+ <html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#10121c"><title>AniHub</title><link rel="stylesheet" href="css/app.css?v=0.3.3"></head>
3
3
  <body><div id="shell">
4
- <aside><div class="brand"><span class="brand-mark">A<span>+</span></span><strong>ANIHUB<br>TV</strong></div><nav id="nav"><button data-action="home" class="nav-item">Início</button><button data-action="search" class="nav-item">Buscar anime</button><button data-action="favorites" class="nav-item">Minha lista</button><button data-action="history" class="nav-item">Continuar assistindo</button><button data-action="account" class="nav-item">Conta AniStream</button><button data-action="settings" class="nav-item">Preferências</button></nav><div class="sidebar-bottom"><span class="live-dot"></span>Cliente independente<br><small>v0.3.2 · Tizen 3.0</small></div></aside>
4
+ <aside><div class="brand"><span class="brand-mark">A<span>+</span></span><strong>ANIHUB<br>TV</strong></div><nav id="nav"><button data-action="home" class="nav-item">Início</button><button data-action="search" class="nav-item">Buscar anime</button><button data-action="favorites" class="nav-item">Minha lista</button><button data-action="history" class="nav-item">Continuar assistindo</button><button data-action="account" class="nav-item">Conta AniStream</button><button data-action="settings" class="nav-item">Preferências</button></nav><div class="sidebar-bottom"><span class="live-dot"></span>Cliente independente<br><small>v0.3.3 · Tizen 3.0</small></div></aside>
5
5
  <main id="main"><header><span id="breadcrumb">Seu próximo episódio</span><button id="refresh" data-action="refresh">Atualizar agora</button></header><div id="status" role="status" aria-live="polite"></div><section id="content"></section></main>
6
6
  </div><div id="dialog" class="hidden" role="dialog" aria-modal="true" aria-labelledby="dialog-title"><div class="dialog-box"><span class="eyebrow">ANIHUB TV</span><h2 id="dialog-title"></h2><p id="dialog-text"></p><div id="dialog-options"></div></div></div>
7
7
  <div id="player" class="hidden"><video id="video" preload="metadata"></video><div id="player-hud"><span class="eyebrow" id="player-state">Preparando vídeo</span><h2 id="player-title"></h2><p id="player-quality"></p><div class="timeline"><div id="progress-bar"></div></div><div id="player-time">00:00 / --:--</div><div class="player-buttons"><button data-action="seek-back">−10 s</button><button data-action="play-pause">Pausar / continuar</button><button data-action="seek-forward">+10 s</button><button data-action="quality">Qualidade</button><button data-action="next">Próximo episódio</button><button data-action="close-player">Voltar ao anime</button></div></div></div>
8
8
  <div id="toast" class="hidden" role="status"></div>
9
- <script src="js/top-provider.js?v=0.3.2"></script><script src="js/core.js?v=0.3.2"></script><script src="js/vendor/hls.light.min.js?v=1.6.13"></script><script src="js/ani-client.js?v=0.3.2"></script><script src="js/app.js?v=0.3.2"></script></body></html>
9
+ <script src="js/top-provider.js?v=0.3.3"></script><script src="js/core.js?v=0.3.3"></script><script src="js/vendor/hls.light.min.js?v=1.6.13"></script><script src="js/ani-client.js?v=0.3.3"></script><script src="js/playback-buffer.js?v=0.3.3"></script><script src="js/app.js?v=0.3.3"></script></body></html>
package/js/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
  'use strict';
3
- var C = window.ABTV, A = window.ABTVAniClient, BASE = 'http://127.0.0.1:8238', store, aniValidated = false, quickTimer, captionTimer;
3
+ var C = window.ABTV, A = window.ABTVAniClient, BASE = 'http://127.0.0.1:8238', store, aniValidated = false, quickTimer, captionTimer, editingInput = null, playbackBuffer = null, userPaused = false;
4
4
  try { store = new C.Store(window.localStorage); } catch (ignore) { store = new C.Store({ getItem: function () { return null; }, setItem: function () { throw new Error('Armazenamento bloqueado'); } }); }
5
5
  var gate = new C.Gate(), requests = [], state = { view: 'home', anime: null, episode: null, season: 1, query: '' }, stack = [], detailCache = {}, sourceData = null;
6
6
  var video = document.getElementById('video'), hlsPlayer = null, playing = false, format = null, captionsReady = true, captionsFailed = false, playbackEpisode = null, playbackAnime = null, lastSaved = 0, lastResume = 0, wasHidden = false, loadSeq = 0, searchTimer, hudTimer, toastTimer, dialogReturn, focusReturn;
@@ -11,7 +11,16 @@
11
11
  function status(message) { el('status').textContent = message; }
12
12
  function timeAt(at) { return new Date(at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); }
13
13
  function visible(n) { return !!(n && n.getBoundingClientRect().width && n.getBoundingClientRect().height); }
14
- function focus(n) { if (!n) { return; } n.focus(); if (n.scrollIntoViewIfNeeded) { n.scrollIntoViewIfNeeded(false); } else { var r = n.getBoundingClientRect(); if (r.top < 0 || r.bottom > window.innerHeight) { n.scrollIntoView(false); } } }
14
+ function focus(n) { if (!n) { return; } if (n._editButton && editingInput !== n) { n = n._editButton; } n.focus(); if (n.scrollIntoViewIfNeeded) { n.scrollIntoViewIfNeeded(false); } else { var r = n.getBoundingClientRect(); if (r.top < 0 || r.bottom > window.innerHeight) { n.scrollIntoView(false); } } }
15
+ function armInput(input, label) {
16
+ var edit = button(label, 'edit-input', input.id);
17
+ input._editButton = edit; input.readOnly = true; input.tabIndex = -1;
18
+ input.onclick = function () { beginInput(input); };
19
+ input.onblur = function () { input.readOnly = true; if (editingInput === input) { editingInput = null; } };
20
+ return edit;
21
+ }
22
+ function beginInput(input) { if (!input) { return; } editingInput = input; input.readOnly = false; input.focus(); }
23
+ function finishInput() { var input = editingInput; if (!input) { return; } editingInput = null; input.readOnly = true; input.blur(); focus(input._editButton); }
15
24
  function rememberFocus() { var n = document.activeElement; return n && n.getAttribute ? { action: n.getAttribute('data-action'), value: n.getAttribute('data-value'), scroll: el('main').scrollTop } : null; }
16
25
  function restoreFocus(saved) {
17
26
  var bs = el('content').querySelectorAll('button'), i;
@@ -29,7 +38,7 @@
29
38
  xhr.onerror = function () { finish('Serviço local indisponível. Feche e abra o TizenBrew.'); }; xhr.ontimeout = function () { finish('A consulta demorou demais. Use Atualizar.'); }; xhr.onabort = function () { done = true; };
30
39
  xhr.send(); return xhr;
31
40
  }
32
- function cleanContent(label) { el('breadcrumb').textContent = label; el('content').textContent = ''; el('main').scrollTop = 0; status(''); }
41
+ function cleanContent(label) { finishInput(); el('breadcrumb').textContent = label; el('content').textContent = ''; el('main').scrollTop = 0; status(''); }
33
42
  function empty(title, message, target) { var box = node('div', 'empty'); box.appendChild(node('h2', '', title)); box.appendChild(node('p', 'muted', message)); (target || el('content')).appendChild(box); }
34
43
  function error(message) { status(message); if (!el('content').textContent) { empty('Não foi possível consultar agora', message + ' Você pode tentar novamente pelo botão Atualizar agora.'); } }
35
44
  function activate() { var ns = el('nav').querySelectorAll('button'), i; for (i = 0; i < ns.length; i++) { ns[i].classList.toggle('active', ns[i].getAttribute('data-action') === state.view); } }
@@ -76,7 +85,7 @@
76
85
  api('/page', '/', function (err, d) { if (err) { error(err); return; } var items = C.cards(d.html, 'episodio'), sections = C.homeSections(d.html); var f = rememberFocus(); store.set('home', { items: items.slice(0, 48), sections: sections, checkedAt: d.checkedAt }); showHome(items, d.checkedAt, saved || f, sections); });
77
86
  }
78
87
  function search(saved) {
79
- cleanContent('Buscar anime'); title('O que você quer assistir?'); var input = node('input', 'search-input'); input.id = 'query'; input.type = 'text'; input.placeholder = 'Digite o nome do anime'; input.value = state.query || ''; input.setAttribute('aria-label', 'Buscar anime'); input.oninput = function () { state.query = input.value; scheduleSearch(); }; el('content').appendChild(input);
88
+ cleanContent('Buscar anime'); title('O que você quer assistir?'); var input = node('input', 'search-input'); input.id = 'query'; input.type = 'text'; input.placeholder = 'Digite o nome do anime'; input.value = state.query || ''; input.setAttribute('aria-label', 'Buscar anime'); input.oninput = function () { state.query = input.value; scheduleSearch(); }; el('content').appendChild(input); el('content').appendChild(armInput(input, 'Digitar busca'));
80
89
  var keyboard = node('div', 'keyboard'); 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.split('').forEach(function (k) { keyboard.appendChild(button(k, 'key', k)); }); keyboard.appendChild(button('Espaço', 'key', ' ', 'wide')); keyboard.appendChild(button('Apagar', 'erase', undefined, 'wide')); keyboard.appendChild(button('Buscar', 'submit', undefined, 'wide primary')); el('content').appendChild(keyboard); var results = node('div'); results.id = 'results'; el('content').appendChild(results);
81
90
  if (state.query) { runSearch(); } else { empty('Encontre a próxima história', 'Use o teclado da tela ou digite no campo de busca.', results); }
82
91
  if (saved) { restoreFocus(saved); } else { focus(input); }
@@ -162,19 +171,24 @@
162
171
  if (!playbackEpisode || !video.currentTime || video.currentTime < 2) { return; } var record = { position: video.currentTime, duration: isFinite(video.duration) ? video.duration : 0, at: Date.now() };
163
172
  store.set(C.progressKey(playbackEpisode), record); var history = store.get('history', []).filter(function (r) { return r.episode.path !== playbackEpisode.path; }); history.unshift({ episode: playbackEpisode, anime: { path: playbackAnime.path, title: playbackAnime.title, image: playbackAnime.image }, position: record.position }); store.set('history', history.slice(0, 30));
164
173
  }
165
- function playSafe() { if (!captionsReady) { if (captionsFailed) { toast('A legenda falhou. Volte e use Atualizar fontes.'); } return; } try { var p = video.play(); if (p && typeof p.then === 'function') { p.then(function () {}, function () { toast('Pressione Pausar / continuar para iniciar.'); }); } } catch (ignore) { toast('Não foi possível iniciar o player. Atualize as fontes.'); } }
174
+ function playNow() { try { var p = video.play(); if (p && typeof p.then === 'function') { p.then(function () {}, function () { toast('Pressione Pausar / continuar para iniciar.'); }); } } catch (ignore) { toast('Não foi possível iniciar o player. Atualize as fontes.'); } }
175
+
176
+ function pauseByUser() { userPaused = true; if (playbackBuffer) { playbackBuffer.pause(); } else { video.pause(); } }
177
+ function playSafe(explicit) { if (explicit) { userPaused = false; } if (userPaused) { return; } if (!captionsReady) { if (captionsFailed) { toast('A legenda falhou. Volte e use Atualizar fontes.'); } return; } if (playbackBuffer) { playbackBuffer.play(); } else { playNow(); } }
166
178
  function clearCaptions() { clearTimeout(captionTimer); var tracks = video.querySelectorAll('track'), i; for (i = 0; i < tracks.length; i++) { tracks[i].onload = null; tracks[i].onerror = null; video.removeChild(tracks[i]); } }
167
179
  function positionCaptions(raised) { try { var tracks = video.textTracks, i, j; for (i = 0; i < tracks.length; i++) { var cues = tracks[i].activeCues || []; for (j = 0; j < cues.length; j++) { cues[j].line = raised ? -10 : -1; } } } catch (ignore) {} }
168
180
  function startPlayer(f, changing) {
169
181
  var position = changing ? video.currentTime || 0 : (store.get(C.progressKey(state.episode), {}) || {}).position || 0;
170
182
  if (!changing) { focusReturn = rememberFocus(); playbackEpisode = state.episode; playbackAnime = state.anime; }
171
- if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); } format = f; playing = true; loadSeq++; var seq = loadSeq; captionsReady = !f.subtitleUrl; captionsFailed = false; clearCaptions(); video.crossOrigin = 'anonymous'; el('player').classList.remove('hidden'); el('player-title').textContent = playbackAnime.title + ' · T' + playbackEpisode.season + ' E' + playbackEpisode.number; el('player-state').textContent = 'Preparando vídeo'; el('player-quality').textContent = f.height + 'p informado · aguardando resolução efetiva';
172
- if (f.subtitleUrl) { var track = node('track'); track.kind = 'subtitles'; track.label = 'Português'; track.srclang = f.subtitleLanguage || 'pt'; track.default = true; track.src = f.subtitleUrl; track.onload = function () { if (seq !== loadSeq) { return; } captionsReady = true; clearTimeout(captionTimer); try { track.track.mode = 'showing'; track.track.oncuechange = function () { positionCaptions(!el('player-hud').classList.contains('hidden')); }; } catch (ignore) {} positionCaptions(true); playSafe(); }; track.onerror = function () { if (seq !== loadSeq) { return; } clearTimeout(captionTimer); captionsFailed = true; captionsReady = false; video.pause(); el('player-state').textContent = 'Legenda indisponível'; showHud(); toast('Não foi possível carregar a legenda portuguesa. Volte e use Atualizar fontes.'); }; video.appendChild(track); try { track.track.mode = 'showing'; } catch (ignore2) {} captionTimer = setTimeout(function () { if (seq === loadSeq && !captionsReady && track.onerror) { track.onerror(); } }, 20000); }
183
+ if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); } if (playbackBuffer) { playbackBuffer.destroy(); } playbackBuffer = null; userPaused = false; format = f; playing = true; loadSeq++; var seq = loadSeq; captionsReady = !f.subtitleUrl; captionsFailed = false; clearCaptions(); video.crossOrigin = 'anonymous'; el('player').classList.remove('hidden'); el('player-title').textContent = playbackAnime.title + ' · T' + playbackEpisode.season + ' E' + playbackEpisode.number; el('player-state').textContent = 'Preparando vídeo'; el('player-quality').textContent = f.height + 'p informado · aguardando resolução efetiva';
184
+ video.preload = 'auto';
185
+ if (f.transport === 'hls' && window.ABTVBuffer) { playbackBuffer = window.ABTVBuffer(video, playNow, function (message) { if (seq === loadSeq) { el('player-state').textContent = message; if (message !== 'Reproduzindo') { showHud(); } } }); }
186
+ if (f.subtitleUrl) { var track = node('track'); track.kind = 'subtitles'; track.label = 'Português'; track.srclang = f.subtitleLanguage || 'pt'; track.default = true; track.src = f.subtitleUrl; track.onload = function () { if (seq !== loadSeq) { return; } captionsReady = true; clearTimeout(captionTimer); try { track.track.mode = 'showing'; track.track.oncuechange = function () { positionCaptions(!el('player-hud').classList.contains('hidden')); }; } catch (ignore) {} positionCaptions(true); playSafe(); }; track.onerror = function () { if (seq !== loadSeq) { return; } clearTimeout(captionTimer); captionsFailed = true; captionsReady = false; if (playbackBuffer) { playbackBuffer.pause(); } else { video.pause(); } el('player-state').textContent = 'Legenda indisponível'; showHud(); toast('Não foi possível carregar a legenda portuguesa. Volte e use Atualizar fontes.'); }; video.appendChild(track); try { track.track.mode = 'showing'; } catch (ignore2) {} captionTimer = setTimeout(function () { if (seq === loadSeq && !captionsReady && track.onerror) { track.onerror(); } }, 20000); }
173
187
  video.onloadedmetadata = function () { if (seq !== loadSeq) { return; } if (position > 0 && isFinite(video.duration) && position < video.duration - 5) { try { video.currentTime = position; } catch (ignore) {} } updateResolution(); playSafe(); };
174
188
  video.onerror = function () { if (seq !== loadSeq) { return; } showHud(); el('player-state').textContent = 'Falha de reprodução'; toast('O vídeo falhou ou a fonte expirou. Volte e use Atualizar fontes.'); };
175
189
  if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; }
176
190
  if (f.transport === 'hls' && !video.canPlayType('application/vnd.apple.mpegurl') && !video.canPlayType('application/x-mpegURL') && window.Hls && window.Hls.isSupported()) {
177
- hlsPlayer = new window.Hls({ enableWorker: false, maxBufferLength: 20, maxMaxBufferLength: 30, backBufferLength: 15, maxBufferSize: 32 * 1024 * 1024 });
191
+ hlsPlayer = new window.Hls({ enableWorker: false, maxBufferLength: 45, maxMaxBufferLength: 60, backBufferLength: 10, maxBufferSize: 48 * 1024 * 1024 });
178
192
  hlsPlayer.on(window.Hls.Events.ERROR, function (event, data) { if (data.fatal && seq === loadSeq && video.onerror) { video.onerror(); } });
179
193
  hlsPlayer.loadSource(f.url); hlsPlayer.attachMedia(video); hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function () { if (seq === loadSeq) { playSafe(); } });
180
194
  } else { video.src = f.url; video.load(); playSafe(); }
@@ -182,7 +196,7 @@
182
196
  }
183
197
  function updateResolution() { if (!playing) { return; } var dims = video.videoWidth && video.videoHeight ? video.videoWidth + '×' + video.videoHeight + ' efetivo' : 'Resolução efetiva ainda não informada'; el('player-quality').textContent = (format.provider ? format.provider + ' · ' : format.original ? 'Original do site · ' : 'Blogger · ') + format.height + 'p informado · ' + dims + (format.subtitleTitle ? ' · Legenda: ' + format.subtitleTitle : ''); }
184
198
  function showHud() { clearTimeout(hudTimer); el('player-hud').classList.remove('hidden'); positionCaptions(true); hudTimer = setTimeout(function () { if (!video.paused && el('dialog').classList.contains('hidden')) { el('player-hud').classList.add('hidden'); positionCaptions(false); } }, 8000); }
185
- function closePlayer(quiet) { if (!playing) { return; } saveProgress(); if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); format.ticket = null; } playing = false; loadSeq++; clearTimeout(hudTimer); if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; } video.pause(); clearCaptions(); captionsReady = true; video.removeAttribute('src'); video.load(); video.onloadedmetadata = null; video.onerror = null; el('player').classList.add('hidden'); if (!quiet) { restoreFocus(focusReturn); } }
199
+ function closePlayer(quiet) { if (!playing) { return; } saveProgress(); if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); format.ticket = null; } playing = false; loadSeq++; if (playbackBuffer) { playbackBuffer.destroy(); playbackBuffer = null; } clearTimeout(hudTimer); if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; } video.pause(); clearCaptions(); captionsReady = true; video.removeAttribute('src'); video.load(); video.onloadedmetadata = null; video.onerror = null; el('player').classList.add('hidden'); if (!quiet) { restoreFocus(focusReturn); } }
186
200
  function nextEpisode() {
187
201
  if (!playbackAnime || !playbackAnime.episodes || !playbackAnime.episodes.length) { toast('Abra Ver temporadas para escolher o próximo episódio.'); return; }
188
202
  var list = playbackAnime.episodes, i, next = null; for (i = 0; i < list.length - 1; i++) { if (list[i].path === playbackEpisode.path) { next = list[i + 1]; break; } }
@@ -198,12 +212,12 @@
198
212
  if (A.connected()) { box.appendChild(node('h3', '', A.name())); box.appendChild(node('p', 'muted', A.persistent() ? 'A sessão está salva neste aparelho. Ao abrir o app, ela será verificada novamente.' : 'A sessão está apenas na memória. O armazenamento não permitiu salvá-la; será preciso entrar novamente ao fechar.')); box.appendChild(button('Usar AniStream como principal', 'ani-primary', undefined, 'primary')); box.appendChild(button('Usar Animes Brasil', 'ani-secondary')); box.appendChild(button('Sair deste perfil no app', 'ani-logout')); }
199
213
  else {
200
214
  box.appendChild(node('p', 'muted', 'Use o usuário e a senha do perfil Jellyfin do AniStream. Essa senha pode ser diferente da senha da conta por e-mail.'));
201
- var user = node('input', 'search-input'); user.id = 'ani-user'; user.type = 'text'; user.placeholder = 'Usuário do perfil AniStream'; user.setAttribute('aria-label', 'Usuário do perfil AniStream'); user.setAttribute('autocomplete', 'off'); box.appendChild(user);
202
- var pass = node('input', 'search-input'); pass.id = 'ani-password'; pass.type = 'password'; pass.placeholder = 'Senha do perfil Jellyfin'; pass.setAttribute('aria-label', 'Senha do perfil Jellyfin'); pass.setAttribute('autocomplete', 'off'); box.appendChild(pass);
215
+ var user = node('input', 'search-input'); user.id = 'ani-user'; user.type = 'text'; user.placeholder = 'Usuário do perfil AniStream'; user.setAttribute('aria-label', 'Usuário do perfil AniStream'); user.setAttribute('autocomplete', 'off'); box.appendChild(user); box.appendChild(armInput(user, 'Digitar usuário'));
216
+ var pass = node('input', 'search-input'); pass.id = 'ani-password'; pass.type = 'password'; pass.placeholder = 'Senha do perfil Jellyfin'; pass.setAttribute('aria-label', 'Senha do perfil Jellyfin'); pass.setAttribute('autocomplete', 'off'); box.appendChild(pass); box.appendChild(armInput(pass, 'Digitar senha'));
203
217
  box.appendChild(button('Entrar e lembrar neste aparelho', 'ani-login', undefined, 'primary')); box.appendChild(button('Vincular por código Quick Connect', 'ani-quick'));
204
218
  var code = node('div'); code.id = 'ani-code'; box.appendChild(code);
205
219
  }
206
- box.appendChild(node('p', 'muted', 'Sua senha não é salva. Somente a sessão fica neste aparelho. O armazenamento do TizenBrew é compartilhado entre módulos. Se o AniStream encerrar a sessão, será necessário entrar novamente.')); el('content').appendChild(box); focus(box.querySelector('input,button'));
220
+ box.appendChild(node('p', 'muted', 'Sua senha não é salva. Somente a sessão fica neste aparelho. O armazenamento do TizenBrew é compartilhado entre módulos. Se o AniStream encerrar a sessão, será necessário entrar novamente.')); el('content').appendChild(box); focus(box.querySelector('[data-action="ani-quick"]') || box.querySelector('button'));
207
221
  }
208
222
  function aniLoggedIn(d) { A.save(d); aniValidated = true; detailCache = {}; toast(A.persistent() ? 'AniStream conectado. A sessão será lembrada neste aparelho.' : 'AniStream conectado apenas nesta sessão: armazenamento indisponível.'); top('home'); }
209
223
  function aniLogin() {
@@ -222,6 +236,7 @@
222
236
  function render(saved) { if (state.view === 'home') { home(saved); } else if (state.view === 'search') { search(saved); } else if (state.view === 'detail') { detail(saved); } else if (state.view === 'episode') { episodePage(); } else if (state.view === 'account') { account(); } else if (state.view === 'settings') { settings(); } else { library(); } }
223
237
  function refresh() { if (playing) { toast('Volte à tela do episódio para atualizar sem interromper o vídeo.'); return; } cancel(); if (state.view === 'episode' && state.episode) { resolveSources(); } else if (state.view === 'detail') { detail(rememberFocus()); } else if (state.view === 'search') { runSearch(); } else { render(rememberFocus()); } }
224
238
  function action(a, value, target) {
239
+ if (a === 'edit-input') { beginInput(el(value)); return; }
225
240
  if (a === 'home' || a === 'search' || a === 'favorites' || a === 'history' || a === 'settings' || a === 'account') { top(a); }
226
241
  else if (a === 'ani-login') { aniLogin(); }
227
242
  else if (a === 'ani-quick') { aniQuick(); }
@@ -241,7 +256,7 @@
241
256
  else if (a === 'submit') { clearTimeout(searchTimer); runSearch(); }
242
257
  else if (a === 'toggle-lower') { store.set('allowLower', !store.get('allowLower', false)); settings(); }
243
258
  else if (a === 'close-player') { closePlayer(); }
244
- else if (a === 'play-pause') { if (video.paused) { playSafe(); } else { video.pause(); } showHud(); }
259
+ else if (a === 'play-pause') { if (userPaused || (video.paused && !(playbackBuffer && playbackBuffer.holding()))) { playSafe(true); } else { pauseByUser(); } showHud(); }
245
260
  else if (a === 'seek-back' || a === 'seek-forward') { try { video.currentTime = Math.max(0, Math.min(isFinite(video.duration) ? video.duration : video.currentTime + 10, video.currentTime + (a === 'seek-back' ? -10 : 10))); } catch (ignore) {} showHud(); }
246
261
  else if (a === 'next') { nextEpisode(); }
247
262
  else if (a === 'confirm-language') { dialog('Você conferiu o idioma?', 'Confirme somente se este episódio está com áudio japonês e legenda PT-BR legível.', [{ label: 'Sim, confirmei os dois', fn: function () { store.set('language:' + playbackEpisode.path, true); toast('Idioma confirmado por você neste episódio'); } }, { label: 'Cancelar', fn: function () {} }]); }
@@ -250,7 +265,7 @@
250
265
  }
251
266
  document.addEventListener('click', function (e) { var n = e.target; while (n && n !== document) { if (n.getAttribute && n.getAttribute('data-action')) { action(n.getAttribute('data-action'), n.getAttribute('data-value'), n); return; } n = n.parentNode; } });
252
267
  function navigate(code) {
253
- var container = !el('dialog').classList.contains('hidden') ? el('dialog') : playing ? el('player-hud') : document, all = container.querySelectorAll('button,input'), current = document.activeElement, r = current && current.getBoundingClientRect ? current.getBoundingClientRect() : null, best = null, score = Infinity, i;
268
+ var container = !el('dialog').classList.contains('hidden') ? el('dialog') : playing ? el('player-hud') : document, all = container.querySelectorAll('button'), current = document.activeElement, r = current && current.getBoundingClientRect ? current.getBoundingClientRect() : null, best = null, score = Infinity, i;
254
269
  if (!r || !visible(current)) { for (i = 0; i < all.length; i++) { if (visible(all[i])) { focus(all[i]); return; } } }
255
270
  var x = r.left + r.width / 2, y = r.top + r.height / 2;
256
271
  for (i = 0; i < all.length; i++) { var n = all[i]; if (n === current || !visible(n) || n.disabled) { continue; } var q = n.getBoundingClientRect(), dx = q.left + q.width / 2 - x, dy = q.top + q.height / 2 - y, primary = code === 37 ? -dx : code === 39 ? dx : code === 38 ? -dy : dy, secondary = code === 37 || code === 39 ? Math.abs(dy) : Math.abs(dx); if (primary <= 3) { continue; } var s = primary + secondary * 2.5; if (s < score) { score = s; best = n; } }
@@ -258,14 +273,16 @@
258
273
  }
259
274
  document.addEventListener('keydown', function (e) {
260
275
  var k = e.keyCode || e.which, inDialog = !el('dialog').classList.contains('hidden');
276
+ if (editingInput) { if (k === 13 || k === 65376 || k === 65385 || k === 10009 || k === 27 || k === 461) { var searched = editingInput.id === 'query' && (k === 13 || k === 65376); e.preventDefault(); finishInput(); if (searched) { clearTimeout(searchTimer); runSearch(); } } return; }
261
277
  if (k === 10009 || k === 27 || k === 461) { e.preventDefault(); back(); return; }
262
- if (playing && !inDialog && [415, 19, 10252, 413, 412, 417].indexOf(k) !== -1) { e.preventDefault(); if (k === 413) { closePlayer(); } else if (k === 415) { playSafe(); } else if (k === 19) { video.pause(); } else { action(k === 412 ? 'seek-back' : k === 417 ? 'seek-forward' : 'play-pause'); } showHud(); return; }
278
+ if (playing && !inDialog && [415, 19, 10252, 413, 412, 417].indexOf(k) !== -1) { e.preventDefault(); if (k === 413) { closePlayer(); } else if (k === 415) { playSafe(true); } else if (k === 19) { pauseByUser(); } else { action(k === 412 ? 'seek-back' : k === 417 ? 'seek-forward' : 'play-pause'); } showHud(); return; }
263
279
  if (k >= 37 && k <= 40) { if (document.activeElement === el('query') && (k === 37 || k === 39)) { return; } e.preventDefault(); if (playing) { showHud(); } navigate(k); }
264
280
  if (k === 13 && document.activeElement === el('query')) { e.preventDefault(); clearTimeout(searchTimer); runSearch(); }
265
281
  else if (k === 13 && document.activeElement && document.activeElement.tagName === 'BUTTON') { e.preventDefault(); document.activeElement.click(); }
266
282
  });
283
+ video.addEventListener('progress', function () { if (playbackBuffer) { playbackBuffer.progress(); } });
267
284
  video.addEventListener('timeupdate', function () { if (!playing) { return; } el('player-time').textContent = clock(video.currentTime) + ' / ' + clock(video.duration); el('progress-bar').style.width = (video.duration ? Math.min(100, video.currentTime / video.duration * 100) : 0) + '%'; updateResolution(); if (Date.now() - lastSaved > 10000) { lastSaved = Date.now(); saveProgress(); } });
268
- video.addEventListener('playing', function () { el('player-state').textContent = 'Reproduzindo'; showHud(); }); video.addEventListener('waiting', function () { el('player-state').textContent = 'Carregando vídeo…'; }); video.addEventListener('pause', function () { if (playing) { el('player-state').textContent = 'Pausado'; showHud(); saveProgress(); } }); video.addEventListener('ended', function () { saveProgress(); nextEpisode(); });
285
+ video.addEventListener('playing', function () { el('player-state').textContent = 'Reproduzindo'; showHud(); }); video.addEventListener('waiting', function () { el('player-state').textContent = 'Carregando vídeo…'; if (playbackBuffer) { playbackBuffer.waiting(); } }); video.addEventListener('pause', function () { if (playing && !(playbackBuffer && playbackBuffer.holding())) { el('player-state').textContent = 'Pausado'; showHud(); saveProgress(); } }); video.addEventListener('ended', function () { saveProgress(); nextEpisode(); });
269
286
  function resume() { var now = Date.now(); if (now - lastResume < 1500) { return; } lastResume = now; if (playing) { saveProgress(); return; } if (!el('dialog').classList.contains('hidden')) { return; } refresh(); }
270
287
  document.addEventListener('visibilitychange', function () { if (document.hidden) { wasHidden = true; saveProgress(); } else if (wasHidden) { wasHidden = false; resume(); } }); window.addEventListener('pageshow', function (e) { if (e.persisted) { resume(); } });
271
288
  try { if (window.tizen && window.tizen.tvinputdevice) { ['MediaPlay', 'MediaPause', 'MediaPlayPause', 'MediaStop', 'MediaRewind', 'MediaFastForward'].forEach(function (k) { try { window.tizen.tvinputdevice.registerKey(k); } catch (ignore) {} }); } } catch (ignore) {}
@@ -0,0 +1,35 @@
1
+ /* ES5: bounded prebuffering for native HLS and MSE, without quality changes. */
2
+ (function (root) {
3
+ 'use strict';
4
+ root.ABTVBuffer = function (video, play, report, clock) {
5
+ clock = clock || { now: Date.now, set: setTimeout, clear: clearTimeout };
6
+ var active = true, wanted = false, holding = false, started = false, since = 0, timer, target = 8;
7
+ function ahead() {
8
+ var i, ranges = video.buffered, time = video.currentTime || 0;
9
+ for (i = 0; ranges && i < ranges.length; i++) {
10
+ if (ranges.start(i) <= time + 0.1 && ranges.end(i) > time) { return ranges.end(i) - time; }
11
+ }
12
+ return 0;
13
+ }
14
+ function tick() {
15
+ clock.clear(timer);
16
+ if (!active || !wanted || !holding) { return; }
17
+ var seconds = ahead(), remaining = isFinite(video.duration) ? video.duration - video.currentTime : Infinity;
18
+ // Some native players limit downloading while paused. Never wait indefinitely.
19
+ if (seconds >= target || (remaining > 0 && seconds >= remaining - 0.25) || clock.now() - since >= 12000) {
20
+ holding = false; started = true; report('Reproduzindo'); play(); return;
21
+ }
22
+ report('Preparando reprodução · ' + Math.floor(seconds) + ' s carregados');
23
+ timer = clock.set(tick, 500);
24
+ }
25
+ function hold() { holding = true; since = clock.now(); video.pause(); tick(); }
26
+ return {
27
+ play: function () { if (!active) { return; } wanted = true; if (holding) { tick(); } else if (!started) { hold(); } else { play(); } },
28
+ pause: function () { wanted = false; holding = false; clock.clear(timer); video.pause(); },
29
+ waiting: function () { if (active && wanted && started && !holding && !video.seeking && ahead() < 0.75) { target = 12; hold(); } },
30
+ progress: tick,
31
+ holding: function () { return holding; },
32
+ destroy: function () { active = false; wanted = false; holding = false; clock.clear(timer); }
33
+ };
34
+ };
35
+ }(window));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anihub-tv",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "appName": "AniHub",
5
5
  "description": "Cliente independente ES5 para AniStream, Animes Brasil e TopAnimes no Tizen 3.0",
6
6
  "packageType": "app",
@@ -39,13 +39,14 @@ var ABTVAniService = (function () {
39
39
  function create(options) {
40
40
  options = options || {};
41
41
  var https = options.https || require('https'), url = require('url'), crypto = require('crypto'), tickets = {}, quick = {}, revoked = {};
42
- function auth(s) { return 'MediaBrowser Client="AniHub", Device="Samsung Tizen", DeviceId="' + encodeURIComponent(s.deviceId) + '", Version="0.3.2"' + (s.token ? ', Token="' + encodeURIComponent(s.token) + '"' : ''); }
42
+ var mediaAgent = https.Agent ? new https.Agent({ keepAlive: true, maxSockets: 4, maxFreeSockets: 2, keepAliveMsecs: 1000 }) : undefined;
43
+ function auth(s) { return 'MediaBrowser Client="AniHub", Device="Samsung Tizen", DeviceId="' + encodeURIComponent(s.deviceId) + '", Version="0.3.3"' + (s.token ? ', Token="' + encodeURIComponent(s.token) + '"' : ''); }
43
44
  function error(code, message, status) { var e = new Error(message); e.code = code; e.status = status || 502; return e; }
44
45
  function request(s, method, path, body, cb) {
45
46
  if (options.request) { options.request(s, method, path, body, cb); return; }
46
47
  var opts = url.parse(BASE + path), done = false, timer;
47
48
  var data = body === undefined ? null : JSON.stringify(body);
48
- opts.method = method; opts.headers = { Authorization: auth(s), 'User-Agent': 'AniHub/0.3.2 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity', Accept: 'application/json' };
49
+ opts.method = method; opts.headers = { Authorization: auth(s), 'User-Agent': 'AniHub/0.3.3 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity', Accept: 'application/json' };
49
50
  if (data !== null) { opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Length'] = Buffer.byteLength(data); }
50
51
  function finish(e, value) { if (done) { return; } done = true; clearTimeout(timer); cb(e, value); }
51
52
  var req = https.request(opts, function (res) {
@@ -153,7 +154,7 @@ var ABTVAniService = (function () {
153
154
  if (t.kind === 'image') { address = '/Items/' + t.data.id + '/Images/Primary?maxWidth=480&quality=85'; }
154
155
  else if (t.kind === 'subtitle') { address = t.data.path; }
155
156
  else { address = query.part === undefined ? t.data.path : /^\d+$/.test(String(query.part)) ? t.data.parts[Number(query.part)] : ''; if (!address || !safeMedia(address)) { res.writeHead(403); res.end(); return; } }
156
- var opts = url.parse(BASE + relative(address)); opts.headers = { Authorization: auth(t.session), 'User-Agent': 'AniHub/0.3.2 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity' };
157
+ var opts = url.parse(BASE + relative(address)); opts.agent = mediaAgent; opts.headers = { Authorization: auth(t.session), 'User-Agent': 'AniHub/0.3.3 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity' };
157
158
  upstream = https.get(opts, function (r) {
158
159
  if (r.statusCode !== 200 && r.statusCode !== 206) { r.resume(); res.writeHead(r.statusCode === 401 ? 401 : 502); res.end('Origem indisponível: HTTP ' + r.statusCode + '.'); return; }
159
160
  if (t.kind === 'subtitle') { var subtitleParts = [], subtitleSize = 0; r.on('data', function (b) { subtitleSize += b.length; if (subtitleSize > 16 * 1024 * 1024) { upstream.destroy(); } else { subtitleParts.push(b); } }); r.on('end', function () { if (res.finished || done) { return; } try { var text = toVtt(Buffer.concat(subtitleParts).toString('utf8'), t.data.codec); t.data.vtt = text; res.writeHead(200, { 'Content-Type': 'text/vtt; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(text); } catch (ignore) { res.writeHead(502); res.end('Legenda WebVTT indisponível.'); } }); r.on('error', function (e) { if (!res.headersSent) { res.writeHead(502); res.end('Falha na transferência da legenda. Atualize as fontes.'); } else { res.destroy(); } }); return; }
@@ -169,7 +170,7 @@ var ABTVAniService = (function () {
169
170
  r.on('error', function () { res.destroy(); });
170
171
  });
171
172
  upstream.setTimeout(25000, function () { upstream.destroy(); }); upstream.on('error', function (e) { if (!done) { if (!res.headersSent) { res.writeHead(502); res.end('Falha na mídia: ' + String(e.code || 'NETWORK') + '.'); } else { res.destroy(); } } });
172
- res.on('close', function () { done = true; upstream.destroy(); });
173
+ res.on('close', function () { done = true; if (!res.finished) { upstream.destroy(); } });
173
174
  }
174
175
  return { handle: handle, relay: relay, session: session, safeMedia: safeMedia };
175
176
  }
package/service/index.js CHANGED
@@ -128,13 +128,14 @@ var ABTVAniService = (function () {
128
128
  function create(options) {
129
129
  options = options || {};
130
130
  var https = options.https || require('https'), url = require('url'), crypto = require('crypto'), tickets = {}, quick = {}, revoked = {};
131
- function auth(s) { return 'MediaBrowser Client="AniHub", Device="Samsung Tizen", DeviceId="' + encodeURIComponent(s.deviceId) + '", Version="0.3.2"' + (s.token ? ', Token="' + encodeURIComponent(s.token) + '"' : ''); }
131
+ var mediaAgent = https.Agent ? new https.Agent({ keepAlive: true, maxSockets: 4, maxFreeSockets: 2, keepAliveMsecs: 1000 }) : undefined;
132
+ function auth(s) { return 'MediaBrowser Client="AniHub", Device="Samsung Tizen", DeviceId="' + encodeURIComponent(s.deviceId) + '", Version="0.3.3"' + (s.token ? ', Token="' + encodeURIComponent(s.token) + '"' : ''); }
132
133
  function error(code, message, status) { var e = new Error(message); e.code = code; e.status = status || 502; return e; }
133
134
  function request(s, method, path, body, cb) {
134
135
  if (options.request) { options.request(s, method, path, body, cb); return; }
135
136
  var opts = url.parse(BASE + path), done = false, timer;
136
137
  var data = body === undefined ? null : JSON.stringify(body);
137
- opts.method = method; opts.headers = { Authorization: auth(s), 'User-Agent': 'AniHub/0.3.2 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity', Accept: 'application/json' };
138
+ opts.method = method; opts.headers = { Authorization: auth(s), 'User-Agent': 'AniHub/0.3.3 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity', Accept: 'application/json' };
138
139
  if (data !== null) { opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Length'] = Buffer.byteLength(data); }
139
140
  function finish(e, value) { if (done) { return; } done = true; clearTimeout(timer); cb(e, value); }
140
141
  var req = https.request(opts, function (res) {
@@ -242,7 +243,7 @@ var ABTVAniService = (function () {
242
243
  if (t.kind === 'image') { address = '/Items/' + t.data.id + '/Images/Primary?maxWidth=480&quality=85'; }
243
244
  else if (t.kind === 'subtitle') { address = t.data.path; }
244
245
  else { address = query.part === undefined ? t.data.path : /^\d+$/.test(String(query.part)) ? t.data.parts[Number(query.part)] : ''; if (!address || !safeMedia(address)) { res.writeHead(403); res.end(); return; } }
245
- var opts = url.parse(BASE + relative(address)); opts.headers = { Authorization: auth(t.session), 'User-Agent': 'AniHub/0.3.2 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity' };
246
+ var opts = url.parse(BASE + relative(address)); opts.agent = mediaAgent; opts.headers = { Authorization: auth(t.session), 'User-Agent': 'AniHub/0.3.3 (Samsung Tizen 3.0; Jellyfin compatible client)', 'Accept-Encoding': 'identity' };
246
247
  upstream = https.get(opts, function (r) {
247
248
  if (r.statusCode !== 200 && r.statusCode !== 206) { r.resume(); res.writeHead(r.statusCode === 401 ? 401 : 502); res.end('Origem indisponível: HTTP ' + r.statusCode + '.'); return; }
248
249
  if (t.kind === 'subtitle') { var subtitleParts = [], subtitleSize = 0; r.on('data', function (b) { subtitleSize += b.length; if (subtitleSize > 16 * 1024 * 1024) { upstream.destroy(); } else { subtitleParts.push(b); } }); r.on('end', function () { if (res.finished || done) { return; } try { var text = toVtt(Buffer.concat(subtitleParts).toString('utf8'), t.data.codec); t.data.vtt = text; res.writeHead(200, { 'Content-Type': 'text/vtt; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(text); } catch (ignore) { res.writeHead(502); res.end('Legenda WebVTT indisponível.'); } }); r.on('error', function (e) { if (!res.headersSent) { res.writeHead(502); res.end('Falha na transferência da legenda. Atualize as fontes.'); } else { res.destroy(); } }); return; }
@@ -258,7 +259,7 @@ var ABTVAniService = (function () {
258
259
  r.on('error', function () { res.destroy(); });
259
260
  });
260
261
  upstream.setTimeout(25000, function () { upstream.destroy(); }); upstream.on('error', function (e) { if (!done) { if (!res.headersSent) { res.writeHead(502); res.end('Falha na mídia: ' + String(e.code || 'NETWORK') + '.'); } else { res.destroy(); } } });
261
- res.on('close', function () { done = true; upstream.destroy(); });
262
+ res.on('close', function () { done = true; if (!res.finished) { upstream.destroy(); } });
262
263
  }
263
264
  return { handle: handle, relay: relay, session: session, safeMedia: safeMedia };
264
265
  }
@@ -577,7 +578,7 @@ var ABTVAniService = (function () {
577
578
  req.on('end', function () { if (tooLarge) { return; } var body; try { body = JSON.parse(aniBody); } catch (ignore) { json(res, 400, { error: 'JSON inválido.' }); return; } aniBody = ''; aniCall(body); }); return;
578
579
  }
579
580
  if (req.method !== 'GET') { json(res, 405, { error: 'Método não permitido.' }); return; }
580
- if (p.pathname === '/health') { json(res, 200, { app: 'anihub-tv', version: '0.3.2' }); return; }
581
+ if (p.pathname === '/health') { json(res, 200, { app: 'anihub-tv', version: '0.3.3' }); return; }
581
582
  if (p.pathname === '/video') { media(req, res, String(p.query.key || '')); return; }
582
583
  if (p.pathname === '/hls.m3u8') { relayHls(req, res, String(p.query.key || ''), null); return; }
583
584
  if (p.pathname === '/hls-segment') { var segmentIndex = /^\d+$/.test(String(p.query.i)) ? Number(p.query.i) : -1; relayHls(req, res, String(p.query.key || ''), segmentIndex); return; }
@@ -310,7 +310,7 @@
310
310
  req.on('end', function () { if (tooLarge) { return; } var body; try { body = JSON.parse(aniBody); } catch (ignore) { json(res, 400, { error: 'JSON inválido.' }); return; } aniBody = ''; aniCall(body); }); return;
311
311
  }
312
312
  if (req.method !== 'GET') { json(res, 405, { error: 'Método não permitido.' }); return; }
313
- if (p.pathname === '/health') { json(res, 200, { app: 'anihub-tv', version: '0.3.2' }); return; }
313
+ if (p.pathname === '/health') { json(res, 200, { app: 'anihub-tv', version: '0.3.3' }); return; }
314
314
  if (p.pathname === '/video') { media(req, res, String(p.query.key || '')); return; }
315
315
  if (p.pathname === '/hls.m3u8') { relayHls(req, res, String(p.query.key || ''), null); return; }
316
316
  if (p.pathname === '/hls-segment') { var segmentIndex = /^\d+$/.test(String(p.query.i)) ? Number(p.query.i) : -1; relayHls(req, res, String(p.query.key || ''), segmentIndex); return; }
package/tv.html CHANGED
@@ -28,7 +28,7 @@ video::cue { font-size: 32px; color: #fff; background-color: rgba(0,0,0,.75); }
28
28
  } };
29
29
  }());
30
30
  </script><div id="shell">
31
- <aside><div class="brand"><span class="brand-mark">A<span>+</span></span><strong>ANIHUB<br>TV</strong></div><nav id="nav"><button data-action="home" class="nav-item">Início</button><button data-action="search" class="nav-item">Buscar anime</button><button data-action="favorites" class="nav-item">Minha lista</button><button data-action="history" class="nav-item">Continuar assistindo</button><button data-action="account" class="nav-item">Conta AniStream</button><button data-action="settings" class="nav-item">Preferências</button></nav><div class="sidebar-bottom"><span class="live-dot"></span>Cliente independente<br><small>v0.3.2 · Tizen 3.0</small></div></aside>
31
+ <aside><div class="brand"><span class="brand-mark">A<span>+</span></span><strong>ANIHUB<br>TV</strong></div><nav id="nav"><button data-action="home" class="nav-item">Início</button><button data-action="search" class="nav-item">Buscar anime</button><button data-action="favorites" class="nav-item">Minha lista</button><button data-action="history" class="nav-item">Continuar assistindo</button><button data-action="account" class="nav-item">Conta AniStream</button><button data-action="settings" class="nav-item">Preferências</button></nav><div class="sidebar-bottom"><span class="live-dot"></span>Cliente independente<br><small>v0.3.3 · Tizen 3.0</small></div></aside>
32
32
  <main id="main"><header><span id="breadcrumb">Seu próximo episódio</span><button id="refresh" data-action="refresh">Atualizar agora</button></header><div id="status" role="status" aria-live="polite"></div><section id="content"></section></main>
33
33
  </div><div id="dialog" class="hidden" role="dialog" aria-modal="true" aria-labelledby="dialog-title"><div class="dialog-box"><span class="eyebrow">ANIHUB TV</span><h2 id="dialog-title"></h2><p id="dialog-text"></p><div id="dialog-options"></div></div></div>
34
34
  <div id="player" class="hidden"><video id="video" preload="metadata"></video><div id="player-hud"><span class="eyebrow" id="player-state">Preparando vídeo</span><h2 id="player-title"></h2><p id="player-quality"></p><div class="timeline"><div id="progress-bar"></div></div><div id="player-time">00:00 / --:--</div><div class="player-buttons"><button data-action="seek-back">−10 s</button><button data-action="play-pause">Pausar / continuar</button><button data-action="seek-forward">+10 s</button><button data-action="quality">Qualidade</button><button data-action="next">Próximo episódio</button><button data-action="close-player">Voltar ao anime</button></div></div></div>
@@ -207,10 +207,47 @@ var ABTVAniClient = (function () {
207
207
  return { call: call, save: save, forget: forget, device: device, setPrimary: setPrimary, isPrimary: function () { return primary; }, connected: function () { return !!current; }, name: function () { return current && current.name || 'Perfil AniStream'; }, persistent: function () { return persistent; }, onAuthLost: function (fn) { authLost = fn; } };
208
208
  }());
209
209
 
210
+ </script><script>
211
+ /* ES5: bounded prebuffering for native HLS and MSE, without quality changes. */
212
+ (function (root) {
213
+ 'use strict';
214
+ root.ABTVBuffer = function (video, play, report, clock) {
215
+ clock = clock || { now: Date.now, set: setTimeout, clear: clearTimeout };
216
+ var active = true, wanted = false, holding = false, started = false, since = 0, timer, target = 8;
217
+ function ahead() {
218
+ var i, ranges = video.buffered, time = video.currentTime || 0;
219
+ for (i = 0; ranges && i < ranges.length; i++) {
220
+ if (ranges.start(i) <= time + 0.1 && ranges.end(i) > time) { return ranges.end(i) - time; }
221
+ }
222
+ return 0;
223
+ }
224
+ function tick() {
225
+ clock.clear(timer);
226
+ if (!active || !wanted || !holding) { return; }
227
+ var seconds = ahead(), remaining = isFinite(video.duration) ? video.duration - video.currentTime : Infinity;
228
+ // Some native players limit downloading while paused. Never wait indefinitely.
229
+ if (seconds >= target || (remaining > 0 && seconds >= remaining - 0.25) || clock.now() - since >= 12000) {
230
+ holding = false; started = true; report('Reproduzindo'); play(); return;
231
+ }
232
+ report('Preparando reprodução · ' + Math.floor(seconds) + ' s carregados');
233
+ timer = clock.set(tick, 500);
234
+ }
235
+ function hold() { holding = true; since = clock.now(); video.pause(); tick(); }
236
+ return {
237
+ play: function () { if (!active) { return; } wanted = true; if (holding) { tick(); } else if (!started) { hold(); } else { play(); } },
238
+ pause: function () { wanted = false; holding = false; clock.clear(timer); video.pause(); },
239
+ waiting: function () { if (active && wanted && started && !holding && !video.seeking && ahead() < 0.75) { target = 12; hold(); } },
240
+ progress: tick,
241
+ holding: function () { return holding; },
242
+ destroy: function () { active = false; wanted = false; holding = false; clock.clear(timer); }
243
+ };
244
+ };
245
+ }(window));
246
+
210
247
  </script><script>
211
248
  (function () {
212
249
  'use strict';
213
- var C = window.ABTV, A = window.ABTVAniClient, BASE = 'http://127.0.0.1:8238', store, aniValidated = false, quickTimer, captionTimer;
250
+ var C = window.ABTV, A = window.ABTVAniClient, BASE = 'http://127.0.0.1:8238', store, aniValidated = false, quickTimer, captionTimer, editingInput = null, playbackBuffer = null, userPaused = false;
214
251
  try { store = new C.Store(window.localStorage); } catch (ignore) { store = new C.Store({ getItem: function () { return null; }, setItem: function () { throw new Error('Armazenamento bloqueado'); } }); }
215
252
  var gate = new C.Gate(), requests = [], state = { view: 'home', anime: null, episode: null, season: 1, query: '' }, stack = [], detailCache = {}, sourceData = null;
216
253
  var video = document.getElementById('video'), hlsPlayer = null, playing = false, format = null, captionsReady = true, captionsFailed = false, playbackEpisode = null, playbackAnime = null, lastSaved = 0, lastResume = 0, wasHidden = false, loadSeq = 0, searchTimer, hudTimer, toastTimer, dialogReturn, focusReturn;
@@ -221,7 +258,16 @@ var ABTVAniClient = (function () {
221
258
  function status(message) { el('status').textContent = message; }
222
259
  function timeAt(at) { return new Date(at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); }
223
260
  function visible(n) { return !!(n && n.getBoundingClientRect().width && n.getBoundingClientRect().height); }
224
- function focus(n) { if (!n) { return; } n.focus(); if (n.scrollIntoViewIfNeeded) { n.scrollIntoViewIfNeeded(false); } else { var r = n.getBoundingClientRect(); if (r.top < 0 || r.bottom > window.innerHeight) { n.scrollIntoView(false); } } }
261
+ function focus(n) { if (!n) { return; } if (n._editButton && editingInput !== n) { n = n._editButton; } n.focus(); if (n.scrollIntoViewIfNeeded) { n.scrollIntoViewIfNeeded(false); } else { var r = n.getBoundingClientRect(); if (r.top < 0 || r.bottom > window.innerHeight) { n.scrollIntoView(false); } } }
262
+ function armInput(input, label) {
263
+ var edit = button(label, 'edit-input', input.id);
264
+ input._editButton = edit; input.readOnly = true; input.tabIndex = -1;
265
+ input.onclick = function () { beginInput(input); };
266
+ input.onblur = function () { input.readOnly = true; if (editingInput === input) { editingInput = null; } };
267
+ return edit;
268
+ }
269
+ function beginInput(input) { if (!input) { return; } editingInput = input; input.readOnly = false; input.focus(); }
270
+ function finishInput() { var input = editingInput; if (!input) { return; } editingInput = null; input.readOnly = true; input.blur(); focus(input._editButton); }
225
271
  function rememberFocus() { var n = document.activeElement; return n && n.getAttribute ? { action: n.getAttribute('data-action'), value: n.getAttribute('data-value'), scroll: el('main').scrollTop } : null; }
226
272
  function restoreFocus(saved) {
227
273
  var bs = el('content').querySelectorAll('button'), i;
@@ -239,7 +285,7 @@ var ABTVAniClient = (function () {
239
285
  xhr.onerror = function () { finish('Serviço local indisponível. Feche e abra o TizenBrew.'); }; xhr.ontimeout = function () { finish('A consulta demorou demais. Use Atualizar.'); }; xhr.onabort = function () { done = true; };
240
286
  xhr.send(); return xhr;
241
287
  }
242
- function cleanContent(label) { el('breadcrumb').textContent = label; el('content').textContent = ''; el('main').scrollTop = 0; status(''); }
288
+ function cleanContent(label) { finishInput(); el('breadcrumb').textContent = label; el('content').textContent = ''; el('main').scrollTop = 0; status(''); }
243
289
  function empty(title, message, target) { var box = node('div', 'empty'); box.appendChild(node('h2', '', title)); box.appendChild(node('p', 'muted', message)); (target || el('content')).appendChild(box); }
244
290
  function error(message) { status(message); if (!el('content').textContent) { empty('Não foi possível consultar agora', message + ' Você pode tentar novamente pelo botão Atualizar agora.'); } }
245
291
  function activate() { var ns = el('nav').querySelectorAll('button'), i; for (i = 0; i < ns.length; i++) { ns[i].classList.toggle('active', ns[i].getAttribute('data-action') === state.view); } }
@@ -286,7 +332,7 @@ var ABTVAniClient = (function () {
286
332
  api('/page', '/', function (err, d) { if (err) { error(err); return; } var items = C.cards(d.html, 'episodio'), sections = C.homeSections(d.html); var f = rememberFocus(); store.set('home', { items: items.slice(0, 48), sections: sections, checkedAt: d.checkedAt }); showHome(items, d.checkedAt, saved || f, sections); });
287
333
  }
288
334
  function search(saved) {
289
- cleanContent('Buscar anime'); title('O que você quer assistir?'); var input = node('input', 'search-input'); input.id = 'query'; input.type = 'text'; input.placeholder = 'Digite o nome do anime'; input.value = state.query || ''; input.setAttribute('aria-label', 'Buscar anime'); input.oninput = function () { state.query = input.value; scheduleSearch(); }; el('content').appendChild(input);
335
+ cleanContent('Buscar anime'); title('O que você quer assistir?'); var input = node('input', 'search-input'); input.id = 'query'; input.type = 'text'; input.placeholder = 'Digite o nome do anime'; input.value = state.query || ''; input.setAttribute('aria-label', 'Buscar anime'); input.oninput = function () { state.query = input.value; scheduleSearch(); }; el('content').appendChild(input); el('content').appendChild(armInput(input, 'Digitar busca'));
290
336
  var keyboard = node('div', 'keyboard'); 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.split('').forEach(function (k) { keyboard.appendChild(button(k, 'key', k)); }); keyboard.appendChild(button('Espaço', 'key', ' ', 'wide')); keyboard.appendChild(button('Apagar', 'erase', undefined, 'wide')); keyboard.appendChild(button('Buscar', 'submit', undefined, 'wide primary')); el('content').appendChild(keyboard); var results = node('div'); results.id = 'results'; el('content').appendChild(results);
291
337
  if (state.query) { runSearch(); } else { empty('Encontre a próxima história', 'Use o teclado da tela ou digite no campo de busca.', results); }
292
338
  if (saved) { restoreFocus(saved); } else { focus(input); }
@@ -372,19 +418,24 @@ var ABTVAniClient = (function () {
372
418
  if (!playbackEpisode || !video.currentTime || video.currentTime < 2) { return; } var record = { position: video.currentTime, duration: isFinite(video.duration) ? video.duration : 0, at: Date.now() };
373
419
  store.set(C.progressKey(playbackEpisode), record); var history = store.get('history', []).filter(function (r) { return r.episode.path !== playbackEpisode.path; }); history.unshift({ episode: playbackEpisode, anime: { path: playbackAnime.path, title: playbackAnime.title, image: playbackAnime.image }, position: record.position }); store.set('history', history.slice(0, 30));
374
420
  }
375
- function playSafe() { if (!captionsReady) { if (captionsFailed) { toast('A legenda falhou. Volte e use Atualizar fontes.'); } return; } try { var p = video.play(); if (p && typeof p.then === 'function') { p.then(function () {}, function () { toast('Pressione Pausar / continuar para iniciar.'); }); } } catch (ignore) { toast('Não foi possível iniciar o player. Atualize as fontes.'); } }
421
+ function playNow() { try { var p = video.play(); if (p && typeof p.then === 'function') { p.then(function () {}, function () { toast('Pressione Pausar / continuar para iniciar.'); }); } } catch (ignore) { toast('Não foi possível iniciar o player. Atualize as fontes.'); } }
422
+
423
+ function pauseByUser() { userPaused = true; if (playbackBuffer) { playbackBuffer.pause(); } else { video.pause(); } }
424
+ function playSafe(explicit) { if (explicit) { userPaused = false; } if (userPaused) { return; } if (!captionsReady) { if (captionsFailed) { toast('A legenda falhou. Volte e use Atualizar fontes.'); } return; } if (playbackBuffer) { playbackBuffer.play(); } else { playNow(); } }
376
425
  function clearCaptions() { clearTimeout(captionTimer); var tracks = video.querySelectorAll('track'), i; for (i = 0; i < tracks.length; i++) { tracks[i].onload = null; tracks[i].onerror = null; video.removeChild(tracks[i]); } }
377
426
  function positionCaptions(raised) { try { var tracks = video.textTracks, i, j; for (i = 0; i < tracks.length; i++) { var cues = tracks[i].activeCues || []; for (j = 0; j < cues.length; j++) { cues[j].line = raised ? -10 : -1; } } } catch (ignore) {} }
378
427
  function startPlayer(f, changing) {
379
428
  var position = changing ? video.currentTime || 0 : (store.get(C.progressKey(state.episode), {}) || {}).position || 0;
380
429
  if (!changing) { focusReturn = rememberFocus(); playbackEpisode = state.episode; playbackAnime = state.anime; }
381
- if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); } format = f; playing = true; loadSeq++; var seq = loadSeq; captionsReady = !f.subtitleUrl; captionsFailed = false; clearCaptions(); video.crossOrigin = 'anonymous'; el('player').classList.remove('hidden'); el('player-title').textContent = playbackAnime.title + ' · T' + playbackEpisode.season + ' E' + playbackEpisode.number; el('player-state').textContent = 'Preparando vídeo'; el('player-quality').textContent = f.height + 'p informado · aguardando resolução efetiva';
382
- if (f.subtitleUrl) { var track = node('track'); track.kind = 'subtitles'; track.label = 'Português'; track.srclang = f.subtitleLanguage || 'pt'; track.default = true; track.src = f.subtitleUrl; track.onload = function () { if (seq !== loadSeq) { return; } captionsReady = true; clearTimeout(captionTimer); try { track.track.mode = 'showing'; track.track.oncuechange = function () { positionCaptions(!el('player-hud').classList.contains('hidden')); }; } catch (ignore) {} positionCaptions(true); playSafe(); }; track.onerror = function () { if (seq !== loadSeq) { return; } clearTimeout(captionTimer); captionsFailed = true; captionsReady = false; video.pause(); el('player-state').textContent = 'Legenda indisponível'; showHud(); toast('Não foi possível carregar a legenda portuguesa. Volte e use Atualizar fontes.'); }; video.appendChild(track); try { track.track.mode = 'showing'; } catch (ignore2) {} captionTimer = setTimeout(function () { if (seq === loadSeq && !captionsReady && track.onerror) { track.onerror(); } }, 20000); }
430
+ if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); } if (playbackBuffer) { playbackBuffer.destroy(); } playbackBuffer = null; userPaused = false; format = f; playing = true; loadSeq++; var seq = loadSeq; captionsReady = !f.subtitleUrl; captionsFailed = false; clearCaptions(); video.crossOrigin = 'anonymous'; el('player').classList.remove('hidden'); el('player-title').textContent = playbackAnime.title + ' · T' + playbackEpisode.season + ' E' + playbackEpisode.number; el('player-state').textContent = 'Preparando vídeo'; el('player-quality').textContent = f.height + 'p informado · aguardando resolução efetiva';
431
+ video.preload = 'auto';
432
+ if (f.transport === 'hls' && window.ABTVBuffer) { playbackBuffer = window.ABTVBuffer(video, playNow, function (message) { if (seq === loadSeq) { el('player-state').textContent = message; if (message !== 'Reproduzindo') { showHud(); } } }); }
433
+ if (f.subtitleUrl) { var track = node('track'); track.kind = 'subtitles'; track.label = 'Português'; track.srclang = f.subtitleLanguage || 'pt'; track.default = true; track.src = f.subtitleUrl; track.onload = function () { if (seq !== loadSeq) { return; } captionsReady = true; clearTimeout(captionTimer); try { track.track.mode = 'showing'; track.track.oncuechange = function () { positionCaptions(!el('player-hud').classList.contains('hidden')); }; } catch (ignore) {} positionCaptions(true); playSafe(); }; track.onerror = function () { if (seq !== loadSeq) { return; } clearTimeout(captionTimer); captionsFailed = true; captionsReady = false; if (playbackBuffer) { playbackBuffer.pause(); } else { video.pause(); } el('player-state').textContent = 'Legenda indisponível'; showHud(); toast('Não foi possível carregar a legenda portuguesa. Volte e use Atualizar fontes.'); }; video.appendChild(track); try { track.track.mode = 'showing'; } catch (ignore2) {} captionTimer = setTimeout(function () { if (seq === loadSeq && !captionsReady && track.onerror) { track.onerror(); } }, 20000); }
383
434
  video.onloadedmetadata = function () { if (seq !== loadSeq) { return; } if (position > 0 && isFinite(video.duration) && position < video.duration - 5) { try { video.currentTime = position; } catch (ignore) {} } updateResolution(); playSafe(); };
384
435
  video.onerror = function () { if (seq !== loadSeq) { return; } showHud(); el('player-state').textContent = 'Falha de reprodução'; toast('O vídeo falhou ou a fonte expirou. Volte e use Atualizar fontes.'); };
385
436
  if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; }
386
437
  if (f.transport === 'hls' && !video.canPlayType('application/vnd.apple.mpegurl') && !video.canPlayType('application/x-mpegURL') && window.Hls && window.Hls.isSupported()) {
387
- hlsPlayer = new window.Hls({ enableWorker: false, maxBufferLength: 20, maxMaxBufferLength: 30, backBufferLength: 15, maxBufferSize: 32 * 1024 * 1024 });
438
+ hlsPlayer = new window.Hls({ enableWorker: false, maxBufferLength: 45, maxMaxBufferLength: 60, backBufferLength: 10, maxBufferSize: 48 * 1024 * 1024 });
388
439
  hlsPlayer.on(window.Hls.Events.ERROR, function (event, data) { if (data.fatal && seq === loadSeq && video.onerror) { video.onerror(); } });
389
440
  hlsPlayer.loadSource(f.url); hlsPlayer.attachMedia(video); hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function () { if (seq === loadSeq) { playSafe(); } });
390
441
  } else { video.src = f.url; video.load(); playSafe(); }
@@ -392,7 +443,7 @@ var ABTVAniClient = (function () {
392
443
  }
393
444
  function updateResolution() { if (!playing) { return; } var dims = video.videoWidth && video.videoHeight ? video.videoWidth + '×' + video.videoHeight + ' efetivo' : 'Resolução efetiva ainda não informada'; el('player-quality').textContent = (format.provider ? format.provider + ' · ' : format.original ? 'Original do site · ' : 'Blogger · ') + format.height + 'p informado · ' + dims + (format.subtitleTitle ? ' · Legenda: ' + format.subtitleTitle : ''); }
394
445
  function showHud() { clearTimeout(hudTimer); el('player-hud').classList.remove('hidden'); positionCaptions(true); hudTimer = setTimeout(function () { if (!video.paused && el('dialog').classList.contains('hidden')) { el('player-hud').classList.add('hidden'); positionCaptions(false); } }, 8000); }
395
- function closePlayer(quiet) { if (!playing) { return; } saveProgress(); if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); format.ticket = null; } playing = false; loadSeq++; clearTimeout(hudTimer); if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; } video.pause(); clearCaptions(); captionsReady = true; video.removeAttribute('src'); video.load(); video.onloadedmetadata = null; video.onerror = null; el('player').classList.add('hidden'); if (!quiet) { restoreFocus(focusReturn); } }
446
+ function closePlayer(quiet) { if (!playing) { return; } saveProgress(); if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); format.ticket = null; } playing = false; loadSeq++; if (playbackBuffer) { playbackBuffer.destroy(); playbackBuffer = null; } clearTimeout(hudTimer); if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; } video.pause(); clearCaptions(); captionsReady = true; video.removeAttribute('src'); video.load(); video.onloadedmetadata = null; video.onerror = null; el('player').classList.add('hidden'); if (!quiet) { restoreFocus(focusReturn); } }
396
447
  function nextEpisode() {
397
448
  if (!playbackAnime || !playbackAnime.episodes || !playbackAnime.episodes.length) { toast('Abra Ver temporadas para escolher o próximo episódio.'); return; }
398
449
  var list = playbackAnime.episodes, i, next = null; for (i = 0; i < list.length - 1; i++) { if (list[i].path === playbackEpisode.path) { next = list[i + 1]; break; } }
@@ -408,12 +459,12 @@ var ABTVAniClient = (function () {
408
459
  if (A.connected()) { box.appendChild(node('h3', '', A.name())); box.appendChild(node('p', 'muted', A.persistent() ? 'A sessão está salva neste aparelho. Ao abrir o app, ela será verificada novamente.' : 'A sessão está apenas na memória. O armazenamento não permitiu salvá-la; será preciso entrar novamente ao fechar.')); box.appendChild(button('Usar AniStream como principal', 'ani-primary', undefined, 'primary')); box.appendChild(button('Usar Animes Brasil', 'ani-secondary')); box.appendChild(button('Sair deste perfil no app', 'ani-logout')); }
409
460
  else {
410
461
  box.appendChild(node('p', 'muted', 'Use o usuário e a senha do perfil Jellyfin do AniStream. Essa senha pode ser diferente da senha da conta por e-mail.'));
411
- var user = node('input', 'search-input'); user.id = 'ani-user'; user.type = 'text'; user.placeholder = 'Usuário do perfil AniStream'; user.setAttribute('aria-label', 'Usuário do perfil AniStream'); user.setAttribute('autocomplete', 'off'); box.appendChild(user);
412
- var pass = node('input', 'search-input'); pass.id = 'ani-password'; pass.type = 'password'; pass.placeholder = 'Senha do perfil Jellyfin'; pass.setAttribute('aria-label', 'Senha do perfil Jellyfin'); pass.setAttribute('autocomplete', 'off'); box.appendChild(pass);
462
+ var user = node('input', 'search-input'); user.id = 'ani-user'; user.type = 'text'; user.placeholder = 'Usuário do perfil AniStream'; user.setAttribute('aria-label', 'Usuário do perfil AniStream'); user.setAttribute('autocomplete', 'off'); box.appendChild(user); box.appendChild(armInput(user, 'Digitar usuário'));
463
+ var pass = node('input', 'search-input'); pass.id = 'ani-password'; pass.type = 'password'; pass.placeholder = 'Senha do perfil Jellyfin'; pass.setAttribute('aria-label', 'Senha do perfil Jellyfin'); pass.setAttribute('autocomplete', 'off'); box.appendChild(pass); box.appendChild(armInput(pass, 'Digitar senha'));
413
464
  box.appendChild(button('Entrar e lembrar neste aparelho', 'ani-login', undefined, 'primary')); box.appendChild(button('Vincular por código Quick Connect', 'ani-quick'));
414
465
  var code = node('div'); code.id = 'ani-code'; box.appendChild(code);
415
466
  }
416
- box.appendChild(node('p', 'muted', 'Sua senha não é salva. Somente a sessão fica neste aparelho. O armazenamento do TizenBrew é compartilhado entre módulos. Se o AniStream encerrar a sessão, será necessário entrar novamente.')); el('content').appendChild(box); focus(box.querySelector('input,button'));
467
+ box.appendChild(node('p', 'muted', 'Sua senha não é salva. Somente a sessão fica neste aparelho. O armazenamento do TizenBrew é compartilhado entre módulos. Se o AniStream encerrar a sessão, será necessário entrar novamente.')); el('content').appendChild(box); focus(box.querySelector('[data-action="ani-quick"]') || box.querySelector('button'));
417
468
  }
418
469
  function aniLoggedIn(d) { A.save(d); aniValidated = true; detailCache = {}; toast(A.persistent() ? 'AniStream conectado. A sessão será lembrada neste aparelho.' : 'AniStream conectado apenas nesta sessão: armazenamento indisponível.'); top('home'); }
419
470
  function aniLogin() {
@@ -432,6 +483,7 @@ var ABTVAniClient = (function () {
432
483
  function render(saved) { if (state.view === 'home') { home(saved); } else if (state.view === 'search') { search(saved); } else if (state.view === 'detail') { detail(saved); } else if (state.view === 'episode') { episodePage(); } else if (state.view === 'account') { account(); } else if (state.view === 'settings') { settings(); } else { library(); } }
433
484
  function refresh() { if (playing) { toast('Volte à tela do episódio para atualizar sem interromper o vídeo.'); return; } cancel(); if (state.view === 'episode' && state.episode) { resolveSources(); } else if (state.view === 'detail') { detail(rememberFocus()); } else if (state.view === 'search') { runSearch(); } else { render(rememberFocus()); } }
434
485
  function action(a, value, target) {
486
+ if (a === 'edit-input') { beginInput(el(value)); return; }
435
487
  if (a === 'home' || a === 'search' || a === 'favorites' || a === 'history' || a === 'settings' || a === 'account') { top(a); }
436
488
  else if (a === 'ani-login') { aniLogin(); }
437
489
  else if (a === 'ani-quick') { aniQuick(); }
@@ -451,7 +503,7 @@ var ABTVAniClient = (function () {
451
503
  else if (a === 'submit') { clearTimeout(searchTimer); runSearch(); }
452
504
  else if (a === 'toggle-lower') { store.set('allowLower', !store.get('allowLower', false)); settings(); }
453
505
  else if (a === 'close-player') { closePlayer(); }
454
- else if (a === 'play-pause') { if (video.paused) { playSafe(); } else { video.pause(); } showHud(); }
506
+ else if (a === 'play-pause') { if (userPaused || (video.paused && !(playbackBuffer && playbackBuffer.holding()))) { playSafe(true); } else { pauseByUser(); } showHud(); }
455
507
  else if (a === 'seek-back' || a === 'seek-forward') { try { video.currentTime = Math.max(0, Math.min(isFinite(video.duration) ? video.duration : video.currentTime + 10, video.currentTime + (a === 'seek-back' ? -10 : 10))); } catch (ignore) {} showHud(); }
456
508
  else if (a === 'next') { nextEpisode(); }
457
509
  else if (a === 'confirm-language') { dialog('Você conferiu o idioma?', 'Confirme somente se este episódio está com áudio japonês e legenda PT-BR legível.', [{ label: 'Sim, confirmei os dois', fn: function () { store.set('language:' + playbackEpisode.path, true); toast('Idioma confirmado por você neste episódio'); } }, { label: 'Cancelar', fn: function () {} }]); }
@@ -460,7 +512,7 @@ var ABTVAniClient = (function () {
460
512
  }
461
513
  document.addEventListener('click', function (e) { var n = e.target; while (n && n !== document) { if (n.getAttribute && n.getAttribute('data-action')) { action(n.getAttribute('data-action'), n.getAttribute('data-value'), n); return; } n = n.parentNode; } });
462
514
  function navigate(code) {
463
- var container = !el('dialog').classList.contains('hidden') ? el('dialog') : playing ? el('player-hud') : document, all = container.querySelectorAll('button,input'), current = document.activeElement, r = current && current.getBoundingClientRect ? current.getBoundingClientRect() : null, best = null, score = Infinity, i;
515
+ var container = !el('dialog').classList.contains('hidden') ? el('dialog') : playing ? el('player-hud') : document, all = container.querySelectorAll('button'), current = document.activeElement, r = current && current.getBoundingClientRect ? current.getBoundingClientRect() : null, best = null, score = Infinity, i;
464
516
  if (!r || !visible(current)) { for (i = 0; i < all.length; i++) { if (visible(all[i])) { focus(all[i]); return; } } }
465
517
  var x = r.left + r.width / 2, y = r.top + r.height / 2;
466
518
  for (i = 0; i < all.length; i++) { var n = all[i]; if (n === current || !visible(n) || n.disabled) { continue; } var q = n.getBoundingClientRect(), dx = q.left + q.width / 2 - x, dy = q.top + q.height / 2 - y, primary = code === 37 ? -dx : code === 39 ? dx : code === 38 ? -dy : dy, secondary = code === 37 || code === 39 ? Math.abs(dy) : Math.abs(dx); if (primary <= 3) { continue; } var s = primary + secondary * 2.5; if (s < score) { score = s; best = n; } }
@@ -468,14 +520,16 @@ var ABTVAniClient = (function () {
468
520
  }
469
521
  document.addEventListener('keydown', function (e) {
470
522
  var k = e.keyCode || e.which, inDialog = !el('dialog').classList.contains('hidden');
523
+ if (editingInput) { if (k === 13 || k === 65376 || k === 65385 || k === 10009 || k === 27 || k === 461) { var searched = editingInput.id === 'query' && (k === 13 || k === 65376); e.preventDefault(); finishInput(); if (searched) { clearTimeout(searchTimer); runSearch(); } } return; }
471
524
  if (k === 10009 || k === 27 || k === 461) { e.preventDefault(); back(); return; }
472
- if (playing && !inDialog && [415, 19, 10252, 413, 412, 417].indexOf(k) !== -1) { e.preventDefault(); if (k === 413) { closePlayer(); } else if (k === 415) { playSafe(); } else if (k === 19) { video.pause(); } else { action(k === 412 ? 'seek-back' : k === 417 ? 'seek-forward' : 'play-pause'); } showHud(); return; }
525
+ if (playing && !inDialog && [415, 19, 10252, 413, 412, 417].indexOf(k) !== -1) { e.preventDefault(); if (k === 413) { closePlayer(); } else if (k === 415) { playSafe(true); } else if (k === 19) { pauseByUser(); } else { action(k === 412 ? 'seek-back' : k === 417 ? 'seek-forward' : 'play-pause'); } showHud(); return; }
473
526
  if (k >= 37 && k <= 40) { if (document.activeElement === el('query') && (k === 37 || k === 39)) { return; } e.preventDefault(); if (playing) { showHud(); } navigate(k); }
474
527
  if (k === 13 && document.activeElement === el('query')) { e.preventDefault(); clearTimeout(searchTimer); runSearch(); }
475
528
  else if (k === 13 && document.activeElement && document.activeElement.tagName === 'BUTTON') { e.preventDefault(); document.activeElement.click(); }
476
529
  });
530
+ video.addEventListener('progress', function () { if (playbackBuffer) { playbackBuffer.progress(); } });
477
531
  video.addEventListener('timeupdate', function () { if (!playing) { return; } el('player-time').textContent = clock(video.currentTime) + ' / ' + clock(video.duration); el('progress-bar').style.width = (video.duration ? Math.min(100, video.currentTime / video.duration * 100) : 0) + '%'; updateResolution(); if (Date.now() - lastSaved > 10000) { lastSaved = Date.now(); saveProgress(); } });
478
- video.addEventListener('playing', function () { el('player-state').textContent = 'Reproduzindo'; showHud(); }); video.addEventListener('waiting', function () { el('player-state').textContent = 'Carregando vídeo…'; }); video.addEventListener('pause', function () { if (playing) { el('player-state').textContent = 'Pausado'; showHud(); saveProgress(); } }); video.addEventListener('ended', function () { saveProgress(); nextEpisode(); });
532
+ video.addEventListener('playing', function () { el('player-state').textContent = 'Reproduzindo'; showHud(); }); video.addEventListener('waiting', function () { el('player-state').textContent = 'Carregando vídeo…'; if (playbackBuffer) { playbackBuffer.waiting(); } }); video.addEventListener('pause', function () { if (playing && !(playbackBuffer && playbackBuffer.holding())) { el('player-state').textContent = 'Pausado'; showHud(); saveProgress(); } }); video.addEventListener('ended', function () { saveProgress(); nextEpisode(); });
479
533
  function resume() { var now = Date.now(); if (now - lastResume < 1500) { return; } lastResume = now; if (playing) { saveProgress(); return; } if (!el('dialog').classList.contains('hidden')) { return; } refresh(); }
480
534
  document.addEventListener('visibilitychange', function () { if (document.hidden) { wasHidden = true; saveProgress(); } else if (wasHidden) { wasHidden = false; resume(); } }); window.addEventListener('pageshow', function (e) { if (e.persisted) { resume(); } });
481
535
  try { if (window.tizen && window.tizen.tvinputdevice) { ['MediaPlay', 'MediaPause', 'MediaPlayPause', 'MediaStop', 'MediaRewind', 'MediaFastForward'].forEach(function (k) { try { window.tizen.tvinputdevice.registerKey(k); } catch (ignore) {} }); } } catch (ignore) {}