init: media-center v2

Rewrite of ESH-Media v1 with separated main/renderer/shared architecture
(vite-plugin-electron, React 18, react-router-dom). Includes NeDB storage,
electron-store config, proxy manager with FoxyProxy/uBlock extensions,
custom server-checked updater, NSIS installer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 23:49:43 +03:00
commit ecb5e7e49f
52 changed files with 11718 additions and 0 deletions

135
search-scripts/README.md Normal file
View File

@@ -0,0 +1,135 @@
# Поисковые Скрипты для Media Center
Эта папка содержит кастомные JavaScript скрипты для поиска фильмов и сериалов на различных сайтах.
## Как работают скрипты
Каждый скрипт выполняется в защищенном контексте Electron и получает доступ к следующим инструментам:
- **axios** - для HTTP запросов
- **cheerio** - для парсинга HTML (jQuery-подобный синтаксис)
- **proxyConfig** - настройки прокси (если включен для сайта)
## Структура скрипта
Каждый скрипт должен экспортировать функцию `search` со следующей сигнатурой:
```javascript
async function search(query, siteUrl, useProxy, axios, cheerio, proxyConfig) {
// Ваш код поиска
return [{name: "Название фильма", url: "https://..."}];
}
```
### Параметры:
- `query` (string) - поисковый запрос пользователя
- `siteUrl` (string) - базовый URL сайта (например, "https://kinogo.biz")
- `useProxy` (boolean) - нужно ли использовать прокси
- `axios` (object) - экземпляр axios для HTTP запросов
- `cheerio` (object) - библиотека для парсинга HTML
- `proxyConfig` (object) - настройки прокси `{host: string, port: number}`
### Возвращаемое значение:
Массив объектов с результатами поиска. Каждый объект должен содержать:
**Обязательные поля:**
- `name` (string) - название фильма/сериала
- `url` (string) - ссылка на страницу фильма
**Опциональные поля:**
- `image` (string) - URL постера
- `year` (string) - год выпуска
- `description` (string) - описание
- `rating` (string) - рейтинг
## Примеры
### Пример 1: JSON API
```javascript
async function search(query, siteUrl, useProxy, axios, cheerio, proxyConfig) {
const config = {
params: { q: query },
timeout: 15000
};
if (useProxy && proxyConfig) {
config.proxy = { host: proxyConfig.host, port: proxyConfig.port };
}
const response = await axios.get(`${siteUrl}/api/search`, config);
return response.data.results.map(item => ({
name: item.title,
url: item.link,
image: item.poster
}));
}
```
### Пример 2: HTML парсинг
```javascript
async function search(query, siteUrl, useProxy, axios, cheerio, proxyConfig) {
const config = {
params: { q: query },
timeout: 15000
};
if (useProxy && proxyConfig) {
config.proxy = { host: proxyConfig.host, port: proxyConfig.port };
}
const response = await axios.get(`${siteUrl}/search`, config);
const $ = cheerio.load(response.data);
const results = [];
$('.movie-card').each((i, el) => {
const $el = $(el);
results.push({
name: $el.find('.title').text().trim(),
url: siteUrl + $el.find('a').attr('href'),
image: $el.find('img').attr('src')
});
});
return results;
}
```
## Добавление нового скрипта
1. Создайте файл `sitename.js` в этой папке
2. Используйте `SCRIPT_TEMPLATE.js` как основу
3. Реализуйте функцию `search()`
4. Обновите конфигурацию сайта в настройках приложения
## Обновление скриптов
Скрипты можно обновлять с сервера через настройки приложения. При обновлении конфигурации новые скрипты автоматически загружаются.
## Расположение скриптов
Скрипты хранятся в двух местах:
1. **Встроенные скрипты**: `<app>/search-scripts/` (только для чтения)
2. **Пользовательские скрипты**: `<userData>/search-scripts/` (можно обновлять)
Пользовательские скрипты имеют приоритет над встроенными.
## Безопасность
- Скрипты выполняются в изолированном контексте
- Таймаут выполнения: 30 секунд
- Доступ только к axios и cheerio
- Нет доступа к файловой системе или другим модулям Node.js
## Отладка
Ошибки скриптов логируются в консоль Electron DevTools. Для отладки:
1. Откройте DevTools (F12)
2. Выполните поиск
3. Проверьте консоль на наличие ошибок

View File

