anihub-tv 0.3.1 → 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.1`. 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.1
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.1** 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**: 48 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,15 +1,15 @@
1
- Instalação alternativa pelo npm: no TizenBrew, escolha Adicionar módulo → npm e digite `anihub-tv@0.3.1`. 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.1**. 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
 
9
9
  1. Abra TizenBrew → Gerenciador de módulos → Adicionar módulo → GitHub.
10
10
  2. Digite **srwayne15-bot/AniHub@main**, sem `gh/` no campo GitHub.
11
11
  3. Confirme e abra **AniHub**. Não remova Anime-TV, Cinefy ou StremioK.
12
- 4. Confira **v0.3.1** na lateral. Se o serviço não responder, feche e reabra o TizenBrew. Não limpe seus dados.
12
+ 4. Confira **v0.3.2** na lateral. Se o serviço não responder, feche e reabra o TizenBrew. Não limpe seus dados.
13
13
 
14
14
  O caminho `gh/srwayne15-bot/AniHub@main` é a forma interna usada pelo CDN/TizenBrew; o campo de módulo GitHub acrescenta o prefixo. Se sua versão do gerenciador tiver um único campo genérico, confira o exemplo mostrado por ela antes de inserir o identificador.
15
15
 
@@ -37,4 +37,16 @@ Tokyo Revengers também foi consultado no computador: a fonte organiza 56 episó
37
37
 
38
38
  Informe **em qual etapa**, a mensagem exibida e se houve **imagem, som e legenda**. Uma foto da tela ajuda; não inclua senha nem token. Não faça reinicialização forçada ou limpeza global de dados antes de relatar o resultado.
39
39
 
40
- Os 73 testes automatizados e a reprodução real no computador não substituem este teste. O carregamento do serviço foi simulado no contrato de VM do TizenBrew, mas não houve execução em Node 4 nativo nem na TV. HLS, WebVTT, TLS, memória e codecs dependem dessa verificação física.
40
+ Os 76 testes automatizados e a reprodução real no computador não substituem este teste. O carregamento do serviço foi simulado no contrato de VM do TizenBrew, mas não houve execução em Node 4 nativo nem na TV. HLS, WebVTT, TLS, memória e codecs dependem dessa verificação física.
41
+
42
+ ## Correção 0.3.2
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.1"></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.1 · 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.1"></script><script src="js/core.js?v=0.3.1"></script><script src="js/vendor/hls.light.min.js?v=1.6.13"></script><script src="js/ani-client.js?v=0.3.1"></script><script src="js/app.js?v=0.3.1"></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) {}
package/js/boot.js ADDED
@@ -0,0 +1,23 @@
1
+ /* ES5 startup diagnostics: no account information, URLs or tokens displayed. */
2
+ (function () {
3
+ 'use strict';
4
+ var completed = false, failed = false, timer;
5
+ function fail(message) {
6
+ var box = document.getElementById('boot-error');
7
+ failed = true;
8
+ if (box) {
9
+ box.textContent = 'AniHub: falha na abertura\n' + message + '\nFeche e abra o TizenBrew. Se continuar, envie uma foto desta mensagem.';
10
+ box.style.display = 'block';
11
+ }
12
+ }
13
+ window.addEventListener('error', function (event) {
14
+ if (!completed) { fail('Código de início: JS' + (Number(event.lineno) || 0)); }
15
+ });
16
+ timer = setTimeout(function () { if (!completed) { fail('Código de início: TEMPO'); } }, 12000);
17
+ window.AniHubBoot = { ready: function () {
18
+ clearTimeout(timer);
19
+ if (!window.ABTVApp || !window.ABTVAniClient || !window.ABTV) { fail('Código de início: MODULOS'); return; }
20
+ completed = true;
21
+ if (!failed) { document.getElementById('boot-error').style.display = 'none'; }
22
+ } };
23
+ }());
@@ -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,10 +1,10 @@
1
1
  {
2
2
  "name": "anihub-tv",
3
- "version": "0.3.1",
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",
7
- "appPath": "index.html",
7
+ "appPath": "tv.html",
8
8
  "serviceFile": "service/index.js",
9
9
  "keys": [
10
10
  "MediaPlay",
@@ -16,7 +16,8 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "start": "node service/index.js",
19
- "test": "node tests/run.cjs && node tests/anistream.cjs"
19
+ "test": "node tests/run.cjs && node tests/anistream.cjs",
20
+ "build": "node tools/build-service.cjs && node tools/build-tv.cjs"
20
21
  },
21
22
  "devDependencies": {
22
23
  "acorn": "8.19.0",
@@ -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.1"' + (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.1 (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.1 (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.1"' + (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.1 (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.1 (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.1' }); 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.1' }); 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; }