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 +4 -4
- package/docs/TESTE-NA-TV.md +16 -4
- package/index.html +3 -3
- package/js/app.js +33 -16
- package/js/boot.js +23 -0
- package/js/playback-buffer.js +35 -0
- package/package.json +4 -3
- package/service/anistream.js +5 -4
- package/service/index.js +6 -5
- package/service/runtime.js +1 -1
- package/tv.html +542 -0
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
|
+
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.
|
|
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.
|
|
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**:
|
|
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
|
|
package/docs/TESTE-NA-TV.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
Instalação alternativa pelo npm: no TizenBrew, escolha Adicionar módulo → npm e digite `anihub-tv@0.3.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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('
|
|
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 {
|
|
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
|
|
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) {
|
|
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.
|
|
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": "
|
|
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",
|
package/service/anistream.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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; }
|
package/service/runtime.js
CHANGED
|
@@ -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.
|
|
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; }
|