(function () { 'use strict'; function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); } function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var n = 0, F = function () {}; return { s: F, n: function () { return n >= r.length ? { done: !0 } : { done: !1, value: r[n++] }; }, e: function (r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function () { t = t.call(r); }, n: function () { var r = t.next(); return a = r.done, r; }, e: function (r) { u = !0, o = r; }, f: function () { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function (t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i.return) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine(u), _regeneratorDefine(u, o, "Generator"), _regeneratorDefine(u, n, function () { return this; }), _regeneratorDefine(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function () { return { w: i, m: f }; })(); } function _regeneratorDefine(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine = function (e, r, n, t) { function o(r, n) { _regeneratorDefine(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine(e, r, n, t); } function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _wrapRegExp() { _wrapRegExp = function (e, r) { return new BabelRegExp(e, void 0, r); }; var e = RegExp.prototype, r = new WeakMap(); function BabelRegExp(e, t, p) { var o = RegExp(e, t); return r.set(o, p || r.get(e)), _setPrototypeOf(o, BabelRegExp.prototype); } function buildGroups(e, t) { var p = r.get(t); return Object.keys(p).reduce(function (r, t) { var o = p[t]; if ("number" == typeof o) r[t] = e[o];else { for (var i = 0; void 0 === e[o[i]] && i + 1 < o.length;) i++; r[t] = e[o[i]]; } return r; }, Object.create(null)); } return _inherits(BabelRegExp, RegExp), BabelRegExp.prototype.exec = function (r) { var t = e.exec.call(this, r); if (t) { t.groups = buildGroups(t, this); var p = t.indices; p && (p.groups = buildGroups(p, this)); } return t; }, BabelRegExp.prototype[Symbol.replace] = function (t, p) { if ("string" == typeof p) { var o = r.get(this); return e[Symbol.replace].call(this, t, p.replace(/\$<([^>]+)(>|$)/g, function (e, r, t) { if ("" === t) return e; var p = o[r]; return Array.isArray(p) ? "$" + p.join("$") : "number" == typeof p ? "$" + p : ""; })); } if ("function" == typeof p) { var i = this; return e[Symbol.replace].call(this, t, function () { var e = arguments; return "object" != typeof e[e.length - 1] && (e = [].slice.call(e)).push(buildGroups(e, i)), p.apply(this, e); }); } return e[Symbol.replace].call(this, t, p); }, _wrapRegExp.apply(this, arguments); } function Component$1() { Lampa.Lang.add({ clientChoose: { ru: "Выбор клиента", en: "Select client", uk: "Вибір клієnta", zh: "选择客户端", ro: "Selectați clientul" }, pathMovie: { "ru": "Путь сохранения фильмов", "en": "Path to save movies", "uk": "Шлях збереження фільмів", "ro": "Calea de salvare a filmelor" }, pathTV: { "ru": "Путь сохранения сериалов", "en": "Path to save TV shows", "uk": "Шлях збереження серіалів", "ro": "Calea de salvare a serialelor" }, pathMovieQbit: { "ru": "Категория/Метка для фильмов", "en": "Category/Label for movies", "uk": "Категорія/Мітка фільмів", "ro": "Categorie/Etichetă filme" }, pathTVQbit: { "ru": "Категория/Метка для сериалов", "en": "Category/Label for TV shows", "uk": "Категорія/Мітка серіалів", "ro": "Categorie/Etichetă seriale" }, universalAction: { ru: "Действие", en: "Action", uk: "Дія", zh: "行动", ro: "Acțiune" }, openUniversal: { ru: "Открывать в клиенте", en: "Open in external client", uk: "Відкрити у застосунку", zh: "在外部客户端打开", ro: "Deschide în client extern" }, copyUniversal: { ru: "Копировать ссылку", en: "Copy link", uk: "Скопіювати посилання", zh: "复制到缓冲区", ro: "Copiază link-ul" }, openAndroid: { ru: "Открыть через Android (выбор приложения)", en: "Open via Android (app chooser)", uk: "Відкрити через Android (вибір додатку)", zh: "通过 Android 打开(应用选择器)", ro: "Deschide prin Android (selector aplicații)" }, tweak: { ru: "Tweak", en: "Tweak", uk: "Tweak", zh: "高级设置", ro: "Optimizare (Tweak)" }, LMEProxy: { ru: "Прокси", en: "Proxy", uk: "Проксі", zh: "Proxy", ro: "Proxy" }, LMEProxyDescription: { en: "The proxy service from the LME project can help when connecting through external addresses. For example, Keenetic or Synology DNS", ru: "Прокси сервис от проекта LME, может помочь при подключении через внешние адреса. Например Keenetic или Synology DNS", uk: "Проксі-сервіс від проєкту LME може допомогти при підключенні через зовнішні адреси. Наприклад, Keenetic або Synology DNS", ro: "Serviciul proxy din proiectul LME poate ajuta la conectarea prin adrese externe. De exemplu, Keenetic sau Synology DNS" }, TransmissionRPCRoute: { ru: "RPC Path", en: "RPC Path", uk: "RPC Path", zh: "RPC 路径", ro: "Cale RPC" }, TransmissionRPCRouteDescription: { ru: "Изменение пути API. Не трогать без необходимости, если используете KeenDNS указать /app/transmission/rpc", en: "Change api route. Do not change without need, if you use KeenDNS, specify /app/transmission/rpc", uk: "Змінити маршрут API. Не чіпати без нагальної потреби, якщо ви використовуєте KeenDNS, вкажіть /app/transmission/rpc", zh: "更改API路径。如无必要,请勿更改, 如使用 KeenDNS,请指定 /app/transmission/rpc", ro: "Modifică ruta API. Nu schimbați fără necesitate, dacă folosiți KeenDNS, specificați /app/transmission/rpc" }, //Auth AuthSuccess: { en: "Authentication successful", ru: "Аутентификация успешна", uk: "Аутентифікація успішна", ro: "Autentificare reușită" }, AuthDenied: { en: "Authentication denied", ru: "Аутентификация отклонена", uk: "Аутентифікацію відхилено", ro: "Autentificare respinsă" }, torrentmanager_auth_mode: { en: "Auth mode", ru: "Режим авторизации", uk: "Режим авторизації", ro: "Mod autentificare" }, torrentmanager_auth_mode_session: { en: "Session (qB <= 5.1)", ru: "Сессия (qB <= 5.1)", uk: "Сесія (qB <= 5.1)", ro: "Sesiune (qB <= 5.1)" }, torrentmanager_auth_mode_apikey: { en: "API key (qB >= 5.2 beta)", ru: "API key (qB >= 5.2 beta)", uk: "API key (qB >= 5.2 beta)", ro: "Cheie API (qB >= 5.2 beta)" }, torrentmanager_api_key: { en: "API key", ru: "API key", uk: "API key", ro: "Cheie API" }, torrentmanager_test_connection: { en: "Test connection", ru: "Проверить подключение", uk: "Перевірити підключення", ro: "Testează conexiunea" }, torrentmanager_reauth_now: { en: "Re-auth now", ru: "Переавторизоваться", uk: "Переавторизуватись", ro: "Re-autentificare acum" }, torrentmanager_retry: { en: "Retry", ru: "Повторить", uk: "Повторити", ro: "Reîncearcă" }, torrentmanager_open_settings: { en: "Open settings", ru: "Открыть настройки", uk: "Відкрити налаштування", ro: "Deschide setările" }, torrentmanager_state_loading: { en: "Loading torrents", ru: "Загрузка торрентов", uk: "Завантаження торрентів", ro: "Se încarcă torrentele" }, torrentmanager_state_empty_title: { en: "No torrents found", ru: "Торренты не найдены", uk: "Торрентів не знайдено", ro: "Nu s-au găsit torrente" }, torrentmanager_state_empty_description: { en: "The list is empty right now", ru: "Список сейчас пуст", uk: "Список зараз порожній", ro: "Lista este goală în acest moment" }, torrentmanager_state_auth_title: { en: "Authorization required", ru: "Требуется авторизация", uk: "Потрібна авторизація", ro: "Necesită autentificare" }, torrentmanager_state_auth_description: { en: "Check credentials and reconnect to the torrent client", ru: "Проверьте учетные данные и повторите подключение к торрент-клиенту", uk: "Перевірте облікові дані та повторіть підключення до торрент-клієнта", ro: "Verifică datele de acces și reconectează-te la clientul torrent" }, torrentmanager_state_network_title: { en: "Connection error", ru: "Ошибка соединения", uk: "Помилка з'єднання", ro: "Eroare de conexiune" }, torrentmanager_state_network_description: { en: "Cannot reach torrent client. Check address, proxy, and network", ru: "Не удалось подключиться к торрент-клиенту. Проверьте адрес, прокси и сеть", uk: "Не вдалося підключитися до торрент-клієнта. Перевірте адресу, проксі та мережу", ro: "Clientul torrent nu poate fi accesat. Verifică adresa, proxy-ul și rețeaua" }, torrentmanager_connection_error: { en: "Connection error: server unreachable", ru: "Ошибка соединения: сервер недоступен", uk: "Помилка з'єднання: сервер недоступний", ro: "Eroare de conexiune: serverul nu răspunde" }, torrentmanager_server_error: { en: "Server error", ru: "Ошибка сервера", uk: "Помилка сервера", ro: "Eroare de server" }, torrentmanager_url_requires_protocol: { en: "Address must start with http:// or https://", ru: "Адрес должен начинаться с http:// или https://", uk: "Адреса має починатися з http:// або https://", ro: "Adresa trebuie să înceapă cu http:// sau https://" }, torrentmanager_state_server_title: { en: "Server unavailable", ru: "Сервер недоступен", uk: "Сервер недоступний", ro: "Server indisponibil" }, torrentmanager_state_server_description: { en: "Torrent client returned a server error", ru: "Торрент-клиент вернул серверную ошибку", uk: "Торрент-клієнт повернув серверну помилку", ro: "Clientul torrent a raportat o eroare de server" }, torrentmanager_secret_set: { en: "saved", ru: "сохранен", uk: "збережено", ro: "salvat" }, torrentmanager_secret_not_set: { en: "not set", ru: "не задан", uk: "не задано", ro: "nesetat" }, torrentmanager_secret_saved: { en: "Secret updated", ru: "Секрет обновлен", uk: "Секрет оновлено", ro: "Secret actualizat" }, //Panel clientNotClient: { en: "Either the client is not configured or the server is not responding, check the console, you might find the answer there", ru: "Или клиент не настроен или сервер не отвечает, узри консоль, возможно там найдешь ответ", uk: "Або клієнт не налаштований, або сервер не відповідає, перевір консоль, можливо там знайдеш відповідь", ro: "Clientul nu este configurat sau serverul nu răspunde, verifică consola pentru detalii" }, somethingWentWrong: { en: "Something went wrong", ru: "Что-то пошло не так", uk: "Щось пішло не так", ro: "Ceva nu a mers bine" }, //Panel action actionWithTorrent: { en: "Action with torrent", ru: "Действие с торрентом", uk: "Дія з торрентом", ro: "Acțiune cu torrentul" }, resume: { en: "Resume", ru: "Возобновить", uk: "Відновити", ro: "Reia" }, pause: { en: "Pause", ru: "Пауза", uk: "Пауза", ro: "Pauză" }, "delete": { en: "Delete", ru: "Удалить", uk: "Видалити", ro: "Șterge" }, fullDelete: { en: "Full delete", ru: "Полное удаление", uk: "Повне видалення", ro: "Ștergere completă" }, actionSentSuccessfully: { "en": "Action sent successfully", "ru": "Действие успешно отправлено", "uk": "Дію успішно надіслано", "ro": "Acțiune trimisă cu succes" }, actionReturnedError: { en: "Action returned an error", ru: "Действие вернуло ошибку", uk: "Дія повернула помилку", ro: "Acțiunea a returnat o eroare" }, torrent_select_file: { en: "Select file to play", ru: "Выберите файл для воспроизведения", uk: "Виберіть файл для відтворення", ro: "Selectați fișierul pentru redare" }, sort_by_name: { en: "Sort by name", ru: "Сортировать по имени", uk: "Сортувати за назвою", ro: "Sortare după nume" }, sort_by_size: { en: "Sort by size", ru: "Сортировать по размеру", uk: "Сортувати за розміром", ro: "Sortare după mărime" }, sort_by_progress: { en: "Sort by progress", ru: "Сортировать по прогрессу", uk: "Сортувати за прогресом", ro: "Sortare după progres" }, openCard: { en: "Open", ru: "Открыть", uk: "Відкрити", ro: "Deschide" }, add_metadata: { en: "Add metadata", ru: "Добавить метаданные", uk: "Додати метадані", ro: "Adaugă metadate" }, torrentmanager_nothing_found: { en: "Nothing found", ru: "Ничего не найдено", uk: "Нічого не знайдено", ro: "Nu s-a găsit nimic" }, torrentmanager_metadata_updated: { en: "Metadata updated", ru: "Метаданные обновлены", uk: "Метадані оновлено", ro: "Metadate actualizate" }, torrentmanager_metadata_error: { en: "Metadata error", ru: "Ошибка метаданных", uk: "Помилка метаданів", ro: "Eroare metadate" }, torrentmanager_metadata_updated_all: { en: "Metadata updated", ru: "Метаданные обновлены", uk: "Метадані оновлено", ro: "Metadate actualizate" }, torrentmanager_metadata_added: { en: "Metadata added", ru: "Метаданные добавлены", uk: "Метадані додано", ro: "Metadate adăugate" }, add_metadata_all: { en: "Parse all", ru: "Парсить все", uk: "Парсити все", ro: "Procesează tot" }, torrentmanager_settings_thanks: { en: "Thanks", ru: "Благодарность", uk: "Подяка", ro: "Mulțumesc" }, torrentmanager_settings_thanks_description: { en: "Support for Torrent Manager development is voluntary. Thank you for your contribution.", ru: "Поддержка развития Torrent Manager добровольная. Спасибо за вклад.", uk: "Підтримка розвитку Torrent Manager добровільна. Дякуємо за внесок.", ro: "Susținerea dezvoltării Torrent Manager este voluntară. Vă mulțumim pentru contribuție." }, selectResult: { en: "Select result", ru: "Выберите результат", uk: "Виберіть результат", ro: "Selectați rezultatul" }, // Section titles for status-based grouping torrentmanager_section_downloading: { en: "Downloading", ru: "Загрузка", uk: "Завантаження", zh: "下载中", ro: "Se descarcă" }, torrentmanager_section_seeding: { en: "Seeding", ru: "Раздача", uk: "Роздача", zh: "做种中", ro: "Seederi" }, torrentmanager_section_paused: { en: "Paused", ru: "Приостановлено", uk: "Призупинено", zh: "已暂停", ro: "Pauză" }, torrentmanager_section_checking: { en: "Checking", ru: "Проверка", uk: "Перевірка", zh: "检查中", ro: "Verificare" }, torrentmanager_section_errored: { en: "Errors", ru: "Ошибки", uk: "Помилки", zh: "错误", ro: "Erori" }, torrentmanager_section_completed: { en: "Completed", ru: "Завершено", uk: "Завершено", zh: "已完成", ro: "Finalizat" } }); } /** * Torrent state classification into section groups. * Maps raw client state strings to unified section keys. */ /** * Classify a torrent state string into a section key. * @param {string} state - Raw torrent state from client API * @returns {'downloading'|'seeding'|'completed'|'paused'|'checking'|'errored'} */ function classifyState$1(state) { var normalized = (state || '').toLowerCase().trim(); // Downloading — actively downloading // qB: Downloading, MetaDL, ForcedDL, StalledDL, ForcedMetaDL, Downloading metadata // S: Downloading // T: Downloading if (/^(downloading|metadl|forceddl|stalleddl|forcedmetadl|downloading metadata)$/i.test(normalized)) { return 'downloading'; } // Seeding — actively uploading // qB: Uploading, ForcedUP, StalledUP // T: Seeding if (/^(uploading|forcedup|stalledup|seeding)$/i.test(normalized)) { return 'seeding'; } // Completed — finished but not actively seeding (Synology) // S: Finished (when completed >= 1, normalizeTaskState returns 'Finished') if (/^(finished)$/i.test(normalized)) { return 'completed'; } // Paused / Stopped — manually stopped // qB: PausedDL, PausedUP, StoppedDL, StoppedUP // S: Paused // T: Stopped, Paused if (/^(pauseddl|pausedup|stoppeddl|stoppedup|stopped|paused)$/i.test(normalized)) { return 'paused'; } // Checking / Queued / Waiting — in verification or waiting // qB: CheckingUP, CheckingDL, QueuedUP, QueuedDL, CheckingResumeData, Checking, Queued, Allocating, Moving // S: Queued, Waiting, Extracting // T: Queued to verify local data, Verifying local data, Queued to download, Queued to seed if (/^(checkingup|checkingdl|queuedup|queueddl|checking|queued|allocating|moving|waiting|verifying|extracting)/i.test(normalized) || normalized.indexOf('queued to') >= 0 || normalized.indexOf('verify') >= 0) { return 'checking'; } // Errored — has errors // qB: MissingFiles, Error, Unknown // S: Error, ErrorStatus if (/^(missingfiles|error|unknown|errorstatus)$/i.test(normalized)) { return 'errored'; } // Fallback: unknown states return 'errored'; } /** * Ordered section definitions with metadata. * Order determines render and navigation priority. */ var SECTION_DEFINITIONS = [{ key: 'downloading', langKey: 'torrentmanager_section_downloading', priority: 0 }, { key: 'seeding', langKey: 'torrentmanager_section_seeding', priority: 1 }, { key: 'completed', langKey: 'torrentmanager_section_completed', priority: 2 }, { key: 'paused', langKey: 'torrentmanager_section_paused', priority: 3 }, { key: 'checking', langKey: 'torrentmanager_section_checking', priority: 4 }, { key: 'errored', langKey: 'torrentmanager_section_errored', priority: 5 }]; /** * Get the standardized, translated display name for a raw torrent state. * @param {string} state - Raw torrent state from client API * @returns {string} Translated standardized state name */ function getStandardizedStateName(state) { var sectionKey = classifyState$1(state); for (var i = 0; i < SECTION_DEFINITIONS.length; i++) { if (SECTION_DEFINITIONS[i].key === sectionKey) { return Lampa.Lang.translate(SECTION_DEFINITIONS[i].langKey); } } return state || ''; // Fallback to raw state if section not found } /** * Group an array of torrents by their classified status. * @param {Array} torrents - Array of torrent objects with .state property * @returns {Object} Grouped object with keys: downloading, seeding, completed, paused, checking, errored */ function groupTorrentsByStatus(torrents) { var groups = { downloading: [], seeding: [], completed: [], paused: [], checking: [], errored: [] }; if (!Array.isArray(torrents)) { return groups; } torrents.forEach(function (torrent) { var key = classifyState$1(torrent.state); if (groups[key]) { groups[key].push(torrent); } else { groups.errored.push(torrent); } }); return groups; } var regexp2$1 = /(?:PPV.)?[HP]DTV|(?:HD)?TC|[cC]am|(?:HD)?CAM|B[rR]Rip|WEBRip|WEB-Rip|WEB-DL|WEB|TS|(?:PPV )?WEB-?DL(?: DVDRip)?|H[dD]Rip|DVDRip|DVDRiP|DVDRIP|CamRip|W[EB]B[rR]ip|HDRIP|[Bb]lu[Rr]ay|DvDScr|hdtv/; function getProgressClass$1(progress) { if (progress >= 100) { return 'is-high'; } if (progress >= 50) { return 'is-mid'; } return 'is-low'; } function Item(data) { this.id = data.id; // зберігаємо ідентифікатор торренту var initialProgress = Number((data.completed * 100).toFixed(2)); var itemN = Lampa.Template.get('lmetorrent_item__card', { title: data.name, size: Lampa.Utils.bytesToSize(data.size, 0), state: getStandardizedStateName(data.state), "data-completed": initialProgress, completed: initialProgress + "%", image: data.image, image_src: '', quality: data.name.match(regexp2$1) ? data.name.match(regexp2$1).toString().replace(/[ .()]/g, '') : '' }); itemN.find('.lmetorrent_card__completed').addClass(getProgressClass$1(initialProgress)); this.render = function () { return itemN; }; // Метод для оновлення статусу та прогресу this.update = function (newData) { // Припускається, що відповідні елементи мають класи для статусу та прогресу itemN.find('.lmetorrent_card__state').text(getStandardizedStateName(newData.state)); var progress = Number((newData.completed * 100).toFixed(2)); itemN.attr("data-completed", progress); itemN.find('.lmetorrent_card__completed').removeClass('is-low is-mid is-high').addClass(getProgressClass$1(progress)).text(progress + "%"); }; this.destroy = function () { itemN.remove(); }; } /** * A single section in the torrent panel. * Wraps items_line template with horizontal scroll and PanelItem children. * @param {Object} config * @param {string} config.key - Section key (downloading/seeding/etc.) * @param {string} config.title - Section display title * @param {string} config.icon - Section icon character * @param {Array} config.items - Array of torrent data objects */ function PanelSection(config) { var self = this; this.key = config.key; this.title = config.title; this.icon = config.icon || ''; this.items = []; this.last = null; // Last focused DOM element (vinyl pattern) this.lastFocusedIndex = -1; // Last focused index for scroll tracking this.torrentDataMap = {}; // id -> latest torrent data object var sectionId = 'lmetorrent_section_' + config.key; this.onDown = null; this.onUp = null; this.onBack = null; this.onLeft = null; this.onMore = null; // Callback for "more" button (section-level) this.itemsLimit = 11; // Max items shown before "more" card // Build DOM from items_line template this.html = Lampa.Template.get('items_line', { title: config.title }); this.html.addClass('lmetorrent-section'); this.html.attr('data-section-key', config.key); var titleEl = this.html.find('.items-line__title'); // Prepend icon and append count to title if (this.icon) { titleEl.prepend('' + this.icon + ''); } titleEl.append('(' + config.items.length + ')'); var bodyEl = this.html.find('.items-line__body'); // Create horizontal scroll this.scroll = new Lampa.Scroll({ horizontal: true, step: 300 }); /** * Create the section: render items, append scroll to body. * Limits visible items to itemsLimit and adds "more" card when count exceeds limit. * Follows vinyl/lib/line.js pattern for pagination. */ this.create = function () { self.scroll.render().find('.scroll__body').addClass('items-cards'); self.scroll.render().find('.scroll__body').addClass('mapping--line'); var displayLimit = self.itemsLimit || 11; var items = config.items || []; var displayItems = items.slice(0, displayLimit); var hasMore = items.length > displayLimit && typeof self.onMore === 'function'; // Add "more" button in section header when onMore is provided if (hasMore) { var headMore = $('
' + Lampa.Lang.translate('more') + '
'); headMore.on('hover:enter', function () { if (self.onMore) self.onMore(); }); self.html.find('.items-line__head').append(headMore); } displayItems.forEach(function (torrentData) { self.appendItem(torrentData); }); // Add native card-more at end of scroll if (hasMore) { var nativeMore = $('
' + '
' + '
' + Lampa.Lang.translate('more') + '
' + '
' + '
'); nativeMore.on('hover:focus', function () { self.last = nativeMore[0]; self.scroll.update(nativeMore, true); }).on('hover:enter', function () { if (self.onMore) self.onMore(); }); self.scroll.append(nativeMore); } bodyEl.append(self.scroll.render()); return this.render(); }; /** * Create a PanelItem for torrentData and append to scroll. * Stores torrent data in torrentDataMap for closure-safe retrieval on hover:enter. * @param {Object} torrentData */ this.appendItem = function (torrentData) { var data = torrentData; var id = String(data.id); self.torrentDataMap[id] = data; var item = new Item(data); item.render().on('hover:focus', function () { self.last = item.render()[0]; // Track DOM element (vinyl pattern) self.lastFocusedIndex = self.items.indexOf(item); if (self.lastFocusedIndex >= 0) { self.scroll.update(self.items[self.lastFocusedIndex].render(), true); } }); item.render().on('hover:enter', function () { if (self.onItemEnter) { self.onItemEnter(self.torrentDataMap[id]); } }); self.scroll.append(item.render()); self.items.push(item); }; /** * Return the jQuery element representing this section. */ this.render = function () { return this.html; }; /** * Update section with new items. Uses diff approach — preserves existing * DOM elements and updates them in place when possible, avoiding flicker * from destroy-and-rebuild on every poll cycle. * @param {Array} newItems - Array of new torrent data objects */ this.update = function (newItems) { var limit = self.itemsLimit || 11; var items = Array.isArray(newItems) ? newItems : []; var displayItems = items.slice(0, limit); var hasMore = items.length > limit && typeof self.onMore === 'function'; // Build existing item map by id var existingMap = {}; this.items.forEach(function (item) { existingMap[String(item.id)] = item; }); var newIds = {}; var remaining = []; // Process items within display limit displayItems.forEach(function (torrentData) { var id = String(torrentData.id); newIds[id] = true; self.torrentDataMap[id] = torrentData; if (existingMap[id]) { // Update existing item in place — NO DOM removal existingMap[id].update(torrentData); remaining.push(existingMap[id]); } else { // Create new PanelItem inline (mirrors appendItem logic) var item = new Item(torrentData); item.render().on('hover:focus', function () { self.last = item.render()[0]; var idx = remaining.indexOf(item); if (idx >= 0) { self.lastFocusedIndex = idx; self.scroll.update(item.render(), true); } }); item.render().on('hover:enter', function () { if (self.onItemEnter) { self.onItemEnter(self.torrentDataMap[id]); } }); self.scroll.append(item.render()); remaining.push(item); } }); // Destroy items no longer in display set this.items.forEach(function (item) { var id = String(item.id); if (!newIds[id]) { delete self.torrentDataMap[id]; item.destroy(); } }); this.items = remaining; // Handle more button in header var moreBtnEl = self.html.find('.items-line__more'); if (hasMore && !moreBtnEl.length) { var headMore = $('
' + Lampa.Lang.translate('more') + '
'); headMore.on('hover:enter', function () { if (self.onMore) self.onMore(); }); self.html.find('.items-line__head').append(headMore); } else if (!hasMore && moreBtnEl.length) { moreBtnEl.remove(); } // Handle native card-more in scroll var cardMoreEl = self.scroll.render().find('.card-more'); if (hasMore && !cardMoreEl.length) { var nativeMore = $('
' + '
' + '
' + Lampa.Lang.translate('more') + '
' + '
' + '
'); nativeMore.on('hover:focus', function () { self.last = nativeMore[0]; self.scroll.update(nativeMore, true); }).on('hover:enter', function () { if (self.onMore) self.onMore(); }); self.scroll.append(nativeMore); } else if (!hasMore && cardMoreEl.length) { cardMoreEl.remove(); } // Reset last focused index if out of bounds if (this.lastFocusedIndex >= this.items.length) { this.lastFocusedIndex = this.items.length - 1; } // Update count badge var countEl = self.html.find('.lmetorrent-section__count'); countEl.text('(' + items.length + ')'); }; /** * Focus this section's first or last-focused card. * Sets the Lampa.Controller collection to this section's scroll. */ this.focus = function () { if (this.items.length === 0) return; var targetIndex = this.lastFocusedIndex >= 0 && this.lastFocusedIndex < this.items.length ? this.lastFocusedIndex : 0; Lampa.Controller.collectionSet(this.scroll.render()); Lampa.Controller.collectionFocus(this.items[targetIndex].render()[0] || false, this.scroll.render()); }; /** * Save current focused index for later restoration. * Focus index is tracked automatically via hover:focus handler. */ this.saveFocus = function () { // lastFocusedIndex is updated on hover:focus, so already tracked }; /** * Get the last focused DOM element, or false if none. * @returns {HTMLElement|false} */ this.getLastFocused = function () { return self.last || false; }; /** * Toggle this section's own controller. * Registers a Lampa.Controller for this section and activates it. * Uses Navigator.move for left/right within the section, following the exact * vinyl pattern (line/Line module) for horizontal scroll navigation. */ this.toggle = function () { Lampa.Controller.add(sectionId, { toggle: function toggle() { Lampa.Controller.collectionSet(self.scroll.render()); Lampa.Controller.collectionFocus(self.last || false, self.scroll.render()); }, right: function right() { Navigator.move('right'); }, left: function left() { if (Navigator.canmove('left')) Navigator.move('left');else if (self.onLeft) self.onLeft();else Lampa.Controller.toggle('menu'); }, down: function down() { if (self.onDown) self.onDown(); }, up: function up() { if (self.onUp) self.onUp(); }, gone: function gone() {}, back: function back() { if (self.onBack) self.onBack(); } }); Lampa.Controller.toggle(sectionId); }; /** * Navigate left within the section. */ this.scrollLeft = function () { if (Navigator.canmove('left')) { Navigator.move('left'); } }; /** * Navigate right within the section. */ this.scrollRight = function () { if (Navigator.canmove('right')) { Navigator.move('right'); } }; /** * Check if section has no items. * @returns {boolean} */ this.isEmpty = function () { return this.items.length === 0; }; /** * Get number of items in this section. * @returns {number} */ this.getItemCount = function () { return this.items.length; }; /** * Destroy the section: scroll, items, DOM removal. */ this.destroy = function () { this.last = null; Lampa.Arrays.destroy(this.items); this.items = []; this.torrentDataMap = {}; if (this.scroll && typeof this.scroll.destroy === 'function') { this.scroll.destroy(); } this.scroll = null; this.html.remove(); this.html = null; }; } function Header(data) { var item = Lampa.Template.get("lmetorrent_header", { space: Lampa.Utils.bytesToSize(data.space, 0) }); this.render = function () { return item; }; this.destroy = function () { item.remove(); }; } function buildTmdbImageUrl(path) { var posterSize = Lampa.Storage.field('poster_size') || 'w200'; return Lampa.Api.img(path, posterSize); } function buildTmdbImagesApiUrl(label) { var tmdbLang = Lampa.Storage.field('tmdb_lang') || Lampa.Storage.get('language') || 'en'; return Lampa.TMDB.api("".concat(label, "/images?api_key=").concat(Lampa.TMDB.key(), "&language=").concat(tmdbLang)); } function fetchPosterFromSource(method, id) { var source = Lampa.Storage.get('source', 'tmdb'); if (!source || source === 'tmdb') return Promise.resolve(''); return new Promise(function (resolve) { Lampa.Api.full({ id: id, method: method, source: source }, function (data) { var movie = data.movie || data; var poster = movie.poster || movie.img || ''; if (poster) return resolve(poster); var path = movie.poster_path || movie.backdrop_path || movie.profile_path || ''; if (path) return resolve(buildTmdbImageUrl(path)); resolve(''); }, function () { return resolve(''); }); }); } function getPosterFromLabels(_x) { return _getPosterFromLabels.apply(this, arguments); } function _getPosterFromLabels() { _getPosterFromLabels = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(labels) { var labelArray, label, _label$split, _label$split2, method, id, sourcePoster, response, poster, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: // Ищем лейблы, которые начинаются на tv или movie и содержат цифры после косой labelArray = Array.isArray(labels) ? labels : labels.split(','); // Find label matching tv/movie pattern label = labelArray.find(function (label) { return /^(tv|movie)\/\d+$/.test(label); }); if (label) { _context.n = 1; break; } return _context.a(2, './img/img_load.svg'); case 1: _label$split = label.split('/'), _label$split2 = _slicedToArray(_label$split, 2), method = _label$split2[0], id = _label$split2[1]; _context.n = 2; return fetchPosterFromSource(method, id); case 2: sourcePoster = _context.v; if (!sourcePoster) { _context.n = 3; break; } return _context.a(2, sourcePoster); case 3: _context.p = 3; _context.n = 4; return new Promise(function (resolve, reject) { Lampa.Network.silent(buildTmdbImagesApiUrl(label), resolve, reject); }); case 4: response = _context.v; poster = response && response.posters && response.posters[0]; if (!(poster && poster.file_path)) { _context.n = 5; break; } return _context.a(2, buildTmdbImageUrl(poster.file_path)); case 5: _context.n = 7; break; case 6: _context.p = 6; _t = _context.v; console.log('TDM', 'getPosterFromLabels: images API failed', _t); case 7: return _context.a(2, './img/img_load.svg'); } }, _callee, null, [[3, 6]]); })); return _getPosterFromLabels.apply(this, arguments); } var LME_PROXY_URL = "https://apx.lme.isroot.in/destination/"; var LME_SIMKL_URL = "https://apx.lme.isroot.in/simkl/"; var HTTP_URL_PATTERN = /^https?:\/\/[^/\s?#]+(?:[/?#]|$)/i; var INVALID_STRING_VALUES = { undefined: true, "null": true }; function sanitizeConfigString(value) { var fallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; var normalized = typeof value === "string" ? value.trim() : value === null || typeof value === "undefined" ? "" : String(value).trim(); if (!normalized) { return fallback; } if (INVALID_STRING_VALUES[normalized.toLowerCase()]) { return fallback; } return normalized; } function normalizeBaseUrl(rawUrl) { var normalized = sanitizeConfigString(rawUrl); if (!normalized) { return ""; } return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized; } function normalizeApiPath(rawPath) { var fallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; var normalizedFallback = sanitizeConfigString(fallback); var normalizedPath = sanitizeConfigString(rawPath, normalizedFallback); if (!normalizedPath) { return ""; } return normalizedPath.charAt(0) === "/" ? normalizedPath : "/".concat(normalizedPath); } function createConfigError(message) { var error = new Error(message); error.name = "TorrentClientConfigError"; error.isConfigError = true; return error; } function isHttpUrl(url) { return HTTP_URL_PATTERN.test(url); } function isRelativeUrl(url) { return url.charAt(0) === "/"; } function normalizeProxyUrl(rawProxyUrl) { var normalized = sanitizeConfigString(rawProxyUrl, LME_PROXY_URL); return normalized.endsWith("/") ? normalized : "".concat(normalized, "/"); } function resolveClientBaseUrl() { var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var clientName = options.clientName || "Torrent client"; var normalizedUrl = normalizeBaseUrl(config.url); if (!normalizedUrl) { throw createConfigError("".concat(clientName, " URL is not configured")); } if (config.useProxy) { if (!isHttpUrl(normalizedUrl)) { throw createConfigError("".concat(clientName, " URL must include http:// or https:// when proxy is enabled")); } return "".concat(normalizeProxyUrl(config.proxy)).concat(normalizedUrl); } if (!isHttpUrl(normalizedUrl) && !isRelativeUrl(normalizedUrl)) { throw createConfigError("".concat(clientName, " URL has invalid format")); } return normalizedUrl; } var CONFIG_PREFIX$1 = "lmetorrentqBittorent"; function getConfig$1() { var rawApiKey = Lampa.Storage.field("".concat(CONFIG_PREFIX$1, "ApiKey")); return { url: sanitizeConfigString(Lampa.Storage.field("".concat(CONFIG_PREFIX$1, "Url"))), useProxy: Lampa.Storage.field("".concat(CONFIG_PREFIX$1, "Proxy")) === true, proxy: LME_PROXY_URL, apiKey: rawApiKey && rawApiKey !== "undefined" && rawApiKey !== "null" ? rawApiKey : "" }; } function getApiBase$1() { return resolveClientBaseUrl(getConfig$1(), { clientName: "qBittorrent" }); } function getHeaders$1(contentType) { var config = getConfig$1(); var headers = {}; if (contentType) { headers["Content-Type"] = contentType; } if (config.useProxy) { headers["x-requested-with"] = "lme-plugins"; } if (config.apiKey) { headers.Authorization = "Bearer ".concat(config.apiKey); } return headers; } function normalizeError(error, fallback) { var message = error && (error.responseText || error.statusText || error.message) || fallback; var normalized = new Error(message); if (error && typeof error.status !== "undefined") { normalized.status = error.status; } return normalized; } function ajaxRequest(path) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; getConfig$1(); var method = (options.method || "GET").toUpperCase(); var settings = { url: "".concat(getApiBase$1()).concat(path), method: method, timeout: typeof options.timeout === "number" ? options.timeout : 10000, headers: getHeaders$1(options.contentType) }; if (options.data) { settings.data = options.data; } return $.ajax(settings); } function makeRequest(_x) { return _makeRequest.apply(this, arguments); } function _makeRequest() { _makeRequest = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(path) { var options, config, _args = arguments, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: options = _args.length > 1 && _args[1] !== undefined ? _args[1] : {}; config = getConfig$1(); if (config.url) { _context.n = 1; break; } throw new Error("qBittorrent URL is not configured"); case 1: _context.p = 1; _context.n = 2; return ajaxRequest(path, options); case 2: return _context.a(2, _context.v); case 3: _context.p = 3; _t = _context.v; throw normalizeError(_t, "qBittorrent request failed"); case 4: return _context.a(2); } }, _callee, null, [[1, 3]]); })); return _makeRequest.apply(this, arguments); } function auth$1() { return _auth$1.apply(this, arguments); } function _auth$1() { _auth$1 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() { var showNotification, config, _args2 = arguments, _t2; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: showNotification = _args2.length > 0 && _args2[0] !== undefined ? _args2[0] : true; config = getConfig$1(); if (config.url) { _context2.n = 1; break; } if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate("AuthDenied") }); } throw new Error("qBittorrent URL is not configured"); case 1: if (config.apiKey) { _context2.n = 2; break; } if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate("AuthDenied") }); } throw new Error("qBittorrent API key is not configured"); case 2: _context2.p = 2; _context2.n = 3; return ajaxRequest("/api/v2/app/version", { method: "GET" }); case 3: if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate("AuthSuccess") }); } return _context2.a(2, true); case 4: _context2.p = 4; _t2 = _context2.v; console.log("TDM", "qBittorrent auth error:", _t2); if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate("AuthDenied") }); } throw normalizeError(_t2, "qBittorrent auth failed"); case 5: return _context2.a(2); } }, _callee2, null, [[2, 4]]); })); return _auth$1.apply(this, arguments); } function GetData$1() { return _GetData$1.apply(this, arguments); } function _GetData$1() { _GetData$1 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() { var response, torrents, _t0; return _regenerator().w(function (_context4) { while (1) switch (_context4.p = _context4.n) { case 0: _context4.p = 0; _context4.n = 1; return makeRequest("/api/v2/sync/maindata", { method: "GET" }); case 1: response = _context4.v; if (!(!response || !response.torrents)) { _context4.n = 2; break; } return _context4.a(2, []); case 2: torrents = Object.values(response.torrents); return _context4.a(2, Promise.all(torrents.map(/*#__PURE__*/function () { var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(torrent) { var _t3, _t4, _t5, _t6, _t7, _t8, _t9; return _regenerator().w(function (_context3) { while (1) switch (_context3.n) { case 0: _t3 = torrent.name; _t4 = torrent.infohash_v1 || torrent.hash; _t5 = torrent.size; _t6 = torrent.state ? torrent.state.charAt(0).toUpperCase() + torrent.state.slice(1) : ""; _t7 = torrent.tags; _context3.n = 1; return getPosterFromLabels(torrent.tags); case 1: _t8 = _context3.v; _t9 = Number(torrent.progress) || 0; return _context3.a(2, { name: _t3, id: _t4, size: _t5, state: _t6, labels: _t7, image: _t8, completed: _t9 }); } }, _callee3); })); return function (_x9) { return _ref.apply(this, arguments); }; }()))); case 3: _context4.p = 3; _t0 = _context4.v; console.log("TDM", "qBittorrent GetData error:", _t0); throw _t0; case 4: return _context4.a(2); } }, _callee4, null, [[0, 3]]); })); return _GetData$1.apply(this, arguments); } function GetInfo$1() { return _GetInfo$1.apply(this, arguments); } function _GetInfo$1() { _GetInfo$1 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5() { var response; return _regenerator().w(function (_context5) { while (1) switch (_context5.n) { case 0: _context5.n = 1; return makeRequest("/api/v2/sync/maindata", { method: "GET" }); case 1: response = _context5.v; return _context5.a(2, { space: response && response.server_state ? response.server_state.free_space_on_disk : 0 }); } }, _callee5); })); return _GetInfo$1.apply(this, arguments); } function normalizeActionByVersion(action, versionString) { var match = String(versionString || "").match(/^v?(\d+)(?:\.\d+)?/i); var major = match ? Number(match[1]) : 0; var normalized = _objectSpread2({}, action); if (major >= 5) { if (normalized.action === "resume") normalized.action = "start"; if (normalized.action === "pause") normalized.action = "stop"; } return normalized; } function SendCommand$1(_x2, _x3) { return _SendCommand$1.apply(this, arguments); } function _SendCommand$1() { _SendCommand$1 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(action, torrentData) { var version, normalizedAction, commandPath, requestData, _t1; return _regenerator().w(function (_context6) { while (1) switch (_context6.p = _context6.n) { case 0: _context6.p = 0; _context6.n = 1; return makeRequest("/api/v2/app/version", { method: "GET" }); case 1: version = _context6.v; normalizedAction = normalizeActionByVersion(action, version); commandPath = "/api/v2/torrents/".concat(normalizedAction.action); requestData = { hashes: torrentData.id }; if (normalizedAction.action === "delete") { requestData.deleteFiles = normalizedAction.deleteFiles ? "true" : "false"; } _context6.n = 2; return makeRequest(commandPath, { method: "POST", contentType: "application/x-www-form-urlencoded; charset=UTF-8", data: requestData }); case 2: Lampa.Bell.push({ text: Lampa.Lang.translate("actionSentSuccessfully") }); _context6.n = 4; break; case 3: _context6.p = 3; _t1 = _context6.v; console.log("TDM", "qBittorrent SendCommand error:", _t1); Lampa.Bell.push({ text: Lampa.Lang.translate("actionReturnedError") }); throw _t1; case 4: return _context6.a(2); } }, _callee6, null, [[0, 3]]); })); return _SendCommand$1.apply(this, arguments); } function SendTask$1(_x4, _x5, _x6) { return _SendTask$1.apply(this, arguments); } function _SendTask$1() { _SendTask$1 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(selectedTorrent, labels, dtype) { var _t10; return _regenerator().w(function (_context7) { while (1) switch (_context7.p = _context7.n) { case 0: if (selectedTorrent) { _context7.n = 1; break; } throw new Error("No torrent selected"); case 1: _context7.p = 1; _context7.n = 2; return makeRequest("/api/v2/torrents/add", { method: "POST", contentType: "application/x-www-form-urlencoded; charset=UTF-8", data: { tags: labels || "", urls: selectedTorrent.MagnetUri || selectedTorrent.Link, category: Lampa.Storage.get("lmetorrentqBittorent".concat(dtype)) || "", firstLastPiecePrio: Lampa.Storage.field("lmetorrentqBittorentfirstLastPiecePrio") ? "true" : "false", sequentialDownload: Lampa.Storage.field("lmetorrentqBittorentSequentialDownload") ? "true" : "false" } }); case 2: Lampa.Bell.push({ text: Lampa.Lang.translate("actionSentSuccessfully") }); _context7.n = 4; break; case 3: _context7.p = 3; _t10 = _context7.v; console.log("TDM", "qBittorrent SendTask error:", _t10); Lampa.Bell.push({ text: Lampa.Lang.translate("actionReturnedError") }); throw _t10; case 4: return _context7.a(2); } }, _callee7, null, [[1, 3]]); })); return _SendTask$1.apply(this, arguments); } function setTags(_x7, _x8) { return _setTags.apply(this, arguments); } function _setTags() { _setTags = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8(torrentId, tags) { var tagValue; return _regenerator().w(function (_context8) { while (1) switch (_context8.n) { case 0: tagValue = Array.isArray(tags) ? tags.join(",") : tags; return _context8.a(2, makeRequest("/api/v2/torrents/addTags", { method: "POST", contentType: "application/x-www-form-urlencoded; charset=UTF-8", data: { hashes: torrentId, tags: tagValue } })); } }, _callee8); })); return _setTags.apply(this, arguments); } var Qbittorent = { auth: auth$1, GetData: GetData$1, GetInfo: GetInfo$1, SendCommand: SendCommand$1, SendTask: SendTask$1, setTags: setTags }; /** * Torrent Parser Utility * * This module provides functionality to parse torrent names and * fetch corresponding metadata from TMDB. */ /** * Clean torrent name by removing quality, year, and other technical information * * @param {string} name - Original torrent name * @returns {string} - Cleaned name suitable for metadata search */ function cleanName(name) { if (!name) return { query: '', year: null }; // Regular expression to extract the main title from torrent name // Removes season/episode markers, year, quality indicators, etc. var regex = /*#__PURE__*/_wrapRegExp(/^(.+?)(?:[.\s(](19\d{2}|20\d{2})[.\s)]|S\d{1,2}(?:E\d{1,2})?|[.\s](?:PPV.)?[HP]DTV|(?:HD)?TC|[cC]am|(?:HD)?CAM|B[rR]Rip|WEBRip|WEB-Rip|WEB-DL|WEB|TS|(?:PPV )?WEB-?DL(?: DVDRip)?|H[dD]Rip|DVDRip|DVDRiP|DVDRIP|CamRip|W[EB]B[rR]ip|HDRIP|[Bb]lu[Rr]ay|DvDScr|hdtv)/i, { title: 1, year: 2 }); var match = name.match(regex); // If we have a match with a title group, use it if (match && match.groups && match.groups.title) { // Replace dots between words with spaces return { query: match.groups.title.replace(/\./g, ' ').trim(), year: match.groups.year || null }; } // Fallback: just replace dots with spaces return { query: name.replace(/\./g, ' ').trim(), year: null }; } /** * Search for media metadata on TMDB * * @param {string} query - Search query * @returns {Promise} - Promise resolving to TMDB response */ function searchTMDB(_x) { return _searchTMDB.apply(this, arguments); } /** * Handle case when multiple results are found * * @param {Array} results - Array of TMDB results * @returns {Promise} - Promise resolving to selected media info */ function _searchTMDB() { _searchTMDB = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(query) { var tmdbLang, url, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: tmdbLang = Lampa.Storage.field('tmdb_lang') || Lampa.Storage.get('language') || 'en'; url = Lampa.TMDB.api("search/multi?include_adult=true&query=".concat(encodeURIComponent(query), "&api_key=").concat(Lampa.TMDB.key(), "&language=").concat(tmdbLang)); _context.p = 1; _context.n = 2; return new Promise(function (resolve, reject) { Lampa.Network.silent(url, resolve, reject, null, { timeout: 10000 }); }); case 2: return _context.a(2, _context.v); case 3: _context.p = 3; _t = _context.v; console.error('TMDB', 'search error:', _t); throw new Error("Failed to fetch data from TMDB: ".concat(_t.statusText || _t.message)); case 4: return _context.a(2); } }, _callee, null, [[1, 3]]); })); return _searchTMDB.apply(this, arguments); } function handleMultipleResults(results) { return new Promise(function (resolve, reject) { var enabled = Lampa.Controller.enabled().name; // Format menu items with title and date information var menuItems = results.map(function (result) { return { title: "".concat(result.title || result.name, " (").concat(result.media_type, ")") + (result.media_type === 'tv' && result.first_air_date ? " - ".concat(result.first_air_date) : '') + (result.media_type === 'movie' && result.release_date ? " - ".concat(result.release_date) : ''), media_type: result.media_type, id: result.id }; }); // Show selection menu Lampa.Select.show({ title: Lampa.Lang.translate('selectResult'), items: menuItems, onBack: function onBack() { // Повертаємось до попереднього контролера, якщо він існує var currentController = Lampa.Controller.enabled(); if (currentController && currentController.name !== enabled) { Lampa.Controller.toggle(enabled); } else { // Якщо контролер не змінився, просто вимикаємо меню Lampa.Controller.toggle('menu'); } reject(new Error('Selection canceled by user')); }, onSelect: function onSelect(selected) { resolve({ media_type: selected.media_type, id: selected.id }); } }); }); } /** * Process torrents to find metadata * * @param {Object|Array} torrentData - Torrent data to process * @returns {Promise} - Promise resolving to array of media info */ function processTorrents$1(_x2) { return _processTorrents.apply(this, arguments); } function _processTorrents() { _processTorrents = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(torrentData) { var torrents, results, _t3; return _regenerator().w(function (_context3) { while (1) switch (_context3.p = _context3.n) { case 0: // Ensure we're working with an array torrents = Array.isArray(torrentData) ? torrentData : [torrentData]; _context3.p = 1; _context3.n = 2; return Promise.all(torrents.map(/*#__PURE__*/function () { var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(torrent) { var name, cleanedNameInfo, response, strictMatches, _t2; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: _context2.p = 0; // Clean the torrent name name = torrent.name || torrent; cleanedNameInfo = cleanName(name); if (cleanedNameInfo.query) { _context2.n = 1; break; } console.warn('TDM', 'Empty torrent name after cleaning'); return _context2.a(2, null); case 1: _context2.n = 2; return searchTMDB(cleanedNameInfo.query); case 2: response = _context2.v; if (!(!response.results || response.results.length === 0)) { _context2.n = 3; break; } return _context2.a(2, null); case 3: if (!(response.results.length === 1)) { _context2.n = 4; break; } return _context2.a(2, response.results[0]); case 4: if (!cleanedNameInfo.year) { _context2.n = 5; break; } strictMatches = response.results.filter(function (r) { var rYear = (r.release_date || r.first_air_date || '').slice(0, 4); return rYear === cleanedNameInfo.year; }); if (!(strictMatches.length === 1)) { _context2.n = 5; break; } return _context2.a(2, strictMatches[0]); case 5: _context2.n = 6; return handleMultipleResults(response.results); case 6: return _context2.a(2, _context2.v); case 7: _context2.p = 7; _t2 = _context2.v; console.error('TDM', 'torrent processing error:', _t2); return _context2.a(2, null); } }, _callee2, null, [[0, 7]]); })); return function (_x3) { return _ref.apply(this, arguments); }; }())); case 2: results = _context3.v; return _context3.a(2, results.filter(function (result) { return result !== null; })); case 3: _context3.p = 3; _t3 = _context3.v; console.error('TDM', 'torrent processing overall error:', _t3); throw _t3; case 4: return _context3.a(2); } }, _callee3, null, [[1, 3]]); })); return _processTorrents.apply(this, arguments); } var DEFAULT_RPC_PATH = "/transmission/rpc"; var DEFAULT_STATUS_MAP = { 0: "Stopped", 1: "Queued to verify local data", 2: "Verifying local data", 3: "Queued to download", 4: "Downloading", 5: "Queued to seed", 6: "Seeding" }; function createTransmissionLikeClient() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var configPrefix = options.configPrefix; var clientName = options.clientName || "Transmission-like"; var statusMap = options.statusMap || DEFAULT_STATUS_MAP; var extraFields = Array.isArray(options.extraFields) ? options.extraFields : []; var getExtraConfig = typeof options.getExtraConfig === "function" ? options.getExtraConfig : function () { return {}; }; var mapTorrentData = typeof options.mapTorrentData === "function" ? options.mapTorrentData : null; var handleCustomAction = typeof options.handleCustomAction === "function" ? options.handleCustomAction : null; if (!configPrefix) { throw new Error("configPrefix is required for transmissionLikeClient"); } function getConfig() { return _objectSpread2({ url: sanitizeConfigString(Lampa.Storage.field("".concat(configPrefix, "Url"))), user: sanitizeConfigString(Lampa.Storage.field("".concat(configPrefix, "User"))), pass: sanitizeConfigString(Lampa.Storage.field("".concat(configPrefix, "Pass"))), path: normalizeApiPath(Lampa.Storage.get("".concat(configPrefix, "Path")), DEFAULT_RPC_PATH), useProxy: Lampa.Storage.field("".concat(configPrefix, "Proxy")) === true, autostart: Lampa.Storage.field("".concat(configPrefix, "Autostart")), sequentialDownload: Lampa.Storage.field("".concat(configPrefix, "SequentialDownload")), proxy: LME_PROXY_URL }, getExtraConfig(configPrefix)); } function getHeaders() { var config = getConfig(); var headers = { Authorization: "Basic ".concat(btoa((config.user || "") + ":" + (config.pass || ""))), "Content-Type": "application/json" }; var sessionId = Lampa.Storage.get("".concat(configPrefix, "Key")); if (sessionId) { headers["X-Transmission-Session-Id"] = sessionId; } if (config.useProxy) { headers["x-requested-with"] = "lme-plugins"; } return headers; } function getApiUrl() { var config = getConfig(); return "".concat(resolveClientBaseUrl(config, { clientName: clientName })).concat(config.path); } function reguest(headers, body, timeout) { return new Promise(function (resolve, reject) { var network = new Lampa.Reguest(); network.timeout(timeout); network.quiet(getApiUrl(), resolve, reject, body, { headers: headers, type: "POST", dataType: "json" }); }); } function makeRequest(_x) { return _makeRequest.apply(this, arguments); } function _makeRequest() { _makeRequest = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(data) { var timeout, body, sessionId, _args = arguments, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: timeout = _args.length > 1 && _args[1] !== undefined ? _args[1] : 10000; body = JSON.stringify(data); _context.p = 1; _context.n = 2; return reguest(getHeaders(), body, timeout); case 2: return _context.a(2, _context.v); case 3: _context.p = 3; _t = _context.v; if (!(_t.status === 409)) { _context.n = 4; break; } sessionId = _t.getResponseHeader ? _t.getResponseHeader("X-Transmission-Session-Id") : null; if (!sessionId) { _context.n = 4; break; } Lampa.Storage.set("".concat(configPrefix, "Key"), sessionId); return _context.a(2, reguest(getHeaders(), body, timeout)); case 4: throw _t; case 5: return _context.a(2); } }, _callee, null, [[1, 3]]); })); return _makeRequest.apply(this, arguments); } function normalizeRequestError(error, fallbackMessage) { var message = error && (error.statusText || error.message) ? error.statusText || error.message : fallbackMessage; var normalized = new Error(String(message || fallbackMessage)); if (error && typeof error.status !== "undefined") { normalized.status = error.status; } // Preserve diagnostic fields from jqXHR if (error && error.decode_error) { normalized.decode_error = error.decode_error; } if (error && error.error_time) { normalized.error_time = error.error_time; } return normalized; } function setLabels(_x2, _x3) { return _setLabels.apply(this, arguments); } function _setLabels() { _setLabels = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(torrentId, labels) { var labelList; return _regenerator().w(function (_context2) { while (1) switch (_context2.n) { case 0: labelList = Array.isArray(labels) ? labels : [labels]; _context2.n = 1; return makeRequest({ method: "torrent-set", arguments: { ids: [torrentId], labels: labelList } }); case 1: return _context2.a(2); } }, _callee2); })); return _setLabels.apply(this, arguments); } function auth() { return _auth.apply(this, arguments); } function _auth() { _auth = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() { var showNotification, sessionId, status, _args3 = arguments, _t2; return _regenerator().w(function (_context3) { while (1) switch (_context3.p = _context3.n) { case 0: showNotification = _args3.length > 0 && _args3[0] !== undefined ? _args3[0] : true; _context3.p = 1; _context3.n = 2; return makeRequest({ method: "session-get" }); case 2: if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate("AuthSuccess") }); } _context3.n = 5; break; case 3: _context3.p = 3; _t2 = _context3.v; if (!(_t2.status === 409)) { _context3.n = 4; break; } sessionId = _t2.getResponseHeader ? _t2.getResponseHeader("X-Transmission-Session-Id") : null; if (!sessionId) { _context3.n = 4; break; } Lampa.Storage.set("".concat(configPrefix, "Key"), sessionId); if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate("AuthSuccess") }); } return _context3.a(2); case 4: console.error("TDM", "".concat(clientName, " authentication failed"), _t2); if (showNotification) { status = Number(_t2 && _t2.status); if (status === 0) { // Network connection error (server unreachable) Lampa.Bell.push({ text: Lampa.Lang.translate("torrentmanager_connection_error") }); } else if (status === 401 || status === 403) { // Authentication denied Lampa.Bell.push({ text: Lampa.Lang.translate("AuthDenied") }); } else if (status >= 500) { // Server-side error Lampa.Bell.push({ text: Lampa.Lang.translate("torrentmanager_server_error") }); } else { // Fallback Lampa.Bell.push({ text: Lampa.Lang.translate("AuthDenied") }); } } throw _t2; case 5: return _context3.a(2); } }, _callee3, null, [[1, 3]]); })); return _auth.apply(this, arguments); } function GetData() { return _GetData.apply(this, arguments); } function _GetData() { _GetData = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5() { var fields, response, _t0; return _regenerator().w(function (_context5) { while (1) switch (_context5.p = _context5.n) { case 0: _context5.p = 0; fields = ["labels", "status", "totalSize", "percentComplete", "percentDone", "name", "id"].concat(_toConsumableArray(extraFields)); _context5.n = 1; return makeRequest({ method: "torrent-get", arguments: { fields: fields } }); case 1: response = _context5.v; return _context5.a(2, Promise.all(response.arguments.torrents.map(/*#__PURE__*/function () { var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(torrent) { var baseItem, extraData, _t3, _t4, _t5, _t6, _t7, _t8, _t9; return _regenerator().w(function (_context4) { while (1) switch (_context4.n) { case 0: _t3 = torrent.name; _t4 = torrent.id; _t5 = torrent.totalSize; _t6 = statusMap[torrent.status]; _t7 = torrent.labels; _context4.n = 1; return getPosterFromLabels(torrent.labels); case 1: _t8 = _context4.v; _t9 = torrent.percentDone; baseItem = { name: _t3, id: _t4, size: _t5, state: _t6, labels: _t7, image: _t8, completed: _t9 }; if (mapTorrentData) { _context4.n = 2; break; } return _context4.a(2, baseItem); case 2: extraData = mapTorrentData(torrent, baseItem, getConfig()) || {}; return _context4.a(2, _objectSpread2(_objectSpread2({}, baseItem), extraData)); } }, _callee4); })); return function (_x11) { return _ref.apply(this, arguments); }; }()))); case 2: _context5.p = 2; _t0 = _context5.v; console.error("TDM", "".concat(clientName, " GetData error:"), _t0); throw normalizeRequestError(_t0, "Failed to fetch torrent data"); case 3: return _context5.a(2); } }, _callee5, null, [[0, 2]]); })); return _GetData.apply(this, arguments); } function GetInfo() { return _GetInfo.apply(this, arguments); } function _GetInfo() { _GetInfo = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6() { var response, _t1; return _regenerator().w(function (_context6) { while (1) switch (_context6.p = _context6.n) { case 0: _context6.p = 0; _context6.n = 1; return makeRequest({ method: "session-get" }); case 1: response = _context6.v; return _context6.a(2, { space: response.arguments["download-dir-free-space"] }); case 2: _context6.p = 2; _t1 = _context6.v; console.error("TDM", "".concat(clientName, " GetInfo error:"), _t1); throw normalizeRequestError(_t1, "Failed to fetch session info"); case 3: return _context6.a(2); } }, _callee6, null, [[0, 2]]); })); return _GetInfo.apply(this, arguments); } function handleParseAction(_x4, _x5) { return _handleParseAction.apply(this, arguments); } function _handleParseAction() { _handleParseAction = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(action, torrentData) { var response, mediaTypeId; return _regenerator().w(function (_context7) { while (1) switch (_context7.n) { case 0: if (!(action.action !== "parse" && action.action !== "parse-all")) { _context7.n = 1; break; } return _context7.a(2, false); case 1: _context7.n = 2; return processTorrents$1(torrentData); case 2: response = _context7.v; if (!(!response || response.length === 0)) { _context7.n = 3; break; } throw new Error("No valid metadata found"); case 3: mediaTypeId = "".concat(response[0].media_type, "/").concat(response[0].id); _context7.n = 4; return makeRequest({ method: "torrent-set", arguments: { ids: torrentData.id, labels: [mediaTypeId] } }); case 4: Lampa.Bell.push({ text: Lampa.Lang.translate("actionSentSuccessfully") }); return _context7.a(2, true); } }, _callee7); })); return _handleParseAction.apply(this, arguments); } function handleDefaultCommand(_x6, _x7) { return _handleDefaultCommand.apply(this, arguments); } function _handleDefaultCommand() { _handleDefaultCommand = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8(action, torrentData) { var method, _t10; return _regenerator().w(function (_context8) { while (1) switch (_context8.n) { case 0: method = ""; _t10 = action.action; _context8.n = _t10 === "resume" ? 1 : _t10 === "pause" ? 2 : _t10 === "delete" ? 3 : 4; break; case 1: method = "torrent-start"; return _context8.a(3, 5); case 2: method = "torrent-stop"; return _context8.a(3, 5); case 3: method = "torrent-remove"; return _context8.a(3, 5); case 4: throw new Error("Unknown action: ".concat(action.action)); case 5: _context8.n = 6; return makeRequest({ method: method, arguments: { ids: torrentData.id, "delete-local-data": action.deleteFiles ? true : false } }); case 6: Lampa.Bell.push({ text: Lampa.Lang.translate("actionSentSuccessfully") }); case 7: return _context8.a(2); } }, _callee8); })); return _handleDefaultCommand.apply(this, arguments); } function SendCommand(_x8, _x9) { return _SendCommand.apply(this, arguments); } function _SendCommand() { _SendCommand = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9(action, torrentData) { var parsed, handled, _t11; return _regenerator().w(function (_context9) { while (1) switch (_context9.p = _context9.n) { case 0: _context9.p = 0; _context9.n = 1; return handleParseAction(action, torrentData); case 1: parsed = _context9.v; if (!parsed) { _context9.n = 2; break; } return _context9.a(2); case 2: if (!handleCustomAction) { _context9.n = 4; break; } _context9.n = 3; return handleCustomAction({ action: action, torrentData: torrentData, getConfig: getConfig, makeRequest: makeRequest, statusMap: statusMap }); case 3: handled = _context9.v; if (!handled) { _context9.n = 4; break; } return _context9.a(2); case 4: _context9.n = 5; return handleDefaultCommand(action, torrentData); case 5: _context9.n = 7; break; case 6: _context9.p = 6; _t11 = _context9.v; console.error("TDM", "".concat(clientName, " SendCommand error:"), _t11); Lampa.Bell.push({ text: Lampa.Lang.translate("actionReturnedError") }); throw _t11; case 7: return _context9.a(2); } }, _callee9, null, [[0, 6]]); })); return _SendCommand.apply(this, arguments); } function SendTask(_x0, _x1, _x10) { return _SendTask.apply(this, arguments); } function _SendTask() { _SendTask = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0(selectedTorrent, labels, dtype) { var config, addResponse, torrentId, labelResponse, _t12; return _regenerator().w(function (_context0) { while (1) switch (_context0.p = _context0.n) { case 0: if (selectedTorrent) { _context0.n = 1; break; } throw new Error("No torrent selected"); case 1: config = getConfig(); _context0.p = 2; _context0.n = 3; return makeRequest({ method: "torrent-add", arguments: _objectSpread2({ paused: config.autostart ? true : false, sequentialDownload: config.sequentialDownload ? true : false, filename: selectedTorrent.MagnetUri || selectedTorrent.Link, labels: [labels] }, Lampa.Storage.get("".concat(configPrefix).concat(dtype)) ? { "download-dir": Lampa.Storage.get("".concat(configPrefix).concat(dtype)) } : {}) }); case 3: addResponse = _context0.v; if (!(addResponse.result !== "success" || Object.keys(addResponse.arguments).length === 0)) { _context0.n = 4; break; } throw new Error("Failed to add torrent: ".concat(addResponse.result)); case 4: torrentId = addResponse.arguments["torrent-added"] && addResponse.arguments["torrent-added"].id; if (torrentId) { _context0.n = 5; break; } throw new Error("Torrent added but no ID returned"); case 5: _context0.n = 6; return makeRequest({ method: "torrent-set", arguments: { ids: torrentId, labels: [labels] } }); case 6: labelResponse = _context0.v; if (!(labelResponse.result !== "success")) { _context0.n = 7; break; } throw new Error("Failed to set labels: ".concat(labelResponse.result)); case 7: Lampa.Bell.push({ text: Lampa.Lang.translate("actionSentSuccessfully") }); _context0.n = 9; break; case 8: _context0.p = 8; _t12 = _context0.v; console.error("TDM", "".concat(clientName, " SendTask error:"), _t12); Lampa.Bell.push({ text: _t12.message || Lampa.Lang.translate("actionReturnedError") }); throw _t12; case 9: return _context0.a(2); } }, _callee0, null, [[2, 8]]); })); return _SendTask.apply(this, arguments); } return { auth: auth, GetData: GetData, GetInfo: GetInfo, SendCommand: SendCommand, SendTask: SendTask, setLabels: setLabels, transmissionStatus: statusMap }; } var Transmission = createTransmissionLikeClient({ configPrefix: "lmetorrenttransmission", clientName: "Transmission" }); /** * Генерує зображення з текстом за допомогою Canvas API. * @param {string} text - Текст для відображення на зображенні. * @returns {string} - URL зображення у форматі data:image/png;base64. */ function textToImage(text) { var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var width = 400; var height = 600; canvas.width = width; canvas.height = height; // Фон context.fillStyle = '#1a202c'; // Темно-сірий фон context.fillRect(0, 0, width, height); // Налаштування тексту context.fillStyle = '#ffffff'; // Білий колір тексту context.font = 'bold 48px Arial'; context.textAlign = 'center'; context.textBaseline = 'middle'; // Розбивка тексту на рядки // Спочатку розбиваємо по наявних переносах рядків var initialLines = text.split('\n'); var lines = []; // Функція для розбиття довгого слова на частини function breakLongWord(word, maxWidth) { var brokenLines = []; var currentPart = ''; for (var i = 0; i < word.length; i++) { var _char = word[i]; var testPart = currentPart + _char; var metrics = context.measureText(testPart); var testWidth = metrics.width; if (testWidth > maxWidth && currentPart !== '') { brokenLines.push(currentPart); currentPart = _char; } else { currentPart = testPart; } } if (currentPart !== '') { brokenLines.push(currentPart); } return brokenLines; } // Потім для кожного рядка застосовуємо переноси за шириною initialLines.forEach(function (initialLine) { var words = initialLine.split(' '); var currentLine = ''; for (var i = 0; i < words.length; i++) { var word = words[i]; // Перевіряємо, чи слово не занадто довге var wordMetrics = context.measureText(word); if (wordMetrics.width > width - 40) { // Якщо є поточний рядок, додаємо його до списку if (currentLine !== '') { lines.push(currentLine); currentLine = ''; } // Розбиваємо довге слово на частини var brokenWordLines = breakLongWord(word, width - 40); lines.push.apply(lines, _toConsumableArray(brokenWordLines)); } else { // Слово не занадто довге, обробляємо звичайним чином var testLine = currentLine === '' ? word : currentLine + ' ' + word; var metrics = context.measureText(testLine); var testWidth = metrics.width; if (testWidth > width - 40 && currentLine !== '') { lines.push(currentLine); currentLine = word; } else { currentLine = testLine; } } } // Додаємо останній рядок, якщо він не порожній if (currentLine !== '') { lines.push(currentLine); } }); // Відображення тексту var lineHeight = 58; var startY = (height - lines.length * lineHeight) / 2 + lineHeight / 2; lines.forEach(function (line, index) { context.fillText(line, width / 2, startY + index * lineHeight); }); return canvas.toDataURL('image/png'); } /** * @file IndexedDB утиліти для зберігання метаданих торрентів. * Інкапсулює логіку роботи з Lampa.DB (IndexedDB). */ var DB_NAME = 'lme_torrentmanager'; var DB_VERSION = 4; // Збільшено версію для гарантованого оновлення /** * Отримує список всіх можливих імен таблиць для всіх клієнтів. * @returns {string[]} Масив імен таблиць. */ function getAllTableNames() { var clients = ['synology', 'qbittorent', 'transmission', 'keenetic', 'universal']; // Додайте сюди інших клієнтів за потреби return clients.map(function (client) { return "".concat(client, "_metadata"); }); } /** * Допоміжна функція для отримання назви таблиці (сховища) на основі активного клієнта. * @returns {string} Назва таблиці, напр. 'synology_metadata'. */ function getTableName() { var client = Lampa.Storage.field('lmetorrentSelect'); if (!client) { console.error('TDM', 'Активний клієнт не вибрано!'); return 'default_metadata'; } return "".concat(client.toLowerCase(), "_metadata"); } var dbPromise = null; /** * Ініціалізує та відкриває IndexedDB, використовуючи патерн singleton promise. * @returns {Promise} Promise, який вирішується об'єктом бази даних Lampa.DB. */ function initDB() { if (dbPromise) { return dbPromise; } dbPromise = new Promise(/*#__PURE__*/function () { var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(resolve, reject) { var allTables, db, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: if (Lampa.DB) { _context.n = 1; break; } console.error('TDM', 'Lampa.DB не визначено.'); dbPromise = null; // Скидаємо для повторної спроби return _context.a(2, reject(new Error('Lampa.DB is not defined'))); case 1: allTables = getAllTableNames(); db = new Lampa.DB(DB_NAME, allTables, DB_VERSION); _context.p = 2; _context.n = 3; return db.openDatabase(); case 3: resolve(db); _context.n = 5; break; case 4: _context.p = 4; _t = _context.v; console.error("TDM", "Critical error '".concat(DB_NAME, "':"), _t); dbPromise = null; // Скидаємо для повторної спроби reject(_t); case 5: return _context.a(2); } }, _callee, null, [[2, 4]]); })); return function (_x, _x2) { return _ref.apply(this, arguments); }; }()); return dbPromise; } /** * Зберігає метадані для ключа. * @param {string} key - Ключ (ідентифікатор торренту). * @param {any} value - Значення для збереження. * @returns {Promise} */ function saveMetadata(_x3, _x4) { return _saveMetadata.apply(this, arguments); } /** * Отримує метадані за ключем. * @param {string} key - Ключ (ідентифікатор торренту). * @returns {Promise} */ function _saveMetadata() { _saveMetadata = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(key, value) { var db, tableName, _t2; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: _context2.p = 0; _context2.n = 1; return initDB(); case 1: db = _context2.v; tableName = getTableName(); _context2.n = 2; return db.rewriteData(tableName, key, value); case 2: _context2.n = 4; break; case 3: _context2.p = 3; _t2 = _context2.v; console.error("TDM", "Error in saveMetadata:", _t2); throw _t2; case 4: return _context2.a(2); } }, _callee2, null, [[0, 3]]); })); return _saveMetadata.apply(this, arguments); } function getMetadata(_x5) { return _getMetadata.apply(this, arguments); } /** * Search metadata by TMDB ID across all stored records. * Uses getData with no key to retrieve all entries, then filters by tmdb_id. * @param {number|string} tmdbId - TMDB ID to search for * @returns {Promise} - Found metadata record or null */ function _getMetadata() { _getMetadata = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(key) { var db, tableName, result, _t3; return _regenerator().w(function (_context3) { while (1) switch (_context3.p = _context3.n) { case 0: _context3.p = 0; _context3.n = 1; return initDB(); case 1: db = _context3.v; tableName = getTableName(); _context3.n = 2; return db.getData(tableName, key); case 2: result = _context3.v; if (!result) { _context3.n = 3; break; } return _context3.a(2, result); case 3: return _context3.a(2, null); case 4: _context3.n = 6; break; case 5: _context3.p = 5; _t3 = _context3.v; console.error("TDM", "Error in getMetadata:", _t3); throw _t3; case 6: return _context3.a(2); } }, _callee3, null, [[0, 5]]); })); return _getMetadata.apply(this, arguments); } function getMetadataByTmdbId(_x6) { return _getMetadataByTmdbId.apply(this, arguments); } /** * Check if cached metadata is still fresh (within TTL). * @param {string} key - Cache key (torrent identifier) * @param {number} ttlHours - Time to live in hours (default: 24) * @returns {Promise} */ function _getMetadataByTmdbId() { _getMetadataByTmdbId = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(tmdbId) { var db, tableName, allRecords, _t4; return _regenerator().w(function (_context4) { while (1) switch (_context4.p = _context4.n) { case 0: _context4.p = 0; _context4.n = 1; return initDB(); case 1: db = _context4.v; tableName = getTableName(); _context4.n = 2; return db.getData(tableName, null); case 2: allRecords = _context4.v; if (!(!allRecords || !allRecords.length)) { _context4.n = 3; break; } return _context4.a(2, null); case 3: return _context4.a(2, allRecords.find(function (r) { return r.tmdb_id === tmdbId; }) || null); case 4: _context4.p = 4; _t4 = _context4.v; console.error('TDM', 'Error in getMetadataByTmdbId:', _t4); return _context4.a(2, null); } }, _callee4, null, [[0, 4]]); })); return _getMetadataByTmdbId.apply(this, arguments); } var SIMKL_SEARCH_URL = "".concat(LME_SIMKL_URL, "search/file"); function parseResponse$2(response) { if (!response) { return null; } if (typeof response === 'string') { try { return JSON.parse(response); } catch (error) { return null; } } if (_typeof(response) !== 'object') { return null; } return response; } function normalizeTmdbId(value) { var id = String(value || '').trim(); if (!/^\d+$/.test(id)) { return null; } var numericId = Number(id); return Number.isFinite(numericId) && numericId > 0 ? numericId : null; } function normalizeMoviePayload(payload) { var movieIds = payload && payload.movie && payload.movie.ids ? payload.movie.ids : {}; var tmdbId = normalizeTmdbId(movieIds.moviedb || movieIds.tmdb); if (!tmdbId) { return null; } return { media_type: 'movie', tmdb_id: tmdbId }; } function normalizeShowPayload(payload) { var showIds = payload && payload.show && payload.show.ids ? payload.show.ids : {}; var tmdbId = normalizeTmdbId(showIds.tmdbtv || showIds.tmdb); if (!tmdbId) { return null; } return { media_type: 'tv', tmdb_id: tmdbId }; } function normalizeSimklPayload(payload) { var type = String(payload && payload.type || '').toLowerCase(); if (type === 'movie') { return normalizeMoviePayload(payload); } if (type === 'episode' || type === 'show') { return normalizeShowPayload(payload); } return null; } function buildRequestOptions() { return { timeout: 10000, type: 'POST', dataType: 'json', headers: { Accept: 'application/json', 'Content-Type': 'application/json' } }; } function requestSearchByFileName(_x) { return _requestSearchByFileName.apply(this, arguments); } function _requestSearchByFileName() { _requestSearchByFileName = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(fileName) { return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: return _context.a(2, new Promise(function (resolve, reject) { Lampa.Network.silent(SIMKL_SEARCH_URL, resolve, reject, JSON.stringify({ file: fileName, part: 1 }), buildRequestOptions()); })); } }, _callee); })); return _requestSearchByFileName.apply(this, arguments); } function resolveTmdbFromSimkl(_x2) { return _resolveTmdbFromSimkl.apply(this, arguments); } function _resolveTmdbFromSimkl() { _resolveTmdbFromSimkl = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(fileName) { var normalizedFileName, response, payload, _t; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: normalizedFileName = String(fileName || '').trim(); if (normalizedFileName) { _context2.n = 1; break; } return _context2.a(2, null); case 1: _context2.p = 1; _context2.n = 2; return requestSearchByFileName(normalizedFileName); case 2: response = _context2.v; payload = parseResponse$2(response); if (payload) { _context2.n = 3; break; } return _context2.a(2, null); case 3: return _context2.a(2, normalizeSimklPayload(payload)); case 4: _context2.p = 4; _t = _context2.v; console.log('TDM', 'Simkl search error:', _t); return _context2.a(2, null); } }, _callee2, null, [[1, 4]]); })); return _resolveTmdbFromSimkl.apply(this, arguments); } var CONFIG_PREFIX = 'lmetorrentsynology'; var AUTH_ERROR_CODES = [105, 106, 107, 119]; var SIMKL_RETRY_INTERVAL = 1000 * 60 * 60 * 12; var simklSearchCache = {}; function getConfig() { return { url: sanitizeConfigString(Lampa.Storage.field("".concat(CONFIG_PREFIX, "Url"))), user: sanitizeConfigString(Lampa.Storage.field("".concat(CONFIG_PREFIX, "User"))), pass: sanitizeConfigString(Lampa.Storage.field("".concat(CONFIG_PREFIX, "Pass"))), useProxy: Lampa.Storage.field("".concat(CONFIG_PREFIX, "Proxy")) === true, proxy: LME_PROXY_URL }; } function getApiBase() { return resolveClientBaseUrl(getConfig(), { clientName: 'Synology' }); } function getHeaders() { var config = getConfig(); var headers = { Authorization: "Basic ".concat(btoa((config.user || '') + ':' + (config.pass || ''))), 'Content-Type': 'application/json' }; if (config.useProxy) { headers['x-requested-with'] = 'lme-plugins'; } return headers; } function buildQuery(params) { return Object.keys(params).filter(function (key) { return params[key] !== undefined && params[key] !== null; }).map(function (key) { return "".concat(encodeURIComponent(key), "=").concat(encodeURIComponent(params[key])); }).join('&'); } function makeUrl(path, params) { var query = buildQuery(params || {}); var base = getApiBase(); var urlPath = "".concat(base).concat(path); if (!query) return urlPath; return urlPath.includes('?') ? "".concat(urlPath, "&").concat(query) : "".concat(urlPath, "?").concat(query); } function parseResponse$1(response) { if (typeof response === 'string') { return JSON.parse(response); } if (!response || _typeof(response) !== 'object') { throw new Error('Invalid Synology response'); } return response; } function parseErrorPayload(error) { if (!error) { return null; } if (error.responseJSON) { return error.responseJSON; } if (typeof error.responseText === 'string' && error.responseText.trim()) { try { return JSON.parse(error.responseText); } catch (e) { return null; } } return null; } function getSynologyErrorCode(payload) { if (!payload || payload.success !== false || !payload.error) { return null; } var code = Number(payload.error.code); return Number.isNaN(code) ? null : code; } function createSynologyError(message, code) { var error = new Error(message); if (code !== undefined && code !== null) { error.code = Number(code); } return error; } function clearSid() { Lampa.Storage.set("".concat(CONFIG_PREFIX, "Key"), ''); } function saveSid(sid) { Lampa.Storage.set("".concat(CONFIG_PREFIX, "Key"), { sID: sid, createDate: Math.floor(Date.now() / 1000) }); } function getSid() { var session = Lampa.Storage.field("".concat(CONFIG_PREFIX, "Key")); if (session && _typeof(session) === 'object' && session.sID) { return session.sID; } return ''; } var authPromise = null; function auth() { return _auth.apply(this, arguments); } function _auth() { _auth = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() { var showNotification, _args2 = arguments; return _regenerator().w(function (_context2) { while (1) switch (_context2.n) { case 0: showNotification = _args2.length > 0 && _args2[0] !== undefined ? _args2[0] : true; if (!authPromise) { _context2.n = 1; break; } return _context2.a(2, authPromise); case 1: authPromise = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() { var config, response, payload, code, _payload, _code, normalizedError, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: config = getConfig(); if (!(!config.url || !config.user || !config.pass)) { _context.n = 1; break; } clearSid(); if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate('AuthDenied') }); } authPromise = null; throw createSynologyError('Synology credentials are not configured'); case 1: _context.p = 1; _context.n = 2; return new Promise(function (resolve, reject) { var network = new Lampa.Reguest(); network.quiet(makeUrl('/webapi/auth.cgi', { api: 'SYNO.API.Auth', method: 'login', account: config.user, passwd: config.pass, format: 'sid', version: 7, session: 'LMETorrentManager' }), resolve, reject, null, { headers: getHeaders(), type: 'GET', dataType: 'json' }); }); case 2: response = _context.v; payload = parseResponse$1(response); code = getSynologyErrorCode(payload); if (!(code !== null || !payload.data || !payload.data.sid)) { _context.n = 3; break; } throw createSynologyError("Synology auth error: ".concat(code), code); case 3: saveSid(payload.data.sid); if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate('AuthSuccess') }); } authPromise = null; return _context.a(2, payload.data.sid); case 4: _context.p = 4; _t = _context.v; clearSid(); _payload = parseErrorPayload(_t); _code = getSynologyErrorCode(_payload); normalizedError = _code !== null ? createSynologyError("Synology auth error: ".concat(_code), _code) : _t instanceof Error ? _t : createSynologyError('Synology auth failed'); console.log('TDM', 'Synology auth', normalizedError.code || '', normalizedError); if (showNotification) { Lampa.Bell.push({ text: Lampa.Lang.translate('AuthDenied') }); } authPromise = null; throw normalizedError; case 5: return _context.a(2); } }, _callee, null, [[1, 4]]); }))(); return _context2.a(2, authPromise); } }, _callee2); })); return _auth.apply(this, arguments); } function requestSynology(_x) { return _requestSynology.apply(this, arguments); } function _requestSynology() { _requestSynology = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(path) { var params, options, requestOptions, requestParams, sid, response, payload, code, _payload2, _code2, _args3 = arguments, _t2; return _regenerator().w(function (_context3) { while (1) switch (_context3.p = _context3.n) { case 0: params = _args3.length > 1 && _args3[1] !== undefined ? _args3[1] : {}; options = _args3.length > 2 && _args3[2] !== undefined ? _args3[2] : {}; requestOptions = { method: options.method || 'GET', requiresSid: options.requiresSid !== false, retryOnAuthError: options.retryOnAuthError !== false }; requestParams = _objectSpread2({}, params); if (!requestOptions.requiresSid) { _context3.n = 4; break; } sid = getSid(); if (sid) { _context3.n = 2; break; } _context3.n = 1; return auth(false); case 1: sid = getSid(); case 2: if (sid) { _context3.n = 3; break; } throw createSynologyError('Synology SID is not available'); case 3: requestParams._sid = sid; case 4: _context3.p = 4; _context3.n = 5; return new Promise(function (resolve, reject) { var network = new Lampa.Reguest(); network.quiet(makeUrl(path, requestParams), resolve, reject, null, { headers: getHeaders(), type: requestOptions.method, dataType: 'json' }); }); case 5: response = _context3.v; payload = parseResponse$1(response); code = getSynologyErrorCode(payload); if (!(code !== null && AUTH_ERROR_CODES.includes(code) && requestOptions.requiresSid && requestOptions.retryOnAuthError)) { _context3.n = 7; break; } if (getSid() === requestParams._sid) { clearSid(); } _context3.n = 6; return auth(false); case 6: return _context3.a(2, requestSynology(path, params, _objectSpread2(_objectSpread2({}, requestOptions), {}, { retryOnAuthError: false }))); case 7: if (!(code !== null)) { _context3.n = 8; break; } throw createSynologyError("Synology API error: ".concat(code), code); case 8: return _context3.a(2, payload); case 9: _context3.p = 9; _t2 = _context3.v; _payload2 = parseErrorPayload(_t2); _code2 = getSynologyErrorCode(_payload2); if (!(_code2 !== null && AUTH_ERROR_CODES.includes(_code2) && requestOptions.requiresSid && requestOptions.retryOnAuthError)) { _context3.n = 11; break; } if (getSid() === requestParams._sid) { clearSid(); } _context3.n = 10; return auth(false); case 10: return _context3.a(2, requestSynology(path, params, _objectSpread2(_objectSpread2({}, requestOptions), {}, { retryOnAuthError: false }))); case 11: throw _code2 !== null ? createSynologyError("Synology API error: ".concat(_code2), _code2) : _t2 instanceof Error ? _t2 : createSynologyError('Synology request failed'); case 12: return _context3.a(2); } }, _callee3, null, [[4, 9]]); })); return _requestSynology.apply(this, arguments); } function getFileTransferTotals(task) { var files = task && task.additional && Array.isArray(task.additional.file) ? task.additional.file : []; return files.reduce(function (acc, file) { acc.size += Number(file && file.size) || 0; acc.downloaded += Number(file && file.size_downloaded) || 0; return acc; }, { size: 0, downloaded: 0 }); } function getTaskProgress(task) { var transfer = task && task.additional && task.additional.transfer ? task.additional.transfer : {}; var fileTotals = getFileTransferTotals(task); var size = Number(task && task.size) || 0; var downloaded = Number(transfer.size_downloaded) || 0; // Synology may return size=0 or downloaded=0 for some tasks even when file stats are present. if (!size && fileTotals.size > 0) { size = fileTotals.size; } if (!downloaded && fileTotals.downloaded > 0) { downloaded = fileTotals.downloaded; } var completed = size > 0 ? downloaded / size : 0; var status = String(task && task.status || '').toLowerCase(); if ((status === 'finished' || status === 'seeding') && completed < 1) { completed = 1; } completed = Math.max(0, Math.min(1, completed)); return { size: size, completed: completed }; } function normalizeTaskState(status, completed) { var raw = String(status || ''); var lower = raw.toLowerCase(); // If progress is already full, do not keep "downloading" in UI. if (completed >= 1 && lower.indexOf('download') >= 0) { return 'Finished'; } return raw ? raw.charAt(0).toUpperCase() + raw.slice(1) : ''; } function hasMetadataReference(metadata) { return Boolean(metadata && metadata.tmdb_id && (metadata.media_type === 'movie' || metadata.media_type === 'tv')); } function shouldTrySimklLookup(metadata, title) { if (!title) { return false; } if (hasMetadataReference(metadata)) { return false; } var failedAt = Number(metadata && metadata.simkl_failed_at) || 0; if (!failedAt) { return true; } return Date.now() - failedAt > SIMKL_RETRY_INTERVAL; } function getSimklCacheKey(title) { return String(title || '').trim().toLowerCase(); } function resolveSimklWithCache(_x2) { return _resolveSimklWithCache.apply(this, arguments); } function _resolveSimklWithCache() { _resolveSimklWithCache = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(title) { var cacheKey; return _regenerator().w(function (_context4) { while (1) switch (_context4.n) { case 0: cacheKey = getSimklCacheKey(title); if (cacheKey) { _context4.n = 1; break; } return _context4.a(2, null); case 1: if (!simklSearchCache[cacheKey]) { simklSearchCache[cacheKey] = resolveTmdbFromSimkl(title)["finally"](function () { delete simklSearchCache[cacheKey]; }); } return _context4.a(2, simklSearchCache[cacheKey]); } }, _callee4); })); return _resolveSimklWithCache.apply(this, arguments); } function resolvePosterByMetadata(_x3) { return _resolvePosterByMetadata.apply(this, arguments); } function _resolvePosterByMetadata() { _resolvePosterByMetadata = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(metadata) { var label, resolvedPoster; return _regenerator().w(function (_context5) { while (1) switch (_context5.n) { case 0: if (hasMetadataReference(metadata)) { _context5.n = 1; break; } return _context5.a(2, ''); case 1: label = "".concat(metadata.media_type, "/").concat(metadata.tmdb_id); _context5.n = 2; return getPosterFromLabels(label); case 2: resolvedPoster = _context5.v; if (!(!resolvedPoster || resolvedPoster === './img/img_load.svg')) { _context5.n = 3; break; } return _context5.a(2, ''); case 3: return _context5.a(2, resolvedPoster); } }, _callee5); })); return _resolvePosterByMetadata.apply(this, arguments); } function hydrateMetadataFromSimkl(_x4, _x5, _x6) { return _hydrateMetadataFromSimkl.apply(this, arguments); } function _hydrateMetadataFromSimkl() { _hydrateMetadataFromSimkl = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(torrentId, title, metadata) { var now, currentMetadata, match, failedMetadata, nextMetadata, resolvedPoster, _t3; return _regenerator().w(function (_context6) { while (1) switch (_context6.p = _context6.n) { case 0: if (shouldTrySimklLookup(metadata, title)) { _context6.n = 1; break; } return _context6.a(2, metadata); case 1: now = Date.now(); currentMetadata = metadata && _typeof(metadata) === 'object' ? metadata : {}; _context6.n = 2; return resolveSimklWithCache(title); case 2: match = _context6.v; if (!(!match || !match.tmdb_id || !match.media_type)) { _context6.n = 4; break; } failedMetadata = _objectSpread2(_objectSpread2({}, currentMetadata), {}, { simkl_checked_at: now, simkl_failed_at: now }); _context6.n = 3; return saveMetadata(String(torrentId), failedMetadata); case 3: return _context6.a(2, failedMetadata); case 4: nextMetadata = _objectSpread2(_objectSpread2({}, currentMetadata), {}, { tmdb_id: match.tmdb_id, media_type: match.media_type, simkl_checked_at: now, simkl_failed_at: 0, updated_at: now }); _context6.p = 5; _context6.n = 6; return resolvePosterByMetadata(nextMetadata); case 6: resolvedPoster = _context6.v; if (resolvedPoster) { nextMetadata.poster = resolvedPoster; } _context6.n = 8; break; case 7: _context6.p = 7; _t3 = _context6.v; console.log('TDM', 'Synology Simkl poster resolve error:', _t3); case 8: _context6.n = 9; return saveMetadata(String(torrentId), nextMetadata); case 9: return _context6.a(2, nextMetadata); } }, _callee6, null, [[5, 7]]); })); return _hydrateMetadataFromSimkl.apply(this, arguments); } function GetData() { return _GetData.apply(this, arguments); } function _GetData() { _GetData = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() { var payload, tasks, _t5; return _regenerator().w(function (_context8) { while (1) switch (_context8.p = _context8.n) { case 0: _context8.p = 0; _context8.n = 1; return requestSynology('/webapi/DownloadStation/task.cgi', { api: 'SYNO.DownloadStation.Task', version: 3, method: 'list', additional: 'file,transfer', limit: -1 }); case 1: payload = _context8.v; tasks = payload && payload.data && Array.isArray(payload.data.tasks) ? payload.data.tasks : []; return _context8.a(2, Promise.all(tasks.map(/*#__PURE__*/function () { var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(torrent) { var metadata, title, progress, imageUrl, resolvedPoster, nextMetadata, _t4; return _regenerator().w(function (_context7) { while (1) switch (_context7.p = _context7.n) { case 0: _context7.n = 1; return getMetadata(torrent.id); case 1: metadata = _context7.v; title = torrent.title || torrent.id; progress = getTaskProgress(torrent); _context7.n = 2; return hydrateMetadataFromSimkl(torrent.id, title, metadata); case 2: metadata = _context7.v; imageUrl = metadata && metadata.poster ? metadata.poster : ''; // Backward compatibility for older metadata rows without cached poster. if (!(!imageUrl && hasMetadataReference(metadata))) { _context7.n = 8; break; } _context7.p = 3; _context7.n = 4; return resolvePosterByMetadata(metadata); case 4: resolvedPoster = _context7.v; if (!resolvedPoster) { _context7.n = 6; break; } imageUrl = resolvedPoster; nextMetadata = _objectSpread2(_objectSpread2({}, metadata), {}, { poster: resolvedPoster, updated_at: Date.now() }); _context7.n = 5; return saveMetadata(String(torrent.id), _objectSpread2({}, nextMetadata)); case 5: metadata = nextMetadata; case 6: _context7.n = 8; break; case 7: _context7.p = 7; _t4 = _context7.v; console.log('TDM', 'Synology poster resolve error:', _t4); case 8: if (!imageUrl) { imageUrl = textToImage(title); } return _context7.a(2, { name: title, id: torrent.id, size: progress.size, state: normalizeTaskState(torrent.status, progress.completed), completed: progress.completed, image: imageUrl, tmdb_id: metadata ? metadata.tmdb_id : null, media_type: metadata ? metadata.media_type : null }); } }, _callee7, null, [[3, 7]]); })); return function (_x10) { return _ref2.apply(this, arguments); }; }()))); case 2: _context8.p = 2; _t5 = _context8.v; console.error('TDM', 'GetData error:', _t5); throw new Error("\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0435 \u0434\u0430\u043D\u043D\u044B\u0445 \u0438\u043B\u0438 AJAX \u0437\u0430\u043F\u0440\u043E\u0441\u0430: ".concat(_t5.message || _t5)); case 3: return _context8.a(2); } }, _callee8, null, [[0, 2]]); })); return _GetData.apply(this, arguments); } function normalizePath(path) { if (!path || typeof path !== 'string') { return ''; } return path.startsWith('/') ? path : "/".concat(path); } function isShareMatched(sharePath, targetPath) { if (!sharePath || !targetPath) { return false; } return targetPath === sharePath || targetPath.indexOf("".concat(sharePath, "/")) === 0; } function getShareFreeSpace(share) { if (!share || !share.additional || !share.additional.volume_status || typeof share.additional.volume_status.freespace === 'undefined') { return null; } var freeSpace = Number(share.additional.volume_status.freespace); return Number.isNaN(freeSpace) ? null : freeSpace; } function GetInfo() { return _GetInfo.apply(this, arguments); } function _GetInfo() { _GetInfo = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() { var payload, shares, moviesPath, tvPath, selectedShare, _t6; return _regenerator().w(function (_context9) { while (1) switch (_context9.p = _context9.n) { case 0: _context9.p = 0; _context9.n = 1; return requestSynology('/webapi/entry.cgi', { version: 2, api: 'SYNO.FileStation.List', method: 'list_share', additional: 'volume_status' }); case 1: payload = _context9.v; shares = payload && payload.data && Array.isArray(payload.data.shares) ? payload.data.shares : []; if (shares.length) { _context9.n = 2; break; } throw new Error('Failed to fetch volume information'); case 2: moviesPath = normalizePath(Lampa.Storage.get("".concat(CONFIG_PREFIX, "PathMovies"))); tvPath = normalizePath(Lampa.Storage.get("".concat(CONFIG_PREFIX, "PathTV"))); selectedShare = null; if (moviesPath || tvPath) { selectedShare = shares.find(function (share) { var sharePath = share.path; return isShareMatched(sharePath, moviesPath) || isShareMatched(sharePath, tvPath); }) || null; } if (!selectedShare || getShareFreeSpace(selectedShare) === null) { selectedShare = shares.find(function (share) { return getShareFreeSpace(share) !== null; }) || null; } return _context9.a(2, { space: selectedShare ? getShareFreeSpace(selectedShare) : 0 }); case 3: _context9.p = 3; _t6 = _context9.v; console.error('TDM', 'Error fetching session info:', _t6); throw new Error("Failed to fetch session info: ".concat(_t6.message)); case 4: return _context9.a(2); } }, _callee9, null, [[0, 3]]); })); return _GetInfo.apply(this, arguments); } function SendCommand(_x7, _x8) { return _SendCommand.apply(this, arguments); } function _SendCommand() { _SendCommand = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0(action, torrentData) { var _t7; return _regenerator().w(function (_context0) { while (1) switch (_context0.p = _context0.n) { case 0: _context0.p = 0; _context0.n = 1; return requestSynology('/webapi/DownloadStation/task.cgi', { api: 'SYNO.DownloadStation.Task', version: 1, method: action.action, id: torrentData.id }); case 1: Lampa.Bell.push({ text: Lampa.Lang.translate('actionSentSuccessfully') }); _context0.n = 3; break; case 2: _context0.p = 2; _t7 = _context0.v; console.log('TDM', 'Send action:', _t7); Lampa.Bell.push({ text: Lampa.Lang.translate('actionReturnedError') }); throw _t7; case 3: return _context0.a(2); } }, _callee0, null, [[0, 2]]); })); return _SendCommand.apply(this, arguments); } function SendTask(_x9, _x0, _x1) { return _SendTask.apply(this, arguments); } function _SendTask() { _SendTask = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1(selectedTorrent, labels, dtype) { var sourceUri, destination, _t8; return _regenerator().w(function (_context1) { while (1) switch (_context1.p = _context1.n) { case 0: _context1.p = 0; sourceUri = selectedTorrent && (selectedTorrent.MagnetUri || selectedTorrent.Link); if (sourceUri) { _context1.n = 1; break; } throw new Error('No torrent URI provided'); case 1: destination = Lampa.Storage.get("".concat(CONFIG_PREFIX, "Path").concat(dtype)) || ''; _context1.n = 2; return requestSynology('/webapi/DownloadStation/task.cgi', { api: 'SYNO.DownloadStation.Task', version: 1, method: 'create', uri: sourceUri, destination: destination || undefined }); case 2: Lampa.Bell.push({ text: Lampa.Lang.translate('actionSentSuccessfully') }); _context1.n = 4; break; case 3: _context1.p = 3; _t8 = _context1.v; console.log('TDM', 'Send file:', _t8); Lampa.Bell.push({ text: Lampa.Lang.translate('actionReturnedError') }); throw _t8; case 4: return _context1.a(2); } }, _callee1, null, [[0, 3]]); })); return _SendTask.apply(this, arguments); } var Synology = { auth: auth, GetData: GetData, GetInfo: GetInfo, SendCommand: SendCommand, SendTask: SendTask }; var MEDIA_EXTENSIONS = ["mp4", "mkv", "avi", "mov", "webm", "ts", "m4v", "mpg", "mpeg"]; function normalizeSortName(name) { return String(name || "").replace(/\d+/g, function (match) { return match.length > 3 ? match : "000".concat(match).slice(-4); }); } function sortFiles(files, sortType) { return _toConsumableArray(files).sort(function (a, b) { if (sortType === "name") { return normalizeSortName(a.name).localeCompare(normalizeSortName(b.name)); } if (sortType === "size") { return Number(b.length || 0) - Number(a.length || 0); } if (sortType === "progress") { var aProgress = Number(a.bytesCompleted || 0) / Number(a.length || 1); var bProgress = Number(b.bytesCompleted || 0) / Number(b.length || 1); return bProgress - aProgress; } return 0; }); } function buildWebdavUrl(config, dirName, filePath) { var webdavFilePath = "".concat(config.webdavUrl, "/").concat(dirName, "/").concat(filePath); var authUrl = "".concat(config.user, ":").concat(config.pass, "@").concat(webdavFilePath.replace(/^https?:\/\//, "")); return "https://".concat(authUrl); } function buildPlaylistItems(config, mediaFiles, dirName) { return mediaFiles.map(function (file) { return { url: buildWebdavUrl(config, dirName, file.name), title: String(file.name || "").split("/").pop(), timeline: false }; }); } function buildFileSelectItem(file) { var fileName = String(file.name || "").split("/").pop(); var fileSize = file.length ? Lampa.Utils.bytesToSize(file.length) : ""; var fileExt = String(fileName || "").split(".").pop().toLowerCase(); var progressText = ""; if (typeof file.bytesCompleted !== "undefined" && file.length) { var percent = Math.round(file.bytesCompleted / file.length * 100); progressText = "".concat(percent, "% \u2022 "); } var icon = "🎥"; if (["mp4", "mov", "m4v"].indexOf(fileExt) >= 0) icon = "🎬";else if (["mkv", "avi"].indexOf(fileExt) >= 0) icon = "🎞️";else if (["ts", "mpg", "mpeg"].indexOf(fileExt) >= 0) icon = "📺"; return { title: "".concat(icon, " ").concat(fileName), subtitle: "".concat(progressText).concat(fileSize), file: file, template: "selectbox_item", selected: false }; } function handleKeeneticAction(_x) { return _handleKeeneticAction.apply(this, arguments); } function _handleKeeneticAction() { _handleKeeneticAction = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(_ref) { var action, torrentData, getConfig, config, mediaFiles, downloadDir, dirName, file, _openSelector; return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: action = _ref.action, torrentData = _ref.torrentData, getConfig = _ref.getConfig; if (!(action.action !== "play")) { _context.n = 1; break; } return _context.a(2, false); case 1: if (!(!torrentData.files || torrentData.files.length === 0)) { _context.n = 2; break; } throw new Error("No files available for playback"); case 2: config = getConfig(); if (config.webdavUrl) { _context.n = 3; break; } throw new Error("WebDAV URL not configured"); case 3: mediaFiles = torrentData.files.filter(function (file) { var ext = String(file.name || "").split(".").pop().toLowerCase(); return MEDIA_EXTENSIONS.indexOf(ext) >= 0; }); if (!(mediaFiles.length === 0)) { _context.n = 4; break; } throw new Error("No media files found in torrent"); case 4: downloadDir = String(torrentData.downloadDir || ""); dirName = downloadDir.split("/").filter(Boolean).pop() || ""; if (!(mediaFiles.length === 1)) { _context.n = 5; break; } file = mediaFiles[0]; Lampa.Player.play({ url: buildWebdavUrl(config, dirName, file.name), title: String(file.name || "").split("/").pop(), timeline: false }); return _context.a(2, true); case 5: _openSelector = function openSelector(sortType) { var sortedFiles = sortFiles(mediaFiles, sortType || "name"); var sortButtons = [{ title: "\uD83D\uDD24 ".concat(Lampa.Lang.translate("sort_by_name")), sort: "name", separator: true }, { title: "\uD83D\uDCCA ".concat(Lampa.Lang.translate("sort_by_size")), sort: "size" }, { title: "\uD83D\uDCE5 ".concat(Lampa.Lang.translate("sort_by_progress")), sort: "progress" }]; var fileItems = sortedFiles.map(buildFileSelectItem); Lampa.Select.show({ title: Lampa.Lang.translate("torrent_select_file"), items: [].concat(sortButtons, _toConsumableArray(fileItems)), onSelect: function onSelect(item) { if (item.sort) { _openSelector(item.sort); return; } var selectedFile = item.file; var playlist = buildPlaylistItems(config, sortedFiles, dirName); var selectedIndex = sortedFiles.findIndex(function (file) { return file.name === selectedFile.name; }); if (selectedIndex >= 0) { Lampa.Player.play(playlist[selectedIndex]); Lampa.Player.playlist(playlist); } }, onBack: function onBack() { Lampa.Controller.toggle("content"); } }); }; _openSelector("name"); return _context.a(2, true); } }, _callee); })); return _handleKeeneticAction.apply(this, arguments); } var Keenetic = createTransmissionLikeClient({ configPrefix: "lmetorrentkeenetic", clientName: "Keenetic", extraFields: ["downloadDir", "files"], getExtraConfig: function getExtraConfig(configPrefix) { return { webdavUrl: Lampa.Storage.field("".concat(configPrefix, "WebdavUrl")) }; }, mapTorrentData: function mapTorrentData(torrent) { return { downloadDir: torrent.downloadDir, files: torrent.files }; }, handleCustomAction: handleKeeneticAction }); var CLIENTS = { qBittorent: Qbittorent, qbittorent: Qbittorent, transmission: Transmission, synology: Synology, keenetic: Keenetic }; var authInFlight = {}; var pollingCircuitState = {}; var POLLING_METHODS = { GetData: true, GetInfo: true }; var BACKOFF_AFTER_FAILURES = 3; var BACKOFF_STEPS_MS = [30000, 60000, 120000, 300000]; var BACKOFF_JITTER_RATIO = 0.15; function getStatusCode(error) { if (!error || typeof error.status === "undefined") { return null; } var status = Number(error.status); return Number.isNaN(status) ? null : status; } function isPollingMethod(methodName) { return POLLING_METHODS[methodName] === true; } function getPollingKey(clientName, methodName) { return "".concat(clientName, ":").concat(methodName); } function getPollingState(clientName, methodName) { var key = getPollingKey(clientName, methodName); if (!pollingCircuitState[key]) { pollingCircuitState[key] = { consecutiveFailures: 0, blockedUntil: 0, hardBlocked: false, lastStatus: null, lastMessage: "", lastErrorAt: 0 }; } return pollingCircuitState[key]; } function resetPollingState(clientName, methodName) { var state = getPollingState(clientName, methodName); state.consecutiveFailures = 0; state.blockedUntil = 0; state.hardBlocked = false; state.lastStatus = null; state.lastMessage = ""; state.lastErrorAt = 0; } function isHardClientStatus(status) { return status >= 400 && status < 500 && status !== 401 && status !== 403 && status !== 408 && status !== 409 && status !== 429; } function isBackoffCandidate(error) { var status = getStatusCode(error); if (status === null) { var message = String(error && error.message || "").toLowerCase(); return message.indexOf("timeout") >= 0 || message.indexOf("network") >= 0 || message.indexOf("cors") >= 0; } if (status === 0 || status === 401 || status === 403 || status === 408 || status === 429) { return true; } return status >= 500 && status < 600; } function createPollingBlockedError(clientName, methodName, state) { var statusSuffix = state.lastStatus !== null ? " (HTTP ".concat(state.lastStatus, ")") : ""; if (state.hardBlocked) { var hardError = new Error("".concat(clientName, " ").concat(methodName, " polling blocked after repeated client errors").concat(statusSuffix)); hardError.status = state.lastStatus; hardError.client = clientName; hardError.method = methodName; hardError.isBackoffError = true; hardError.isCircuitOpen = true; hardError.isHardBlocked = true; return hardError; } var retryAfterMs = Math.max(0, state.blockedUntil - Date.now()); var retryAfterSec = Math.ceil(retryAfterMs / 1000); var softError = new Error("".concat(clientName, " ").concat(methodName, " polling paused for ").concat(retryAfterSec, "s").concat(statusSuffix)); softError.status = state.lastStatus; softError.client = clientName; softError.method = methodName; softError.retryAfterMs = retryAfterMs; softError.isBackoffError = true; softError.isCircuitOpen = true; softError.isHardBlocked = false; return softError; } function getPollingBlockedError(clientName, methodName) { var state = getPollingState(clientName, methodName); if (state.hardBlocked) { return createPollingBlockedError(clientName, methodName, state); } if (state.blockedUntil > Date.now()) { return createPollingBlockedError(clientName, methodName, state); } if (state.blockedUntil > 0) { state.blockedUntil = 0; } return null; } function registerPollingFailure(clientName, methodName, error) { var state = getPollingState(clientName, methodName); var status = getStatusCode(error); state.consecutiveFailures += 1; state.lastStatus = status; state.lastMessage = String(error && error.message || ""); state.lastErrorAt = Date.now(); if (status !== null && isHardClientStatus(status) && state.consecutiveFailures >= BACKOFF_AFTER_FAILURES) { state.hardBlocked = true; state.blockedUntil = 0; return; } if (!isBackoffCandidate(error) || state.consecutiveFailures < BACKOFF_AFTER_FAILURES) { return; } var rawStep = state.consecutiveFailures - BACKOFF_AFTER_FAILURES; var stepIndex = Math.min(rawStep, BACKOFF_STEPS_MS.length - 1); var delayMs = BACKOFF_STEPS_MS[stepIndex]; var jitterMs = Math.round(delayMs * BACKOFF_JITTER_RATIO * Math.random()); state.blockedUntil = Date.now() + delayMs + jitterMs; } function getClient(clientName) { return CLIENTS[clientName] || null; } function hasClient(clientName) { return Boolean(getClient(clientName)); } function getActiveClientName() { return Lampa.Storage.field("lmetorrentSelect"); } function getErrorCode(error) { if (!error) { return null; } if (typeof error.code === "number" && !Number.isNaN(error.code)) { return error.code; } if (error.responseJSON && error.responseJSON.error && typeof error.responseJSON.error.code !== "undefined") { var parsed = Number(error.responseJSON.error.code); return Number.isNaN(parsed) ? null : parsed; } return null; } function isAuthError(clientName, error) { var status = Number(error && error.status); var code = getErrorCode(error); if (status === 401 || status === 403) { return true; } if (clientName === "synology" && code === 105) { return true; } return false; } function normalizeClientError(clientName, methodName, error) { var normalized = error instanceof Error ? error : new Error(String(error || "Client request failed")); if (error && typeof error.status !== "undefined") { normalized.status = error.status; } var code = getErrorCode(error); if (code !== null) { normalized.code = code; } normalized.client = clientName; normalized.method = methodName; normalized.isAuthError = isAuthError(clientName, error); return normalized; } function authenticateClient(_x) { return _authenticateClient.apply(this, arguments); } function _authenticateClient() { _authenticateClient = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(clientName) { var silent, client, _args = arguments; return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: silent = _args.length > 1 && _args[1] !== undefined ? _args[1] : true; client = getClient(clientName); if (!(!client || typeof client.auth !== "function")) { _context.n = 1; break; } return _context.a(2); case 1: if (!authInFlight[clientName]) { _context.n = 2; break; } return _context.a(2, authInFlight[clientName]); case 2: authInFlight[clientName] = Promise.resolve().then(function () { return client.auth(!silent); })["finally"](function () { authInFlight[clientName] = null; }); return _context.a(2, authInFlight[clientName]); } }, _callee); })); return _authenticateClient.apply(this, arguments); } function executeClientMethod(_x2, _x3) { return _executeClientMethod.apply(this, arguments); } function _executeClientMethod() { _executeClientMethod = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(clientName, methodName) { var args, options, client, retryAuth, silentAuth, usePollingGuard, blockedError, result, normalized, retryResult, normalizedRetryError, _args2 = arguments, _t, _t2; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: args = _args2.length > 2 && _args2[2] !== undefined ? _args2[2] : []; options = _args2.length > 3 && _args2[3] !== undefined ? _args2[3] : {}; client = getClient(clientName); if (client) { _context2.n = 1; break; } throw new Error("Unknown client type: ".concat(clientName)); case 1: if (!(typeof client[methodName] !== "function")) { _context2.n = 2; break; } throw new Error("Unknown method '".concat(methodName, "' for client '").concat(clientName, "'")); case 2: retryAuth = options.retryAuth !== false; silentAuth = options.silentAuth !== false; usePollingGuard = options.backgroundPolling === true && isPollingMethod(methodName); if (!usePollingGuard) { _context2.n = 3; break; } blockedError = getPollingBlockedError(clientName, methodName); if (!blockedError) { _context2.n = 3; break; } throw blockedError; case 3: _context2.p = 3; _context2.n = 4; return client[methodName].apply(client, _toConsumableArray(args)); case 4: result = _context2.v; if (isPollingMethod(methodName)) { resetPollingState(clientName, methodName); } return _context2.a(2, result); case 5: _context2.p = 5; _t = _context2.v; normalized = normalizeClientError(clientName, methodName, _t); if (!(retryAuth && normalized.isAuthError && typeof client.auth === "function")) { _context2.n = 10; break; } _context2.p = 6; _context2.n = 7; return authenticateClient(clientName, silentAuth); case 7: _context2.n = 8; return client[methodName].apply(client, _toConsumableArray(args)); case 8: retryResult = _context2.v; if (isPollingMethod(methodName)) { resetPollingState(clientName, methodName); } return _context2.a(2, retryResult); case 9: _context2.p = 9; _t2 = _context2.v; normalizedRetryError = normalizeClientError(clientName, methodName, _t2); if (usePollingGuard) { registerPollingFailure(clientName, methodName, normalizedRetryError); } throw normalizedRetryError; case 10: if (usePollingGuard) { registerPollingFailure(clientName, methodName, normalized); } throw normalized; case 11: return _context2.a(2); } }, _callee2, null, [[6, 9], [3, 5]]); })); return _executeClientMethod.apply(this, arguments); } /** * Оновлення метаданих для одного торента. * Викликається з torrent_actions.js * * @param {Object} torrent - об'єкт торента (як у твоєму списку) * @param {Object} client - інформація про клієнта (тип + налаштування) */ function updateMetadataForTorrent(_x, _x2) { return _updateMetadataForTorrent.apply(this, arguments); } /** * Простий TMDB-флоу для пошуку медіа інформації * * @param {Object} torrent - об'єкт торента * @returns {Promise} - нормалізований результат TMDB або null */ function _updateMetadataForTorrent() { _updateMetadataForTorrent = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(torrent, client) { var mediaInfo, _t; return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: _context.n = 1; return findMediaInfoViaTMDB(torrent); case 1: mediaInfo = _context.v; if (mediaInfo) { _context.n = 2; break; } return _context.a(2); case 2: _t = client.type; _context.n = _t === 'transmission' ? 3 : _t === 'keenetic' ? 3 : _t === 'qbittorrent' ? 5 : _t === 'synology' ? 7 : 9; break; case 3: _context.n = 4; return updateTransmissionLikeMetadata(torrent, client, mediaInfo); case 4: return _context.a(3, 10); case 5: _context.n = 6; return updateQbittorrentMetadata(torrent, client, mediaInfo); case 6: return _context.a(3, 10); case 7: _context.n = 8; return updateSynologyMetadata(torrent, client, mediaInfo); case 8: return _context.a(3, 10); case 9: console.log('TDM', 'Unknown client type for metadata:', client.type); case 10: return _context.a(2); } }, _callee); })); return _updateMetadataForTorrent.apply(this, arguments); } function findMediaInfoViaTMDB(_x3) { return _findMediaInfoViaTMDB.apply(this, arguments); } /** * Пошук в TMDB з використанням існуючого парсера * * @param {Object} torrent - торент для пошуку * @returns {Promise} - результати пошуку */ function _findMediaInfoViaTMDB() { _findMediaInfoViaTMDB = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(torrent) { var results; return _regenerator().w(function (_context2) { while (1) switch (_context2.n) { case 0: _context2.n = 1; return tmdbSearch(torrent); case 1: results = _context2.v; if (!(!results || !results.length)) { _context2.n = 2; break; } Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_nothing_found') //'Нічого не знайдено для ' + torrent.name }); return _context2.a(2, null); case 2: return _context2.a(2, normalizeTmdbResult(results[0])); } }, _callee2); })); return _findMediaInfoViaTMDB.apply(this, arguments); } function tmdbSearch(_x4) { return _tmdbSearch.apply(this, arguments); } /** * Показ модального вікна для вибору результату TMDB * * @param {Array} results - масив результатів пошуку * @returns {Promise} - обраний результат або null */ function _tmdbSearch() { _tmdbSearch = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(torrent) { var results, _t2; return _regenerator().w(function (_context3) { while (1) switch (_context3.p = _context3.n) { case 0: _context3.p = 0; _context3.n = 1; return processTorrents$1([torrent]); case 1: results = _context3.v; return _context3.a(2, results || []); case 2: _context3.p = 2; _t2 = _context3.v; console.error('TDM', 'TMDB search error:', _t2); return _context3.a(2, []); } }, _callee3, null, [[0, 2]]); })); return _tmdbSearch.apply(this, arguments); } /** * Нормалізація результату TMDB до простого об'єкта * * @param {Object} item - результат з TMDB * @returns {Object} - нормалізований об'єкт */ function normalizeTmdbResult(item) { return { id: item.id, media_type: item.media_type || 'movie', // або за твоїми правилами title: item.title || item.name, year: (item.release_date || item.first_air_date || '').slice(0, 4) || null }; } /** * Форматування тегу для метаданих * * @param {Object} mediaInfo - інформація про медіа * @returns {string} - відформатований тег */ function buildMetadataTag(mediaInfo) { // ПОВИННО відповідати поточному формату тегу в плагіні! // приклад, якщо так уже є: return mediaInfo.media_type + '/' + mediaInfo.id; } /** * Оновлення метаданих для Transmission/Keenetic * * @param {Object} torrent - об'єкт торента * @param {Object} client - інформація про клієнта * @param {Object} mediaInfo - інформація про медіа з TMDB */ function updateTransmissionLikeMetadata(_x5, _x6, _x7) { return _updateTransmissionLikeMetadata.apply(this, arguments); } /** * Оновлення метаданих для qBittorrent * * @param {Object} torrent - об'єкт торента * @param {Object} client - інформація про клієнта * @param {Object} mediaInfo - інформація про медіа з TMDB */ function _updateTransmissionLikeMetadata() { _updateTransmissionLikeMetadata = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(torrent, client, mediaInfo) { var label, _t3; return _regenerator().w(function (_context4) { while (1) switch (_context4.p = _context4.n) { case 0: label = buildMetadataTag(mediaInfo); _context4.p = 1; if (!(client.type === 'transmission')) { _context4.n = 3; break; } _context4.n = 2; return Transmission.setLabels(torrent.id, label); case 2: _context4.n = 4; break; case 3: _context4.n = 4; return Keenetic.setLabels(torrent.id, label); case 4: Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_metadata_updated') //'Метадані оновлено' }); _context4.n = 6; break; case 5: _context4.p = 5; _t3 = _context4.v; console.error('TDM', 'updateTransmissionLikeMetadata error:', _t3); Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_metadata_error') //'Помилка оновлення метаданих' }); case 6: return _context4.a(2); } }, _callee4, null, [[1, 5]]); })); return _updateTransmissionLikeMetadata.apply(this, arguments); } function updateQbittorrentMetadata(_x8, _x9, _x0) { return _updateQbittorrentMetadata.apply(this, arguments); } /** * Оновлення метаданих для Synology * * @param {Object} torrent - об'єкт торента * @param {Object} client - інформація про клієнта * @param {Object} mediaInfo - інформація про медіа з TMDB */ function _updateQbittorrentMetadata() { _updateQbittorrentMetadata = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(torrent, client, mediaInfo) { var tagValue, _t4; return _regenerator().w(function (_context5) { while (1) switch (_context5.p = _context5.n) { case 0: tagValue = buildMetadataTag(mediaInfo); // той самий формат, що й при додаванні _context5.p = 1; _context5.n = 2; return Qbittorent.setTags(torrent.id, tagValue); case 2: Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_metadata_updated') //'Метадані оновлено' }); _context5.n = 4; break; case 3: _context5.p = 3; _t4 = _context5.v; console.error('TDM', 'updateQbittorrentMetadata error:', _t4); Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_metadata_error') //'Помилка оновлення метаданих' }); case 4: return _context5.a(2); } }, _callee5, null, [[1, 3]]); })); return _updateQbittorrentMetadata.apply(this, arguments); } function updateSynologyMetadata(_x1, _x10, _x11) { return _updateSynologyMetadata.apply(this, arguments); } /** * Масове оновлення метаданих для всіх торентів * * @param {Array} torrents - масив торентів * @param {Object} client - інформація про клієнта */ function _updateSynologyMetadata() { _updateSynologyMetadata = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(torrent, client, mediaInfo) { var key, metadataTag, poster, _t5; return _regenerator().w(function (_context6) { while (1) switch (_context6.p = _context6.n) { case 0: _context6.p = 0; key = String(torrent && torrent.id || ""); if (key) { _context6.n = 1; break; } throw new Error("Synology metadata update: empty torrent id"); case 1: metadataTag = buildMetadataTag(mediaInfo); _context6.n = 2; return getPosterFromLabels(metadataTag); case 2: poster = _context6.v; _context6.n = 3; return saveMetadata(key, { tmdb_id: mediaInfo.id, media_type: mediaInfo.media_type, poster: poster && poster !== './img/img_load.svg' ? poster : '', updated_at: Date.now() }); case 3: Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_metadata_updated') //'Метадані оновлено' }); _context6.n = 5; break; case 4: _context6.p = 4; _t5 = _context6.v; console.error('TDM', 'updateSynologyMetadata error:', _t5); Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_metadata_error') //'Помилка оновлення метаданих' }); case 5: return _context6.a(2); } }, _callee6, null, [[0, 4]]); })); return _updateSynologyMetadata.apply(this, arguments); } function updateAllMetadata(_x12, _x13) { return _updateAllMetadata.apply(this, arguments); } function _updateAllMetadata() { _updateAllMetadata = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(torrents, client) { var _iterator, _step, torrent, _t6; return _regenerator().w(function (_context7) { while (1) switch (_context7.p = _context7.n) { case 0: _iterator = _createForOfIteratorHelper(torrents); _context7.p = 1; _iterator.s(); case 2: if ((_step = _iterator.n()).done) { _context7.n = 5; break; } torrent = _step.value; _context7.n = 3; return updateMetadataForTorrent(torrent, client); case 3: _context7.n = 4; return new Promise(function (resolve) { return setTimeout(resolve, 500); }); case 4: _context7.n = 2; break; case 5: _context7.n = 7; break; case 6: _context7.p = 6; _t6 = _context7.v; _iterator.e(_t6); case 7: _context7.p = 7; _iterator.f(); return _context7.f(7); case 8: Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_metadata_updated_all') //'Оновлення метаданих завершено' }); case 9: return _context7.a(2); } }, _callee7, null, [[1, 6, 7, 8]]); })); return _updateAllMetadata.apply(this, arguments); } var METADATA_CLIENTS = { qBittorent: { type: "qbittorrent", prefix: "lmetorrentqBittorent", withCredentials: false }, transmission: { type: "transmission", prefix: "lmetorrenttransmission", withCredentials: true }, keenetic: { type: "keenetic", prefix: "lmetorrentkeenetic", withCredentials: true }, synology: { type: "synology", prefix: "lmetorrentsynology", withCredentials: true } }; function buildMetadataClientConfig(clientName) { var client = METADATA_CLIENTS[clientName]; if (!client) { return null; } var config = { type: client.type, url: Lampa.Storage.field("".concat(client.prefix, "Url")), useProxy: Lampa.Storage.field("".concat(client.prefix, "Proxy")) === true }; if (client.withCredentials) { config.user = Lampa.Storage.field("".concat(client.prefix, "User")); config.pass = Lampa.Storage.field("".concat(client.prefix, "Pass")); } return config; } function isMetadataAction(actionType) { return actionType === "parse" || actionType === "parse-all"; } function openTorrentCard(torrentData) { var cardInfo = null; if (torrentData.tmdb_id && torrentData.media_type) { cardInfo = { tID: torrentData.tmdb_id, method: torrentData.media_type }; } else { cardInfo = parseLabels(torrentData.labels); } if (!cardInfo) { Lampa.Bell.push({ text: "No metadata available for this torrent" }); return; } Lampa.Activity.push({ url: "", component: "full", id: cardInfo.tID, method: cardInfo.method, source: Lampa.Storage.field("source") || "tmdb" }); } function handleMetadataAction(_x, _x2, _x3, _x4) { return _handleMetadataAction.apply(this, arguments); } function _handleMetadataAction() { _handleMetadataAction = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(actionType, torrentData, torrentList, clientName) { var clientConfig; return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: clientConfig = buildMetadataClientConfig(clientName); if (clientConfig) { _context.n = 1; break; } Lampa.Bell.push({ text: "Unknown client type" }); return _context.a(2); case 1: if (!(actionType === "parse")) { _context.n = 3; break; } _context.n = 2; return updateMetadataForTorrent(torrentData, clientConfig); case 2: return _context.a(2); case 3: _context.n = 4; return updateAllMetadata(torrentList, clientConfig); case 4: return _context.a(2); } }, _callee); })); return _handleMetadataAction.apply(this, arguments); } function parseLabels(labels) { if (!labels) { return null; } var labelArray = Array.isArray(labels) ? labels : labels.split(","); var matchedLabel = labelArray.find(function (label) { return /^(tv|movie)\/\d+$/.test(label); }); if (!matchedLabel) { return null; } var _matchedLabel$split = matchedLabel.split("/"), _matchedLabel$split2 = _slicedToArray(_matchedLabel$split, 2), method = _matchedLabel$split2[0], tID = _matchedLabel$split2[1]; return { method: method, tID: tID }; } function showTorrentMenu(torrentData) { var allTorrents = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var client = Lampa.Storage.field("lmetorrentSelect"); var enabled = Lampa.Controller.enabled().name; var hasMetadata = torrentData.tmdb_id && torrentData.media_type || parseLabels(torrentData.labels); var torrentList = Array.isArray(allTorrents) ? allTorrents : []; var menuItems = [{ title: Lampa.Lang.translate("resume"), action: "resume" }, { title: Lampa.Lang.translate("pause"), action: "pause" }, { title: Lampa.Lang.translate("delete"), action: "delete" }].concat(_toConsumableArray(hasMetadata ? [{ title: Lampa.Lang.translate("openCard"), action: "card" }] : []), [{ title: Lampa.Lang.translate("add_metadata"), action: "parse" }]); if (torrentList.length > 1) { menuItems.push({ title: Lampa.Lang.translate("add_metadata_all"), action: "parse-all" }); } if (client === "keenetic") { menuItems.push({ title: "Play", action: "play" }); } if (client !== "synology") { menuItems.push({ title: Lampa.Lang.translate("fullDelete"), action: "delete", deleteFiles: true }); } Lampa.Select.show({ title: torrentData.name, items: menuItems, onBack: function onBack() { var currentController = Lampa.Controller.enabled(); if (currentController && currentController.name !== enabled) { Lampa.Controller.toggle(enabled); } else { Lampa.Controller.toggle("menu"); } }, onSelect: function onSelect(action) { handleTorrentAction(action, torrentData, torrentList); } }); } function handleTorrentAction(_x5, _x6) { return _handleTorrentAction.apply(this, arguments); } function _handleTorrentAction() { _handleTorrentAction = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(action, torrentData) { var allTorrents, client, torrentList, actionType, _args2 = arguments, _t; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: allTorrents = _args2.length > 2 && _args2[2] !== undefined ? _args2[2] : []; client = Lampa.Storage.field("lmetorrentSelect"); torrentList = Array.isArray(allTorrents) ? allTorrents : []; actionType = action && action.action; if (!(actionType === "card")) { _context2.n = 1; break; } openTorrentCard(torrentData); return _context2.a(2); case 1: _context2.p = 1; if (!isMetadataAction(actionType)) { _context2.n = 3; break; } _context2.n = 2; return handleMetadataAction(actionType, torrentData, torrentList, client); case 2: return _context2.a(2); case 3: _context2.n = 4; return executeClientMethod(client, "SendCommand", [action, torrentData], { silentAuth: true }); case 4: _context2.n = 6; break; case 5: _context2.p = 5; _t = _context2.v; console.error("TDM", "Error handling action:", _t); Lampa.Bell.push({ text: Lampa.Lang.translate("actionReturnedError") }); case 6: return _context2.a(2); } }, _callee2, null, [[1, 5]]); })); return _handleTorrentAction.apply(this, arguments); } var TorrentStateManager = /*#__PURE__*/function () { function TorrentStateManager() { _classCallCheck(this, TorrentStateManager); this.torrents = []; this.timer = null; this.listener = Lampa.Listener; this.updateInProgress = false; } return _createClass(TorrentStateManager, [{ key: "start", value: function start() { if (this.timer) { return; } // Call update immediately on start to populate torrents right away this.update(); this.timer = this.update.bind(this); Lampa.Timer.add(15000, this.timer, true); } }, { key: "stop", value: function stop() { if (this.timer) { Lampa.Timer.remove(this.timer); this.timer = null; } } }, { key: "update", value: function () { var _update = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() { var client_name, isUniversal, new_torrents, has_active_downloads_before, has_active_downloads_after, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: if (!this.updateInProgress) { _context.n = 1; break; } return _context.a(2); case 1: this.updateInProgress = true; _context.p = 2; client_name = Lampa.Storage.get('lmetorrentSelect'); isUniversal = client_name === 'universal' || client_name === 'universalClient'; if (!(isUniversal || !hasClient(client_name))) { _context.n = 3; break; } return _context.a(2); case 3: _context.n = 4; return executeClientMethod(client_name, 'GetData', [], { silentAuth: true, backgroundPolling: true }); case 4: new_torrents = _context.v; if (new_torrents) { has_active_downloads_before = this.hasActiveDownloads(); this.torrents = new_torrents; has_active_downloads_after = this.hasActiveDownloads(); this.listener.send('torrents:updated', { torrents: this.torrents }); if (has_active_downloads_before !== has_active_downloads_after) { this.listener.send('torrents:status_changed', { active: has_active_downloads_after }); } } _context.n = 6; break; case 5: _context.p = 5; _t = _context.v; console.error('TDM', 'TorrentStateManager.update error:', _t); case 6: _context.p = 6; this.updateInProgress = false; return _context.f(6); case 7: return _context.a(2); } }, _callee, this, [[2, 5, 6, 7]]); })); function update() { return _update.apply(this, arguments); } return update; }() }, { key: "hasActiveDownloads", value: function hasActiveDownloads() { return this.torrents.some(function (torrent) { var state = String(torrent && torrent.state || '').toLowerCase(); return state.indexOf('download') >= 0 || state.indexOf('check') >= 0; }); } }, { key: "on", value: function on(event, callback) { this.listener.follow(event, callback); } }]); }(); var TorrentStateManager$1 = new TorrentStateManager(); var UPDATE_INTERVAL_MS = 15000; var ERROR_KIND = { AUTH: "auth", NETWORK: "network", SERVER: "server", UNKNOWN: "unknown" }; function Component() { var self = this; var client = Lampa.Storage.field("lmetorrentSelect"); var network = new Lampa.Reguest(); var updateTick = null; var updateInProgress = false; var sections = []; // Array of PanelSection instances var headerItem = null; var allTorrents = []; // Full list of all torrents (for context menu) var activeSectionIndex = 0; var isHeaderFocused = false; var scroll = new Lampa.Scroll({ mask: true, over: true, step: 250 }); var html = $("
"); var head = $("
"); var body = $('
'); scroll.append(head); scroll.append(body); html.append(scroll.render()); function reloadPanel() { Lampa.Activity.replace({ url: "", title: (client || "").toUpperCase() + " Manager", component: "lmetorrentPanel", page: 1 }); } function openSettings() { if (Lampa.Controller) Lampa.Controller.toggle('settings'); setTimeout(function () { if (Lampa.Settings && typeof Lampa.Settings.create === 'function') { Lampa.Settings.create('lmetorrent'); } }, 10); } function stopAutoUpdate() { if (updateTick) { Lampa.Timer.remove(updateTick); updateTick = null; } } function pauseHeaderPolling() { TorrentStateManager$1.stop(); } function resumeHeaderPolling() { TorrentStateManager$1.start(); } function focusHeader() { if (!headerItem) return; Lampa.Controller.collectionSet(scroll.render()); Lampa.Controller.collectionFocus(headerItem.render()[0] || false, scroll.render()); scroll.update(headerItem.render(), true); } function destroyAllSections() { Lampa.Arrays.destroy(sections); sections = []; activeSectionIndex = 0; isHeaderFocused = false; allTorrents = []; } function renderState(title, description) { var actions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; if (options.resetHeader) { head.empty(); destroyAllSections(); } body.empty().removeClass("lmetorrent-sections").addClass("lmetorrent-catalog--state"); var container = $('
'); container.append("
".concat(title, "
")); if (description) { container.append("
".concat(description, "
")); } if (actions.length) { var actionList = $('
'); actions.forEach(function (action) { var button = $("
".concat(action.title, "
")); button.on("hover:enter", action.onEnter); button.on("hover:focus", function () { scroll.update(button, true); }); actionList.append(button); }); container.append(actionList); } body.append(container); } function classifyError(error) { if (!error) { return ERROR_KIND.UNKNOWN; } if (error.isAuthError) { return ERROR_KIND.AUTH; } var status = Number(error.status); if (status === 401 || status === 403) { return ERROR_KIND.AUTH; } if (status >= 500) { return ERROR_KIND.SERVER; } var message = String(error.message || "").toLowerCase(); if (status === 0 || message.indexOf("timeout") >= 0 || message.indexOf("network") >= 0 || message.indexOf("cors") >= 0) { return ERROR_KIND.NETWORK; } return ERROR_KIND.UNKNOWN; } function fetchClientData(_x) { return _fetchClientData.apply(this, arguments); } function _fetchClientData() { _fetchClientData = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(clientType) { var _yield$Promise$all, _yield$Promise$all2, data, info; return _regenerator().w(function (_context2) { while (1) switch (_context2.n) { case 0: if (hasClient(clientType)) { _context2.n = 1; break; } throw new Error("Unknown client type"); case 1: _context2.n = 2; return Promise.all([executeClientMethod(clientType, "GetData", [], { silentAuth: true }), executeClientMethod(clientType, "GetInfo", [], { silentAuth: true })]); case 2: _yield$Promise$all = _context2.v; _yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2); data = _yield$Promise$all2[0]; info = _yield$Promise$all2[1]; return _context2.a(2, { data: data, info: info }); } }, _callee2); })); return _fetchClientData.apply(this, arguments); } this.renderLoadingState = function () { renderState(Lampa.Lang.translate("torrentmanager_state_loading"), "", [], { resetHeader: true }); }; this.renderEmptyState = function () { renderState(Lampa.Lang.translate("torrentmanager_state_empty_title"), Lampa.Lang.translate("torrentmanager_state_empty_description"), [{ title: Lampa.Lang.translate("torrentmanager_retry"), onEnter: reloadPanel }]); }; this.renderErrorState = function (error) { console.error("TDM", "Panel load error:", error); var kind = classifyError(error); var actions = [{ title: Lampa.Lang.translate("torrentmanager_retry"), onEnter: reloadPanel }, { title: Lampa.Lang.translate("torrentmanager_open_settings"), onEnter: openSettings }]; if (kind === ERROR_KIND.AUTH) { renderState(Lampa.Lang.translate("torrentmanager_state_auth_title"), Lampa.Lang.translate("torrentmanager_state_auth_description"), actions, { resetHeader: true }); return; } if (kind === ERROR_KIND.NETWORK) { renderState(Lampa.Lang.translate("torrentmanager_state_network_title"), Lampa.Lang.translate("torrentmanager_state_network_description"), actions, { resetHeader: true }); return; } if (kind === ERROR_KIND.SERVER) { renderState(Lampa.Lang.translate("torrentmanager_state_server_title"), Lampa.Lang.translate("torrentmanager_state_server_description"), actions, { resetHeader: true }); return; } renderState(Lampa.Lang.translate("somethingWentWrong"), Lampa.Lang.translate("clientNotClient"), actions, { resetHeader: true }); }; this.create = /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() { var result, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: pauseHeaderPolling(); stopAutoUpdate(); this.activity.loader(true); self.renderLoadingState(); _context.p = 1; _context.n = 2; return fetchClientData(client); case 2: result = _context.v; self.build(result); startAutoUpdate(); _context.n = 4; break; case 3: _context.p = 3; _t = _context.v; self.renderErrorState(_t); case 4: this.activity.loader(false); this.activity.toggle(); return _context.a(2, this.render()); } }, _callee, this, [[1, 3]]); })); this.build = function (result) { scroll.minus(); head.empty(); body.empty().removeClass("lmetorrent-catalog--state").addClass("lmetorrent-sections"); destroyAllSections(); if (result && result.info) { self.renderHeader(result.info); } var torrents = Array.isArray(result && result.data) ? result.data : []; if (!torrents.length) { self.renderEmptyState(); return; } allTorrents = torrents; // Group into sections by status var grouped = groupTorrentsByStatus(torrents); self.buildSections(grouped); // Initial focus on header, not first section if (sections.length > 0) { isHeaderFocused = true; } }; this.buildSections = function (grouped) { var fragment = $(document.createDocumentFragment()); SECTION_DEFINITIONS.forEach(function (def) { var groupItems = grouped[def.key]; if (!groupItems || groupItems.length === 0) return; var section = new PanelSection({ key: def.key, title: Lampa.Lang.translate(def.langKey), icon: def.icon, items: groupItems }); // Wire up item enter handler with access to full torrent list section.onItemEnter = function (torrentData) { showTorrentMenu(torrentData, allTorrents); }; // Wire up section navigation callbacks (vinyl pattern) section.onDown = self.down.bind(self); section.onUp = self.up.bind(self); section.onBack = self.back.bind(self); section.onMore = function (key) { return function () { Lampa.Activity.push({ url: '', title: Lampa.Lang.translate('torrentmanager_section_' + key) + " \u2014 " + (Lampa.Storage.field("lmetorrentSelect") || '').toUpperCase() + ' Manager', component: 'lmetorrentSectionView', page: 1, filterSection: key }); }; }(def.key); section.create(); fragment.append(section.render()); sections.push(section); }); body.append(fragment); }; this.renderHeader = function (data) { headerItem = new Header(data); headerItem.render().on("hover:enter", reloadPanel); headerItem.render().on("hover:focus", function () { isHeaderFocused = true; scroll.update(headerItem.render(), true); }); head.append(headerItem.render()); }; function refreshTorrents() { return _refreshTorrents.apply(this, arguments); } function _refreshTorrents() { _refreshTorrents = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() { var torrents, grouped, newSections, existingSectionElements, existingKeys, bodyFragment, _t2; return _regenerator().w(function (_context3) { while (1) switch (_context3.p = _context3.n) { case 0: if (!updateInProgress) { _context3.n = 1; break; } return _context3.a(2); case 1: updateInProgress = true; _context3.p = 2; _context3.n = 3; return executeClientMethod(client, "GetData", [], { silentAuth: true, backgroundPolling: true }); case 3: torrents = _context3.v; if (Array.isArray(torrents)) { _context3.n = 4; break; } return _context3.a(2); case 4: if (torrents.length) { _context3.n = 5; break; } if (!body.hasClass("lmetorrent-catalog--state") || sections.length > 0) { self.renderEmptyState(); } return _context3.a(2); case 5: allTorrents = torrents; // Classify and diff sections grouped = groupTorrentsByStatus(torrents); newSections = []; SECTION_DEFINITIONS.forEach(function (def) { var groupItems = grouped[def.key]; var existing = null; // Find existing section by key for (var i = 0; i < sections.length; i++) { if (sections[i].key === def.key) { existing = sections[i]; break; } } if (groupItems && groupItems.length > 0) { if (existing) { // Update existing section existing.update(groupItems); newSections.push(existing); } else { // Create new section var section = new PanelSection({ key: def.key, title: Lampa.Lang.translate(def.langKey), icon: def.icon, items: groupItems }); section.onItemEnter = function (torrentData) { showTorrentMenu(torrentData, allTorrents); }; // Wire up section navigation callbacks (vinyl pattern) section.onDown = self.down.bind(self); section.onUp = self.up.bind(self); section.onBack = self.back.bind(self); section.onMore = function (key) { return function () { Lampa.Activity.push({ url: '', title: Lampa.Lang.translate('torrentmanager_section_' + key) + " \u2014 " + (Lampa.Storage.field("lmetorrentSelect") || '').toUpperCase() + ' Manager', component: 'lmetorrentSectionView', page: 1, filterSection: key }); }; }(def.key); section.create(); newSections.push(section); } } else { // Section has no items — destroy if exists if (existing) { existing.destroy(); } } }); // Diff body DOM — preserve existing section elements, remove // stale, add new, reorder. Avoids flicker from body.empty(). existingSectionElements = body.children('.lmetorrent-section'); existingKeys = {}; existingSectionElements.each(function () { existingKeys[$(this).attr('data-section-key')] = $(this); }); bodyFragment = $(document.createDocumentFragment()); newSections.forEach(function (section) { var key = section.key; if (existingKeys[key]) { // Section exists — reuse DOM element (avoids flicker) bodyFragment.append(existingKeys[key]); delete existingKeys[key]; } else { // New section — append fresh bodyFragment.append(section.render()); } }); // Remove sections that are no longer present Object.keys(existingKeys).forEach(function (key) { existingKeys[key].remove(); }); // Replace body content atomically body.empty().append(bodyFragment); sections = newSections; // Adjust active index if needed if (activeSectionIndex >= sections.length) { activeSectionIndex = sections.length - 1; } _context3.n = 7; break; case 6: _context3.p = 6; _t2 = _context3.v; if (!_t2 || !_t2.isBackoffError) { console.error("TDM", "Auto update error:", _t2); } case 7: _context3.p = 7; updateInProgress = false; return _context3.f(7); case 8: return _context3.a(2); } }, _callee3, null, [[2, 6, 7, 8]]); })); return _refreshTorrents.apply(this, arguments); } function startAutoUpdate() { if (!hasClient(client) || updateTick) { return; } updateTick = refreshTorrents; Lampa.Timer.add(UPDATE_INTERVAL_MS, updateTick); } this.down = function () { if (isHeaderFocused) { // From header, go to first section isHeaderFocused = false; if (sections.length > 0) { activeSectionIndex = 0; sections[0].toggle(); scroll.update(sections[0].render()); } } else if (activeSectionIndex < sections.length - 1) { activeSectionIndex++; sections[activeSectionIndex].toggle(); scroll.update(sections[activeSectionIndex].render()); } }; this.up = function () { if (isHeaderFocused) { // At header, go to system head Lampa.Controller.toggle('head'); } else if (activeSectionIndex <= 0) { // At first section, go to header isHeaderFocused = true; focusHeader(); } else { activeSectionIndex--; sections[activeSectionIndex].toggle(); scroll.update(sections[activeSectionIndex].render()); } }; this.back = function () { Lampa.Activity.backward(); }; this.background = function () { Lampa.Background.immediately(""); }; this.start = function () { if (Lampa.Activity.active().activity !== this.activity) return; this.background(); Lampa.Controller.add("content", { toggle: function toggle() { if (isHeaderFocused && headerItem) { focusHeader(); } else if (sections.length) { sections[activeSectionIndex].toggle(); } }, up: function up() { self.up(); }, down: function down() { self.down(); }, left: function left() { Lampa.Controller.toggle('menu'); }, back: self.back }); Lampa.Controller.toggle("content"); }; this.pause = function () { stopAutoUpdate(); resumeHeaderPolling(); }; this.stop = function () { stopAutoUpdate(); resumeHeaderPolling(); }; this.render = function () { return html; }; this.destroy = function () { stopAutoUpdate(); resumeHeaderPolling(); network.clear(); destroyAllSections(); if (headerItem && typeof headerItem.destroy === 'function') { headerItem.destroy(); headerItem = null; } scroll.destroy(); html.remove(); sections = null; allTorrents = null; network = null; }; } // plugins/torrentmanager/utils/panel/sectionView.js — Standalone grid component function SectionView(object) { var network = new Lampa.Reguest(); var scroll = new Lampa.Scroll({ mask: true, over: true }); var html = $('
'); var grid = $('
'); var items = []; var last = null; // Lifecycle this.create = function () { this.activity.loader(true); var filterSection = object.filterSection; if (!filterSection) { this.empty(); return this.render(); } // Find section definition for title var sectionDef = null; for (var i = 0; i < SECTION_DEFINITIONS.length; i++) { if (SECTION_DEFINITIONS[i].key === filterSection) { sectionDef = SECTION_DEFINITIONS[i]; break; } } var title = sectionDef ? Lampa.Lang.translate(sectionDef.langKey) : filterSection; // Update activity title var client = Lampa.Storage.field("lmetorrentSelect") || ''; this.activity.title = title + " \u2014 " + client.toUpperCase() + ' Manager'; scroll.minus(); scroll.append(grid); html.append(scroll.render()); this.load(); return this.render(); }; this.load = function () { var filterSection = object.filterSection; var client = Lampa.Storage.field("lmetorrentSelect"); if (!hasClient(client)) { this.empty(); return; } var self = this; executeClientMethod(client, "GetData", [], { silentAuth: true }).then(function (torrents) { if (!Array.isArray(torrents) || !torrents.length) { self.activity.loader(false); self.empty(); return; } // Filter by section var filtered = torrents.filter(function (t) { return classifyState$1(t.state) === filterSection; }); if (!filtered.length) { self.activity.loader(false); self.empty(); return; } // Render cards filtered.forEach(function (torrentData) { var item = new Item(torrentData); item.render().on('hover:focus', function () { last = item.render()[0]; scroll.update(item.render(), true); }); item.render().on('hover:enter', function () { showTorrentMenu(torrentData, filtered); }); grid.append(item.render()); items.push(item); }); self.activity.loader(false); self.activity.toggle(); })["catch"](function (error) { console.error("TDM", "Section view load error:", error); self.activity.loader(false); self.empty(); }); }; this.empty = function () { var emptyEl = $('
' + Lampa.Lang.translate("torrentmanager_state_empty_title") + '
'); grid.append(emptyEl); this.activity.loader(false); this.activity.toggle(); }; this.start = function () { if (Lampa.Activity.active() && Lampa.Activity.active().activity !== this.activity) return; Lampa.Controller.add("content", { toggle: function toggle() { Lampa.Controller.collectionSet(scroll.render()); Lampa.Controller.collectionFocus(last || false, scroll.render()); }, right: function right() { Navigator.move('right'); }, left: function left() { if (Navigator.canmove('left')) Navigator.move('left');else Lampa.Controller.toggle('menu'); }, down: function down() { Navigator.move('down'); }, up: function up() { if (Navigator.canmove('up')) Navigator.move('up');else Lampa.Controller.toggle('head'); }, back: function back() { Lampa.Activity.backward(); } }); Lampa.Controller.toggle("content"); }; this.pause = function () {}; this.stop = function () {}; this.render = function () { return html; }; this.destroy = function () { network.clear(); if (items) { items.forEach(function (item) { item.destroy(); }); } if (scroll) scroll.destroy(); html.remove(); items = null; grid = null; scroll = null; network = null; }; } var UKRAINIAN_THANKS_URL = 'https://lampame.donatik.me/'; var DEFAULT_THANKS_URL = 'https://t.me/tribute/app?startapp=d5WS'; function getCurrentLanguage() { return String(Lampa.Storage.get('language', 'ru') || 'ru').toLowerCase(); } function getThanksUrlByLanguage() { return getCurrentLanguage() === 'uk' ? UKRAINIAN_THANKS_URL : DEFAULT_THANKS_URL; } function openThanksModal() { var thanksUrl = getThanksUrlByLanguage(); var html = Lampa.Template.get('modal_qr', { title: Lampa.Lang.translate('torrentmanager_settings_thanks'), text: Lampa.Lang.translate('torrentmanager_settings_thanks_description'), qr_text: "").concat(thanksUrl, "") }); var qrElement = html.find('.account-modal-split__qr-code'); var enabledController = Lampa.Controller.enabled().name; Lampa.Utils.qrcode(thanksUrl, qrElement, function (error) { console.error('TDM', 'Unable to generate thanks QR code', error); qrElement.text(thanksUrl); }); Lampa.Modal.open({ title: '', html: html, size: 'medium', onBack: function onBack() { Lampa.Modal.close(); Lampa.Controller.toggle(enabledController); } }); } function Main$1(manifest) { //Создание пункта меню Lampa.SettingsApi.addComponent({ component: manifest.component, name: manifest.name, icon: manifest.icon }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + 'Thanks', type: 'button' }, field: { name: Lampa.Lang.translate('torrentmanager_settings_thanks') }, onChange: function onChange() { openThanksModal(); } }); function hasSecretValue(value) { return Boolean(String(value || "").trim()); } function updateSecretLabel(item, label, storageKey) { var status = hasSecretValue(Lampa.Storage.field(storageKey)) ? Lampa.Lang.translate('torrentmanager_secret_set') : Lampa.Lang.translate('torrentmanager_secret_not_set'); item.find('.settings-param__name').text("".concat(label, " (").concat(status, ")")); } function openSecretEditor(storageKey, title) { Lampa.Input.edit({ title: title, value: Lampa.Storage.field(storageKey) || "", free: true, nosave: true, password: true }, function (newValue) { Lampa.Storage.set(storageKey, String(newValue || "")); Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_secret_saved') }); Lampa.Settings.update(); }); } function runClientAuth(clientName, clearSession) { var client = null; var sessionKey = ""; if (clientName === "qBittorent") { client = Qbittorent; } else if (clientName === "transmission") { client = Transmission; sessionKey = "".concat(manifest.component, "transmissionKey"); } else if (clientName === "synology") { client = Synology; sessionKey = "".concat(manifest.component, "synologyKey"); } else if (clientName === "keenetic") { client = Keenetic; sessionKey = "".concat(manifest.component, "keeneticKey"); } if (!client || typeof client.auth !== "function") { return; } if (clearSession && sessionKey) { Lampa.Storage.set(sessionKey, ""); } Promise.resolve().then(function () { return client.auth(true); })["catch"](function () {}); } function normalizeInputValue(value) { if (typeof value === "string") { return value.trim(); } if (value === null || typeof value === "undefined") { return ""; } return String(value).trim(); } function isAddressWithProtocol(value) { return /^https?:\/\/\S+$/i.test(value); } function createAddressValidationHandler(storageKey) { var lastValidValue = ""; var storedValue = normalizeInputValue(Lampa.Storage.field(storageKey)); if (isAddressWithProtocol(storedValue)) { lastValidValue = storedValue; } return function (newValue) { var normalizedValue = normalizeInputValue(newValue); if (!normalizedValue) { lastValidValue = ""; Lampa.Storage.set(storageKey, ""); Lampa.Settings.update(); return; } if (!isAddressWithProtocol(normalizedValue)) { Lampa.Storage.set(storageKey, lastValidValue); Lampa.Bell.push({ text: Lampa.Lang.translate('torrentmanager_url_requires_protocol') }); Lampa.Settings.update(); return; } lastValidValue = normalizedValue; if (newValue !== normalizedValue) { Lampa.Storage.set(storageKey, normalizedValue); } Lampa.Settings.update(); }; } //Select Client Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + 'Select', type: 'select', "default": 'no_client', values: { no_client: 'None', universalClient: "Universal", qBittorent: "qBittorent", transmission: "Transmission", keenetic: "Keenetic Transmission", synology: "Synology" } }, field: { name: Lampa.Lang.translate('clientChoose') }, onChange: function onChange(value) { Lampa.Settings.update(); } }); //Proxy TMDB // Lampa.SettingsApi.addParam({ // component: manifest.component, // param: { // name: manifest.component + 'proxyTMDB', // type: 'trigger', // default: 'false', // values: { // true: Lampa.Lang.translate('true'), // false: Lampa.Lang.translate('false') // }, // }, // field: { // name: 'Proxy TMDB posters', // }, // onRender: function (item) { // var forbiddenValues = ["universalClient", "synology", "no_client"]; // var clientValue = Lampa.Storage.field(manifest.component + 'proxyTMDB'); // // indexOf возвращает -1, если значения нет в массиве // if (forbiddenValues.indexOf(clientValue) === -1) { // item.show(); // } else item.hide(); // }, // onChange: function () { // Lampa.Settings.update(); // } // }) //Universal action var universalActionValues = Lampa.Platform.is('android') ? { openAndroid: Lampa.Lang.translate('openUniversal'), click: Lampa.Lang.translate('copyUniversal') } : { open: Lampa.Lang.translate('openUniversal'), click: Lampa.Lang.translate('copyUniversal') }; Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + 'UniversalAction', type: 'select', "default": 'no_client', values: universalActionValues }, field: { name: Lampa.Lang.translate('UniversalAction') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "universalClient") { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); //qBittorent Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "qBittorentUrl", type: "input", //доступно select,input,trigger,title,static //values: `${Lampa.Storage.get("qBittorentUrl") || ""}`, placeholder: '', values: '', "default": '' }, field: { name: "Address" }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent") { item.show(); } else item.hide(); }, onChange: createAddressValidationHandler(manifest.component + "qBittorentUrl") }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "qBittorentApiKey", type: "button" }, field: { name: Lampa.Lang.translate('torrentmanager_api_key') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent") { updateSecretLabel(item, Lampa.Lang.translate('torrentmanager_api_key'), manifest.component + "qBittorentApiKey"); item.show(); } else item.hide(); }, onChange: function onChange() { openSecretEditor(manifest.component + "qBittorentApiKey", 'qBittorrent API key'); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "qBittorentTestConnection" }, field: { name: Lampa.Lang.translate('torrentmanager_test_connection') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("qBittorent", false); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "qBittorentReauth" }, field: { name: Lampa.Lang.translate('torrentmanager_reauth_now') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("qBittorent", true); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "trigger", //доступно select,input,trigger,title,static "default": false, name: manifest.component + "qBittorentTweak" }, field: { name: Lampa.Lang.translate('tweak') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); // qBittorent Tweak part Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "qBittorentSequentialDownload", type: "trigger", //доступно select,input,trigger,title,static "default": false }, field: { name: 'Sequential Download' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent" && Lampa.Storage.field(manifest.component + "qBittorentTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "qBittorentfirstLastPiecePrio", type: "trigger", //доступно select,input,trigger,title,static "default": false }, field: { name: 'Prioritize download first last piece' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent" && Lampa.Storage.field(manifest.component + "qBittorentTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "qBittorentProxy", type: "trigger", //доступно select,input,trigger,title,static "default": false }, field: { name: Lampa.Lang.translate('LMEProxy'), description: Lampa.Lang.translate('LMEProxyDescription') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent" && Lampa.Storage.field(manifest.component + "qBittorentTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "qBittorentMovies", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathMovieQbit') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent" && Lampa.Storage.field(manifest.component + "qBittorentTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "qBittorentTV", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathTVQbit') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "qBittorent" && Lampa.Storage.field(manifest.component + "qBittorentTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); //Transmission Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionUrl", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: 'Address' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission") { item.show(); } else item.hide(); }, onChange: createAddressValidationHandler(manifest.component + "transmissionUrl") }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionUser", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: 'Login' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionPass", type: "button" //доступно select,input,trigger,title,static }, field: { name: 'Password' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission") { updateSecretLabel(item, 'Password', manifest.component + "transmissionPass"); item.show(); } else item.hide(); }, onChange: function onChange() { openSecretEditor(manifest.component + "transmissionPass", 'Transmission Password'); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "transmissionTestConnection" }, field: { name: Lampa.Lang.translate('torrentmanager_test_connection') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("transmission", false); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "transmissionReauth" }, field: { name: Lampa.Lang.translate('torrentmanager_reauth_now') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("transmission", true); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "trigger", //доступно select,input,trigger,title,static "default": false, name: manifest.component + "transmissionTweak" }, field: { name: Lampa.Lang.translate('tweak') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); // Transmission Tweak part Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionAutostart", type: "trigger", //доступно select,input,trigger,title,static "default": true }, field: { name: 'Autostop' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission" && Lampa.Storage.field(manifest.component + "transmissionTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionSequentialDownload", type: "trigger", //доступно select,input,trigger,title,static "default": false }, field: { name: 'SequentialDownload' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission" && Lampa.Storage.field(manifest.component + "transmissionTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionProxy", type: "trigger", //доступно select,input,trigger,title,static "default": false }, field: { name: Lampa.Lang.translate('LMEProxy'), description: Lampa.Lang.translate('LMEProxyDescription') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission" && Lampa.Storage.field(manifest.component + "transmissionTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionPath", type: "input", //доступно select,input,trigger,title,static placeholder: '/transmission/rpc', values: '/transmission/rpc', "default": '/transmission/rpc' }, field: { name: Lampa.Lang.translate('TransmissionRPCRoute'), description: Lampa.Lang.translate('TransmissionRPCRouteDescription') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission" && Lampa.Storage.field(manifest.component + "transmissionTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionMovies", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathMovie') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission" && Lampa.Storage.field(manifest.component + "transmissionTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "transmissionTV", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathTV') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "transmission" && Lampa.Storage.field(manifest.component + "transmissionTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); //Synology Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "synologyUrl", type: "input", //доступно select,input,trigger,title,static //values: `${Lampa.Storage.get("synologyUrl") || ""}`, placeholder: '', values: '', "default": '' }, field: { name: 'Address' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { item.show(); } else item.hide(); }, onChange: createAddressValidationHandler(manifest.component + "synologyUrl") }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "synologyUser", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: 'Login' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "synologyPass", type: "button" //доступно select,input,trigger,title,static }, field: { name: 'Password' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { updateSecretLabel(item, 'Password', manifest.component + "synologyPass"); item.show(); } else item.hide(); }, onChange: function onChange() { openSecretEditor(manifest.component + "synologyPass", 'Synology Password'); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "synologyTestConnection" }, field: { name: Lampa.Lang.translate('torrentmanager_test_connection') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("synology", false); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "synologyReauth" }, field: { name: Lampa.Lang.translate('torrentmanager_reauth_now') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("synology", true); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "synologyPathMovies", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathMovie') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "synologyPathTV", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathTV') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "trigger", //доступно select,input,trigger,title,static "default": false, name: manifest.component + "synologyProxy" }, field: { name: Lampa.Lang.translate('LMEProxy'), description: Lampa.Lang.translate('LMEProxyDescription') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "synology") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); // Synology Tweak part // Keenetic Transmission Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticUrl", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: 'Address' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic") { item.show(); } else item.hide(); }, onChange: createAddressValidationHandler(manifest.component + "keeneticUrl") }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticUser", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: 'Login' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticPass", type: "button" //доступно select,input,trigger,title,static }, field: { name: 'Password' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic") { updateSecretLabel(item, 'Password', manifest.component + "keeneticPass"); item.show(); } else item.hide(); }, onChange: function onChange() { openSecretEditor(manifest.component + "keeneticPass", 'Keenetic Password'); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "keeneticTestConnection" }, field: { name: Lampa.Lang.translate('torrentmanager_test_connection') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("keenetic", false); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "button", name: manifest.component + "keeneticReauth" }, field: { name: Lampa.Lang.translate('torrentmanager_reauth_now') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic") { item.show(); } else item.hide(); }, onChange: function onChange() { runClientAuth("keenetic", true); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticWebdavUrl", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: 'WebDAV URL', description: 'URL for WebDAV access to files' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic") { item.show(); } else item.hide(); }, onChange: createAddressValidationHandler(manifest.component + "keeneticWebdavUrl") }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { type: "trigger", //доступно select,input,trigger,title,static "default": false, name: manifest.component + "keeneticTweak" }, field: { name: Lampa.Lang.translate('tweak') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic") { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); // Keenetic Tweak part Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticAutostart", type: "trigger", //доступно select,input,trigger,title,static "default": true }, field: { name: 'Autostop' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic" && Lampa.Storage.field(manifest.component + "keeneticTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticSequentialDownload", type: "trigger", //доступно select,input,trigger,title,static "default": false }, field: { name: 'SequentialDownload' }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic" && Lampa.Storage.field(manifest.component + "keeneticTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticProxy", type: "trigger", //доступно select,input,trigger,title,static "default": false }, field: { name: Lampa.Lang.translate('LMEProxy'), description: Lampa.Lang.translate('LMEProxyDescription') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic" && Lampa.Storage.field(manifest.component + "keeneticTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(value) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticPath", type: "input", //доступно select,input,trigger,title,static placeholder: '/transmission/rpc', values: '/transmission/rpc', "default": '/transmission/rpc' }, field: { name: Lampa.Lang.translate('TransmissionRPCRoute'), description: Lampa.Lang.translate('TransmissionRPCRouteDescription') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic" && Lampa.Storage.field(manifest.component + "keeneticTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticMovies", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathMovie') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic" && Lampa.Storage.field(manifest.component + "keeneticTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); Lampa.SettingsApi.addParam({ component: manifest.component, param: { name: manifest.component + "keeneticTV", type: "input", //доступно select,input,trigger,title,static placeholder: '', values: '', "default": '' }, field: { name: Lampa.Lang.translate('pathTV') }, onRender: function onRender(item) { if (Lampa.Storage.field(manifest.component + 'Select') === "keenetic" && Lampa.Storage.field(manifest.component + "keeneticTweak") === true) { item.show(); } else item.hide(); }, onChange: function onChange(item) { Lampa.Settings.update(); } }); } function Main(selectedTorrent) { var uri = selectedTorrent.MagnetUri ? selectedTorrent.MagnetUri : selectedTorrent.Link; var action = Lampa.Storage.field("lmetorrentUniversalAction"); if (action === "openAndroid" && typeof AndroidJS !== 'undefined' && typeof AndroidJS.openTorrentLink !== 'undefined') { AndroidJS.openTorrentLink(uri, "{}"); } else if (action === "open") { window.location.assign(uri); } else { Lampa.Utils.copyTextToClipboard(uri, function () { Lampa.Bell.push({ text: Lampa.Lang.translate('copy_secuses') }); }, function () { Lampa.Bell.push({ text: Lampa.Lang.translate('copy_error') }); }); } } var DOWNLOAD_ICON$1 = ""; var CLIENT_MENU$1 = { qBittorent: { title: "qBittorrent", type: "client", clientName: "qBittorent" }, transmission: { title: "Transmission", type: "client", clientName: "transmission" }, keenetic: { title: "Keenetic", type: "client", clientName: "keenetic" }, synology: { title: "Synology", type: "client", clientName: "synology" }, universalClient: { title: "Universal", type: "universal" } }; function sendToClient$1(clientName, selectedTorrent, labels, dtype) { executeClientMethod(clientName, "SendTask", [selectedTorrent, labels, dtype], { silentAuth: true })["catch"](function () {}); } function sendToUniversal$1(selectedTorrent) { Main(selectedTorrent); } function buildButtonTitle$1(label) { return "
".concat(DOWNLOAD_ICON$1).concat(label, "
"); } function resolveTorrentLabels$1(movie) { var type = movie && movie.first_air_date ? "tv" : "movie"; return "".concat(type, "/").concat(movie && movie.id); } function resolveDestinationType$1(movie) { return movie && movie.first_air_date ? "TV" : "Movies"; } function Send() { Lampa.Listener.follow("torrent", function (e) { if (e.type !== "onlong") { return; } var selectedTorrent = e.element; var activeMovie = Lampa.Activity.active().movie; var labels = resolveTorrentLabels$1(activeMovie); var dtype = resolveDestinationType$1(activeMovie); var selectedClient = Lampa.Storage.field("lmetorrentSelect"); var menuConfig = CLIENT_MENU$1[selectedClient]; if (!menuConfig) { return; } e.menu.push({ title: buildButtonTitle$1(menuConfig.title), onSelect: function onSelect() { if (menuConfig.type === "universal") { sendToUniversal$1(selectedTorrent); return; } sendToClient$1(menuConfig.clientName, selectedTorrent, labels, dtype); } }); }); } var DOWNLOAD_ICON = ""; var CLIENT_MENU = { qBittorent: { title: "qBittorrent", type: "client", clientName: "qBittorent" }, transmission: { title: "Transmission", type: "client", clientName: "transmission" }, keenetic: { title: "Keenetic", type: "client", clientName: "keenetic" }, synology: { title: "Synology", type: "client", clientName: "synology" }, universalClient: { title: "Universal", type: "universal" } }; function sendToClient(clientName, selectedTorrent, labels, dtype) { executeClientMethod(clientName, "SendTask", [selectedTorrent, labels, dtype], { silentAuth: true })["catch"](function () {}); } function sendToUniversal(selectedTorrent) { Main(selectedTorrent); } function buildButtonTitle(label) { return "
".concat(DOWNLOAD_ICON).concat(label, "
"); } function resolveTorrentLabels(movie) { var type = movie && movie.first_air_date ? "tv" : "movie"; return "".concat(type, "/").concat(movie && movie.id); } function resolveDestinationType(movie) { return movie && movie.first_air_date ? "TV" : "Movies"; } function buildMagnetUri(hash, title) { return "magnet:?xt=urn:btih:".concat(hash, "&dn=").concat(encodeURIComponent(title)); } function MyTorrents() { Lampa.Listener.follow("mytorrents", function (e) { if (e.type !== "onlong") { return; } var item = e.object; var hash = item && item.hash; var title = item && item.title; var movie = item && item.data && item.data.movie; if (!hash) { return; } var magnetUri = buildMagnetUri(hash, title); var selectedTorrent = { MagnetUri: magnetUri }; var labels = resolveTorrentLabels(movie); var dtype = resolveDestinationType(movie); var selectedClient = Lampa.Storage.field("lmetorrentSelect"); var menuConfig = CLIENT_MENU[selectedClient]; if (!menuConfig) { return; } e.menu.push({ title: buildButtonTitle(menuConfig.title), onSelect: function onSelect() { if (menuConfig.type === "universal") { sendToUniversal(selectedTorrent); return; } sendToClient(menuConfig.clientName, selectedTorrent, labels, dtype); } }); }); } /** * Sonarr/Radarr API Module for Torrent Manager * * Provides external API search as a fallback when label-based matching fails. * Results are cached in-memory with a 12-hour TTL and persisted to Lampa.Storage. * Pattern from MovieEnhancer/utils/wm_quality.js. * Used by cardIntegration.js and full.js to resolve TMDB IDs for clients * that do not support labels (e.g. Synology). */ /* * In-memory cache with LRU eviction — persisted to Lampa.Storage. */ var CACHE_STORAGE_KEY = 'lme_torrentmanager_api_cache'; var CACHE_TTL = 12 * 60 * 60 * 1000; // 12 hours var CACHE_SIZE = 500; var memoryCache = {}; var cacheInitialized = false; function initCache() { if (cacheInitialized) return; memoryCache = Lampa.Storage.get(CACHE_STORAGE_KEY, {}); cacheInitialized = true; } function getCached(key) { initCache(); var entry = memoryCache[key]; if (!entry) return null; if (Date.now() - entry.timestamp > CACHE_TTL) { delete memoryCache[key]; return null; } return entry.value; } function setCached(key, value) { initCache(); // LRU eviction if cache is full var size = Object.keys(memoryCache).length; if (size >= CACHE_SIZE) { var oldest = null; var oldestTime = Infinity; for (var k in memoryCache) { if (memoryCache[k].timestamp < oldestTime) { oldestTime = memoryCache[k].timestamp; oldest = k; } } if (oldest) delete memoryCache[oldest]; } memoryCache[key] = { timestamp: Date.now(), value: value }; saveCache(); } var saveTimeout = null; function saveCache() { if (saveTimeout) clearTimeout(saveTimeout); saveTimeout = setTimeout(function () { Lampa.Storage.set(CACHE_STORAGE_KEY, memoryCache); }, 500); } /* * Inflight map — prevents duplicate concurrent requests for the same key. * Pattern from MovieEnhancer/utils/wm_quality.js (inflight = {}). */ var inflight$1 = {}; /* * CORS proxy helper — wraps a URL through cors.io to avoid browser CORS blocks. * cors.io returns { url, status, headers, body: "..." } — parseResponse handles this. */ var CORS_PROXY = 'https://cors.io/?url='; function proxyUrl(url) { return CORS_PROXY + encodeURIComponent(url); } /** * Normalize response data — handles cors.io wrapped format and raw responses. * cors.io returns: { url, status, headers, body: "" } * Lampa.Network.silent may also return a string or a pre-parsed object. * * @param {*} response - Raw response from Lampa.Network.silent * @returns {Object|Array|null} Parsed data or null */ function parseResponse(response) { if (!response) return null; // cors.io wraps response: { url, status, headers, body: "..." } if (response.url && response.body) { try { return JSON.parse(response.body); } catch (e) { return null; } } // Direct JSON string if (typeof response === 'string') { try { return JSON.parse(response); } catch (e) { return null; } } // Already-parsed object if (_typeof(response) !== 'object') return null; return response; } /** * Build a cache key from query string to avoid special character collisions. * @param {string} prefix - 'sonarr' or 'radarr' * @param {string} query - Raw search query * @returns {string} Sanitized cache key */ function buildCacheKey(prefix, query) { return prefix + '_' + String(query).replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(); } /** * Search Sonarr Skyhook for TV series by title or IMDB ID. * Uses GET https://skyhook.sonarr.tv/v1/tvdb/search/en/?term={query} * For IMDB search, use term=imdb:{id} for exact match. * * @param {string} titleOrImdb - Series title or "imdb:{id}" * @returns {Promise<{tmdb_id: number|null, imdb_id: string|null, title: string|null, year: string|null, matched_via: string}|null>} */ function searchSonarr(_x) { return _searchSonarr.apply(this, arguments); } /** * Search Radarr Video API for movies by title. * Uses GET https://api.radarr.video/v1/search?q={query} * * @param {string} title - Movie title * @returns {Promise<{tmdb_id: number|null, imdb_id: string|null, title: string|null, year: string|null, matched_via: string}|null>} */ function _searchSonarr() { _searchSonarr = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(titleOrImdb) { var cacheKey, cached, url, data, result, normalized, status, _t; return _regenerator().w(function (_context) { while (1) switch (_context.p = _context.n) { case 0: if (titleOrImdb) { _context.n = 1; break; } return _context.a(2, null); case 1: cacheKey = buildCacheKey('sonarr', titleOrImdb); // Check in-memory cache FIRST cached = getCached(cacheKey); if (!cached) { _context.n = 2; break; } return _context.a(2, cached); case 2: if (!inflight$1[cacheKey]) { _context.n = 3; break; } return _context.a(2, null); case 3: inflight$1[cacheKey] = true; url = proxyUrl('https://skyhook.sonarr.tv/v1/tvdb/search/en/?term=' + encodeURIComponent(titleOrImdb)); _context.p = 4; _context.n = 5; return new Promise(function (resolve, reject) { Lampa.Network.silent(url, resolve, function (jqXHR) { reject(jqXHR); }, null, { timeout: 10000 }); }); case 5: data = _context.v; data = parseResponse(data); if (!(!data || !data.length)) { _context.n = 6; break; } return _context.a(2, null); case 6: result = data[0]; normalized = { tmdb_id: result.tmdbId || null, imdb_id: result.imdbId || null, title: result.title || null, year: result.firstAired ? result.firstAired.slice(0, 4) : null, matched_via: 'sonarr', updated_at: Date.now() }; if (normalized.tmdb_id) { setCached(cacheKey, normalized); } return _context.a(2, normalized); case 7: _context.p = 7; _t = _context.v; status = _t && _t.status ? _t.status : 'unknown'; console.warn('TDM', 'Sonarr search failed (HTTP ' + status + ')'); console.error('TDM', 'Sonarr error details:', _t); return _context.a(2, null); case 8: _context.p = 8; delete inflight$1[cacheKey]; return _context.f(8); case 9: return _context.a(2); } }, _callee, null, [[4, 7, 8, 9]]); })); return _searchSonarr.apply(this, arguments); } function searchRadarr(_x2) { return _searchRadarr.apply(this, arguments); } function _searchRadarr() { _searchRadarr = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(title) { var cacheKey, cached, url, data, result, normalized, status, _t2; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: if (title) { _context2.n = 1; break; } return _context2.a(2, null); case 1: cacheKey = buildCacheKey('radarr', title); // Check in-memory cache FIRST cached = getCached(cacheKey); if (!cached) { _context2.n = 2; break; } return _context2.a(2, cached); case 2: if (!inflight$1[cacheKey]) { _context2.n = 3; break; } return _context2.a(2, null); case 3: inflight$1[cacheKey] = true; url = proxyUrl('https://api.radarr.video/v1/search?q=' + encodeURIComponent(title)); _context2.p = 4; _context2.n = 5; return new Promise(function (resolve, reject) { Lampa.Network.silent(url, resolve, function (jqXHR) { reject(jqXHR); }, null, { timeout: 10000 }); }); case 5: data = _context2.v; data = parseResponse(data); if (!(!data || !data.length)) { _context2.n = 6; break; } return _context2.a(2, null); case 6: result = data[0]; normalized = { tmdb_id: result.TmdbId || null, imdb_id: result.ImdbId || null, title: result.Title || null, year: result.Year ? String(result.Year) : null, matched_via: 'radarr', updated_at: Date.now() }; if (normalized.tmdb_id) { setCached(cacheKey, normalized); } return _context2.a(2, normalized); case 7: _context2.p = 7; _t2 = _context2.v; status = _t2 && _t2.status ? _t2.status : 'unknown'; console.warn('TDM', 'Radarr search failed (HTTP ' + status + ')'); console.error('TDM', 'Radarr error details:', _t2); return _context2.a(2, null); case 8: _context2.p = 8; delete inflight$1[cacheKey]; return _context2.f(8); case 9: return _context2.a(2); } }, _callee2, null, [[4, 7, 8, 9]]); })); return _searchRadarr.apply(this, arguments); } function startClient(_x) { return _startClient.apply(this, arguments); } function _startClient() { _startClient = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(client) { var data, info, result, _t2; return _regenerator().w(function (_context2) { while (1) switch (_context2.p = _context2.n) { case 0: _context2.p = 0; if (hasClient(client)) { _context2.n = 1; break; } throw new Error('Unknown client type'); case 1: _context2.n = 2; return executeClientMethod(client, 'GetData', [], { silentAuth: true }); case 2: data = _context2.v; info = null; _context2.p = 3; _context2.n = 4; return executeClientMethod(client, 'GetInfo', [], { silentAuth: true }); case 4: info = _context2.v; _context2.n = 6; break; case 5: _context2.p = 5; info = null; case 6: result = { data: data, info: info }; return _context2.a(2, result); case 7: _context2.p = 7; _t2 = _context2.v; console.error('TDM', 'Error fetching client data:', _t2); throw _t2; case 8: return _context2.a(2); } }, _callee2, null, [[3, 5], [0, 7]]); })); return _startClient.apply(this, arguments); } function torrentInfo() { Lampa.Listener.follow('full', function (e) { if (e.type === 'complite') { setTimeout(function () { function findTorrent(data, method, id) { var searchLabel = "".concat(method, "/").concat(id); return data.find(function (item) { return item.labels && item.labels.includes(searchLabel); }); } var activeClient = getActiveClientName(); if (!hasClient(activeClient)) { return; } startClient(activeClient).then(function (r) { if (r && r.data) { var torrent = findTorrent(r.data, e.object.method, e.object.id); if (torrent) { // Create button element var $button = $("
\n \n ".concat(torrent.completed > 0 ? "".concat(getStandardizedStateName(torrent.state), " - ").concat(Number((torrent.completed * 100).toFixed(2)), "%") : getStandardizedStateName(torrent.state), "\n
")); // Attach event handlers $button.on("hover:enter", /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() { var enabled, menu; return _regenerator().w(function (_context) { while (1) switch (_context.n) { case 0: enabled = Lampa.Controller.enabled().name; menu = []; menu.push({ title: Lampa.Lang.translate('resume'), action: 'resume' }, { title: Lampa.Lang.translate('pause'), action: 'pause' }, { title: Lampa.Lang.translate('delete'), action: 'delete' }); // Add play option for Keenetic client if (activeClient === 'keenetic') { menu.push({ title: Lampa.Lang.translate('play'), action: 'play' }); } // Add full delete option for clients that support it if (activeClient !== 'synology') menu.push({ title: Lampa.Lang.translate('fullDelete'), action: 'delete', deleteFiles: true }); // Set menu Lampa.Select.show({ title: torrent.completed > 0 ? "".concat(getStandardizedStateName(torrent.state), " - ").concat(Number((torrent.completed * 100).toFixed(2)), "%") : getStandardizedStateName(torrent.state), items: menu, onBack: function onBack() { // Повертаємось до попереднього контролера, якщо він існує var currentController = Lampa.Controller.enabled(); if (currentController && currentController.name !== enabled) { Lampa.Controller.toggle(enabled); } else { // Якщо контролер не змінився, просто вимикаємо меню Lampa.Controller.toggle('menu'); } }, onSelect: function onSelect(a) { return executeClientMethod(activeClient, 'SendCommand', [a, torrent], { silentAuth: true }); } }); case 1: return _context.a(2); } }, _callee); }))); // Remove existing torrent button before adding new one e.object.activity.render().find('.full-start-new__buttons').find('.button--lme_torrent').remove(); // Append new button to container e.object.activity.render().find('.full-start-new__buttons').append($button); // Manually bind hover events that Lampa normally binds via .selector in full/start.js $button.on('hover:focus', function () { $(this).addClass('focus'); }).on('hover:blur', function () { $(this).removeClass('focus'); }).on('hover:hover', function () { // hover state is handled by CSS class .selector }).on('hover:touch', function () { // touch state }); // Add button to Controller collection for spatial navigation Lampa.Controller.collectionAppend($button); } else { // Fallback: try Sonarr/Radarr for clients without labels (Synology, etc.) performFallbackLookup(e.object, r.data); } } else { console.error('TDM', 'Failed to get torrent client data'); } }); }, 100); } }); } /** * Fallback lookup when label-based matching fails. * Attempts to resolve the TMDB ID via Sonarr/Radarr external API, * then re-checks the torrent list. Results are cached in IndexedDB. * Non-blocking — always resolves silently on errors. * * @param {Object} movieData - Movie data from the 'full' event (e.object) * @param {Array} torrentData - Array of torrent objects from GetData * @param {string} activeClient - Active torrent client name */ function performFallbackLookup(movieData, torrentData, activeClient) { // Extract movie info from event object var movie = movieData.data && movieData.data.movie ? movieData.data.movie : movieData; var method = movieData.method || (movie.first_air_date ? 'tv' : 'movie'); var id = movieData.id || movie.id; var title = movie.title || movie.original_title || movie.name || ''; var imdbId = movie.imdb_id || movieData.imdb_id; if (!id || !title) return; // Determine search strategy var searchPromise = null; if (method === 'tv' && imdbId) { searchPromise = searchSonarr('imdb:' + imdbId); } else if (method === 'tv') { searchPromise = searchSonarr(title); } else { searchPromise = searchRadarr(title); } if (!searchPromise) return; searchPromise.then(function (meta) { if (!meta || !meta.tmdb_id) return; // Re-check torrents with resolved TMDB ID var resolvedLabel = method + '/' + meta.tmdb_id; var resolvedTorrent = null; for (var i = 0; i < torrentData.length; i++) { var labels = torrentData[i].labels || []; if (labels.indexOf(resolvedLabel) !== -1) { resolvedTorrent = torrentData[i]; break; } } if (resolvedTorrent) { // Cache the result so subsequent visits use IndexedDB directly saveMetadata(String(resolvedTorrent.id), { tmdb_id: meta.tmdb_id, media_type: method, torrent_id: resolvedTorrent.id, matched_via: meta.matched_via || 'external', updated_at: Date.now() })["catch"](function () {}); } })["catch"](function () { // Silent fallback — never block UI on network errors }); } /** * Utility functions for Torrent Manager plugin */ /** * Create a panel navigation item for Lampa.Select.show * @returns {Function} Function that calls Lampa.Activity.push */ function createPanelNavigationItem() { return function () { Lampa.Activity.push({ url: '', title: String(Lampa.Storage.get('lmetorrentSelect') || '').toUpperCase() + " Manager", component: 'lmetorrentPanel', page: 1 }); }; } var HeaderIconController = /*#__PURE__*/function () { function HeaderIconController(element) { _classCallCheck(this, HeaderIconController); this.element = $(element); this.bind(); this.listen(); } return _createClass(HeaderIconController, [{ key: "bind", value: function bind() { this.element.on('hover:enter', this.showSidebar.bind(this)); } }, { key: "listen", value: function listen() { TorrentStateManager$1.on('torrents:status_changed', this.updateColor.bind(this)); } }, { key: "showSidebar", value: function showSidebar() { var torrents = TorrentStateManager$1.torrents.filter(function (torrent) { var state = String(torrent && torrent.state || '').toLowerCase(); var isCompleted = Number(torrent && torrent.completed) >= 1; return !isCompleted && state.indexOf('finish') < 0; }); var sortedTorrents = this.sortTorrents(torrents); var activeDownloads = torrents.filter(function (torrent) { var state = String(torrent && torrent.state || '').toLowerCase(); return state.indexOf('download') >= 0 || state.indexOf('check') >= 0; }).length; // Create items for Lampa.Select.show var items = [ // Panel navigation item //createPanelNavigationItem(), { title: String(Lampa.Storage.get('lmetorrentSelect') || '').toUpperCase() + " Manager", action: 'panel', onSelect: function onSelect() { createPanelNavigationItem()(); } }].concat(_toConsumableArray(sortedTorrents.map(function (torrent) { return { title: "".concat(torrent.name, " (").concat(Math.round((torrent.completed || 0) * 100), "%)"), action: 'torrent', torrent: torrent }; }))); Lampa.Select.show({ title: "Torrents (".concat(activeDownloads, "/").concat(TorrentStateManager$1.torrents.length, ")"), items: items, onBack: function onBack() { Lampa.Controller.toggle('content'); }, onSelect: function onSelect(item) { if (item.action === 'torrent') { showTorrentMenu(item.torrent, TorrentStateManager$1.torrents); } else if (item.action === 'panel') { item.onSelect(); } // For summary item, we could show a general menu or just do nothing } }); } }, { key: "sortTorrents", value: function sortTorrents(torrents) { return torrents.sort(function (a, b) { var aState = String(a && a.state || '').toLowerCase(); var bState = String(b && b.state || '').toLowerCase(); var aDownloading = aState.indexOf('download') >= 0; var bDownloading = bState.indexOf('download') >= 0; if (aDownloading && !bDownloading) return -1; if (!aDownloading && bDownloading) return 1; return String(b && b.id || '').localeCompare(String(a && a.id || '')); }); } }, { key: "updateColor", value: function updateColor(event) { if (event.active) { this.element.css('color', 'yellow'); } else { this.element.css('color', 'limegreen'); } } }]); }(); var TORRENT_ICON = "\n \n \n \n \n \n"; var DomInjector = /*#__PURE__*/function () { function DomInjector() { _classCallCheck(this, DomInjector); } return _createClass(DomInjector, [{ key: "inject", value: function inject() { var iconElement = Lampa.Head.addIcon(TORRENT_ICON); iconElement.addClass('torrent-manager-icon'); return new HeaderIconController(iconElement); } }]); }(); var DomInjector$1 = new DomInjector(); /** * Home Row integration for Torrent Manager. * Registers a Lampa.ContentRows line on the main screen showing * active torrents (downloading + seeding) with a "More" button * that navigates to the full lmetorrentPanel component. */ // Quality detection regex matching panelItem.js for lmetorrent_item__card template var regexp2 = /(?:PPV.)?[HP]DTV|(?:HD)?TC|[cC]am|(?:HD)?CAM|B[rR]Rip|WEBRip|WEB-Rip|WEB-DL|WEB|TS|(?:PPV )?WEB-?DL(?: DVDRip)?|H[dD]Rip|DVDRip|DVDRiP|DVDRIP|CamRip|W[EB]B[rR]ip|HDRIP|[Bb]lu[Rr]ay|DvDScr|hdtv/; /** * Build a CSS class string based on completion progress. * Copies getProgressClass from panelItem.js. * @param {number} progress - 0–100 * @returns {string} CSS class name */ function getProgressClass(progress) { if (progress >= 100) return 'is-high'; if (progress >= 50) return 'is-mid'; return 'is-low'; } /** * Card class for rendering a single torrent item on the home screen row. * Implements the cardClass interface required by Lampa.ContentRows. * Adapts the lmetorrent_item__card template following main_channel.js pattern. */ function HomeTorrentCard(torrentData, playlist) { var self = this; this.data = torrentData; this.playlist = playlist; this.card = null; this.create = function () { var progress = Number((self.data.completed * 100).toFixed(2)); self.card = Lampa.Template.get('lmetorrent_item__card', { title: self.data.name, size: Lampa.Utils.bytesToSize(self.data.size, 0), state: getStandardizedStateName(self.data.state), 'data-completed': progress, completed: progress + '%', image: self.data.image || '', image_src: '', quality: self.data.name && self.data.name.match(regexp2) ? self.data.name.match(regexp2).toString().replace(/[ .()]/g, '') : '' }); self.card.find('.lmetorrent_card__completed').addClass(getProgressClass(progress)); self.card.on('hover:enter', function () { showTorrentMenu(self.data, self.playlist); }); self.card.on('hover:focus', function () { // Scroll tracking handled by the Lampa row system }); }; this.render = function (js) { return js ? self.card[0] : self.card; }; this.destroy = function () { if (self.card) { self.card.remove(); self.card = null; } self.data = null; self.playlist = null; }; this.emit = function () {}; this.use = function () {}; } /** * Card class for the "More" button rendered as the last item in the home row. * Renders immediately with other cards (no IntersectionObserver delay). * Navigates to lmetorrentPanel on hover:enter. */ function HomeMoreCard() { var self = this; this.card = null; this.create = function () { self.card = $('
' + '
' + '
' + Lampa.Lang.translate('more') + '
' + '
' + '
'); self.card.on('hover:enter', function () { Lampa.Activity.push({ url: '', title: 'Torrent Manager', component: 'lmetorrentPanel', page: 1 }); }); }; this.render = function (js) { return js ? self.card[0] : self.card; }; this.destroy = function () { if (self.card) { self.card.remove(); self.card = null; } }; this.emit = function () {}; this.use = function () {}; } /** * Process torrent list, filter to active ones, prepare callback data. * @param {Array} torrents - Array of torrent data objects * @param {Function} callback - ContentRows callback */ function processTorrents(torrents, callback) { // Filter: only downloading and seeding torrents var activeTorrents = []; for (var i = 0; i < torrents.length; i++) { var state = classifyState$1(torrents[i].state); if (state === 'downloading' || state === 'seeding') { activeTorrents.push(torrents[i]); } } if (!activeTorrents.length) { callback(null); return; } // Limit to 6 cards max var displayItems = activeTorrents.slice(0, 6); // Set createInstance on each item for the cardClass interface Lampa.Utils.extendItemsParams(displayItems, { createInstance: function createInstance(item) { return new HomeTorrentCard(item, activeTorrents); } }); // Add "More" card as the last item — rendered immediately with other cards // instead of relying on total_pages onVisible (IntersectionObserver delay). var moreItem = { params: { createInstance: function createInstance() { return new HomeMoreCard(); } } }; displayItems.push(moreItem); callback({ title: 'Torrents', results: displayItems }); } /** * Register the home screen content row for Torrent Manager. * Only shows on screen: ['main'], index 0 (first position). * Displays max 6 active torrents (downloading + seeding). * Navigates to lmetorrentPanel on "More" button. */ function init$1() { if (!Lampa || !Lampa.ContentRows || typeof Lampa.ContentRows.add !== 'function') return; Lampa.ContentRows.add({ name: 'lmetorrent_home', title: 'Torrents', index: 0, screen: ['main'], call: function call(params, screen) { return function (callback) { // Primary data source: TorrentStateManager.torrents (updated every 15s) var torrents = TorrentStateManager$1.torrents; // If state manager has no data yet, try direct API call if (!torrents || !torrents.length) { var client = Lampa.Storage.field('lmetorrentSelect'); if (hasClient(client)) { executeClientMethod(client, 'GetData', [], { silentAuth: true }).then(function (data) { if (Array.isArray(data) && data.length) { processTorrents(data, callback); } else { callback(null); } })["catch"](function () { callback(null); }); return; } callback(null); return; } processTorrents(torrents, callback); }; } }); } var HomeRow = { init: init$1 }; /** * Card Integration Module for Torrent Manager * * Adds torrent status indicators to catalog card icons. * Follows the exact pattern from MovieEnhancer/utils/wm_quality.js: * - Hooks into Lampa.Maker.map('Card').Card.onVisible * - Preserves original method via apply(self) * - Uses inflight map to avoid duplicate API requests * - Silent fallback on errors — never blocks UI * * Lookup chain: * A. Direct label match in TorrentStateManager.torrents * B. IndexedDB cache via db.getMetadataByTmdbId() * C. External API (Sonarr/Radarr) for clients without labels */ // Inflight map to prevent duplicate concurrent API requests for the same card var inflight = {}; /** * Map raw torrent state string to a CSS-friendly status category. * Mirrors classifyState from panel/statusClassifier.js for consistency. * * @param {string} state - Raw torrent state string * @returns {string} Normalized status: downloading|seeding|completed|paused|checking|errored */ function classifyState(state) { var n = (state || '').toLowerCase().trim(); if (/^(downloading|metadl|forceddl|stalleddl|forcedmetadl|downloading metadata)$/.test(n)) return 'downloading'; if (/^(uploading|forcedup|stalledup|seeding)$/.test(n)) return 'seeding'; if (/^(finished)$/.test(n)) return 'completed'; if (/^(pauseddl|pausedup|stoppeddl|stoppedup|stopped|paused)$/.test(n)) return 'paused'; if (/^(checkingup|checkingdl|queuedup|queueddl|checking|queued|allocating|moving|waiting|verifying|extracting)/.test(n) || n.indexOf('queued to') >= 0 || n.indexOf('verify') >= 0) return 'checking'; if (/^(missingfiles|error|unknown|errorstatus)$/.test(n)) return 'errored'; return 'errored'; } /** * Build SVG icon HTML for a given torrent status. * Icons are inline SVGs placed inside .card__icons-inner. * * @param {string} state - Normalized torrent status * @returns {string} SVG HTML string or empty string */ function iconHtml(state) { var svgs = { downloading: '', seeding: '', completed: '', paused: '', checking: '', errored: '' }; return svgs[state] || ''; } /** * Find a torrent by label matching (mediaType/id) in the TorrentStateManager data. * * @param {Array} torrents - Array of torrent objects from TorrentStateManager * @param {string} mediaType - 'movie' or 'tv' * @param {number|string} id - TMDB ID * @returns {Object|null} Matching torrent or null */ function findTorrentByLabels(torrents, mediaType, id) { var label = mediaType + '/' + id; for (var i = 0; i < torrents.length; i++) { var labels = torrents[i].labels || []; if (labels.indexOf(label) !== -1) return torrents[i]; } return null; } /** * Render torrent status indicator on a card element. * Adds CSS classes and an SVG icon inside .card__icons-inner. * * @param {jQuery} $card - Card jQuery element (self.html from onVisible) * @param {Object} torrent - Matched torrent object */ function renderStatus($card, torrent) { if (!$card || !$card.length) return; var state = classifyState(torrent.state); // Update CSS classes on the card $card.removeClass('card--torrent-active card--torrent-downloading card--torrent-seeding ' + 'card--torrent-complete card--torrent-paused'); $card.addClass('card--torrent-active card--torrent-' + state); // Inject SVG icon into the icons container var $icons = $card.find('.card__icons-inner'); if ($icons.length) { $icons.find('.card__torrent-icon').remove(); $icons.append(iconHtml(state)); } } /** * Initialize card integration. * Must be called once after Lampa is ready (from initializePlugin). * Hooks Card.onVisible to add torrent status icons on card render. */ function init() { if (!Lampa.Maker) { console.error('TDM', 'Lampa.Maker is not defined!'); return; } var card = Lampa.Maker.map('Card'); if (!card || !card.Card) { console.error('TDM', 'card.Card is not defined!'); return; } var onVisible = card.Card.onVisible; card.Card.onVisible = function () { var self = this; onVisible.apply(self); // Call original method first // Guard: card must have data if (!self.data) return; // Access movie info — data may be nested under .movie or at root var movie = self.data.movie || self.data; if (!movie || !movie.id) return; var id = movie.id; // Guard: skip non-numeric IDs (music cards, etc.) if (typeof id !== 'number' && !/^\d+$/.test(id)) { return; } var mediaType = movie.first_air_date ? 'tv' : 'movie'; var title = movie.title || movie.original_title || movie.name || ''; var imdbId = movie.imdb_id; // Wrap self.html in jQuery once for use in renderStatus calls var $card = $(self.html); // Step A: Direct label match in TorrentStateManager (fastest path) var torrents = TorrentStateManager$1.torrents; var torrent = null; if (torrents && torrents.length) { torrent = findTorrentByLabels(torrents, mediaType, id); } if (torrent) { // Store card data for live update lookup via torrents:updated self.html.dataset.torrentCardData = JSON.stringify({ id: id, mediaType: mediaType }); renderStatus($card, torrent); return; } // Prevent duplicate async lookups for the same card var inflightKey = String(id) + '_' + mediaType; if (inflight[inflightKey]) return; inflight[inflightKey] = true; // Step B: Check IndexedDB cache for previously resolved metadata getMetadataByTmdbId(id).then(function (cachedMeta) { if (cachedMeta && cachedMeta.torrent_id && torrents && torrents.length) { // Found cached match — verify torrent still exists in state manager for (var i = 0; i < torrents.length; i++) { if (torrents[i].id === cachedMeta.torrent_id) { self.html.dataset.torrentCardData = JSON.stringify({ id: id, mediaType: mediaType }); renderStatus($card, torrents[i]); delete inflight[inflightKey]; return; } } } // Step C: External API search (Sonarr/Radarr) — only for visible cards var searchPromise = null; if (mediaType === 'tv' && imdbId) { // Prefer IMDB ID search for precise match searchPromise = searchSonarr('imdb:' + imdbId); } else if (mediaType === 'tv' && title) { searchPromise = searchSonarr(title); } else if (mediaType === 'movie' && title) { searchPromise = searchRadarr(title); } if (searchPromise) { searchPromise.then(function (meta) { if (meta && meta.tmdb_id) { // Check if a torrent exists for the resolved TMDB ID var resolvedTorrent = findTorrentByLabels(torrents, mediaType, meta.tmdb_id); if (resolvedTorrent) { self.html.dataset.torrentCardData = JSON.stringify({ id: id, mediaType: mediaType }); renderStatus($card, resolvedTorrent); // Cache the resolved mapping for future lookups saveMetadata(String(resolvedTorrent.id), { tmdb_id: meta.tmdb_id, media_type: mediaType, torrent_id: resolvedTorrent.id, matched_via: meta.matched_via || 'external', updated_at: Date.now() }); } } })["catch"](function () { // Silent fallback — never block UI on network errors }).then(function () { delete inflight[inflightKey]; }); } else { delete inflight[inflightKey]; } })["catch"](function () { delete inflight[inflightKey]; }); }; // Subscribe to live torrent updates to refresh visible card icons Lampa.Listener.follow('torrents:updated', function () { var torrents = TorrentStateManager$1.torrents; if (!torrents || !torrents.length) return; // Only update cards that already have torrent indicators $('.card--torrent-active').each(function () { var $card = $(this); var cardDataStr = this.dataset.torrentCardData; if (!cardDataStr) return; var cardData; try { cardData = JSON.parse(cardDataStr); } catch (e) { return; } var updatedTorrent = findTorrentByLabels(torrents, cardData.mediaType, cardData.id); if (updatedTorrent) { renderStatus($card, updatedTorrent); } else { // Torrent no longer exists — remove indicators $card.removeClass('card--torrent-active card--torrent-downloading card--torrent-seeding ' + 'card--torrent-complete card--torrent-paused'); $card.find('.card__torrent-icon').remove(); } }); }); } var CardIntegration = { init: init }; /** * Plugin manifest information */ var MANIFEST = { type: 'other', version: '3.4', author: '@lme_chat', name: 'Torrent Manager', description: 'Manager and Runner query', component: 'lmetorrent', icon: "" }; var SELECT_KEY = "".concat(MANIFEST.component, "Select"); var SYNOLOGY_KEY = "lmetorrentsynologyKey"; /** * Register all UI templates with Lampa */ function registerTemplates() { // CSS styles Lampa.Template.add('lmemStyle', "\n \n "); // Header template Lampa.Template.add('lmetorrent_header', "
Update
\n
Free space: {space}
\n "); // List item template Lampa.Template.add('lmetorrent_item', "
\n
{name}
\n
{state}
\n
{size} / {completed}
\n
"); // Card item template Lampa.Template.add('lmetorrent_item__card', "
\n
\n \"{title}\"\n
\n
\n
\n
\n
{state}
\n
{size}
\n
{completed}
\n
\n
"); } /** * Check if a session key date is older than 5 days * * @param {number} keyDate - Timestamp of the key creation date * @returns {boolean} - True if the key is old or doesn't exist */ function isKeyDateOld(keyDate) { if (!keyDate) { return true; } var currentTimestamp = Math.floor(Date.now() / 1000); var fiveDaysInSeconds = 5 * 24 * 60 * 60; var timestampFiveDaysAgo = currentTimestamp - fiveDaysInSeconds; return keyDate < timestampFiveDaysAgo; } /** * Create and add menu button for the plugin * * @returns {JQuery} - The created button element */ function createMenuButton() { return Lampa.Menu.addButton(MANIFEST.icon, MANIFEST.name, createPanelNavigationItem()); } /** * Initialize client authentication based on selected client */ function initializeClientAuth() { var selectedClient = Lampa.Storage.get(SELECT_KEY); var clientAuthHandlers = { qBittorent: function qBittorent() { return Qbittorent.auth(false)["catch"](function () {}); }, transmission: function transmission() { return Transmission.auth()["catch"](function () {}); }, keenetic: function keenetic() { return Keenetic.auth(); }, synology: function synology() { var synologyKey = Lampa.Storage.field(SYNOLOGY_KEY); var hasValidSynologySid = synologyKey && _typeof(synologyKey) === "object" && synologyKey.sID; if (!hasValidSynologySid || isKeyDateOld(synologyKey.createDate)) { return Synology.auth(); } return Promise.resolve(); } }; var authHandler = clientAuthHandlers[selectedClient]; if (authHandler) { authHandler(); } } /** * Main plugin initialization function */ function initializePlugin() { try { // Initialize language support Component$1(); // Register UI templates registerTemplates(); // Register panel component Lampa.Component.add('lmetorrentPanel', Component); // Register section view component for "more" navigation Lampa.Component.add('lmetorrentSectionView', SectionView); // Register plugin in manifest Lampa.Manifest.plugins = MANIFEST; // Add menu button if not using universal client var selectedClient = Lampa.Storage.get(SELECT_KEY); if (selectedClient !== 'universalClient') { torrentInfo(); createMenuButton(); } // Add CSS styles $('body').append(Lampa.Template.get('lmemStyle', {}, true)); // Initialize configuration Main$1(MANIFEST); // Initialize downloader Send(); // Initialize mytorrents integration MyTorrents(); // Initialize client authentication initializeClientAuth(); // Initialize new header feature DomInjector$1.inject(); TorrentStateManager$1.start(); HomeRow.init(); // Initialize card integration for torrent status indicators try { CardIntegration.init(); } catch (error) { console.error('TDM', 'Error initializing CardIntegration:', error); } } catch (error) { console.error('TDM', 'Error initializing Torrent Manager plugin:', error); } } /** * Start the plugin when Lampa is ready */ function startPlugin() { window.plugin_lmetorrent_ready = true; if (window.appready) { initializePlugin(); } else { Lampa.Listener.follow('app', function (e) { if (e.type === 'ready') { initializePlugin(); } }); } } // Initialize the plugin if not already initialized if (!window.plugin_lmetorrent_ready) { startPlugin(); } })();