// ==UserScript==
// @name CineBusca Pro
// @namespace https://github.com/estudiobrisacriativa/CineBusca-Pro
// @version 1.0.0
// @license MIT
// @description O seu assistente de mídia definitivo. Busque informações detalhadas, trailers e torrents para qualquer filme ou série com um simples atalho (Ctrl+1). Explore lançamentos, veja os títulos mais populares e organize sua lista de favoritos com facilidade.
// @author Estudio Brisa Criativa
// @match *://*/*
// @match https://www.netflix.com/*
// @match https://www.primevideo.com/*
// @match https://www.disneyplus.com/*
// @icon https://raw.githubusercontent.com/estudiobrisacriativa/CineBusca-Pro/refs/heads/main/assets/icon.png
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect api.themoviedb.org
// @connect image.tmdb.org
// @connect yts.mx
// @connect apibay.org
// ==/UserScript==
(function() {
'use strict';
// --- SUA API KEY ---
const tmdbApiKey = '4514ec3b323c023dd7772b82a3772435';
// --- CRIAÇÃO DO SHADOW DOM PARA ISOLAMENTO TOTAL ---
const host = document.createElement('div');
host.id = 'smart-search-host';
document.body.appendChild(host);
const shadowRoot = host.attachShadow({ mode: 'open' });
// --- ESTILOS (INJETADOS DIRETAMENTE NO SHADOW DOM) ---
const styles = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
:host {
--c-text: #e0e0e0; --c-background: #212121; --c-surface: #333333;
--c-primary: #a89166; --c-secondary: #8a8a8a; --c-tertiary: #5f5f5f;
--c-border: #424242; --c-text-dark: #212121; --c-light-bg: #f5f5f5;
--c-favorite: #DC143C; /* Crimson */
--c-success: #5a9e5e;
}
/* --- CSS Reset robusto para isolamento --- */
*, *::before, *::after {
box-sizing: border-box; margin: 0; padding: 0; border: 0;
font-family: 'Inter', sans-serif;
font-size: 16px;
line-height: 1.4;
color: var(--c-text);
font-weight: 400;
}
a { text-decoration: none; color: inherit; }
button { cursor: pointer; background: transparent; font-family: inherit; }
img { display: block; max-width: 100%; }
h3 { font-weight: 700; font-size: 1.05em;}
/* --- Fim do Reset --- */
#smart-search-container {
position: fixed; top: 20px; right: 20px; width: 450px; height: 600px;
background-color: var(--c-background);
border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.5);
z-index: 2147483647;
display: none; flex-direction: column;
border: 1px solid var(--c-border); overflow: hidden;
}
#smart-search-header {
padding: 10px; background-color: var(--c-background); display: flex;
align-items: center; gap: 8px; border-bottom: 1px solid var(--c-border); position: relative; flex-shrink: 0;
}
#search-input-wrapper {
flex-grow: 1; display: flex; align-items: center; gap: 8px; background-color: var(--c-surface);
border-radius: 20px; padding: 0 15px; border: 1px solid var(--c-border);
}
#smart-search-input {
width: 100%; padding: 10px 0; background: none;
font-size: 14px; outline: none;
color: var(--c-text);
flex-grow: 1;
}
#smart-search-input::placeholder, #year-filter-input::placeholder { color: var(--c-secondary); }
#year-filter-input {
width: 60px;
background: none;
border: none;
outline: none;
color: var(--c-text);
font-size: 14px;
text-align: center;
flex-shrink: 0;
}
#year-filter-input::-webkit-outer-spin-button,
#year-filter-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#year-filter-input[type=number] {
-moz-appearance: textfield;
}
.header-btn { cursor: pointer; padding: 5px; display: flex; align-items: center; flex-shrink: 0; }
.header-btn svg { fill: #AAAAAA; width: 22px; height: 22px; transition: fill 0.2s; }
.header-btn:hover svg { fill: var(--c-text); }
#back-btn { display: none; }
#panel-container {
flex-grow: 1;
position: relative;
}
.panel {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease-in-out, visibility 0s 0.2s;
overflow-y: auto;
padding: 15px;
}
.panel.active {
opacity: 1;
visibility: visible;
transition-delay: 0s;
}
.panel::-webkit-scrollbar { width: 5px; }
.panel::-webkit-scrollbar-track { background: var(--c-surface); }
.panel::-webkit-scrollbar-thumb { background-color: var(--c-tertiary); border-radius: 10px; }
.section-title { font-size: 1.1em; font-weight: 700; color: var(--c-primary); margin: 15px 0 10px 0; padding-bottom: 5px; border-bottom: 1px solid var(--c-border); display: flex; justify-content: space-between; align-items: center; }
.section-title:first-child { margin-top: 0; }
#home-nav { display: flex; flex-wrap: wrap; gap: 10px; border-bottom: 1px solid var(--c-border); padding-bottom: 10px; margin-bottom: 10px;}
.home-nav-btn { font-size: 0.9em; color: var(--c-secondary); padding: 5px 10px; border-radius: 6px; transition: all 0.2s; }
.home-nav-btn.active, .home-nav-btn:hover { color: var(--c-text); background-color: var(--c-surface); }
#recent-searches-list { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.recent-search-tag { font-size: 0.75em; font-weight: 500; color: var(--c-text); background-color: var(--c-border); border: 1px solid var(--c-surface); padding: 4px 10px; border-radius: 15px; cursor: pointer; transition: all 0.2s ease; }
.recent-search-tag:hover { background-color: var(--c-primary); border-color: var(--c-primary); color: var(--c-text-dark); }
#clear-recents-btn { font-size: 0.75em; color: var(--c-secondary); background-color: transparent; border: 1px solid var(--c-border); padding: 4px 10px; border-radius: 15px; cursor: pointer; transition: all 0.2s; }
#clear-recents-btn:hover { background-color: var(--c-primary); color: var(--c-text-dark); }
.search-result-item { position: relative; display: flex; margin-bottom: 12px; padding: 10px; background-color: var(--c-surface); border-radius: 8px; transition: background-color 0.2s ease, border-left-color 0.3s ease; align-items: flex-start; border-left: 4px solid transparent; }
.search-result-item:hover { background-color: var(--c-border); }
.search-result-item.movie-item { cursor: pointer; }
.search-result-item.favorited-item { border-left-color: var(--c-favorite); }
.fav-btn-item {
position: absolute; top: 15px; right: 15px; width: 32px; height: 32px;
display: flex; justify-content: center; align-items: center; cursor: pointer;
background-color: rgba(33, 33, 33, 0.7); border-radius: 50%;
transition: all 0.2s ease-in-out; z-index: 2;
}
.fav-btn-item:hover { background-color: rgba(0, 0, 0, 0.9); transform: scale(1.1); }
.fav-btn-item svg { width: 18px; height: 18px; fill: var(--c-secondary); transition: all 0.2s; }
.fav-btn-item:hover svg { fill: var(--c-text); }
@keyframes pulseFavorite { 0% { transform: scale(1); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } }
.fav-btn-item.favorited svg { animation: pulseFavorite 0.3s ease-in-out; fill: var(--c-favorite); }
.result-poster { width: 80px; height: 120px; border-radius: 8px; margin-right: 15px; object-fit: cover; background-color: var(--c-background); flex-shrink: 0; }
.result-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.result-title { font-size: 1.1em; font-weight: 700; color: var(--c-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 50px; }
.result-meta { font-size: 0.85em; color: var(--c-secondary); margin-top: 4px; display: flex; align-items: center; }
.result-meta .star-icon { color: #ffc107; margin-right: 4px; }
.result-overview { font-size: 0.9em; line-height: 1.5; margin-top: 8px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; padding-right: 50px; }
.search-message, .search-spinner { text-align: center; padding: 20px; color: var(--c-secondary); }
.search-spinner { border: 4px solid var(--c-surface); border-top: 4px solid var(--c-primary); border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 20px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
#trailer-modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); z-index: 2147483647; justify-content: center; align-items: center; }
#trailer-modal-content { position: relative; width: 90%; max-width: 800px; aspect-ratio: 16 / 9; background-color: black; }
#trailer-modal-close { position: absolute; top: -30px; right: 0; background: none; border: none; font-size: 2em; color: white; cursor: pointer; }
#trailer-iframe { width: 100%; height: 100%; border: none; }
#details-panel { padding: 0; }
.details-header { position: relative; padding: 20px; color: white; min-height: 200px; display: flex; align-items: flex-end; background-size: cover; background-position: center; }
.details-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to top, rgba(33, 33, 33, 1) 10%, rgba(33, 33, 33, 0.7) 50%, rgba(33, 33, 33, 0.3) 100%); }
.details-header-content { position: relative; z-index: 2; display: flex; gap: 20px; width: 100%; align-items: flex-end;}
.details-poster { width: 100px; height: 150px; border-radius: 8px; object-fit: cover; flex-shrink: 0; border: 2px solid white; }
.details-title-section { flex-grow: 1; }
.details-title { font-size: 1.6em; margin: 0 0 5px 0; font-weight: 700; line-height: 1.2; color: white;}
.details-original-title { font-size: 0.9em; font-style: italic; opacity: 0.8; color: white;}
.details-rating { display: flex; align-items: center; gap: 5px; margin-top: 10px; font-size: 1.1em; font-weight: 500; color: white;}
.details-rating .star-icon { color: #ffc107; }
.details-body { padding: 20px; background-color: var(--c-light-bg);}
.details-section-title { font-size: 1.2em; font-weight: 700; color: var(--c-text-dark); margin: 20px 0 10px 0; padding-bottom: 5px; border-bottom: 1px solid #dcdcdc; }
.details-section-title:first-child { margin-top: 0; }
.details-overview, .details-body p { line-height: 1.6; font-size: 0.9em; color: var(--c-text-dark);}
.cast-list { display: flex; overflow-x: auto; gap: 15px; padding-bottom: 10px; }
.cast-list::-webkit-scrollbar { height: 5px; }
.cast-list::-webkit-scrollbar-track { background: #e0e0e0; }
.cast-list::-webkit-scrollbar-thumb { background-color: var(--c-tertiary); border-radius: 10px; }
.cast-member { text-align: center; width: 80px; flex-shrink: 0; }
.cast-photo { width: 70px; height: 70px; border-radius: 50%; object-fit: cover; background-color: #eee; margin: 0 auto 5px; border: 2px solid #ddd; }
.cast-name { font-size: 0.8em; font-weight: 500; color: var(--c-text-dark);}
.cast-character { font-size: 0.75em; color: #666; }
.social-links { display: flex; gap: 15px; align-items: center; }
.social-link svg { width: 24px; height: 24px; fill: var(--c-text-dark); transition: fill 0.2s, transform 0.2s; }
.social-link:hover svg { fill: var(--c-primary); transform: scale(1.1); }
.result-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px;}
.result-actions button, .result-actions a { font-size: 0.75em; font-weight: 500; color: var(--c-text); background-color: var(--c-border); border: 1px solid var(--c-surface); padding: 4px 10px; border-radius: 15px; cursor: pointer; transition: all 0.2s ease; display: inline-block; text-align: center; }
.result-actions button:hover, .result-actions a:hover { background-color: var(--c-primary); border-color: var(--c-primary); color: var(--c-text-dark); }
#torrents-container { margin-top: 20px; }
.torrent-list { display: flex; flex-direction: column; gap: 10px; }
.torrent-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background-color: #f0f0f0; border-radius: 6px; border-left: 4px solid var(--c-primary); }
.torrent-item.hidden { display: none; }
.torrent-quality { font-weight: 700; font-size: 0.9em; color: var(--c-text-dark); max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; }
.torrent-info { font-size: 0.8em; color: #555; }
.torrent-action-btn { font-size: 0.8em; font-weight: 700; color: white; background-color: var(--c-primary); padding: 6px 12px; border-radius: 15px; text-decoration: none; transition: background-color 0.2s; white-space: nowrap; }
.torrent-action-btn:hover { background-color: #8c754e; }
.torrent-action-btn:disabled { background-color: var(--c-success); cursor: default; }
.ver-mais-btn {
font-size: 0.85em; font-weight: 700; color: var(--c-text-dark);
background-color: #e0e0e0; padding: 8px 15px; border-radius: 20px;
text-decoration: none; transition: background-color 0.2s;
margin-top: 10px; text-align: center; width: 100%;
}
.ver-mais-btn:hover { background-color: #cccccc; }
.brazil-flag { margin-left: 8px; font-size: 1.2em; }
#pagination-controls { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 15px; }
.page-btn { font-size: 0.85em; background-color: var(--c-surface); padding: 8px 15px; border-radius: 20px; }
.page-info { font-size: 0.9em; font-weight: 500; }
`;
const styleSheet = document.createElement('style');
styleSheet.innerText = styles;
shadowRoot.appendChild(styleSheet);
// --- ESTRUTURA HTML ---
const appContainer = document.createElement('div');
appContainer.innerHTML = `
<div id="smart-search-container">
<div id="smart-search-header">
<button id="back-btn" class="header-btn" title="Voltar">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"></path></svg>
</button>
<button id="home-btn" class="header-btn" title="Início">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"></path></svg>
</button>
<div id="search-input-wrapper">
<input id="smart-search-input" type="text" placeholder="Buscar Filmes e Séries...">
<input id="year-filter-input" type="number" placeholder="Ano" min="1900" max="2099">
</div>
<button id="favorites-btn" class="header-btn" title="Favoritos">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"></path></svg>
</button>
</div>
<div id="panel-container">
<div id="smart-search-results" class="panel active"></div>
<div id="favorites-panel" class="panel"></div>
<div id="details-panel" class="panel"></div>
</div>
</div>
<div id="trailer-modal-overlay">
<div id="trailer-modal-content">
<button id="trailer-modal-close">×</button>
<iframe id="trailer-iframe" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div>
</div>
`;
shadowRoot.appendChild(appContainer);
// --- VARIÁVEIS GLOBAIS E ELEMENTOS DO DOM (referenciando o Shadow DOM) ---
const searchBar = shadowRoot.getElementById('smart-search-container');
const searchInput = shadowRoot.getElementById('smart-search-input');
const yearFilterInput = shadowRoot.getElementById('year-filter-input');
const resultsDiv = shadowRoot.getElementById('smart-search-results');
const favoritesPanel = shadowRoot.getElementById('favorites-panel');
const detailsPanel = shadowRoot.getElementById('details-panel');
const favoritesBtn = shadowRoot.getElementById('favorites-btn');
const homeBtn = shadowRoot.getElementById('home-btn');
const backBtn = shadowRoot.getElementById('back-btn');
const trailerModal = shadowRoot.getElementById('trailer-modal-overlay');
const closeModalBtn = shadowRoot.getElementById('trailer-modal-close');
let isBarVisible = false;
let favorites = [];
let viewHistory = ['home'];
let homeViewCache = { releases: [], top: [], topSeries: [] };
let currentHomePage = { view: 'releases', page: 1 };
const ITEMS_PER_PAGE = 4;
const socialSVGs = { facebook: `<svg viewBox="0 0 24 24"><path d="M12 2.04C6.5 2.04 2 6.53 2 12s4.5 9.96 10 9.96c5.5 0 10-4.46 10-9.96S17.5 2.04 12 2.04zm3.5 6.74h-2.1c-.5 0-.6.4-.6 1v1.36h2.7l-.4 2.1H12.8V19h-2.8v-5.76H8v-2.1h2V9.8c0-1.7 1.1-2.8 2.7-2.8h2v2.1z"/></svg>`, twitter: `<svg viewBox="0 0 24 24"><path d="M22.46 6c-.77.35-1.6.58-2.46.67.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.22-1.95-.55v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.52 8.52 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21c7.34 0 11.35-6.08 11.35-11.35 0-.17 0-.34-.01-.51.78-.57 1.45-1.28 1.99-2.08z"/></svg>`, instagram: `<svg viewBox="0 0 24 24"><path d="M7.8 2h8.4C19.4 2 22 4.6 22 7.8v8.4a5.8 5.8 0 0 1-5.8 5.8H7.8C4.6 22 2 19.4 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2m-.2 2A3.6 3.6 0 0 0 4 7.6v8.8A3.6 3.6 0 0 0 7.6 20h8.8A3.6 3.6 0 0 0 20 16.4V7.6A3.6 3.6 0 0 0 16.4 4H7.6m9.65 1.5a1.25 1.25 0 0 1 1.25 1.25A1.25 1.25 0 0 1 17.25 8 1.25 1.25 0 0 1 16 6.75a1.25 1.25 0 0 1 1.25-1.25M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/></svg>` };
let originalBodyOverflow;
// --- ATIVAÇÃO E CONTROLES GERAIS ---
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === '1') {
e.preventDefault();
isBarVisible = !isBarVisible;
searchBar.style.display = isBarVisible ? 'flex' : 'none';
if (isBarVisible) {
searchInput.focus();
if(!resultsDiv.innerHTML.trim()) {
renderHomePage();
}
}
}
});
searchBar.addEventListener('mouseover', () => {
originalBodyOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
});
searchBar.addEventListener('mouseout', () => {
document.body.style.overflow = originalBodyOverflow;
});
closeModalBtn.addEventListener('click', hideTrailer);
trailerModal.addEventListener('click', e => { if (e.target === trailerModal) hideTrailer(); });
homeBtn.addEventListener('click', () => {
renderHomePage();
showPanel('smart-search-results');
});
backBtn.addEventListener('click', () => {
if(viewHistory.length <= 1) return;
viewHistory.pop();
const previousView = viewHistory[viewHistory.length - 1] || 'home';
const panelId = previousView.includes('details') ? 'details-panel'
: (previousView === 'favorites' ? 'favorites-panel'
: 'smart-search-results');
showPanel(panelId, false);
});
[resultsDiv, favoritesPanel].forEach(panel => {
panel.addEventListener('click', e => {
const favButton = e.target.closest('.fav-btn-item');
if (favButton) {
e.stopPropagation();
const itemData = JSON.parse(decodeURIComponent(favButton.dataset.item));
toggleFavorite(itemData);
return;
}
const movieItem = e.target.closest('.search-result-item.movie-item');
if (movieItem) {
const { id, type } = movieItem.dataset;
renderDetailsView(id, type);
}
});
});
detailsPanel.addEventListener('click', e => {
const trailerBtn = e.target.closest('.trailer-btn');
if (trailerBtn) {
e.stopPropagation();
showTrailer(trailerBtn.dataset.key);
return;
}
const copyBtn = e.target.closest('.torrent-copy-btn');
if (copyBtn && !copyBtn.disabled) {
const magnetLink = copyBtn.dataset.magnetLink;
const textArea = document.createElement('textarea');
textArea.value = magnetLink;
textArea.style.position = 'fixed';
textArea.style.top = '-9999px';
textArea.style.left = '-9999px';
shadowRoot.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
copyBtn.textContent = 'Copiado!';
copyBtn.disabled = true;
setTimeout(() => {
copyBtn.textContent = 'Copiar';
copyBtn.disabled = false;
}, 2000);
} catch (err) {
console.error('Falha ao copiar o link magnético: ', err);
copyBtn.textContent = 'Erro!';
setTimeout(() => {
copyBtn.textContent = 'Copiar';
copyBtn.disabled = false;
}, 2000);
}
shadowRoot.removeChild(textArea);
}
});
favoritesBtn.addEventListener('click', () => {
showPanel('favorites-panel');
renderFavoritesPanel();
});
function handleSearch() {
const query = searchInput.value.trim();
const year = yearFilterInput.value.trim();
if (query) {
performSearch(query, year);
} else {
renderHomePage();
}
}
searchInput.addEventListener('keyup', e => {
if (e.key === 'Enter') handleSearch();
});
yearFilterInput.addEventListener('keyup', e => {
if (e.key === 'Enter') handleSearch();
});
function showPanel(panelId, pushToHistory = true) {
shadowRoot.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
shadowRoot.getElementById(panelId).classList.add('active');
let currentView = 'home';
if (panelId === 'details-panel') currentView = `details-${detailsPanel.dataset.id}`;
else if (panelId === 'favorites-panel') currentView = 'favorites';
else if (searchInput.value.trim()) currentView = `search-${searchInput.value.trim()}`;
if (pushToHistory && viewHistory[viewHistory.length - 1] !== currentView) {
viewHistory.push(currentView);
}
backBtn.style.display = viewHistory.length > 1 ? 'flex' : 'none';
}
// --- LÓGICA DE RENDERIZAÇÃO DE CONTEÚDO ---
async function renderHomePage() {
resultsDiv.innerHTML = `<div class="search-spinner"></div>`;
const recentSearches = await GM_getValue('recentSearches', []);
let recentSearchesHTML = '';
if (recentSearches.length > 0) {
recentSearchesHTML = `
<div class="section-title">
<span>Buscas Recentes</span>
<button id="clear-recents-btn" title="Limpar buscas recentes">Apagar</button>
</div>
<div id="recent-searches-list">
${recentSearches.map(term => `<button class="recent-search-tag" data-term="${term}">${term}</button>`).join('')}
</div>`;
}
resultsDiv.innerHTML = `
${recentSearchesHTML}
<div id="home-nav">
<button class="home-nav-btn ${currentHomePage.view === 'releases' ? 'active' : ''}" data-view="releases">Lançamentos</button>
<button class="home-nav-btn ${currentHomePage.view === 'top' ? 'active' : ''}" data-view="top">Top 10 Filmes</button>
<button class="home-nav-btn ${currentHomePage.view === 'topSeries' ? 'active' : ''}" data-view="topSeries">Top 10 Séries</button>
</div>
<div id="movie-list-container"></div>
<div id="pagination-controls"></div>
`;
await renderHomeSubPage();
resultsDiv.querySelectorAll('.home-nav-btn').forEach(btn => {
btn.addEventListener('click', () => {
currentHomePage.view = btn.dataset.view;
currentHomePage.page = 1;
renderHomeSubPage();
});
});
resultsDiv.querySelectorAll('.recent-search-tag').forEach(tag => {
tag.onclick = () => {
searchInput.value = tag.dataset.term;
yearFilterInput.value = '';
performSearch(tag.dataset.term);
};
});
const clearBtn = resultsDiv.querySelector('#clear-recents-btn');
if (clearBtn) {
clearBtn.addEventListener('click', async () => {
await GM_setValue('recentSearches', []);
renderHomePage();
});
}
}
async function renderHomeSubPage() {
const listContainer = shadowRoot.getElementById('movie-list-container');
const paginationContainer = shadowRoot.getElementById('pagination-controls');
listContainer.innerHTML = `<div class="search-spinner"></div>`;
paginationContainer.innerHTML = '';
shadowRoot.querySelectorAll('#home-nav .home-nav-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === currentHomePage.view);
});
if (currentHomePage.view === 'releases') {
if (homeViewCache.releases.length === 0) {
const data = await tmdbRequest('/movie/upcoming', '', 'pt-BR');
homeViewCache.releases = data.results || [];
}
const itemsToDisplay = paginate(homeViewCache.releases, ITEMS_PER_PAGE);
listContainer.innerHTML = '';
itemsToDisplay.forEach(item => renderItem(item, listContainer));
renderPaginationControls(homeViewCache.releases.length, resultsDiv, renderHomeSubPage, ITEMS_PER_PAGE);
} else if (currentHomePage.view === 'top') {
if (homeViewCache.top.length === 0) {
const data = await tmdbRequest('/movie/top_rated', '', 'pt-BR');
homeViewCache.top = data.results || [];
}
const itemsToDisplay = paginate(homeViewCache.top, 10);
listContainer.innerHTML = '';
itemsToDisplay.forEach(item => renderItem(item, listContainer));
renderPaginationControls(homeViewCache.top.length, resultsDiv, renderHomeSubPage, 10);
} else if (currentHomePage.view === 'topSeries') {
if (homeViewCache.topSeries.length === 0) {
const data = await tmdbRequest('/tv/top_rated', '', 'pt-BR');
homeViewCache.topSeries = data.results || [];
}
const itemsToDisplay = paginate(homeViewCache.topSeries, 10);
listContainer.innerHTML = '';
itemsToDisplay.forEach(item => renderItem(item, listContainer));
renderPaginationControls(homeViewCache.topSeries.length, resultsDiv, renderHomeSubPage, 10);
}
}
function paginate(list, itemsPerPage) {
const startIndex = (currentHomePage.page - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return list.slice(startIndex, endIndex);
}
function renderPaginationControls(totalItems, container, pageChangeCallback, itemsPerPage) {
const paginationContainer = container.querySelector('#pagination-controls');
paginationContainer.innerHTML = '';
const totalPages = Math.ceil(totalItems / itemsPerPage);
if (totalPages <= 1) return;
const prevBtn = document.createElement('button');
prevBtn.textContent = 'Anterior';
prevBtn.className = 'page-btn';
prevBtn.disabled = currentHomePage.page === 1;
prevBtn.addEventListener('click', () => {
if (currentHomePage.page > 1) {
currentHomePage.page--;
pageChangeCallback();
}
});
const pageInfo = document.createElement('span');
pageInfo.className = 'page-info';
pageInfo.textContent = `Página ${currentHomePage.page} de ${totalPages}`;
const nextBtn = document.createElement('button');
nextBtn.textContent = 'Próximo';
nextBtn.className = 'page-btn';
nextBtn.disabled = currentHomePage.page === totalPages;
nextBtn.addEventListener('click', () => {
if (currentHomePage.page < totalPages) {
currentHomePage.page++;
pageChangeCallback();
}
});
paginationContainer.appendChild(prevBtn);
paginationContainer.appendChild(pageInfo);
paginationContainer.appendChild(nextBtn);
}
// --- LÓGICA DE BUSCA ---
async function performSearch(query, year = null, save = true) {
if (save) await saveSearchTerm(query);
resultsDiv.innerHTML = `<div class="search-spinner"></div>`;
const movieResults = await performMovieSearch(query, year);
showPanel('smart-search-results');
renderUnifiedResults({ movieResults });
}
async function performMovieSearch(query, year) {
const yearParam = year ? `&year=${year}` : '';
const initialResults = await tmdbRequest(`/search/multi`, `&query=${encodeURIComponent(query)}${yearParam}`);
if (!initialResults || !initialResults.results) return [];
const validResults = initialResults.results.filter(item => (item.media_type === 'movie' || item.media_type === 'tv') && item.poster_path).slice(0, 10);
if (validResults.length === 0) return [];
const detailPromises = validResults.map(async (item) => {
try {
const details = await tmdbRequest(`/${item.media_type}/${item.id}`, `&append_to_response=videos`, 'pt-BR');
if (details) {
details.media_type = item.media_type;
return details;
}
} catch (error) { console.error(`Erro ao buscar detalhes para ${item.id}:`, error); }
return null;
});
return (await Promise.all(detailPromises)).filter(Boolean);
}
function renderUnifiedResults(results) {
resultsDiv.innerHTML = '';
let foundResults = false;
if (results.movieResults && results.movieResults.length > 0) {
foundResults = true;
resultsDiv.innerHTML += `<h3 class="section-title"><span>Resultados</span></h3>`;
results.movieResults.forEach(item => renderItem(item, resultsDiv));
}
if (!foundResults) {
resultsDiv.innerHTML = `<div class="search-message">Nenhum resultado encontrado.</div>`;
}
}
function renderItem(item, container) {
if (!item.media_type && (item.title || item.name)) {
item.media_type = item.title ? 'movie' : 'tv';
}
if (item.media_type === 'movie' || item.media_type === 'tv') {
renderMovieItem(item, container);
}
}
function renderMovieItem(item, container) {
const title = item.title || item.name;
const releaseDate = item.release_date || item.first_air_date;
const year = releaseDate ? new Date(releaseDate).getFullYear() : 'N/D';
const posterPath = item.poster_path ? `https://image.tmdb.org/t/p/w200${item.poster_path}` : 'https://placehold.co/80x120/5a6e4e/f0f0f0?text=Capa';
const overview = item.overview || "Sinopse não disponível.";
const rating = item.vote_average ? item.vote_average.toFixed(1) : "N/D";
const isFav = isFavorited(item);
const itemData = encodeURIComponent(JSON.stringify({id: item.id, title, name: item.name, poster_path: item.poster_path, media_type: item.media_type, overview, vote_average: item.vote_average, release_date: item.release_date, first_air_date: item.first_air_date, type: item.media_type}));
const itemDiv = document.createElement('div');
itemDiv.className = `search-result-item movie-item ${isFav ? 'favorited-item' : ''}`;
itemDiv.dataset.id = item.id;
itemDiv.dataset.type = item.media_type;
itemDiv.innerHTML = `
<img src="${posterPath}" alt="Capa de ${title}" class="result-poster">
<div class="result-info">
<div>
<h3 class="result-title">${title}</h3>
<div class="result-meta"><span>${year}</span><span class="star-icon" style="margin-left: 10px;">⭐</span><span>${rating}</span></div>
<p class="result-overview">${overview}</p>
</div>
</div>
<button class="fav-btn-item ${isFav ? 'favorited' : ''}" title="Adicionar/Remover dos Favoritos" data-item="${itemData}">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"></path></svg>
</button>
`;
container.appendChild(itemDiv);
}
// --- LÓGICA DE DETALHES, FAVORITOS E UTILITÁRIOS ---
async function renderDetailsView(itemId, mediaType) {
detailsPanel.dataset.id = `${mediaType}-${itemId}`;
showPanel('details-panel');
detailsPanel.innerHTML = `<div class="search-spinner"></div>`;
try {
const data = await tmdbRequest(`/${mediaType}/${itemId}`, `&append_to_response=credits,external_ids,videos`, 'pt-BR');
if (!data) throw new Error("Não foi possível obter os dados da API");
const backdropUrl = data.backdrop_path ? `https://image.tmdb.org/t/p/w500${data.backdrop_path}` : '';
const posterUrl = data.poster_path ? `https://image.tmdb.org/t/p/w200${data.poster_path}` : 'https://placehold.co/100x150/5a6e4e/f0f0f0?text=Capa';
const title = data.title || data.name;
const originalTitle = data.original_title || data.original_name;
const rating = data.vote_average ? data.vote_average.toFixed(1) : "N/D";
const imdbId = data.external_ids?.imdb_id;
let creatorsHTML = 'Não informado';
if (mediaType === 'tv' && data.created_by?.length > 0) {
creatorsHTML = data.created_by.map(c => c.name).join(', ');
} else if (mediaType === 'movie' && data.credits?.crew) {
const director = data.credits.crew.find(c => c.job === 'Director');
if (director) creatorsHTML = director.name;
}
const cast = data.credits?.cast?.slice(0, 10) || [];
const castHTML = cast.map(member => `
<div class="cast-member">
<img src="${member.profile_path ? `https://image.tmdb.org/t/p/w200${member.profile_path}` : 'https://placehold.co/70x70/ccc/fff?text=?'}" class="cast-photo" alt="${member.name}">
<div class="cast-name">${member.name}</div>
<div class="cast-character">${member.character}</div>
</div>
`).join('');
const social = data.external_ids || {};
let socialHTML = ['instagram', 'twitter', 'facebook']
.map(platform => ({ platform, id: social[`${platform}_id`] }))
.filter(p => p.id)
.map(p => `<a href="https://www.${p.platform}.com/${p.id}" target="_blank" class="social-link" title="${p.platform}" rel="noopener noreferrer">${socialSVGs[p.platform]}</a>`)
.join('');
if (!socialHTML) socialHTML = 'Nenhuma rede social oficial encontrada.';
const trailer = findBestTrailer(data.videos);
const trailerHTML = trailer ? `<button class="trailer-btn" data-key="${trailer.key}">Assistir Trailer</button>` : '';
detailsPanel.innerHTML = `
<div class="details-header" style="background-image: url('${backdropUrl}')">
<div class="details-header-content">
<img src="${posterUrl}" class="details-poster" alt="Pôster de ${title}">
<div class="details-title-section">
<h2 class="details-title">${title}</h2>
<p class="details-original-title">Título Original: ${originalTitle}</p>
<div class="details-rating"><span class="star-icon">⭐</span> ${rating} / 10</div>
<div class="result-actions" style="margin-top: 10px;">${trailerHTML}</div>
</div>
</div>
</div>
<div class="details-body">
<h3 class="details-section-title">Sinopse Completa</h3>
<p class="details-overview">${data.overview || "Sinopse não disponível."}</p>
<div id="torrents-container"></div>
<h3 class="details-section-title">Criação</h3>
<p>${creatorsHTML}</p>
<h3 class="details-section-title">Elenco Principal</h3>
<div class="cast-list">${castHTML || 'Elenco não informado.'}</div>
<h3 class="details-section-title">Redes Sociais Oficiais</h3>
<div class="social-links">${socialHTML}</div>
</div>
`;
const torrentsContainer = shadowRoot.querySelector('#torrents-container');
torrentsContainer.innerHTML = `<h3 class="details-section-title">Downloads (Torrents)</h3><div id="aggregated-torrents"></div><div id="yts-torrents" style="margin-top:15px;"></div>`;
// Prioriza buscas brasileiras
if (mediaType === 'movie') {
fetchAndRenderAggregatedTorrents(originalTitle, 'movie');
fetchAndRenderYTSTorrents(imdbId, title);
} else if (mediaType === 'tv') {
fetchAndRenderAggregatedTorrents(originalTitle, 'tv');
}
} catch (error) {
console.error("Erro ao carregar detalhes:", error);
detailsPanel.innerHTML = `<p class="search-message">Não foi possível carregar os detalhes.</p>`;
}
}
async function fetchAndRenderYTSTorrents(imdbId, title) {
const ytsContainer = shadowRoot.querySelector('#yts-torrents');
if (!imdbId) {
ytsContainer.innerHTML = '';
return;
}
ytsContainer.innerHTML = `<p style="color: var(--c-text-dark); font-size: 0.9em; font-weight: 500;">Buscando em YTS (Áudio Original)...</p><div class="search-spinner" style="margin: 10px 0;"></div>`;
try {
const ytsData = await apiRequest(`https://yts.mx/api/v2/list_movies.json?query_term=${imdbId}`);
if (ytsData && ytsData.data.movie_count > 0 && ytsData.data.movies[0].torrents) {
const torrents = ytsData.data.movies[0].torrents;
const encodedTitle = encodeURIComponent(title);
const trackers = '&tr=udp://open.demonii.com:1337/announce&tr=udp://tracker.openbittorrent.com:80&tr=udp://tracker.coppersurfer.tk:6969&tr=udp://glotorrents.pw:6969/announce&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://torrent.gresille.org:80/announce&tr=udp://p4p.arenabg.com:1337&tr=udp://tracker.leechers-paradise.org:6969';
const torrentList = document.createElement('div');
torrentList.className = 'torrent-list';
torrents.forEach(t => {
const magnetLink = `magnet:?xt=urn:btih:${t.hash}&dn=${encodedTitle}${trackers}`;
const torrentItem = document.createElement('div');
torrentItem.className = 'torrent-item';
torrentItem.innerHTML = `
<div title="YTS - ${t.quality} (${t.type})">
<div class="torrent-quality">YTS - ${t.quality} (${t.type})</div>
<div class="torrent-info">Tamanho: ${t.size} / Seeds: ${t.seeds} / Peers: ${t.peers}</div>
</div>
<button class="torrent-action-btn torrent-copy-btn" data-magnet-link="${magnetLink}">Copiar</button>
`;
torrentList.appendChild(torrentItem);
});
ytsContainer.innerHTML = '';
ytsContainer.appendChild(torrentList);
} else {
ytsContainer.innerHTML = `<p style="color: var(--c-text-dark); font-size: 0.9em;">Nenhum torrent encontrado em YTS para este filme.</p>`;
}
} catch (error) {
console.error("Erro ao buscar torrents YTS:", error);
ytsContainer.innerHTML = `<p style="color: var(--c-text-dark); font-size: 0.9em;">Não foi possível buscar torrents em YTS.</p>`;
}
}
function isBrazilianTorrent(name) {
const lowerName = name.toLowerCase();
const keywords = ['dublado', 'dual audio', 'nacional', 'pt-br', 'portugues', 'legendado'];
return keywords.some(keyword => lowerName.includes(keyword));
}
async function fetchAndRenderAggregatedTorrents(originalTitle, type) {
const container = shadowRoot.querySelector('#aggregated-torrents');
const queries = [ type === 'movie' ? `${originalTitle} dual audio` : `${originalTitle} dublado`, originalTitle ];
container.innerHTML = `<p style="color: var(--c-text-dark); font-size: 0.9em; font-weight: 500;">Buscando em fontes BR e Internacionais...</p><div class="search-spinner" style="margin: 10px 0;"></div>`;
try {
const searchPromises = queries.map(query => apiRequest(`https://apibay.org/q.php?q=${encodeURIComponent(query)}&cat=200`));
const resultsArrays = await Promise.all(searchPromises);
const combinedResults = [].concat(...resultsArrays);
const uniqueResults = [];
const seenHashes = new Set();
for (const result of combinedResults) {
if (result.name && result.name !== 'No results returned' && !seenHashes.has(result.info_hash)) {
uniqueResults.push(result);
seenHashes.add(result.info_hash);
}
}
uniqueResults.sort((a, b) => parseInt(b.seeders, 10) - parseInt(a.seeders, 10));
if (uniqueResults.length > 0) {
const trackers = '&tr=udp://open.demonii.com:1337/announce&tr=udp://tracker.openbittorrent.com:80&tr=udp://tracker.coppersurfer.tk:6969&tr=udp://glotorrents.pw:6969/announce&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://torrent.gresille.org:80/announce&tr=udp://p4p.arenabg.com:1337&tr=udp://tracker.leechers-paradise.org:6969';
const torrentList = document.createElement('div');
torrentList.className = 'torrent-list';
uniqueResults.forEach((t, index) => {
const isBR = isBrazilianTorrent(t.name);
const flag = isBR ? `<span class="brazil-flag">🇧🇷</span>` : '';
const magnetLink = `magnet:?xt=urn:btih:${t.info_hash}&dn=${encodeURIComponent(t.name)}${trackers}`;
const torrentItem = document.createElement('div');
torrentItem.className = 'torrent-item';
if (index >= 5) {
torrentItem.classList.add('hidden');
}
torrentItem.innerHTML = `
<div title="${t.name}">
<div class="torrent-quality">${t.name}${flag}</div>
<div class="torrent-info">Tamanho: ${(t.size / 1073741824).toFixed(2)} GB / Seeds: ${t.seeders} / Peers: ${t.leechers}</div>
</div>
<button class="torrent-action-btn torrent-copy-btn" data-magnet-link="${magnetLink}">Copiar</button>
`;
torrentList.appendChild(torrentItem);
});
container.innerHTML = '';
container.appendChild(torrentList);
if (uniqueResults.length > 5) {
const verMaisBtn = document.createElement('button');
verMaisBtn.className = 'ver-mais-btn';
verMaisBtn.textContent = `Ver mais ${uniqueResults.length - 5} resultados`;
verMaisBtn.onclick = function() {
torrentList.querySelectorAll('.torrent-item.hidden').forEach(item => item.classList.remove('hidden'));
verMaisBtn.remove();
};
container.appendChild(verMaisBtn);
}
} else {
container.innerHTML = `<p style="color: var(--c-text-dark); font-size: 0.9em;">Nenhum torrent encontrado em fontes adicionais.</p>`;
}
} catch (error) {
console.error("Erro ao buscar torrents agregados:", error);
container.innerHTML = `<p style="color: var(--c-text-dark); font-size: 0.9em;">Não foi possível buscar em fontes adicionais.</p>`;
}
}
async function saveSearchTerm(term) {
let searches = await GM_getValue(`recentSearches`, []);
searches = searches.filter(s => s.toLowerCase() !== term.toLowerCase());
searches.unshift(term);
if (searches.length > 8) searches.length = 8;
await GM_setValue(`recentSearches`, searches);
}
function findBestTrailer(videos) {
if (!videos?.results?.length) return null;
const allTrailers = videos.results.filter(v => v.type === "Trailer" && v.site === "YouTube");
return allTrailers.find(v => v.iso_639_1 === 'pt') || allTrailers.find(v => v.iso_639_1 === 'en') || allTrailers[0];
}
async function loadFavorites() { favorites = await GM_getValue('favoritesList', []); }
async function saveFavorites() { await GM_setValue('favoritesList', favorites); }
async function toggleFavorite(itemData) {
const uniqueId = `${itemData.type}-${itemData.id}`;
const favIndex = favorites.findIndex(fav => `${fav.type}-${fav.id}` === uniqueId);
if (favIndex > -1) {
favorites.splice(favIndex, 1);
} else {
favorites.unshift(itemData);
}
await saveFavorites();
const currentPanel = shadowRoot.querySelector('.panel.active');
const currentQuery = searchInput.value.trim();
if (currentPanel.id === 'smart-search-results') {
if (currentQuery) {
performSearch(currentQuery, false);
} else {
renderHomeSubPage();
}
} else if (currentPanel.id === 'favorites-panel') {
renderFavoritesPanel();
}
}
function isFavorited(item) {
const uniqueId = `${item.media_type}-${item.id}`;
return favorites.some(fav => `${fav.type}-${fav.id}` === uniqueId);
}
function renderFavoritesPanel() {
favoritesPanel.innerHTML = '';
if (favorites.length === 0) {
favoritesPanel.innerHTML = `<div class="search-message">Sua lista de favoritos está vazia.</div>`;
return;
}
favoritesPanel.innerHTML = `<h3 class="section-title"><span>Meus Favoritos</span></h3>`;
favorites.forEach(item => renderItem(item, favoritesPanel));
}
function showTrailer(youtubeKey) {
const trailerIframe = shadowRoot.getElementById('trailer-iframe');
trailerIframe.src = `https://www.youtube.com/embed/${youtubeKey}?autoplay=1&rel=0`;
trailerModal.style.display = 'flex';
}
function hideTrailer() {
const trailerIframe = shadowRoot.getElementById('trailer-iframe');
trailerModal.style.display = 'none';
trailerIframe.src = '';
}
function apiRequest(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET", url: url,
onload: resp => {
try { resolve(JSON.parse(resp.responseText)); }
catch(e) { console.error("Falha na requisição à API para", url, e); reject(e); }
},
onerror: reject
});
});
}
function tmdbRequest(endpoint, params = '', lang = null) {
const langParam = lang ? `&language=${lang}` : '';
return apiRequest(`https://api.themoviedb.org/3${endpoint}?api_key=${tmdbApiKey}${langParam}${params}`);
}
// --- INICIALIZAÇÃO ---
loadFavorites();
})();