Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TelegramBot.Controllers
|
||||
{
|
||||
public class TgNotifyController : BaseController
|
||||
{
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[Route("tg-notify.js")]
|
||||
public ActionResult TgNotifyPlugin()
|
||||
{
|
||||
// Ищем плагин в возможных расположениях модуля (NextGen: module/ или mods/)
|
||||
string file = null;
|
||||
foreach (var p in new[]
|
||||
{
|
||||
"module/Tg-notify.bot/tg-notify.js",
|
||||
"module/Community/Tg-notify.bot/tg-notify.js",
|
||||
"mods/TelegramBot/tg-notify.js",
|
||||
"module/TelegramBot/tg-notify.js",
|
||||
"plugins/tg-notify.js",
|
||||
"wwwroot/plugins/tg-notify.js"
|
||||
})
|
||||
{
|
||||
if (System.IO.File.Exists(p)) { file = p; break; }
|
||||
}
|
||||
|
||||
if (file == null)
|
||||
return Content(string.Empty, "application/javascript; charset=utf-8");
|
||||
|
||||
var js = System.IO.File.ReadAllText(file);
|
||||
js = js.Replace("{localhost}", host);
|
||||
return Content(js, "application/javascript; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api/tg/subscribe")]
|
||||
public async Task<ActionResult> Subscribe()
|
||||
{
|
||||
if (!ModInit.IsRunning)
|
||||
return Json(new { success = false, msg = "bot_not_running" });
|
||||
|
||||
var uid = ModInit.ResolveUid(requestInfo.user_uid, Request.Query["uid"].ToString());
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
return Json(new { success = false, msg = "no_uid" });
|
||||
|
||||
using var reader = new StreamReader(HttpContext.Request.Body);
|
||||
var json = JObject.Parse(await reader.ReadToEndAsync());
|
||||
|
||||
var result = await ModInit.SubscribeApi(
|
||||
uid,
|
||||
json.Value<int>("tmdb_id"),
|
||||
json.Value<string>("title") ?? "",
|
||||
json.Value<string>("voice") ?? "",
|
||||
json.Value<int>("season"),
|
||||
json.Value<int>("episode"),
|
||||
json.Value<string>("mirage_orid") ?? "",
|
||||
json.Value<int>("mirage_voice_id"),
|
||||
json.Value<int>("voice_episode"),
|
||||
json.Value<string>("collaps_orid") ?? "",
|
||||
json.Value<string>("voice_source") ?? "",
|
||||
json.Value<int>("kp_id")
|
||||
);
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api/tg/unsubscribe")]
|
||||
public async Task<ActionResult> Unsubscribe()
|
||||
{
|
||||
if (!ModInit.IsRunning)
|
||||
return Json(new { success = false, msg = "bot_not_running" });
|
||||
|
||||
var uid = ModInit.ResolveUid(requestInfo.user_uid, Request.Query["uid"].ToString());
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
return Json(new { success = false, msg = "no_uid" });
|
||||
|
||||
using var reader = new StreamReader(HttpContext.Request.Body);
|
||||
var json = JObject.Parse(await reader.ReadToEndAsync());
|
||||
|
||||
return Json(ModInit.UnsubscribeApi(uid, json.Value<int>("tmdb_id"), json.Value<string>("voice")));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api/tg/status")]
|
||||
public ActionResult Status(int tmdb_id)
|
||||
{
|
||||
var uid = ModInit.ResolveUid(requestInfo.user_uid, Request.Query["uid"].ToString());
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
return Json(new { success = false, msg = "no_uid" });
|
||||
|
||||
return Json(ModInit.StatusApi(uid, tmdb_id));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api/tg/voices")]
|
||||
public async Task<ActionResult> Voices(string title, int year, int season, int tmdb_id)
|
||||
{
|
||||
return Json(await ModInit.GetVoicesApi(title ?? "", year, season, tmdb_id));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api/tg/link")]
|
||||
public async Task<ActionResult> GetLink()
|
||||
{
|
||||
var uid = requestInfo.user_uid;
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
return Json(new { success = false, msg = "no_uid" });
|
||||
|
||||
return Json(await ModInit.LinkApi(uid));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Список всех подписок пользователя с метаданными (для страницы подписок в Lampa)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Route("/api/tg/subscriptions")]
|
||||
public ActionResult Subscriptions()
|
||||
{
|
||||
var uid = ModInit.ResolveUid(requestInfo.user_uid, Request.Query["uid"].ToString());
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
return Json(new { success = false, msg = "no_uid" });
|
||||
|
||||
return Json(ModInit.SubscriptionsApi(uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
using Shared.Models.Module;
|
||||
|
||||
namespace TelegramBot.Models
|
||||
{
|
||||
public class TelegramBotConf : ModuleBaseConf
|
||||
{
|
||||
public bool enable { get; set; } = true;
|
||||
|
||||
public string bot_token { get; set; } = "YOUR_BOT_TOKEN";
|
||||
|
||||
public string tmdb_api_key { get; set; } = "";
|
||||
|
||||
public string trakt_client_id { get; set; } = "";
|
||||
|
||||
public string lampac_host { get; set; } = "http://127.0.0.1:9118";
|
||||
|
||||
public string lampac_token { get; set; } = "";
|
||||
|
||||
// Ключ kinopoiskapiunofficial.tech — резолвинг tmdb→kinopoisk_id по названию для источника VideoHub.
|
||||
// Пусто → kp_id резолвится только через мост Lampac /externalids (покрытие ~40%).
|
||||
public string kp_api_key { get; set; } = "";
|
||||
|
||||
public int check_interval_minutes { get; set; } = 60;
|
||||
|
||||
public string tmdb_lang { get; set; } = "ru-RU";
|
||||
|
||||
// Каталог для users.json / subscriptions.json. Пусто → database/tgnotify
|
||||
public string data_dir { get; set; } = "database/tgnotify";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
# Tg-notify.bot
|
||||
|
||||
Фоновый **Telegram-бот уведомлений о новых сериях и озвучках** (long polling). Пользователь подписывается на сериал прямо из карточки Lampa, а бот сам отслеживает выход новых эпизодов и появление выбранной озвучки и шлёт уведомление в Telegram с обложкой, номером серии и описанием из TMDB.
|
||||
|
||||
Проект: **`Modules/Tg-notify.bot/`**. C#-namespace модуля — `TelegramBot`.
|
||||
|
||||
Бот объединяет три подсистемы:
|
||||
|
||||
1. **Трекер новых серий** — Trakt API (приоритет) с фоллбэком на TMDB.
|
||||
2. **Трекер озвучек** — параллельный опрос балансеров **Mirage**, **Collaps** и **VideoHub** (поиск по названию или kinopoisk_id, мультиозвучка, чистый JSON). Берётся максимальный эпизод из всех источников — кто первым выложил серию в нужной озвучке, от того и уведомление.
|
||||
3. **Клиентский плагин Lampa** (`tg-notify.js`) — вкладка «TG Подписки», перехват кнопки 🔔 в карточке, меню выбора озвучки.
|
||||
|
||||
---
|
||||
|
||||
## Включение
|
||||
|
||||
1. В шаблоне [`config/base.conf`](../../../config/base.conf) при необходимости проверьте, что **`TelegramBot`** не попал в **`BaseModule.SkipModules`** (если попал — уберите имя из списка, иначе модуль не загрузится).
|
||||
2. [`manifest.json`](manifest.json): выставьте **`"enable": true`** (в репозитории по умолчанию `false`).
|
||||
3. `init.conf`: добавьте секцию **`TelegramBot`**. Пример — [`init.merge.example.json`](init.merge.example.json).
|
||||
4. Перезапустите сервис: `systemctl restart lampac` (или имя вашего systemd-юнита).
|
||||
|
||||
При `enable: true` и пустом `bot_token` long polling **не стартует** — в лог пишется предупреждение.
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация (секция `init.conf`)
|
||||
|
||||
```json
|
||||
"TelegramBot": {
|
||||
"enable": true,
|
||||
"bot_token": "ТОКЕН_ОТ_BOTFATHER",
|
||||
"tmdb_api_key": "ВАШ_КЛЮЧ_TMDB",
|
||||
"trakt_client_id": "",
|
||||
"lampac_host": "http://127.0.0.1:9118",
|
||||
"lampac_token": "",
|
||||
"kp_api_key": "",
|
||||
"check_interval_minutes": 60,
|
||||
"tmdb_lang": "ru-RU",
|
||||
"data_dir": "database/tgnotify"
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Назначение |
|
||||
|------|-----------|
|
||||
| `bot_token` | Токен бота от [@BotFather](https://t.me/BotFather). **Обязателен.** |
|
||||
| `tmdb_api_key` | Ключ TMDB v3 — обложки, описания эпизодов, определение актуального сезона. |
|
||||
| `trakt_client_id` | Client ID приложения Trakt. Если задан — основной источник новых серий; если пуст — используется только TMDB. |
|
||||
| `lampac_host` | Базовый URL самого Lampac для запросов к балансерам (`/lite/mirage`, `/lite/collaps`). Обычно локальный: `http://127.0.0.1:9118`. |
|
||||
| `lampac_token` | Токен **accsdb**. Обязателен, если у вас включена защита `accsdb` — без него балансеры вернут `{"accsdb":true,"msg":"Войдите в аккаунт"}`. |
|
||||
| `kp_api_key` | Ключ [kinopoiskapiunofficial.tech](https://kinopoiskapiunofficial.tech/) — резолвинг `tmdb_id → kinopoisk_id` по названию для источника **VideoHub**. Опционально: если пусто, `kinopoisk_id` берётся только из встроенного моста Lampac `/externalids` (покрытие ~40%); с ключом — фоллбэк через поиск по названию поднимает покрытие почти до полного. |
|
||||
| `check_interval_minutes` | Период фоновой проверки подписок. По умолчанию 60. Первый прогон — через 2 минуты после старта. |
|
||||
| `tmdb_lang` | Язык метаданных TMDB. По умолчанию `ru-RU` (с фоллбэком на `en-US` при пустом описании). |
|
||||
| `data_dir` | Каталог хранения `users.json` / `subscriptions.json`. По умолчанию `database/tgnotify`. |
|
||||
|
||||
---
|
||||
|
||||
## Зависимости
|
||||
|
||||
- **`Telegram.Bot` 22.4.4** — кладётся в [`references/`](references/) (для рантайм-компиляции Roslyn) и подтягивается через `PackageReference` при локальной сборке.
|
||||
- **TMDB API** — ключ v3 (`tmdb_api_key`).
|
||||
- **Trakt API** (опционально) — `trakt_client_id` для более точного и быстрого трекинга выхода серий.
|
||||
- Балансеры **Mirage**, **Collaps** и **VideoHub** (`/lite/cdnvideohub`) должны быть доступны по `lampac_host`.
|
||||
- **kinopoiskapiunofficial.tech** (опционально) — `kp_api_key`. Нужен только для VideoHub: резолвит `kinopoisk_id` по названию, когда встроенный мост Lampac его не знает.
|
||||
|
||||
---
|
||||
|
||||
## Структура модуля
|
||||
|
||||
```
|
||||
Tg-notify.bot/
|
||||
├── manifest.json # dynamic:true, references+tree
|
||||
├── Tg-notify.bot.csproj # только для локальной сборки; на сервере не используется
|
||||
├── init.merge.example.json # пример секции init.conf
|
||||
├── ModInit.cs # ядро: polling, трекинг серий, парсеры озвучек, HTTP API
|
||||
├── Models/
|
||||
│ └── TelegramBotConf.cs # конфиг (наследует ModuleBaseConf)
|
||||
├── Controllers/
|
||||
│ └── TgNotifyController.cs # HTTP API + раздача tg-notify.js
|
||||
├── references/
|
||||
│ └── Telegram.Bot.dll # 22.4.4
|
||||
└── tg-notify.js # клиентский плагин Lampa
|
||||
```
|
||||
|
||||
> На сервере модуль компилируется рантайм-движком **Roslyn** по `manifest.json` (`dynamic: true`) — `.csproj` нужен только для локальной разработки. Поля `references` и `tree` обязательны.
|
||||
|
||||
---
|
||||
|
||||
## Как работает
|
||||
|
||||
### Привязка аккаунта
|
||||
|
||||
UID пользователя определяется так же, как для закладок и тайм-кодов: плагин передаёт `token`, `account_email` и `uid` устройства, сервер берёт первое непустое (`RequestInfo.getuid`). Вошёл в аккаунт CUB — подписки общие для всех его устройств.
|
||||
|
||||
Из вкладки «TG Подписки» в Lampa пользователь получает deep-link на бота. Telegram пропускает в параметре только `A-Za-z0-9_-` (до 64 символов), поэтому UID вида email уходит как `link64_{base64url(uid)}`, остальные — как раньше, `link_{uid}`. Бот сохраняет связку `chat_id ↔ lampac_uid` (`users.json`), после чего подписки этого UID становятся «его». Привязка, сделанная раньше по `uid` устройства, продолжает работать: если под основным UID привязки нет, API ищет её по `uid` устройства.
|
||||
|
||||
### Подписка
|
||||
|
||||
В карточке сериала плагин перехватывает кнопку 🔔 и показывает меню озвучек (`/api/tg/voices` → Mirage + Collaps + VideoHub). Выбор отправляется в `/api/tg/subscribe` и сохраняется в `subscriptions.json`.
|
||||
|
||||
### Фоновый цикл `CheckAll`
|
||||
|
||||
Раз в `check_interval_minutes` (и вручную командой `/check`) по каждой подписке:
|
||||
|
||||
1. **Новые серии** — `CheckNewEpisodes` (Trakt → TMDB). Если у подписки не выбрана конкретная озвучка — уведомление шлётся о самом факте выхода серии.
|
||||
2. **Озвучки** (если озвучка выбрана) — **параллельно** (`Task.WhenAll`) опрашиваются Mirage, Collaps и VideoHub; берётся максимальный доступный эпизод; кто первый нашёл новую серию — от того уведомление. На каждую новую серию шлётся отдельное сообщение с инфой из TMDB.
|
||||
|
||||
> **VideoHub и kinopoisk_id.** VideoHub ищет контент только по `kinopoisk_id`, тогда как бот работает по `tmdb_id`. Резолвинг выполняется лениво и кешируется в самой подписке: сначала через встроенный мост Lampac `/externalids`, при неудаче — поиском по названию через `kp_api_key`. Имя озвучки в запросе URL-кодируется (кириллица иначе даёт HTTP 400). Если `kinopoisk_id` не нашёлся, VideoHub для этой подписки молча пропускается — Mirage и Collaps продолжают работать.
|
||||
|
||||
Формат уведомления:
|
||||
|
||||
```
|
||||
🎬 Название
|
||||
🎙 Озвучка
|
||||
📺 S05E08 — Название эпизода
|
||||
|
||||
Описание (до 300 символов)
|
||||
```
|
||||
|
||||
С обложкой кадра (`SendPhoto`), если у эпизода есть `still_path`.
|
||||
|
||||
---
|
||||
|
||||
## Команды бота
|
||||
|
||||
| Команда / кнопка | Действие |
|
||||
|------------------|----------|
|
||||
| `/start` | Приветствие; `/start link_{uid}` или `/start link64_{base64url(uid)}` — привязка аккаунта |
|
||||
| `/list` · «📋 Подписки» | Список активных подписок |
|
||||
| `/check` · «🔍 Проверить» | Принудительная проверка (в фоне) |
|
||||
| `/help` · «📖 Помощь» | Справка |
|
||||
| `/unlink` · «🔓 Отвязать» | Отвязать аккаунт |
|
||||
|
||||
---
|
||||
|
||||
## HTTP API (`TgNotifyController : BaseController`)
|
||||
|
||||
| Метод | Маршрут | Назначение |
|
||||
|-------|---------|-----------|
|
||||
| `GET` | `/tg-notify.js` | Раздаёт клиентский плагин с автозаменой `{localhost}` → реальный хост. `[AllowAnonymous]` |
|
||||
| `POST` | `/api/tg/subscribe` | Подписка на сериал/озвучку |
|
||||
| `POST` | `/api/tg/unsubscribe` | Отписка |
|
||||
| `GET` | `/api/tg/status?tmdb_id=` | Статус подписки текущего пользователя |
|
||||
| `GET` | `/api/tg/voices?title=&year=&season=&tmdb_id=` | Список доступных озвучек |
|
||||
| `GET` | `/api/tg/link` | Ссылка привязки бота |
|
||||
| `GET` | `/api/tg/subscriptions` | Все подписки пользователя с метаданными |
|
||||
|
||||
Все маршруты, кроме `/tg-notify.js` и `/api/tg/voices`, требуют `requestInfo.user_uid`.
|
||||
|
||||
---
|
||||
|
||||
## Клиентский плагин (`tg-notify.js`)
|
||||
|
||||
Подключается через `customPlugins` в `init.conf` либо раздаётся самим модулем по `/tg-notify.js`:
|
||||
|
||||
```json
|
||||
"LampaWeb": {
|
||||
"customPlugins": [
|
||||
{ "url": "{localhost}/tg-notify.js", "status": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Возможности: вкладка «TG Подписки» (сетка карточек с бейджем `S5 E4`), перехват кнопки 🔔 в полной карточке, меню выбора озвучки (`Lampa.Select.show`, метки `Mirage [—]` / `Collaps [C]` / `VideoHub [V]`), загрузка метаданных через `Lampa.TMDB` / `Lampa.Network.silent` с кешем. Хост подставляется через `{localhost}` — хардкода адресов нет.
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
<RootNamespace>TelegramBot</RootNamespace>
|
||||
<AssemblyName>TelegramBot</AssemblyName>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Telegram.Bot" Version="22.4.4" />
|
||||
<ProjectReference Include="..\..\Shared\Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Кладёт Telegram.Bot.dll в references/ для рантайм-компиляции Roslyn на сервере -->
|
||||
<Target Name="CopyTelegramBotToModReferences" AfterTargets="ResolveReferences">
|
||||
<ItemGroup>
|
||||
<_TelegramBotRef Include="@(ReferencePath)" Condition="'%(Filename)' == 'Telegram.Bot'" />
|
||||
</ItemGroup>
|
||||
<MakeDir Directories="$(MSBuildProjectDirectory)\references" />
|
||||
<Copy SourceFiles="@(_TelegramBotRef)" DestinationFolder="$(MSBuildProjectDirectory)\references"
|
||||
SkipUnchangedFiles="true" Condition="@(_TelegramBotRef->Count()) != 0" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"TelegramBot": {
|
||||
"enable": true,
|
||||
"bot_token": "ВАШ_ТОКЕН_ОТ_BOTFATHER",
|
||||
"tmdb_api_key": "ВАШ_КЛЮЧ_TMDB",
|
||||
"trakt_client_id": "",
|
||||
"lampac_host": "http://127.0.0.1:9118",
|
||||
"lampac_token": "",
|
||||
"check_interval_minutes": 60,
|
||||
"tmdb_lang": "ru-RU",
|
||||
"data_dir": "database/tgnotify"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"enable": true,
|
||||
"dynamic": true,
|
||||
"references": [
|
||||
"references/"
|
||||
],
|
||||
"tree": [
|
||||
"Models",
|
||||
"Controllers",
|
||||
"ModInit.cs"
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,407 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var lampac_host = '{localhost}';
|
||||
var network = new Lampa.Reguest();
|
||||
|
||||
// Пользователя выбирает сервер, как в account() остальных плагинов Lampac: token,
|
||||
// account_email и uid передаются отдельными параметрами, RequestInfo.getuid берёт первое
|
||||
// непустое. Иначе подписки живут под номером устройства, а закладки и тайм-коды того же
|
||||
// человека — под его аккаунтом.
|
||||
function apiUrl(path) {
|
||||
var url = lampac_host + path;
|
||||
var ids = {
|
||||
token: Lampa.Storage.get('token', ''),
|
||||
account_email: Lampa.Storage.get('account_email', ''),
|
||||
uid: Lampa.Storage.get('lampac_unic_id', '')
|
||||
};
|
||||
for (var name in ids) {
|
||||
if (ids[name]) url = Lampa.Utils.addUrlComponent(url, name + '=' + encodeURIComponent(ids[name]));
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
||||
|
||||
var TMDB_KEY = '';
|
||||
|
||||
// TMDB image/api с поддержкой прокси — используем встроенные методы Lampa
|
||||
function tmdbImage(url) {
|
||||
try { return Lampa.TMDB.image(url); } catch(e) {}
|
||||
return 'https://image.tmdb.org/' + url;
|
||||
}
|
||||
|
||||
function tmdbApi(path) {
|
||||
try { return Lampa.TMDB.api(path + '&api_key=' + (Lampa.TMDB.key ? Lampa.TMDB.key() : TMDB_KEY)); } catch(e) {}
|
||||
return 'https://api.themoviedb.org/3/' + path + '&api_key=' + TMDB_KEY;
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Компонент подписок — построен по образцу
|
||||
// src/interaction/items/category/base.js
|
||||
// =============================================
|
||||
function TgSubscribesComponent(object) {
|
||||
var scroll = new Lampa.Scroll({ mask: true, over: true, step: 250, end_ratio: 2 });
|
||||
var items = [];
|
||||
var html = document.createElement('div');
|
||||
var body = $(document.createElement('div')); // jQuery объект, как в Lampa
|
||||
var last;
|
||||
var active = 0;
|
||||
|
||||
this.create = function () {
|
||||
var self = this;
|
||||
|
||||
// Как в base.js: сначала scroll.minus(), потом append body
|
||||
scroll.minus();
|
||||
scroll.append(body);
|
||||
html.appendChild(scroll.render(true));
|
||||
|
||||
// Layer.visible при скролле — для ленивой загрузки изображений
|
||||
scroll.onScroll = function () {
|
||||
Lampa.Layer.visible(scroll.render(true));
|
||||
};
|
||||
|
||||
self.activity.loader(true);
|
||||
|
||||
var net = new Lampa.Reguest();
|
||||
net.timeout(15000);
|
||||
|
||||
net['native'](apiUrl('/api/tg/subscriptions'), function (result) {
|
||||
if (!result.linked || !result.results || !result.results.length) {
|
||||
self.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
var subs = result.results;
|
||||
var lang = Lampa.Storage.field('tmdb_lang') || 'ru-RU';
|
||||
var total = subs.length;
|
||||
var done = 0;
|
||||
var tmdbData = {};
|
||||
|
||||
function buildCards() {
|
||||
// Классы сетки из items.js: mapping--grid + cols--6
|
||||
body.addClass('mapping--grid');
|
||||
body.addClass('cols--6');
|
||||
|
||||
var frag = document.createDocumentFragment();
|
||||
|
||||
subs.forEach(function (sub) {
|
||||
var t = tmdbData[sub.tmdb_id] || {};
|
||||
|
||||
var cardData = {
|
||||
id: sub.tmdb_id,
|
||||
tmdb_id: sub.tmdb_id,
|
||||
tmdb: sub.tmdb_id,
|
||||
name: t.name || sub.title,
|
||||
title: t.name || sub.title,
|
||||
original_name: t.original_name || sub.title,
|
||||
poster_path: t.poster_path || '',
|
||||
backdrop_path: t.backdrop_path || '',
|
||||
vote_average: t.vote_average || 0,
|
||||
first_air_date: t.first_air_date || '',
|
||||
number_of_seasons: t.number_of_seasons || sub.last_season,
|
||||
source: 'tmdb'
|
||||
};
|
||||
|
||||
if (t.poster_path)
|
||||
cardData.img = tmdbImage('t/p/w300' + t.poster_path);
|
||||
|
||||
var card = new Lampa.Card(cardData, { object: object, card_category: true });
|
||||
card.create();
|
||||
|
||||
var season = parseInt(sub.last_season) || 0;
|
||||
var episode = parseInt(sub.last_episode) || 0;
|
||||
var voice = (sub.voice || '').trim();
|
||||
|
||||
// Бейдж подписки — без pad() как в стандартном Subscribe модуле Lampa
|
||||
var badge = document.createElement('div');
|
||||
badge.className = 'card__subscribe';
|
||||
badge.innerHTML =
|
||||
'<div class="card__subscribe-status on"></div>' +
|
||||
'<div class="card__subscribe-position">S' + season + ' E' + episode + '</div>' +
|
||||
'<div class="card__subscribe-voice">' + (voice || 'Любая') + '</div>';
|
||||
var view = card.render(true).querySelector('.card__view');
|
||||
if (view) view.after(badge);
|
||||
|
||||
card.onFocus = function (target, data) {
|
||||
last = target;
|
||||
active = items.indexOf(card);
|
||||
scroll.update($(target), true);
|
||||
// Используем стандартную функцию Lampa — сама выбирает постер/backdrop по настройкам
|
||||
Lampa.Background.change(Lampa.Utils.cardImgBackground(data));
|
||||
};
|
||||
|
||||
card.onEnter = function (target, data) {
|
||||
Lampa.Activity.push({
|
||||
url: '', component: 'full',
|
||||
id: data.tmdb_id || data.id,
|
||||
method: 'tv', card: data, source: 'tmdb'
|
||||
});
|
||||
};
|
||||
|
||||
frag.appendChild(card.render(true));
|
||||
items.push(card);
|
||||
});
|
||||
|
||||
body.append(frag);
|
||||
|
||||
// Устанавливаем фон на первую карточку при открытии
|
||||
if (items.length > 0) {
|
||||
Lampa.Background.change(Lampa.Utils.cardImgBackground(items[0].data));
|
||||
}
|
||||
|
||||
// Layer.visible — запускает ленивую загрузку изображений в карточках
|
||||
Lampa.Layer.visible(scroll.render(true));
|
||||
|
||||
self.activity.loader(false);
|
||||
self.activity.toggle();
|
||||
}
|
||||
|
||||
// Параллельная загрузка TMDB через Lampa.Network (с кешем и прокси)
|
||||
subs.forEach(function (sub) {
|
||||
var url = tmdbApi('tv/' + sub.tmdb_id + '?language=' + lang);
|
||||
Lampa.Network.silent(url, function (t) {
|
||||
if (t && !t.status_code) tmdbData[sub.tmdb_id] = t;
|
||||
if (++done >= total) buildCards();
|
||||
},
|
||||
function () { if (++done >= total) buildCards(); },
|
||||
false, { cache: { life: 60 * 24 } }
|
||||
);
|
||||
});
|
||||
|
||||
}, function () { self.empty(); });
|
||||
};
|
||||
|
||||
this.empty = function () {
|
||||
var empty = new Lampa.Empty();
|
||||
html.appendChild(empty.render(true));
|
||||
this.start = empty.start.bind(empty);
|
||||
this.activity.loader(false);
|
||||
this.activity.toggle();
|
||||
};
|
||||
|
||||
// start() — точная копия base.js
|
||||
this.start = function () {
|
||||
var self = this;
|
||||
Lampa.Controller.add('content', {
|
||||
link: self,
|
||||
invisible: true,
|
||||
toggle: function () {
|
||||
scroll.restorePosition ? scroll.restorePosition() : null;
|
||||
Lampa.Controller.collectionSet(scroll.render(true));
|
||||
Lampa.Controller.collectionFocus(last || false, scroll.render(true));
|
||||
},
|
||||
left: function () { if (Navigator.canmove('left')) Navigator.move('left'); else Lampa.Controller.toggle('menu'); },
|
||||
right: function () { if (Navigator.canmove('right')) Navigator.move('right'); },
|
||||
up: function () { if (Navigator.canmove('up')) Navigator.move('up'); else Lampa.Controller.toggle('head'); },
|
||||
down: function () { if (Navigator.canmove('down')) Navigator.move('down'); },
|
||||
back: function () { Lampa.Activity.backward(); }
|
||||
});
|
||||
Lampa.Controller.toggle('content');
|
||||
};
|
||||
|
||||
this.pause = function () {};
|
||||
this.stop = function () {};
|
||||
this.render = function (js) { return js ? html : $(html); };
|
||||
this.destroy = function () {
|
||||
items.forEach(function (c) { try { c.destroy(); } catch(e) {} });
|
||||
scroll.destroy();
|
||||
html.remove();
|
||||
items = [];
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Меню
|
||||
// =============================================
|
||||
var MENU_ICON = '<svg><use xlink:href="#sprite-subscribes"></use></svg>';
|
||||
var MENU_TITLE = 'TG Подписки';
|
||||
var MENU_ACTION = 'tg_subscribes';
|
||||
|
||||
function openSubscribesPage() {
|
||||
Lampa.Activity.push({ url: '', title: MENU_TITLE, component: MENU_ACTION, page: 1 });
|
||||
}
|
||||
|
||||
function addMenu() {
|
||||
if ($('.menu__item[data-action="' + MENU_ACTION + '"]').length) return;
|
||||
Lampa.Menu.addButton(MENU_ICON, MENU_TITLE, openSubscribesPage).attr('data-action', MENU_ACTION);
|
||||
try {
|
||||
var sort = Lampa.Storage.get('menu_sort', '[]');
|
||||
if (typeof sort === 'string') sort = JSON.parse(sort);
|
||||
if (Array.isArray(sort) && sort.indexOf(MENU_TITLE) === -1) {
|
||||
sort.push(MENU_TITLE);
|
||||
Lampa.Storage.set('menu_sort', JSON.stringify(sort));
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function registerComponent() {
|
||||
Lampa.Component.add(MENU_ACTION, TgSubscribesComponent);
|
||||
setTimeout(addMenu, 500);
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Перехват кнопки 🔔 в карточке сериала
|
||||
// =============================================
|
||||
function initSubscribeOverride() {
|
||||
Lampa.Listener.follow('full', function (e) {
|
||||
if (e.type === 'complite')
|
||||
setTimeout(function () { overrideSubscribeButton(e.object); }, 300);
|
||||
});
|
||||
}
|
||||
|
||||
function overrideSubscribeButton(obj) {
|
||||
if (!obj || !obj.card || !obj.card.number_of_seasons) return;
|
||||
var card = obj.card;
|
||||
var button;
|
||||
try { button = obj.activity.render().find('.button--subscribe'); } catch(e) { return; }
|
||||
if (!button || !button.length) return;
|
||||
|
||||
button.removeClass('hide');
|
||||
button.off('hover:enter');
|
||||
|
||||
checkStatus(card.id, function (status) {
|
||||
if (status.subscribed) button.addClass('active').find('path').attr('fill', 'currentColor');
|
||||
button.on('hover:enter', function () {
|
||||
if (!status.linked) showLinkDialog();
|
||||
else showVoiceMenu(card, button, status);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showVoiceMenu(card, button, currentStatus) {
|
||||
var title = card.name || card.title || '';
|
||||
var year = card.first_air_date ? parseInt(card.first_air_date) : (card.year || 0);
|
||||
var tmdbId = card.id || 0;
|
||||
|
||||
Lampa.Loading.start(function () { network.clear(); Lampa.Loading.stop(); });
|
||||
network.clear();
|
||||
network.timeout(30000);
|
||||
network['native'](
|
||||
apiUrl('/api/tg/voices?title=' + encodeURIComponent(title) + '&year=' + year + '&season=0&tmdb_id=' + tmdbId),
|
||||
function (result) {
|
||||
Lampa.Loading.stop();
|
||||
var season = result.season || card.number_of_seasons || 1;
|
||||
var items = [];
|
||||
|
||||
if (currentStatus && currentStatus.subscribed && currentStatus.voices) {
|
||||
currentStatus.voices.forEach(function (v) {
|
||||
items.push({ title: '❌ Отписаться: ' + (v || 'Любая озвучка'), unsubscribe: true, voice: v });
|
||||
});
|
||||
if (items.length) items.push({ title: '', separator: true });
|
||||
}
|
||||
|
||||
items.push({ title: '🔔 Любая озвучка (оригинал)', subtitle: 'При выходе серии', voice: '', orid: '', voice_id: 0, collaps_orid: '', voice_source: '' });
|
||||
|
||||
if (result.success && result.voices && result.voices.length) {
|
||||
items.push({ title: 'Озвучки:', separator: true });
|
||||
result.voices.forEach(function (v) {
|
||||
var src = v.source || 'mirage';
|
||||
var badge = src === 'collaps' ? ' [C]' : src === 'rhs' ? ' [R]' : src === 'videohub' ? ' [V]' : '';
|
||||
items.push({ title: '🎙 ' + v.name + badge, voice: v.name, orid: result.orid || '', voice_id: v.id || 0, collaps_orid: result.collaps_orid || '', kp_id: result.kp_id || 0, voice_source: src });
|
||||
});
|
||||
}
|
||||
|
||||
Lampa.Select.show({
|
||||
title: 'TG Уведомления', items: items,
|
||||
onSelect: function (a) {
|
||||
Lampa.Controller.toggle('content');
|
||||
if (a.unsubscribe) doUnsubscribe(card, button, a.voice);
|
||||
else if (typeof a.voice !== 'undefined') doSubscribe(card, a.voice, season, button, a.orid, a.voice_id, a.collaps_orid, a.voice_source, a.kp_id);
|
||||
},
|
||||
onBack: function () { Lampa.Controller.toggle('content'); }
|
||||
});
|
||||
},
|
||||
function () {
|
||||
Lampa.Loading.stop();
|
||||
var items = [{ title: '🔔 Подписаться (без озвучки)', voice: '', orid: '', voice_id: 0 }];
|
||||
if (currentStatus && currentStatus.subscribed) items.unshift({ title: '❌ Отписаться', unsubscribe: true, voice: '' });
|
||||
Lampa.Select.show({
|
||||
title: 'TG Уведомления', items: items,
|
||||
onSelect: function (a) {
|
||||
Lampa.Controller.toggle('content');
|
||||
if (a.unsubscribe) doUnsubscribe(card, button, null);
|
||||
else doSubscribe(card, '', card.number_of_seasons || 1, button, '', 0);
|
||||
},
|
||||
onBack: function () { Lampa.Controller.toggle('content'); }
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function doSubscribe(card, voice, season, button, orid, voiceId, collapsOrid, voiceSource, kpId) {
|
||||
network.clear();
|
||||
network['native'](apiUrl('/api/tg/subscribe'), function (r) {
|
||||
if (r.success) {
|
||||
Lampa.Noty.show(voice ? '🔔 Подписка: ' + voice : '🔔 Подписка оформлена!');
|
||||
button.addClass('active').find('path').attr('fill', 'currentColor');
|
||||
} else if (r.msg === 'not_linked') {
|
||||
showLinkDialog();
|
||||
} else {
|
||||
Lampa.Noty.show('Ошибка: ' + (r.msg || ''));
|
||||
}
|
||||
}, function () {
|
||||
Lampa.Noty.show('Ошибка подписки');
|
||||
}, JSON.stringify({ tmdb_id: card.id, title: card.name || card.title || '', voice: voice, season: season, episode: 0, mirage_orid: orid || '', mirage_voice_id: voiceId || 0, voice_episode: 0, collaps_orid: collapsOrid || '', voice_source: voiceSource || '', kp_id: (card.kinopoisk_id || card.kp_id || kpId || 0) }),
|
||||
{ dataType: 'json', contentType: 'application/json' });
|
||||
}
|
||||
|
||||
function doUnsubscribe(card, button, voice) {
|
||||
network.clear();
|
||||
network['native'](apiUrl('/api/tg/unsubscribe'), function (r) {
|
||||
if (r.success) {
|
||||
Lampa.Noty.show('Подписка отменена');
|
||||
checkStatus(card.id, function (st) {
|
||||
if (!st.subscribed) button.removeClass('active').find('path').attr('fill', 'transparent');
|
||||
});
|
||||
}
|
||||
}, function () { Lampa.Noty.show('Ошибка'); },
|
||||
JSON.stringify({ tmdb_id: card.id, voice: voice }),
|
||||
{ dataType: 'json', contentType: 'application/json' });
|
||||
}
|
||||
|
||||
function checkStatus(tmdb_id, callback) {
|
||||
var n = new Lampa.Reguest();
|
||||
n.timeout(5000);
|
||||
n['native'](apiUrl('/api/tg/status?tmdb_id=' + tmdb_id),
|
||||
function (r) { callback(r || { success: false, linked: false, subscribed: false, voices: [] }); },
|
||||
function () { callback({ success: false, linked: false, subscribed: false, voices: [] }); }
|
||||
);
|
||||
}
|
||||
|
||||
function showLinkDialog() {
|
||||
var n = new Lampa.Reguest();
|
||||
n['native'](apiUrl('/api/tg/link'), function (r) {
|
||||
if (!r.success || !r.link) { Lampa.Noty.show('Бот не запущен'); return; }
|
||||
Lampa.Select.show({
|
||||
title: 'Привязка Telegram',
|
||||
items: [
|
||||
{ title: 'Открыть ссылку', subtitle: r.link, link: r.link },
|
||||
{ title: 'Скопировать ссылку', show_link: r.link }
|
||||
],
|
||||
onSelect: function (a) {
|
||||
Lampa.Controller.toggle('content');
|
||||
if (a.link) {
|
||||
if (typeof Android !== 'undefined' && Android.openBrowser) Android.openBrowser(a.link);
|
||||
else if (window.open) window.open(a.link, '_blank');
|
||||
}
|
||||
if (a.show_link) Lampa.Noty.show(a.show_link);
|
||||
},
|
||||
onBack: function () { Lampa.Controller.toggle('content'); }
|
||||
});
|
||||
}, function () { Lampa.Noty.show('Модуль TelegramBot недоступен'); });
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Init
|
||||
// =============================================
|
||||
function init() {
|
||||
registerComponent();
|
||||
initSubscribeOverride();
|
||||
console.log('[TG-Notify] Plugin loaded v17');
|
||||
}
|
||||
|
||||
if (window.appready) init();
|
||||
else Lampa.Listener.follow('app', function (e) { if (e.type === 'ready') init(); });
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user