// ==UserScript==
// @name Trakt.tv Universal Search (Anime and Non-Anime)
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Search for anime on hianime.to and non-anime content on 1flix.to from Trakt.tv
// @author konvar
// @match https://trakt.tv/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect hianime.to
// @connect 1flix.to
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const HIANIME_BASE_URL = 'https://hianime.to';
const FLIX_BASE_URL = 'https://1flix.to';
const TOP_RESULTS = 10;
const SIMILARITY_THRESHOLD = 0.4;
const EPISODE_TITLE_SIMILARITY_THRESHOLD = 0.8;
const MAX_SEARCH_PAGES = 1;
GM_addStyle(`
.trakt-universal-search-button {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
/* Remove default button styles */
background: none;
border: none;
padding: 0;
cursor: pointer; /* Maintain pointer cursor */
}
.trakt-universal-search-button:hover {
/* Prevent white glow on hover */
box-shadow: none;
}
.trakt-universal-search-button img {
max-height: 30px;
width: auto;
}
`);
class ContentInfo {
constructor(title, year, isAnime, season, episode, episodeTitle, alternativeTitles, contentType, absoluteEpisode) {
this.title = title;
this.year = year;
this.isAnime = isAnime;
this.season = season;
this.episode = episode;
this.episodeTitle = episodeTitle;
this.alternativeTitles = alternativeTitles;
this.contentType = contentType;
this.absoluteEpisode = absoluteEpisode;
}
static fromDOM() {
let titleElement, yearElement;
if (window.location.pathname.startsWith('/movies/')) {
const movieTitleElement = document.querySelector('h1');
if (movieTitleElement) {
titleElement = movieTitleElement.childNodes[0];
yearElement = movieTitleElement.querySelector('.year');
}
} else {
titleElement = document.querySelector('h2 a[data-safe="true"]');
}
const episodeElement = document.querySelector('h1.episode .main-title-sxe');
const episodeTitleElement = document.querySelector('h1.episode .main-title');
const episodeAbsElement = document.querySelector('h1.episode .main-title-abs');
const genreElements = document.querySelectorAll('.genres .btn');
const additionalStats = document.querySelector('ul.additional-stats');
const alternativeTitleElement = document.querySelector('.additional-stats .meta-data[data-type="alternative_titles"]');
if (titleElement) {
const title = titleElement.textContent.trim().replace(/[:.,!?]+$/, '');
const episodeInfo = episodeElement ? episodeElement.textContent.trim().split('x') : null;
const season = episodeInfo ? parseInt(episodeInfo[0]) : null;
const episode = episodeInfo ? parseInt(episodeInfo[1]) : null;
const episodeTitle = episodeTitleElement ? episodeTitleElement.textContent.trim() : null;
const absoluteEpisode = episodeAbsElement ? parseInt(episodeAbsElement.textContent.trim().replace(/[\(\)]/g, '')) : null;
const genres = Array.from(genreElements).map(el => el.textContent.trim().toLowerCase());
const isAnime = genres.includes('anime') ||
(additionalStats && additionalStats.textContent.toLowerCase().includes('anime')) ||
document.querySelector('.poster img[src*="anime"]') !== null;
let year;
if (yearElement) {
year = yearElement.textContent.trim();
} else if (additionalStats) {
const yearMatch = additionalStats.textContent.match(/(\d{4})/);
year = yearMatch ? yearMatch[1] : null;
}
const alternativeTitles = alternativeTitleElement
? alternativeTitleElement.textContent.split(',').map(t => t.trim())
: [];
const contentType = window.location.pathname.startsWith('/movies/') ? 'movie' : 'tv';
return new ContentInfo(title, year, isAnime, season, episode, episodeTitle, alternativeTitles, contentType, absoluteEpisode);
}
return null;
}
}
class SearchButton {
constructor(contentInfo) {
this.contentInfo = contentInfo;
this.button = this.createButton();
}
createButton() {
const button = document.createElement('button'); // Changed from <a> to <button>
button.className = 'btn btn-block btn-summary trakt-universal-search-button'; // Updated class name
button.style.display = 'none';
const icon = document.createElement('img');
icon.style.width = 'auto';
icon.style.height = '50px';
if (this.contentInfo.isAnime) {
icon.src = `${HIANIME_BASE_URL}/images/logo.png`;
icon.alt = 'Hianime Logo';
} else {
icon.src = 'https://img.1flix.to/xxrz/400x400/100/e4/ca/e4ca1fc10cda9cf762f7b51876dc917b/e4ca1fc10cda9cf762f7b51876dc917b.png';
icon.alt = '1flix Logo';
}
button.appendChild(icon);
return button;
}
addToDOM() {
const container = document.querySelector('.col-lg-4.col-md-5.action-buttons');
if (container && !document.querySelector('.trakt-universal-search-button')) { // Updated class name
container.insertBefore(this.button, container.firstChild);
return true;
}
return false;
}
updateWithContentLink(url) {
this.button.addEventListener('click', () => {
window.open(url, '_blank');
});
this.button.style.display = 'flex';
}
updateButtonText(text) {
const textNode = document.createTextNode(` ${text}`);
this.button.appendChild(textNode);
}
}
class ContentSearcher {
constructor(contentInfo) {
this.contentInfo = contentInfo;
}
generateSearchUrl() {
if (this.contentInfo.isAnime) {
if (this.contentInfo.contentType === 'movie') {
return `${HIANIME_BASE_URL}/search?keyword=${encodeURIComponent(this.contentInfo.title)}&type=1`;
} else {
return `${HIANIME_BASE_URL}/search?keyword=${encodeURIComponent(this.contentInfo.title)}&type=2`;
}
} else {
const searchTerm = this.contentInfo.contentType === 'movie' ?
`${this.contentInfo.title} ${this.contentInfo.year}` :
this.contentInfo.title;
return `${FLIX_BASE_URL}/search/${searchTerm.replace(/\s+/g, '-')}`;
}
}
async search() {
let page = 1;
let allMatches = [];
const searchUrl = this.generateSearchUrl();
while (page <= MAX_SEARCH_PAGES) {
const pageUrl = `${searchUrl}${this.contentInfo.isAnime ? '&' : '?'}page=${page}`;
try {
const response = await this.makeRequest(pageUrl);
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
const pageMatches = this.findTopMatches(doc);
allMatches = allMatches.concat(pageMatches);
if (pageMatches.length === 0) break;
page++;
} catch (error) {
break;
}
}
for (const match of allMatches.slice(0, TOP_RESULTS)) {
const contentUrl = await this.findContentUrl(match.url);
if (contentUrl) {
return contentUrl;
}
}
this.showMessage(`Content not found. Click the button to search manually.`);
return searchUrl;
}
findTopMatches(doc) {
const contentItems = doc.querySelectorAll('.flw-item');
const allTitles = [this.contentInfo.title, ...this.contentInfo.alternativeTitles];
const matches = Array.from(contentItems).map(item => {
const titleElement = item.querySelector('.film-name a');
const posterElement = item.querySelector('.film-poster-img');
const infoElement = item.querySelector('.fd-infor');
if (titleElement && infoElement) {
const itemTitle = titleElement.textContent.trim();
const normalizedItemTitle = this.normalizeTitle(itemTitle);
const bestScore = Math.max(...allTitles.map(title =>
this.calculateMatchScore(this.normalizeTitle(title), normalizedItemTitle)
));
const href = titleElement.getAttribute('href');
const url = `${this.contentInfo.isAnime ? HIANIME_BASE_URL : FLIX_BASE_URL}${href}`;
let itemType, year, duration;
const itemTypeElement = infoElement.querySelector('.fdi-item');
const itemTypeText = itemTypeElement ? itemTypeElement.textContent.trim().toLowerCase() : '';
const seasonMatch = itemTypeText.match(/^ss (\d+)$/);
if (seasonMatch) {
itemType = 'tv';
} else {
const yearRegex = /^\d{4}$/;
if (yearRegex.test(itemTypeText)) {
year = itemTypeText;
itemType = 'movie';
} else {
itemType = itemTypeText;
year = null;
}
}
const durationElement = infoElement.querySelector('.fdi-duration');
duration = durationElement ? durationElement.textContent.trim() : null;
const posterUrl = posterElement ? posterElement.getAttribute('data-src') : null;
const isCorrectType = (
(this.contentInfo.contentType === 'movie' && itemType === 'movie') ||
(this.contentInfo.contentType === 'tv' && itemType === 'tv')
);
return {
title: itemTitle,
score: bestScore,
url: url,
type: itemType,
year: year,
duration: duration,
posterUrl: posterUrl,
isCorrectType: isCorrectType
};
}
return null;
}).filter(match => match !== null && match.score >= SIMILARITY_THRESHOLD && match.isCorrectType)
.sort((a, b) => b.score - a.score);
return matches;
}
async findContentUrl(contentUrl) {
try {
const response = await this.makeRequest(contentUrl);
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
if (this.contentInfo.isAnime && this.contentInfo.contentType === 'movie') {
const syncDataScript = doc.querySelector('#syncData');
if (syncDataScript) {
const syncData = JSON.parse(syncDataScript.textContent);
const seriesUrl = syncData.series_url;
if (seriesUrl) {
const movieId = seriesUrl.split('-').pop();
const watchUrl = `${HIANIME_BASE_URL}/watch/${seriesUrl.slice(seriesUrl.lastIndexOf('/') + 1)}?ep=${movieId}`;
return watchUrl;
}
}
} else if (this.contentInfo.isAnime) {
const movieId = contentUrl.split('-').pop();
const apiUrl = `${HIANIME_BASE_URL}/ajax/v2/episode/list/${movieId}`;
const episodeDataResponse = await this.makeRequest(apiUrl);
const episodeData = JSON.parse(episodeDataResponse.responseText);
if (episodeData.status && episodeData.html) {
const episodeDoc = parser.parseFromString(episodeData.html, 'text/html');
const episodeLinks = episodeDoc.querySelectorAll('.ssl-item.ep-item');
const normalizedSearchTitle = this.normalizeTitle(this.contentInfo.episodeTitle);
let bestMatch = null;
let bestMatchScore = 0;
for (let i = 0; i < episodeLinks.length; i++) {
const link = episodeLinks[i];
const episodeNumber = parseInt(link.getAttribute('data-number'));
const episodeTitle = link.querySelector('.ep-name')?.textContent.trim() || '';
const normalizedEpisodeTitle = this.normalizeTitle(episodeTitle);
const titleMatchScore = this.calculateMatchScore(normalizedSearchTitle, normalizedEpisodeTitle);
if (episodeNumber === this.contentInfo.episode || episodeNumber === this.contentInfo.absoluteEpisode) {
if (titleMatchScore >= 0.3) {
return `${HIANIME_BASE_URL}${link.getAttribute('href')}`;
}
}
if (titleMatchScore >= EPISODE_TITLE_SIMILARITY_THRESHOLD && (episodeNumber === this.contentInfo.episode || episodeNumber === this.contentInfo.absoluteEpisode)) {
return `${HIANIME_BASE_URL}${link.getAttribute('href')}`;
}
if (titleMatchScore > bestMatchScore) {
bestMatch = link;
bestMatchScore = titleMatchScore;
}
}
if (bestMatch && bestMatchScore >= EPISODE_TITLE_SIMILARITY_THRESHOLD) {
return `${HIANIME_BASE_URL}${bestMatch.getAttribute('href')}`;
}
}
} else {
const detailPageWatch = doc.querySelector('.detail_page-watch');
if (!detailPageWatch) {
return null;
}
const movieId = detailPageWatch.getAttribute('data-id');
const movieType = detailPageWatch.getAttribute('data-type');
if (!movieId || !movieType) {
return null;
}
if (this.contentInfo.contentType === 'movie') {
const episodeListUrl = `${FLIX_BASE_URL}/ajax/episode/list/${movieId}`;
const episodeListResponse = await this.makeRequest(episodeListUrl);
const episodeListContent = episodeListResponse.responseText;
const episodeListDoc = parser.parseFromString(episodeListContent, 'text/html');
const serverItem = episodeListDoc.querySelector('.link-item');
if (serverItem) {
const serverId = serverItem.getAttribute('data-linkid');
const watchUrl = contentUrl.replace(/\/movie\//, '/watch-movie/') + `.${serverId}`;
return watchUrl;
}
} else {
const seasonListUrl = `${FLIX_BASE_URL}/ajax/season/list/${movieId}`;
const seasonListResponse = await this.makeRequest(seasonListUrl);
const seasonListContent = seasonListResponse.responseText;
const seasonListDoc = parser.parseFromString(seasonListContent, 'text/html');
const seasonItems = seasonListDoc.querySelectorAll('.ss-item');
for (let seasonItem of seasonItems) {
const seasonNumber = parseInt(seasonItem.textContent.trim().split(' ')[1]);
const seasonId = seasonItem.getAttribute('data-id');
if (seasonNumber === this.contentInfo.season) {
const episodeListUrl = `${FLIX_BASE_URL}/ajax/season/episodes/${seasonId}`;
const episodeListResponse = await this.makeRequest(episodeListUrl);
const episodeListContent = episodeListResponse.responseText;
const episodeListDoc = parser.parseFromString(episodeListContent, 'text/html');
const episodeItems = episodeListDoc.querySelectorAll('.eps-item');
for (let episodeItem of episodeItems) {
const episodeNumber = parseInt(episodeItem.getAttribute('title').split(':')[0].replace('Eps', '').trim());
const episodeTitle = episodeItem.getAttribute('title').split(':')[1].trim();
if (episodeNumber === this.contentInfo.episode) {
const episodeId = episodeItem.getAttribute('data-id');
const serverListUrl = `${FLIX_BASE_URL}/ajax/episode/servers/${episodeId}`;
const serverListResponse = await this.makeRequest(serverListUrl);
const serverListContent = serverListResponse.responseText;
const serverListDoc = parser.parseFromString(serverListContent, 'text/html');
const serverItem = serverListDoc.querySelector('.link-item');
if (serverItem) {
const serverId = serverItem.getAttribute('data-id');
const watchUrl = contentUrl.replace(/\/tv\//, '/watch-tv/') + `.${serverId}`;
return watchUrl;
}
}
}
}
}
}
}
} catch (error) {
}
return null;
}
normalizeTitle(title) {
return title.toLowerCase()
.replace(/[:.,!?'`]+/g, '')
.replace(/\s+/g, ' ')
.replace(/[^\w\s]/g, '')
.trim();
}
calculateMatchScore(searchTitle, itemTitle) {
const words1 = searchTitle.split(' ');
const words2 = itemTitle.split(' ');
const commonWords = words1.filter(word => words2.includes(word));
return commonWords.length / Math.max(words1.length, words2.length);
}
makeRequest(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
},
onload: function(response) {
if (response.status === 200) {
resolve(response);
} else {
reject(new Error(`Failed to fetch content: ${response.status}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
}
showMessage(message) {
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
messageDiv.style.cssText = "position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background-color: #f8d7da; color: #721c24; padding: 10px; border-radius: 5px; z-index: 9999;";
document.body.appendChild(messageDiv);
setTimeout(() => messageDiv.remove(), 5000);
}
}
class TraktTvHandler {
constructor() {
this.isInitialized = false;
}
async init() {
if (this.isInitialized) {
return;
}
const contentInfo = ContentInfo.fromDOM();
if (contentInfo) {
const searchButton = new SearchButton(contentInfo);
if (searchButton.addToDOM()) {
this.isInitialized = true;
const contentSearcher = new ContentSearcher(contentInfo);
const result = await contentSearcher.search();
if (result) {
searchButton.updateWithContentLink(result);
if (result === contentSearcher.generateSearchUrl()) {
searchButton.updateButtonText("Search Manually");
}
}
} else {
setTimeout(() => this.init(), 1000);
}
} else {
setTimeout(() => this.init(), 1000);
}
}
setupObserver() {
const observer = new MutationObserver((mutations) => {
if (!this.isInitialized) {
for (let mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
this.init();
break;
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
if (window.location.hostname === 'trakt.tv') {
if (window.location.pathname.startsWith('/shows/') || window.location.pathname.startsWith('/movies/')) {
setTimeout(() => {
const traktHandler = new TraktTvHandler();
traktHandler.init();
traktHandler.setupObserver();
}, 1000);
}
}
})();