@@ -0,0 +1,96 @@
/**
* Search script template for creating custom site search scripts
*
* Copy this file and modify the search() function to work with your target site.
*
* Available tools:
* - axios: For making HTTP requests
* - cheerio: For parsing HTML (jQuery-like syntax)
* - proxyConfig: Proxy settings if useProxy is true
*
* @param {string} query - Search query entered by user
* @param {string} siteUrl - Base URL of the website (e.g., "https://example.com")
* @param {boolean} useProxy - Whether to use proxy for this request
* @param {object} axios - Axios HTTP client
* @param {object} cheerio - Cheerio HTML parser
* @param {object} proxyConfig - Proxy configuration {host: string, port: number}
* @returns {Promise<Array>} Array of results in format: [{name: string, url: string, image?: string, year?: string, description?: string, rating?: string}]
*/
async function search(query, siteUrl, useProxy, axios, cheerio, proxyConfig) {
try {
// 1. Build the search URL
const searchUrl = `${siteUrl}/search?q=${encodeURIComponent(query)}`;
// 2. Configure the request
const config = {
timeout: 15000, // 15 seconds timeout
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
};
// 3. Add proxy if needed
if (useProxy && proxyConfig) {
config.proxy = {
host: proxyConfig.host,
port: proxyConfig.port
};
}
// 4. Make the HTTP request
const response = await axios.get(searchUrl, config);
// 5a. If the response is JSON:
// const data = response.data;
// const results = data.items.map(item => ({
// name: item.title,
// url: item.link,
// image: item.thumbnail,
// year: item.year,
// description: item.synopsis
// }));
// 5b. If the response is HTML:
const html = response.data;
const $ = cheerio.load(html);
const results = [];
// Parse HTML and extract movie data
$('.movie-card').each((index, element) => {
const $item = $(element);
const name = $item.find('.movie-title').text().trim();
const url = $item.find('a').attr('href');
const image = $item.find('img').attr('src');
const year = $item.find('.year').text().trim();
const description = $item.find('.description').text().trim();
// Only add if name and url exist
if (name && url) {
results.push({
name,
url: url.startsWith('http') ? url : siteUrl + url,
image: image ? (image.startsWith('http') ? image : siteUrl + image) : undefined,
year: year || undefined,
description: description || undefined
});
}
});
// 6. Return the results
return results;
} catch (error) {
console.error('Search error:', error.message);
return []; // Return empty array on error
}
}
// Important notes:
// 1. The function MUST be named 'search'
// 2. It MUST return a Promise that resolves to an array
// 3. Each result MUST have 'name' and 'url' fields
// 4. Other fields (image, year, description, rating) are optional
// 5. Handle errors gracefully and return [] on failure
// 6. Test with different queries to ensure reliability

87
search-scripts/hdrezka.js Normal file
View File

@@ -0,0 +1,87 @@
/**
* HDRezka.ag search script
*
* This script searches for movies/series on hdrezka.ag
*
* @param {string} query - Search query from user
* @param {string} siteUrl - Base URL of the site
* @param {boolean} useProxy - Whether to use proxy for requests
* @param {object} axios - Axios instance for HTTP requests
* @param {object} cheerio - Cheerio instance for HTML parsing
* @param {object} proxyConfig - Proxy configuration {host, port}
* @returns {Promise<Array>} Array of search results [{name, url, image?, year?, description?}]
*/
async function search(query, siteUrl, useProxy, axios, cheerio, proxyConfig) {
try {
const searchUrl = `${siteUrl}/search/`;
// Prepare request config
const config = {
params: {
do: 'search',
subaction: 'search',
q: query
},
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8'
}
};
// Add proxy if needed
if (useProxy && proxyConfig) {
config.proxy = {
host: proxyConfig.host,
port: proxyConfig.port
};
}
// Make request
const response = await axios.get(searchUrl, config);
const html = response.data;
// Parse HTML
const $ = cheerio.load(html);
const results = [];
// Find all movie/series cards
$('.b-content__inline_item').each((index, element) => {
const $item = $(element);
// Extract title and URL
const linkEl = $item.find('.b-content__inline_item-link a').first();
const name = linkEl.text().trim();
const url = linkEl.attr('href');
if (!name || !url) return;
// Extract image
const imageEl = $item.find('.b-content__inline_item-cover img').first();
const image = imageEl.attr('src') || imageEl.attr('data-src');
// Extract year
const infoText = $item.find('.info').text();
const yearMatch = infoText.match(/(\d{4})/);
const year = yearMatch ? yearMatch[1] : '';
// Extract rating if available
const ratingText = $item.find('.imdb').text().trim();
results.push({
name,
url: url.startsWith('http') ? url : siteUrl + url,
image: image ? (image.startsWith('http') ? image : siteUrl + image) : undefined,
year,
rating: ratingText || undefined
});
});
return results;
} catch (error) {
console.error('HDRezka search error:', error.message);
return [];
}
}

