anihub-tv 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/docs/TESTE-NA-TV.md +11 -2
- package/index.html +3 -3
- package/js/app.js +33 -16
- package/js/playback-buffer.js +35 -0
- package/package.json +1 -1
- package/service/anistream.js +5 -4
- package/service/index.js +6 -5
- package/service/runtime.js +1 -1
- package/tv.html +71 -17
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,8 +1,8 @@
|
|
|
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
|
|
|
@@ -41,3 +41,12 @@ Os 76 testes automatizados e a reprodução real no computador não substituem e
|
|
|
41
41
|
|
|
42
42
|
## Correção 0.3.2
|
|
43
43
|
A foto do teste físico de 0.3.1 mostrou HTML sem CSS aplicado, com controles do player expostos. A causa do carregamento externo não foi isolada na TV. A nova entrada tv.html incorpora CSS e scripts, eliminando essas requisições na abertura. Há diagnóstico de erro de início sem dados de conta. Os testes simulados validam interface, player oculto e ES5 sem rede de assets; a correção ainda precisa de novo teste físico. Atualize o módulo para anihub-tv@0.3.2 e feche/reabra o TizenBrew. Nenhuma limpeza de armazenamento é necessária.
|
|
44
|
+
|
|
45
|
+
## Ajustes 0.3.3 — controle e carregamento
|
|
46
|
+
O usuário confirmou a abertura e a vinculação na TV com 0.3.2. Relatou teclado abrindo sozinho e vídeo parando para carregar; no PC o vídeo flui normalmente. A causa completa dos engasgos ainda não foi medida no aparelho.
|
|
47
|
+
|
|
48
|
+
Campos de texto agora ficam fora da navegação por setas. Use Digitar busca/usuário/senha para abrir o teclado; Voltar/Concluído devolve o foco ao botão. Quick Connect recebe o foco inicial na conta desconectada. As letras do teclado do app continuam funcionando pelas setas.
|
|
49
|
+
|
|
50
|
+
HLS acumula alvo de 8 segundos no início e 12 após esgotar a reserva. A espera é limitada a 12 segundos para acomodar players nativos que limitam downloads quando pausados. Pausa manual e saída cancelam a espera; legenda continua obrigatória. No fallback MSE, a reserva configurada passa a 45–60 segundos, com meta de memória de 48 MiB e descarte após 10 segundos já assistidos. HLS nativo controla sua própria reserva. As conexões HTTPS de mídia agora usam keep-alive, quatro sockets e dois ociosos, mantendo TLS e a qualidade da fonte.
|
|
51
|
+
|
|
52
|
+
79 verificações automatizadas passaram, incluindo foco, teclado explícito, reserva/esgotamento, pausa, fim do vídeo, encerramento e compartilhamento do agente HTTPS. Navegação por setas/Enter e saída por Escape conferidas no navegador do computador. Melhoria de streaming ainda depende do teste físico da versão 0.3.3. Testar o mesmo episódio por cinco minutos e anotar se o texto Preparando reprodução aparece repetidamente.
|
package/index.html
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#10121c"><title>AniHub</title><link rel="stylesheet" href="css/app.css?v=0.3.
|
|
2
|
+
<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) {}
|
|
@@ -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
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; }
|
package/tv.html
CHANGED
|
@@ -28,7 +28,7 @@ video::cue { font-size: 32px; color: #fff; background-color: rgba(0,0,0,.75); }
|
|
|
28
28
|
} };
|
|
29
29
|
}());
|
|
30
30
|
</script><div id="shell">
|
|
31
|
-
<aside><div class="brand"><span class="brand-mark">A<span>+</span></span><strong>ANIHUB<br>TV</strong></div><nav id="nav"><button data-action="home" class="nav-item">Início</button><button data-action="search" class="nav-item">Buscar anime</button><button data-action="favorites" class="nav-item">Minha lista</button><button data-action="history" class="nav-item">Continuar assistindo</button><button data-action="account" class="nav-item">Conta AniStream</button><button data-action="settings" class="nav-item">Preferências</button></nav><div class="sidebar-bottom"><span class="live-dot"></span>Cliente independente<br><small>v0.3.
|
|
31
|
+
<aside><div class="brand"><span class="brand-mark">A<span>+</span></span><strong>ANIHUB<br>TV</strong></div><nav id="nav"><button data-action="home" class="nav-item">Início</button><button data-action="search" class="nav-item">Buscar anime</button><button data-action="favorites" class="nav-item">Minha lista</button><button data-action="history" class="nav-item">Continuar assistindo</button><button data-action="account" class="nav-item">Conta AniStream</button><button data-action="settings" class="nav-item">Preferências</button></nav><div class="sidebar-bottom"><span class="live-dot"></span>Cliente independente<br><small>v0.3.3 · Tizen 3.0</small></div></aside>
|
|
32
32
|
<main id="main"><header><span id="breadcrumb">Seu próximo episódio</span><button id="refresh" data-action="refresh">Atualizar agora</button></header><div id="status" role="status" aria-live="polite"></div><section id="content"></section></main>
|
|
33
33
|
</div><div id="dialog" class="hidden" role="dialog" aria-modal="true" aria-labelledby="dialog-title"><div class="dialog-box"><span class="eyebrow">ANIHUB TV</span><h2 id="dialog-title"></h2><p id="dialog-text"></p><div id="dialog-options"></div></div></div>
|
|
34
34
|
<div id="player" class="hidden"><video id="video" preload="metadata"></video><div id="player-hud"><span class="eyebrow" id="player-state">Preparando vídeo</span><h2 id="player-title"></h2><p id="player-quality"></p><div class="timeline"><div id="progress-bar"></div></div><div id="player-time">00:00 / --:--</div><div class="player-buttons"><button data-action="seek-back">−10 s</button><button data-action="play-pause">Pausar / continuar</button><button data-action="seek-forward">+10 s</button><button data-action="quality">Qualidade</button><button data-action="next">Próximo episódio</button><button data-action="close-player">Voltar ao anime</button></div></div></div>
|
|
@@ -207,10 +207,47 @@ var ABTVAniClient = (function () {
|
|
|
207
207
|
return { call: call, save: save, forget: forget, device: device, setPrimary: setPrimary, isPrimary: function () { return primary; }, connected: function () { return !!current; }, name: function () { return current && current.name || 'Perfil AniStream'; }, persistent: function () { return persistent; }, onAuthLost: function (fn) { authLost = fn; } };
|
|
208
208
|
}());
|
|
209
209
|
|
|
210
|
+
</script><script>
|
|
211
|
+
/* ES5: bounded prebuffering for native HLS and MSE, without quality changes. */
|
|
212
|
+
(function (root) {
|
|
213
|
+
'use strict';
|
|
214
|
+
root.ABTVBuffer = function (video, play, report, clock) {
|
|
215
|
+
clock = clock || { now: Date.now, set: setTimeout, clear: clearTimeout };
|
|
216
|
+
var active = true, wanted = false, holding = false, started = false, since = 0, timer, target = 8;
|
|
217
|
+
function ahead() {
|
|
218
|
+
var i, ranges = video.buffered, time = video.currentTime || 0;
|
|
219
|
+
for (i = 0; ranges && i < ranges.length; i++) {
|
|
220
|
+
if (ranges.start(i) <= time + 0.1 && ranges.end(i) > time) { return ranges.end(i) - time; }
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
function tick() {
|
|
225
|
+
clock.clear(timer);
|
|
226
|
+
if (!active || !wanted || !holding) { return; }
|
|
227
|
+
var seconds = ahead(), remaining = isFinite(video.duration) ? video.duration - video.currentTime : Infinity;
|
|
228
|
+
// Some native players limit downloading while paused. Never wait indefinitely.
|
|
229
|
+
if (seconds >= target || (remaining > 0 && seconds >= remaining - 0.25) || clock.now() - since >= 12000) {
|
|
230
|
+
holding = false; started = true; report('Reproduzindo'); play(); return;
|
|
231
|
+
}
|
|
232
|
+
report('Preparando reprodução · ' + Math.floor(seconds) + ' s carregados');
|
|
233
|
+
timer = clock.set(tick, 500);
|
|
234
|
+
}
|
|
235
|
+
function hold() { holding = true; since = clock.now(); video.pause(); tick(); }
|
|
236
|
+
return {
|
|
237
|
+
play: function () { if (!active) { return; } wanted = true; if (holding) { tick(); } else if (!started) { hold(); } else { play(); } },
|
|
238
|
+
pause: function () { wanted = false; holding = false; clock.clear(timer); video.pause(); },
|
|
239
|
+
waiting: function () { if (active && wanted && started && !holding && !video.seeking && ahead() < 0.75) { target = 12; hold(); } },
|
|
240
|
+
progress: tick,
|
|
241
|
+
holding: function () { return holding; },
|
|
242
|
+
destroy: function () { active = false; wanted = false; holding = false; clock.clear(timer); }
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
}(window));
|
|
246
|
+
|
|
210
247
|
</script><script>
|
|
211
248
|
(function () {
|
|
212
249
|
'use strict';
|
|
213
|
-
var C = window.ABTV, A = window.ABTVAniClient, BASE = 'http://127.0.0.1:8238', store, aniValidated = false, quickTimer, captionTimer;
|
|
250
|
+
var C = window.ABTV, A = window.ABTVAniClient, BASE = 'http://127.0.0.1:8238', store, aniValidated = false, quickTimer, captionTimer, editingInput = null, playbackBuffer = null, userPaused = false;
|
|
214
251
|
try { store = new C.Store(window.localStorage); } catch (ignore) { store = new C.Store({ getItem: function () { return null; }, setItem: function () { throw new Error('Armazenamento bloqueado'); } }); }
|
|
215
252
|
var gate = new C.Gate(), requests = [], state = { view: 'home', anime: null, episode: null, season: 1, query: '' }, stack = [], detailCache = {}, sourceData = null;
|
|
216
253
|
var video = document.getElementById('video'), hlsPlayer = null, playing = false, format = null, captionsReady = true, captionsFailed = false, playbackEpisode = null, playbackAnime = null, lastSaved = 0, lastResume = 0, wasHidden = false, loadSeq = 0, searchTimer, hudTimer, toastTimer, dialogReturn, focusReturn;
|
|
@@ -221,7 +258,16 @@ var ABTVAniClient = (function () {
|
|
|
221
258
|
function status(message) { el('status').textContent = message; }
|
|
222
259
|
function timeAt(at) { return new Date(at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); }
|
|
223
260
|
function visible(n) { return !!(n && n.getBoundingClientRect().width && n.getBoundingClientRect().height); }
|
|
224
|
-
function focus(n) { if (!n) { return; } n.focus(); if (n.scrollIntoViewIfNeeded) { n.scrollIntoViewIfNeeded(false); } else { var r = n.getBoundingClientRect(); if (r.top < 0 || r.bottom > window.innerHeight) { n.scrollIntoView(false); } } }
|
|
261
|
+
function focus(n) { if (!n) { return; } if (n._editButton && editingInput !== n) { n = n._editButton; } n.focus(); if (n.scrollIntoViewIfNeeded) { n.scrollIntoViewIfNeeded(false); } else { var r = n.getBoundingClientRect(); if (r.top < 0 || r.bottom > window.innerHeight) { n.scrollIntoView(false); } } }
|
|
262
|
+
function armInput(input, label) {
|
|
263
|
+
var edit = button(label, 'edit-input', input.id);
|
|
264
|
+
input._editButton = edit; input.readOnly = true; input.tabIndex = -1;
|
|
265
|
+
input.onclick = function () { beginInput(input); };
|
|
266
|
+
input.onblur = function () { input.readOnly = true; if (editingInput === input) { editingInput = null; } };
|
|
267
|
+
return edit;
|
|
268
|
+
}
|
|
269
|
+
function beginInput(input) { if (!input) { return; } editingInput = input; input.readOnly = false; input.focus(); }
|
|
270
|
+
function finishInput() { var input = editingInput; if (!input) { return; } editingInput = null; input.readOnly = true; input.blur(); focus(input._editButton); }
|
|
225
271
|
function rememberFocus() { var n = document.activeElement; return n && n.getAttribute ? { action: n.getAttribute('data-action'), value: n.getAttribute('data-value'), scroll: el('main').scrollTop } : null; }
|
|
226
272
|
function restoreFocus(saved) {
|
|
227
273
|
var bs = el('content').querySelectorAll('button'), i;
|
|
@@ -239,7 +285,7 @@ var ABTVAniClient = (function () {
|
|
|
239
285
|
xhr.onerror = function () { finish('Serviço local indisponível. Feche e abra o TizenBrew.'); }; xhr.ontimeout = function () { finish('A consulta demorou demais. Use Atualizar.'); }; xhr.onabort = function () { done = true; };
|
|
240
286
|
xhr.send(); return xhr;
|
|
241
287
|
}
|
|
242
|
-
function cleanContent(label) { el('breadcrumb').textContent = label; el('content').textContent = ''; el('main').scrollTop = 0; status(''); }
|
|
288
|
+
function cleanContent(label) { finishInput(); el('breadcrumb').textContent = label; el('content').textContent = ''; el('main').scrollTop = 0; status(''); }
|
|
243
289
|
function empty(title, message, target) { var box = node('div', 'empty'); box.appendChild(node('h2', '', title)); box.appendChild(node('p', 'muted', message)); (target || el('content')).appendChild(box); }
|
|
244
290
|
function error(message) { status(message); if (!el('content').textContent) { empty('Não foi possível consultar agora', message + ' Você pode tentar novamente pelo botão Atualizar agora.'); } }
|
|
245
291
|
function activate() { var ns = el('nav').querySelectorAll('button'), i; for (i = 0; i < ns.length; i++) { ns[i].classList.toggle('active', ns[i].getAttribute('data-action') === state.view); } }
|
|
@@ -286,7 +332,7 @@ var ABTVAniClient = (function () {
|
|
|
286
332
|
api('/page', '/', function (err, d) { if (err) { error(err); return; } var items = C.cards(d.html, 'episodio'), sections = C.homeSections(d.html); var f = rememberFocus(); store.set('home', { items: items.slice(0, 48), sections: sections, checkedAt: d.checkedAt }); showHome(items, d.checkedAt, saved || f, sections); });
|
|
287
333
|
}
|
|
288
334
|
function search(saved) {
|
|
289
|
-
cleanContent('Buscar anime'); title('O que você quer assistir?'); var input = node('input', 'search-input'); input.id = 'query'; input.type = 'text'; input.placeholder = 'Digite o nome do anime'; input.value = state.query || ''; input.setAttribute('aria-label', 'Buscar anime'); input.oninput = function () { state.query = input.value; scheduleSearch(); }; el('content').appendChild(input);
|
|
335
|
+
cleanContent('Buscar anime'); title('O que você quer assistir?'); var input = node('input', 'search-input'); input.id = 'query'; input.type = 'text'; input.placeholder = 'Digite o nome do anime'; input.value = state.query || ''; input.setAttribute('aria-label', 'Buscar anime'); input.oninput = function () { state.query = input.value; scheduleSearch(); }; el('content').appendChild(input); el('content').appendChild(armInput(input, 'Digitar busca'));
|
|
290
336
|
var keyboard = node('div', 'keyboard'); 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.split('').forEach(function (k) { keyboard.appendChild(button(k, 'key', k)); }); keyboard.appendChild(button('Espaço', 'key', ' ', 'wide')); keyboard.appendChild(button('Apagar', 'erase', undefined, 'wide')); keyboard.appendChild(button('Buscar', 'submit', undefined, 'wide primary')); el('content').appendChild(keyboard); var results = node('div'); results.id = 'results'; el('content').appendChild(results);
|
|
291
337
|
if (state.query) { runSearch(); } else { empty('Encontre a próxima história', 'Use o teclado da tela ou digite no campo de busca.', results); }
|
|
292
338
|
if (saved) { restoreFocus(saved); } else { focus(input); }
|
|
@@ -372,19 +418,24 @@ var ABTVAniClient = (function () {
|
|
|
372
418
|
if (!playbackEpisode || !video.currentTime || video.currentTime < 2) { return; } var record = { position: video.currentTime, duration: isFinite(video.duration) ? video.duration : 0, at: Date.now() };
|
|
373
419
|
store.set(C.progressKey(playbackEpisode), record); var history = store.get('history', []).filter(function (r) { return r.episode.path !== playbackEpisode.path; }); history.unshift({ episode: playbackEpisode, anime: { path: playbackAnime.path, title: playbackAnime.title, image: playbackAnime.image }, position: record.position }); store.set('history', history.slice(0, 30));
|
|
374
420
|
}
|
|
375
|
-
function
|
|
421
|
+
function playNow() { try { var p = video.play(); if (p && typeof p.then === 'function') { p.then(function () {}, function () { toast('Pressione Pausar / continuar para iniciar.'); }); } } catch (ignore) { toast('Não foi possível iniciar o player. Atualize as fontes.'); } }
|
|
422
|
+
|
|
423
|
+
function pauseByUser() { userPaused = true; if (playbackBuffer) { playbackBuffer.pause(); } else { video.pause(); } }
|
|
424
|
+
function playSafe(explicit) { if (explicit) { userPaused = false; } if (userPaused) { return; } if (!captionsReady) { if (captionsFailed) { toast('A legenda falhou. Volte e use Atualizar fontes.'); } return; } if (playbackBuffer) { playbackBuffer.play(); } else { playNow(); } }
|
|
376
425
|
function clearCaptions() { clearTimeout(captionTimer); var tracks = video.querySelectorAll('track'), i; for (i = 0; i < tracks.length; i++) { tracks[i].onload = null; tracks[i].onerror = null; video.removeChild(tracks[i]); } }
|
|
377
426
|
function positionCaptions(raised) { try { var tracks = video.textTracks, i, j; for (i = 0; i < tracks.length; i++) { var cues = tracks[i].activeCues || []; for (j = 0; j < cues.length; j++) { cues[j].line = raised ? -10 : -1; } } } catch (ignore) {} }
|
|
378
427
|
function startPlayer(f, changing) {
|
|
379
428
|
var position = changing ? video.currentTime || 0 : (store.get(C.progressKey(state.episode), {}) || {}).position || 0;
|
|
380
429
|
if (!changing) { focusReturn = rememberFocus(); playbackEpisode = state.episode; playbackAnime = state.anime; }
|
|
381
|
-
if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); } format = f; playing = true; loadSeq++; var seq = loadSeq; captionsReady = !f.subtitleUrl; captionsFailed = false; clearCaptions(); video.crossOrigin = 'anonymous'; el('player').classList.remove('hidden'); el('player-title').textContent = playbackAnime.title + ' · T' + playbackEpisode.season + ' E' + playbackEpisode.number; el('player-state').textContent = 'Preparando vídeo'; el('player-quality').textContent = f.height + 'p informado · aguardando resolução efetiva';
|
|
382
|
-
|
|
430
|
+
if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); } if (playbackBuffer) { playbackBuffer.destroy(); } playbackBuffer = null; userPaused = false; format = f; playing = true; loadSeq++; var seq = loadSeq; captionsReady = !f.subtitleUrl; captionsFailed = false; clearCaptions(); video.crossOrigin = 'anonymous'; el('player').classList.remove('hidden'); el('player-title').textContent = playbackAnime.title + ' · T' + playbackEpisode.season + ' E' + playbackEpisode.number; el('player-state').textContent = 'Preparando vídeo'; el('player-quality').textContent = f.height + 'p informado · aguardando resolução efetiva';
|
|
431
|
+
video.preload = 'auto';
|
|
432
|
+
if (f.transport === 'hls' && window.ABTVBuffer) { playbackBuffer = window.ABTVBuffer(video, playNow, function (message) { if (seq === loadSeq) { el('player-state').textContent = message; if (message !== 'Reproduzindo') { showHud(); } } }); }
|
|
433
|
+
if (f.subtitleUrl) { var track = node('track'); track.kind = 'subtitles'; track.label = 'Português'; track.srclang = f.subtitleLanguage || 'pt'; track.default = true; track.src = f.subtitleUrl; track.onload = function () { if (seq !== loadSeq) { return; } captionsReady = true; clearTimeout(captionTimer); try { track.track.mode = 'showing'; track.track.oncuechange = function () { positionCaptions(!el('player-hud').classList.contains('hidden')); }; } catch (ignore) {} positionCaptions(true); playSafe(); }; track.onerror = function () { if (seq !== loadSeq) { return; } clearTimeout(captionTimer); captionsFailed = true; captionsReady = false; if (playbackBuffer) { playbackBuffer.pause(); } else { video.pause(); } el('player-state').textContent = 'Legenda indisponível'; showHud(); toast('Não foi possível carregar a legenda portuguesa. Volte e use Atualizar fontes.'); }; video.appendChild(track); try { track.track.mode = 'showing'; } catch (ignore2) {} captionTimer = setTimeout(function () { if (seq === loadSeq && !captionsReady && track.onerror) { track.onerror(); } }, 20000); }
|
|
383
434
|
video.onloadedmetadata = function () { if (seq !== loadSeq) { return; } if (position > 0 && isFinite(video.duration) && position < video.duration - 5) { try { video.currentTime = position; } catch (ignore) {} } updateResolution(); playSafe(); };
|
|
384
435
|
video.onerror = function () { if (seq !== loadSeq) { return; } showHud(); el('player-state').textContent = 'Falha de reprodução'; toast('O vídeo falhou ou a fonte expirou. Volte e use Atualizar fontes.'); };
|
|
385
436
|
if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; }
|
|
386
437
|
if (f.transport === 'hls' && !video.canPlayType('application/vnd.apple.mpegurl') && !video.canPlayType('application/x-mpegURL') && window.Hls && window.Hls.isSupported()) {
|
|
387
|
-
hlsPlayer = new window.Hls({ enableWorker: false, maxBufferLength:
|
|
438
|
+
hlsPlayer = new window.Hls({ enableWorker: false, maxBufferLength: 45, maxMaxBufferLength: 60, backBufferLength: 10, maxBufferSize: 48 * 1024 * 1024 });
|
|
388
439
|
hlsPlayer.on(window.Hls.Events.ERROR, function (event, data) { if (data.fatal && seq === loadSeq && video.onerror) { video.onerror(); } });
|
|
389
440
|
hlsPlayer.loadSource(f.url); hlsPlayer.attachMedia(video); hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function () { if (seq === loadSeq) { playSafe(); } });
|
|
390
441
|
} else { video.src = f.url; video.load(); playSafe(); }
|
|
@@ -392,7 +443,7 @@ var ABTVAniClient = (function () {
|
|
|
392
443
|
}
|
|
393
444
|
function updateResolution() { if (!playing) { return; } var dims = video.videoWidth && video.videoHeight ? video.videoWidth + '×' + video.videoHeight + ' efetivo' : 'Resolução efetiva ainda não informada'; el('player-quality').textContent = (format.provider ? format.provider + ' · ' : format.original ? 'Original do site · ' : 'Blogger · ') + format.height + 'p informado · ' + dims + (format.subtitleTitle ? ' · Legenda: ' + format.subtitleTitle : ''); }
|
|
394
445
|
function showHud() { clearTimeout(hudTimer); el('player-hud').classList.remove('hidden'); positionCaptions(true); hudTimer = setTimeout(function () { if (!video.paused && el('dialog').classList.contains('hidden')) { el('player-hud').classList.add('hidden'); positionCaptions(false); } }, 8000); }
|
|
395
|
-
function closePlayer(quiet) { if (!playing) { return; } saveProgress(); if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); format.ticket = null; } playing = false; loadSeq++; clearTimeout(hudTimer); if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; } video.pause(); clearCaptions(); captionsReady = true; video.removeAttribute('src'); video.load(); video.onloadedmetadata = null; video.onerror = null; el('player').classList.add('hidden'); if (!quiet) { restoreFocus(focusReturn); } }
|
|
446
|
+
function closePlayer(quiet) { if (!playing) { return; } saveProgress(); if (format && format.ticket) { A.call('/ani/stop', format.ticket, {}, function () {}); format.ticket = null; } playing = false; loadSeq++; if (playbackBuffer) { playbackBuffer.destroy(); playbackBuffer = null; } clearTimeout(hudTimer); if (hlsPlayer) { hlsPlayer.destroy(); hlsPlayer = null; } video.pause(); clearCaptions(); captionsReady = true; video.removeAttribute('src'); video.load(); video.onloadedmetadata = null; video.onerror = null; el('player').classList.add('hidden'); if (!quiet) { restoreFocus(focusReturn); } }
|
|
396
447
|
function nextEpisode() {
|
|
397
448
|
if (!playbackAnime || !playbackAnime.episodes || !playbackAnime.episodes.length) { toast('Abra Ver temporadas para escolher o próximo episódio.'); return; }
|
|
398
449
|
var list = playbackAnime.episodes, i, next = null; for (i = 0; i < list.length - 1; i++) { if (list[i].path === playbackEpisode.path) { next = list[i + 1]; break; } }
|
|
@@ -408,12 +459,12 @@ var ABTVAniClient = (function () {
|
|
|
408
459
|
if (A.connected()) { box.appendChild(node('h3', '', A.name())); box.appendChild(node('p', 'muted', A.persistent() ? 'A sessão está salva neste aparelho. Ao abrir o app, ela será verificada novamente.' : 'A sessão está apenas na memória. O armazenamento não permitiu salvá-la; será preciso entrar novamente ao fechar.')); box.appendChild(button('Usar AniStream como principal', 'ani-primary', undefined, 'primary')); box.appendChild(button('Usar Animes Brasil', 'ani-secondary')); box.appendChild(button('Sair deste perfil no app', 'ani-logout')); }
|
|
409
460
|
else {
|
|
410
461
|
box.appendChild(node('p', 'muted', 'Use o usuário e a senha do perfil Jellyfin do AniStream. Essa senha pode ser diferente da senha da conta por e-mail.'));
|
|
411
|
-
var user = node('input', 'search-input'); user.id = 'ani-user'; user.type = 'text'; user.placeholder = 'Usuário do perfil AniStream'; user.setAttribute('aria-label', 'Usuário do perfil AniStream'); user.setAttribute('autocomplete', 'off'); box.appendChild(user);
|
|
412
|
-
var pass = node('input', 'search-input'); pass.id = 'ani-password'; pass.type = 'password'; pass.placeholder = 'Senha do perfil Jellyfin'; pass.setAttribute('aria-label', 'Senha do perfil Jellyfin'); pass.setAttribute('autocomplete', 'off'); box.appendChild(pass);
|
|
462
|
+
var user = node('input', 'search-input'); user.id = 'ani-user'; user.type = 'text'; user.placeholder = 'Usuário do perfil AniStream'; user.setAttribute('aria-label', 'Usuário do perfil AniStream'); user.setAttribute('autocomplete', 'off'); box.appendChild(user); box.appendChild(armInput(user, 'Digitar usuário'));
|
|
463
|
+
var pass = node('input', 'search-input'); pass.id = 'ani-password'; pass.type = 'password'; pass.placeholder = 'Senha do perfil Jellyfin'; pass.setAttribute('aria-label', 'Senha do perfil Jellyfin'); pass.setAttribute('autocomplete', 'off'); box.appendChild(pass); box.appendChild(armInput(pass, 'Digitar senha'));
|
|
413
464
|
box.appendChild(button('Entrar e lembrar neste aparelho', 'ani-login', undefined, 'primary')); box.appendChild(button('Vincular por código Quick Connect', 'ani-quick'));
|
|
414
465
|
var code = node('div'); code.id = 'ani-code'; box.appendChild(code);
|
|
415
466
|
}
|
|
416
|
-
box.appendChild(node('p', 'muted', 'Sua senha não é salva. Somente a sessão fica neste aparelho. O armazenamento do TizenBrew é compartilhado entre módulos. Se o AniStream encerrar a sessão, será necessário entrar novamente.')); el('content').appendChild(box); focus(box.querySelector('
|
|
467
|
+
box.appendChild(node('p', 'muted', 'Sua senha não é salva. Somente a sessão fica neste aparelho. O armazenamento do TizenBrew é compartilhado entre módulos. Se o AniStream encerrar a sessão, será necessário entrar novamente.')); el('content').appendChild(box); focus(box.querySelector('[data-action="ani-quick"]') || box.querySelector('button'));
|
|
417
468
|
}
|
|
418
469
|
function aniLoggedIn(d) { A.save(d); aniValidated = true; detailCache = {}; toast(A.persistent() ? 'AniStream conectado. A sessão será lembrada neste aparelho.' : 'AniStream conectado apenas nesta sessão: armazenamento indisponível.'); top('home'); }
|
|
419
470
|
function aniLogin() {
|
|
@@ -432,6 +483,7 @@ var ABTVAniClient = (function () {
|
|
|
432
483
|
function render(saved) { if (state.view === 'home') { home(saved); } else if (state.view === 'search') { search(saved); } else if (state.view === 'detail') { detail(saved); } else if (state.view === 'episode') { episodePage(); } else if (state.view === 'account') { account(); } else if (state.view === 'settings') { settings(); } else { library(); } }
|
|
433
484
|
function refresh() { if (playing) { toast('Volte à tela do episódio para atualizar sem interromper o vídeo.'); return; } cancel(); if (state.view === 'episode' && state.episode) { resolveSources(); } else if (state.view === 'detail') { detail(rememberFocus()); } else if (state.view === 'search') { runSearch(); } else { render(rememberFocus()); } }
|
|
434
485
|
function action(a, value, target) {
|
|
486
|
+
if (a === 'edit-input') { beginInput(el(value)); return; }
|
|
435
487
|
if (a === 'home' || a === 'search' || a === 'favorites' || a === 'history' || a === 'settings' || a === 'account') { top(a); }
|
|
436
488
|
else if (a === 'ani-login') { aniLogin(); }
|
|
437
489
|
else if (a === 'ani-quick') { aniQuick(); }
|
|
@@ -451,7 +503,7 @@ var ABTVAniClient = (function () {
|
|
|
451
503
|
else if (a === 'submit') { clearTimeout(searchTimer); runSearch(); }
|
|
452
504
|
else if (a === 'toggle-lower') { store.set('allowLower', !store.get('allowLower', false)); settings(); }
|
|
453
505
|
else if (a === 'close-player') { closePlayer(); }
|
|
454
|
-
else if (a === 'play-pause') { if (video.paused) { playSafe(); } else {
|
|
506
|
+
else if (a === 'play-pause') { if (userPaused || (video.paused && !(playbackBuffer && playbackBuffer.holding()))) { playSafe(true); } else { pauseByUser(); } showHud(); }
|
|
455
507
|
else if (a === 'seek-back' || a === 'seek-forward') { try { video.currentTime = Math.max(0, Math.min(isFinite(video.duration) ? video.duration : video.currentTime + 10, video.currentTime + (a === 'seek-back' ? -10 : 10))); } catch (ignore) {} showHud(); }
|
|
456
508
|
else if (a === 'next') { nextEpisode(); }
|
|
457
509
|
else if (a === 'confirm-language') { dialog('Você conferiu o idioma?', 'Confirme somente se este episódio está com áudio japonês e legenda PT-BR legível.', [{ label: 'Sim, confirmei os dois', fn: function () { store.set('language:' + playbackEpisode.path, true); toast('Idioma confirmado por você neste episódio'); } }, { label: 'Cancelar', fn: function () {} }]); }
|
|
@@ -460,7 +512,7 @@ var ABTVAniClient = (function () {
|
|
|
460
512
|
}
|
|
461
513
|
document.addEventListener('click', function (e) { var n = e.target; while (n && n !== document) { if (n.getAttribute && n.getAttribute('data-action')) { action(n.getAttribute('data-action'), n.getAttribute('data-value'), n); return; } n = n.parentNode; } });
|
|
462
514
|
function navigate(code) {
|
|
463
|
-
var container = !el('dialog').classList.contains('hidden') ? el('dialog') : playing ? el('player-hud') : document, all = container.querySelectorAll('button
|
|
515
|
+
var container = !el('dialog').classList.contains('hidden') ? el('dialog') : playing ? el('player-hud') : document, all = container.querySelectorAll('button'), current = document.activeElement, r = current && current.getBoundingClientRect ? current.getBoundingClientRect() : null, best = null, score = Infinity, i;
|
|
464
516
|
if (!r || !visible(current)) { for (i = 0; i < all.length; i++) { if (visible(all[i])) { focus(all[i]); return; } } }
|
|
465
517
|
var x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
466
518
|
for (i = 0; i < all.length; i++) { var n = all[i]; if (n === current || !visible(n) || n.disabled) { continue; } var q = n.getBoundingClientRect(), dx = q.left + q.width / 2 - x, dy = q.top + q.height / 2 - y, primary = code === 37 ? -dx : code === 39 ? dx : code === 38 ? -dy : dy, secondary = code === 37 || code === 39 ? Math.abs(dy) : Math.abs(dx); if (primary <= 3) { continue; } var s = primary + secondary * 2.5; if (s < score) { score = s; best = n; } }
|
|
@@ -468,14 +520,16 @@ var ABTVAniClient = (function () {
|
|
|
468
520
|
}
|
|
469
521
|
document.addEventListener('keydown', function (e) {
|
|
470
522
|
var k = e.keyCode || e.which, inDialog = !el('dialog').classList.contains('hidden');
|
|
523
|
+
if (editingInput) { if (k === 13 || k === 65376 || k === 65385 || k === 10009 || k === 27 || k === 461) { var searched = editingInput.id === 'query' && (k === 13 || k === 65376); e.preventDefault(); finishInput(); if (searched) { clearTimeout(searchTimer); runSearch(); } } return; }
|
|
471
524
|
if (k === 10009 || k === 27 || k === 461) { e.preventDefault(); back(); return; }
|
|
472
|
-
if (playing && !inDialog && [415, 19, 10252, 413, 412, 417].indexOf(k) !== -1) { e.preventDefault(); if (k === 413) { closePlayer(); } else if (k === 415) { playSafe(); } else if (k === 19) {
|
|
525
|
+
if (playing && !inDialog && [415, 19, 10252, 413, 412, 417].indexOf(k) !== -1) { e.preventDefault(); if (k === 413) { closePlayer(); } else if (k === 415) { playSafe(true); } else if (k === 19) { pauseByUser(); } else { action(k === 412 ? 'seek-back' : k === 417 ? 'seek-forward' : 'play-pause'); } showHud(); return; }
|
|
473
526
|
if (k >= 37 && k <= 40) { if (document.activeElement === el('query') && (k === 37 || k === 39)) { return; } e.preventDefault(); if (playing) { showHud(); } navigate(k); }
|
|
474
527
|
if (k === 13 && document.activeElement === el('query')) { e.preventDefault(); clearTimeout(searchTimer); runSearch(); }
|
|
475
528
|
else if (k === 13 && document.activeElement && document.activeElement.tagName === 'BUTTON') { e.preventDefault(); document.activeElement.click(); }
|
|
476
529
|
});
|
|
530
|
+
video.addEventListener('progress', function () { if (playbackBuffer) { playbackBuffer.progress(); } });
|
|
477
531
|
video.addEventListener('timeupdate', function () { if (!playing) { return; } el('player-time').textContent = clock(video.currentTime) + ' / ' + clock(video.duration); el('progress-bar').style.width = (video.duration ? Math.min(100, video.currentTime / video.duration * 100) : 0) + '%'; updateResolution(); if (Date.now() - lastSaved > 10000) { lastSaved = Date.now(); saveProgress(); } });
|
|
478
|
-
video.addEventListener('playing', function () { el('player-state').textContent = 'Reproduzindo'; showHud(); }); video.addEventListener('waiting', function () { el('player-state').textContent = 'Carregando vídeo…'; }); video.addEventListener('pause', function () { if (playing) { el('player-state').textContent = 'Pausado'; showHud(); saveProgress(); } }); video.addEventListener('ended', function () { saveProgress(); nextEpisode(); });
|
|
532
|
+
video.addEventListener('playing', function () { el('player-state').textContent = 'Reproduzindo'; showHud(); }); video.addEventListener('waiting', function () { el('player-state').textContent = 'Carregando vídeo…'; if (playbackBuffer) { playbackBuffer.waiting(); } }); video.addEventListener('pause', function () { if (playing && !(playbackBuffer && playbackBuffer.holding())) { el('player-state').textContent = 'Pausado'; showHud(); saveProgress(); } }); video.addEventListener('ended', function () { saveProgress(); nextEpisode(); });
|
|
479
533
|
function resume() { var now = Date.now(); if (now - lastResume < 1500) { return; } lastResume = now; if (playing) { saveProgress(); return; } if (!el('dialog').classList.contains('hidden')) { return; } refresh(); }
|
|
480
534
|
document.addEventListener('visibilitychange', function () { if (document.hidden) { wasHidden = true; saveProgress(); } else if (wasHidden) { wasHidden = false; resume(); } }); window.addEventListener('pageshow', function (e) { if (e.persisted) { resume(); } });
|
|
481
535
|
try { if (window.tizen && window.tizen.tvinputdevice) { ['MediaPlay', 'MediaPause', 'MediaPlayPause', 'MediaStop', 'MediaRewind', 'MediaFastForward'].forEach(function (k) { try { window.tizen.tvinputdevice.registerKey(k); } catch (ignore) {} }); } } catch (ignore) {}
|