85
search-scripts/kinogo.js Normal file
View File

@@ -0,0 +1,85 @@
/**
* Kinogo.biz search script
*
* This script searches for movies/series on kinogo.biz
*
* @param {string} query - Search query from user
* @param {string} siteUrl - Base URL of the site
* @param {boolean} useProxy - Whether to use proxy for requests
* @param {object} axios - Axios instance for HTTP requests
* @param {object} cheerio - Cheerio instance for HTML parsing
* @param {object} proxyConfig - Proxy configuration {host, port}
* @returns {Promise<Array>} Array of search results [{name, url, image?, year?, description?}]
*/
async function search(query, siteUrl, useProxy, axios, cheerio, proxyConfig) {
try {
const searchUrl = `${siteUrl}/index.php`;
// Prepare request config
const config = {
params: {
do: 'search',
subaction: 'search',
story: query
},
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
};
// Add proxy if needed
if (useProxy && proxyConfig) {
config.proxy = {
host: proxyConfig.host,
port: proxyConfig.port
};
}
// Make request
const response = await axios.get(searchUrl, config);
const html = response.data;
// Parse HTML
const $ = cheerio.load(html);
const results = [];
// Find all movie cards
$('.shortstory').each((index, element) => {
const $item = $(element);
// Extract title
const titleEl = $item.find('.shortstory-title a, .title a, h2 a').first();
const name = titleEl.text().trim();
const url = titleEl.attr('href');
if (!name || !url) return;
// Extract image
const imageEl = $item.find('.shortstory-img img, .poster img, img').first();
const image = imageEl.attr('src') || imageEl.attr('data-src');
// Extract year
const yearText = $item.find('.year, .shortstory-year').text().trim();
const yearMatch = yearText.match(/(\d{4})/);
const year = yearMatch ? yearMatch[1] : '';
// Extract description
const description = $item.find('.shortstory-desc, .description').text().trim();
results.push({
name,
url: url.startsWith('http') ? url : siteUrl + url,
image: image ? (image.startsWith('http') ? image : siteUrl + image) : undefined,
year,
description: description || undefined
});
});
return results;
} catch (error) {
console.error('Kinogo search error:', error.message);
return [];
}
}

63
search-scripts/rutube.js Normal file
View File

@@ -0,0 +1,63 @@
/**
* Rutube.ru search script
*
* This script searches for videos on rutube.ru using their API
*
* @param {string} query - Search query from user
* @param {string} siteUrl - Base URL of the site
* @param {boolean} useProxy - Whether to use proxy for requests
* @param {object} axios - Axios instance for HTTP requests
* @param {object} cheerio - Cheerio instance for HTML parsing (not used)
* @param {object} proxyConfig - Proxy configuration {host, port}
* @returns {Promise<Array>} Array of search results [{name, url, image?, description?}]
*/
async function search(query, siteUrl, useProxy, axios, cheerio, proxyConfig) {
try {
const searchUrl = `${siteUrl}/api/search/video/`;
// Prepare request config
const config = {
params: {
query: query,
limit: 20
},
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
};
// Add proxy if needed (Rutube usually doesn't require proxy in Russia)
if (useProxy && proxyConfig) {
config.proxy = {
host: proxyConfig.host,
port: proxyConfig.port
};
}
// Make request
const response = await axios.get(searchUrl, config);
const data = response.data;
// Check if results exist
if (!data.results || !Array.isArray(data.results)) {
return [];
}
// Map results to our format
const results = data.results.map(item => {
return {
name: item.title || '',
url: item.video_url || (siteUrl + '/video/' + item.id),
image: item.thumbnail_url || undefined,
description: item.description || undefined
};
});
return results.filter(r => r.name && r.url);
} catch (error) {
console.error('Rutube search error:', error.message);
return [];
}
}