Files
lampac/plugins/torrentmanager.js
prodik 257b42f518 Каталог плагинов: полный локальный mirror в ./plugins (один directory-mount в wwwroot/plugins), доки и вычистка неиспользуемых модулей
- plugins/: скачаны все 30 плагинов store локально (sources.json - карта исходных URL);
  патчи: showy.js (абсолютный хост подгрузки), pubtorr/td/torrentmanager - тела вместо лоадеров,
  modss.js - local-patched копия (из каталога исключена, upstream мёртв)
- extensions.json: 32 позиции, все ссылки {localhost}/plugins/<файл>.js, удалены мёртвые
  (Skaz TV x3, export.js, hotkeys/gold_theme/select_weapon/want/mult, MODS's, o.js, др.)
- compose: ./plugins:/lampac/wwwroot/plugins (один маунт вместо 30 file-mount)
- вычистка: удалены неиспользуемые mods (DatabaseEditor, ExternalBind, LogUserRequest-Lite,
  Telemetry, Transcoding, WatchTogether) и дубль каталога modules/
- config/init.conf: секции Tracks/AdminPanel/Music, customPlugins без gst.js
- mods/Tracks: правки Controller.cs/plugin.js
- README.md / AGENT.md: mirror, кеш static, правила mount
2026-10-02 16:37:30 +03:00

8274 lines
306 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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('<span class="lmetorrent-section__icon">' + this.icon + '</span>');
}
titleEl.append('<span class="lmetorrent-section__count">(' + config.items.length + ')</span>');
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 = $('<div class="items-line__more selector">' + Lampa.Lang.translate('more') + '</div>');
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 = $('<div class="card-more selector">' + '<div class="card-more__box">' + '<div class="card-more__title">' + Lampa.Lang.translate('more') + '</div>' + '</div>' + '</div>');
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 = $('<div class="items-line__more selector">' + Lampa.Lang.translate('more') + '</div>');
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 = $('<div class="card-more selector">' + '<div class="card-more__box">' + '<div class="card-more__title">' + Lampa.Lang.translate('more') + '</div>' + '</div>' + '</div>');
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<Object>} - 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<Object>} - 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<Array>} - 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<object>} 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<void>}
*/
function saveMetadata(_x3, _x4) {
return _saveMetadata.apply(this, arguments);
}
/**
* Отримує метадані за ключем.
* @param {string} key - Ключ (ідентифікатор торренту).
* @returns {Promise<any|null>}
*/
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<Object|null>} - 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<boolean>}
*/
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<Object|null>} - нормалізований результат 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<Array>} - результати пошуку
*/
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<Object|null>} - обраний результат або 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 = $("<div class='lmetorrent-module'></div>");
var head = $("<div class='lmetorrent-head'></div>");
var body = $('<div class="lmetorrent-sections"></div>');
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 = $('<div class="lmetorrent-state"></div>');
container.append("<div class=\"lmetorrent-state__title\">".concat(title, "</div>"));
if (description) {
container.append("<div class=\"lmetorrent-state__description\">".concat(description, "</div>"));
}
if (actions.length) {
var actionList = $('<div class="lmetorrent-state__actions"></div>');
actions.forEach(function (action) {
var button = $("<div class=\"simple-button selector\">".concat(action.title, "</div>"));
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 = $('<div class="lmetorrent-section-view"></div>');
var grid = $('<div class="lmetorrent-section-view__grid"></div>');
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 = $('<div class="lmetorrent-state"><div class="lmetorrent-state__title">' + Lampa.Lang.translate("torrentmanager_state_empty_title") + '</div></div>');
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: "<a href=\"".concat(thanksUrl, "\">").concat(thanksUrl, "</a>")
});
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 = "<svg class=\"btnTDdownload\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><g id=\"SVGRepo_bgCarrier\" stroke-width=\"0\"></g><g id=\"SVGRepo_tracerCarrier\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></g><g id=\"SVGRepo_iconCarrier\"><path d=\"M8.5 7L8.5 14M8.5 14L11 11M8.5 14L6 11\" stroke=\"#ffffff\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path><path d=\"M15.5 7L15.5 14M15.5 14L18 11M15.5 14L13 11\" stroke=\"#ffffff\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path><path d=\"M18 17H12H6\" stroke=\"#ffffff\" stroke-width=\"1.5\" stroke-linecap=\"round\"></path><path d=\"M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12Z\" stroke=\"#ffffff\" stroke-width=\"1.5\"></path></g></svg>";
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 "<div class=\"btnTDdownload wait\">".concat(DOWNLOAD_ICON$1).concat(label, "</div>");
}
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 = "<svg class=\"btnTDdownload\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><g id=\"SVGRepo_bgCarrier\" stroke-width=\"0\"></g><g id=\"SVGRepo_tracerCarrier\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></g><g id=\"SVGRepo_iconCarrier\"><path d=\"M8.5 7L8.5 14M8.5 14L11 11M8.5 14L6 11\" stroke=\"#ffffff\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path><path d=\"M15.5 7L15.5 14M15.5 14L18 11M15.5 14L13 11\" stroke=\"#ffffff\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path><path d=\"M18 17H12H6\" stroke=\"#ffffff\" stroke-width=\"1.5\" stroke-linecap=\"round\"></path><path d=\"M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12Z\" stroke=\"#ffffff\" stroke-width=\"1.5\"></path></g></svg>";
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 "<div class=\"btnTDdownload wait\">".concat(DOWNLOAD_ICON).concat(label, "</div>");
}
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: "<json string>" }
* 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 = $("<div class=\"full-start__button selector button--lme_torrent\">\n <svg fill=\"currentColor\" version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 588.601 588.6\" xml:space=\"preserve\"><g id=\"SVGRepo_bgCarrier\" stroke-width=\"0\"></g><g id=\"SVGRepo_tracerCarrier\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></g><g id=\"SVGRepo_iconCarrier\"> <g> <path d=\"M168.405,288.048c-3.019,0.084-4.936,0.419-5.864,0.728v20.174l5.632-0.059c6.463-0.078,10.558-4.35,10.558-10.768 C178.73,291.017,174.636,287.869,168.405,288.048z\"></path> <path d=\"M82.324,290.445c-8.177,0.227-12.49,10.278-12.49,22.491c0,12.045,4.588,21.674,12.49,21.737 c8.089,0.079,12.701-9.761,12.701-22.412C95.018,300.86,90.688,290.213,82.324,290.445z\"></path> <path d=\"M125.722,289.235c-2.813,0.082-4.617,0.396-5.484,0.696v19.515l5.276-0.059c6.03-0.074,9.858-4.203,9.858-10.404 C135.372,292.117,131.544,289.074,125.722,289.235z\"></path> <path d=\"M539.568,49.201h-178.2c-0.786,0-1.561,0.074-2.347,0.124V0L11.228,46.419v494.562L359.032,588.6v-50.814 c0.78,0.053,1.55,0.115,2.341,0.115h178.2c20.852,0,37.8-16.959,37.8-37.8v-413.1C577.368,66.161,560.425,49.201,539.568,49.201z M361.368,70.801h178.2c8.928,0,16.2,7.267,16.2,16.2v271.329c-23.272-58.704-70.2-90.393-132.864-99.347 c-18.879-2.705-21.79,0.886-19.687,19.517c1.482,13.252,11.786,9.158,18.747,10.022c56.574,7.085,103.982,49.642,115.288,104.147 c7.267,34.974-1.266,71.872-21.305,101.05h-14.08c36.64-49.116,38.834-102.389,2.964-149.575 c-33.307-43.802-99.62-61.505-145.8-42.604V71.043C359.797,70.93,360.572,70.801,361.368,70.801z M359.032,333.687 c2.226-0.891,4.25-1.703,6.265-2.479c45.752-17.649,97.817-0.606,122.096,39.946c23.757,39.726,14.223,90.034-22.892,122.565 h-14.122c3.912-2.942,7.73-6.181,11.411-9.734c26.314-25.376,35.374-56.162,24.01-90.925 c-11.527-35.258-37.446-55.244-74.007-60.592c-18.114-2.647-36.956,1.244-52.761,9.661V333.687z M359.032,378.891 c0.169-0.163,0.327-0.354,0.506-0.517c22.939-22.17,62.259-21.479,84.555,1.397c22.687,23.277,22.887,60.307-2.479,81.949 c-13.11,11.175-29.995,20.408-46.659,24.49c-11.935,2.921-23.905,4.777-35.923,6.021V378.891z M296.331,275.25l49.401-1.7v11.156 l-19.232,0.514v61.077l-11.938-0.19v-60.57l-18.236,0.493V275.25H296.331z M60.247,292.37l-12.49,0.332v49.265l-7.771-0.11v-48.953 l-11.929,0.321V284.5l32.189-1.113V292.37z M81.857,343.459c-12.677-0.211-20.545-12.983-20.545-30.26 c0-18.077,8.521-31.118,21.209-31.572c13.458-0.466,21.526,12.714,21.526,30.085C104.048,332.153,94.521,343.67,81.857,343.459z M136.836,343.343c-0.738-1.867-1.917-6.982-3.31-14.776c-1.395-8.147-3.73-10.721-8.819-10.895l-4.47,0.025v25.393l-8.701-0.138 v-60.515c3.267-0.828,8.208-1.55,13.324-1.73c7.056-0.242,11.907,1.071,15.238,4.504c2.745,2.797,4.316,7.148,4.316,12.469 c0,8.137-4.398,13.685-9.042,15.868v0.284c3.533,1.641,5.688,6.012,6.951,12.056c1.572,7.857,2.911,15.161,3.963,17.607 L136.836,343.343z M180.312,344.023c-0.788-1.935-2.059-7.229-3.554-15.298c-1.484-8.427-3.995-11.096-9.429-11.264l-4.788,0.021 v26.262l-9.305-0.143v-62.574c3.488-0.865,8.754-1.608,14.241-1.798c7.549-0.274,12.738,1.086,16.313,4.627 c2.942,2.896,4.617,7.394,4.617,12.906c0,8.412-4.704,14.16-9.682,16.428v0.295c3.783,1.688,6.096,6.207,7.446,12.477 c1.68,8.127,3.119,15.684,4.237,18.22L180.312,344.023z M231.742,344.82l-33.874-0.533v-65.646l32.598-1.118v10.083l-22.539,0.609 v17.075l21.266-0.306v9.978l-21.266,0.137v19.438l23.815,0.189V344.82z M241.052,277.151l12.234-0.422l15.515,29.141 c4.061,7.668,7.604,15.688,10.434,23.235h0.19c-0.73-9.313-1.004-18.299-1.004-28.94v-24.301l10.491-0.366v70.208l-11.675-0.18 l-15.881-30.47c-3.828-7.515-7.791-15.884-10.702-23.535l-0.264,0.105c0.43,8.812,0.517,17.819,0.517,29.072v24.421l-9.848-0.152 v-67.816H241.052z M539.568,516.301h-4.915c8.644-11.56,15.746-23.467,21.115-35.743v19.543 C555.769,509.035,548.507,516.301,539.568,516.301z\"></path> </g> </g></svg>\n <span>".concat(torrent.completed > 0 ? "".concat(getStandardizedStateName(torrent.state), " - ").concat(Number((torrent.completed * 100).toFixed(2)), "%") : getStandardizedStateName(torrent.state), "</span>\n </div>"));
// 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 <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 4L12 14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <path d=\"M16 10L12 14L8 10\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <path d=\"M4 18H20\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n </svg>\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 = $('<div class="card-more selector">' + '<div class="card-more__box">' + '<div class="card-more__title">' + Lampa.Lang.translate('more') + '</div>' + '</div>' + '</div>');
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: '<svg class="card__torrent-icon card__torrent-icon--downloading" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M13 5v8h3l-4 5-4-5h3V5h2zm-9 12v2h16v-2H4z"/></svg>',
seeding: '<svg class="card__torrent-icon card__torrent-icon--seeding" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M13 5v5h3l-4 5-4-5h3V5h2zm-9 10v4h16v-4H4z"/></svg>',
completed: '<svg class="card__torrent-icon card__torrent-icon--complete" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>',
paused: '<svg class="card__torrent-icon card__torrent-icon--paused" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>',
checking: '<svg class="card__torrent-icon card__torrent-icon--checking" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>',
errored: '<svg class="card__torrent-icon card__torrent-icon--errored" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'
};
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: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M13.684 23.94a12.013 12.013 0 0 0 9.599-7.79c-.118.044-.26.096-.432.147c-2 .59-3.404-.466-3.687-.649c-.283-.18-.587-.48-.643-.464c-.183 1.132-1.218 2.706-3.58 3.42c-1.295.391-2.687.4-3.681-.157l.328.822c.13.328.351.866.488 1.192c0 0 .858 2.044 1.608 3.48M2.723 7.153l3.54-.66c.323-.059.68.124.794.407l2.432 6.07c.332.633.399.773.615 1.043c0 0 1.68 2.398 4.24 1.812c1.726-.394 2.532-1.69 2.587-2.612c.057-.296-.032-.669-.185-1.016L13.832 5.61c-.117-.266.022-.527.306-.581l2.953-.55a.69.69 0 0 1 .706.376l3.227 6.91c.13.276.394.712.588.966c0 0 .671.964 1.747.78c.266 0 .569-.143.569-.143c.047-.43.072-.866.072-1.31c0-6.627-5.373-12-12.002-12C5.372.06 0 5.433 0 12.06c0 5.319 3.46 9.827 8.252 11.402a24.768 24.768 0 0 1-.919-2.121L2.298 7.808c-.111-.297.083-.59.425-.654\"/></svg>"
};
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 <style>\n @charset 'UTF-8';.btnTDdownload{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}svg.btnTDdownload{width:36px;height:36px;margin-right:5%}.lmetorrent-error_body{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.lmetorrent-error_body .lmetorrent-error_result{margin-top:2em}.lmetorrent-head{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:0 2% 0 2%;margin:0 2% 2% 2%}.lmetorrent-header__update{white-space:nowrap}.lmetorrent-header__space{margin-left:auto}.lmetorrent-catalog--list.category-full{margin-left:0;padding:0 2.5%}.lmetorrent-catalog--state{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;min-height:45vh;padding:0 2%}.lmetorrent-state{max-width:46em;width:100%;text-align:center}.lmetorrent-state__title{font-size:1.35em;font-weight:600}.lmetorrent-state__description{margin-top:.8em;opacity:.85;line-height:1.4}.lmetorrent-state__actions{margin-top:1.3em;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;gap:.7em}.lmetorrent-catalog--list.category-full .card.card--category .card__view{margin-bottom:.72em}.lmetorrent_card__completed{position:absolute;right:0;bottom:0;font-size:.8em;-webkit-border-radius:.3em;-moz-border-radius:.3em;padding:.4em .4em;border-radius:.3em;text-align:center;font-weight:bold;background-color:var(--background-color);color:var(--text-color)}.lmetorrent_card__completed.is-low{--background-color:#fcc;--text-color:#900}.lmetorrent_card__completed.is-mid{--background-color:#ffc;--text-color:#990}.lmetorrent_card__completed.is-high{--background-color:#cfc;--text-color:#090}.lmetorrent_card__state{left:0;top:0}.lmetorrent_card__size{left:0;bottom:0}.lmetorrent_card__size,.lmetorrent_card__state{position:absolute;padding:.4em .4em;background:#fff;color:#000;font-size:.8em;-webkit-border-radius:.3em;border-radius:.3em}@media screen and (max-width:900px){.lmetorrent-head{margin:0 2% 3% 2%;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;row-gap:.6em}.lmetorrent-header__space{margin-left:0;width:100%;font-size:.95em;opacity:.85}.lmetorrent-catalog--list.category-full{margin-left:0;padding:0 1%}}@media screen and (max-width:767px){.lmetorrent-head{margin:0 2% 2.4% 2%;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;gap:.8em}.lmetorrent-header__update{font-size:1.12em}.lmetorrent-header__space{margin-left:auto;width:auto;font-size:1.08em;opacity:.98;white-space:nowrap}.lmetorrent-catalog--list.category-full{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0}.lmetorrent-catalog--list.category-full .card.card--category{width:33.3333% !important;min-width:0;padding-left:.22em;padding-right:.22em}}@media screen and (max-width:560px){.lmetorrent-head{-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.lmetorrent-catalog--list.category-full{padding:0}.lmetorrent-catalog--list.category-full .card.card--category .card__view{margin-bottom:.5em}.lmetorrent_card__size,.lmetorrent_card__state,.lmetorrent_card__completed{font-size:.68em}}@media screen and (max-width:420px){.lmetorrent-head{-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;row-gap:.55em}.lmetorrent-header__space{width:100%;margin-left:0}}.lmetorrent-item{margin-left:.5em;margin-right:.5em;margin-bottom:1em;width:-webkit-calc(14.2857142857% - 1em);width:calc(14.2857142857% - 1em);-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;border:solid .01em #fff;-webkit-border-radius:.8em;border-radius:.8em}.lmetorrent-item.focus{border:solid .26em #fff}.lmetorrent-item__data{margin-bottom:.4em}.lmetorrent-item__state{top:.5em;left:.5em;padding:.1em .3em;font-weight:bold;-webkit-border-radius:.25em;border-radius:.25em;color:#292d32;background-color:#eee}.lmetorrent-item__badge>svg{width:1em;height:1em;vertical-align:bottom}.lmetorrent-item__name{font-size:1.1em;margin-top:.8em;white-space:nowrap;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}@media screen and (max-width:580px){.lmetorrent-item{width:21%}}@media screen and (max-width:385px){.lmetorrent-item__name{display:none}}.torrent-manager-icon{--icon-status-color:limegreen;color:var(--icon-status-color)}.torrent-manager-sidebar{position:fixed;top:0;right:-350px;width:350px;height:100%;background-color:rgba(0,0,0,0.85);z-index:1000;-webkit-transition:right .3s;-o-transition:right .3s;transition:right .3s;color:white;padding:20px;-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.torrent-manager-sidebar.visible{right:0}.torrent-manager-sidebar__header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;margin-bottom:20px;font-weight:bold;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.torrent-manager-sidebar__list{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;gap:10px;overflow-y:auto;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.torrent-manager-sidebar__item{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:10px;background-color:rgba(255,255,255,0.1);-webkit-border-radius:5px;border-radius:5px;cursor:pointer;gap:10px}.torrent-manager-sidebar__item:hover{background-color:rgba(255,255,255,0.2)}.torrent-manager-sidebar__item-name{white-space:nowrap;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.torrent-manager-sidebar__item-percent{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.lmetorrent-section{padding-bottom:1.5em}.lmetorrent-section__icon{margin-right:.4em}.lmetorrent-section__count{margin-left:.5em;opacity:.6;font-size:.85em}.lmetorrent-section .mapping--line .card--category{width:12em;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.lmetorrent-section-view{width:100%;min-height:45vh}.lmetorrent-section-view__grid{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0 2%}.lmetorrent-section-view .card--category{width:14.5%;margin:0 .5% 1em}@media screen and (max-width:767px){.lmetorrent-section-view .card--category{width:30%}}.lmetorrent-section .card-more{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:12.75em;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;height:18em}.lmetorrent-section .card-more .card-more__box{-webkit-transition:border-color .2s,-webkit-transform .3s;transition:border-color .2s,-webkit-transform .3s;-o-transition:transform .3s,border-color .2s;transition:transform .3s,border-color .2s;transition:transform .3s,border-color .2s,-webkit-transform .3s;background:rgba(0,0,0,0.3);-webkit-border-radius:1em;border-radius:1em;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;position:relative}.lmetorrent-section .card-more .card-more__title{position:absolute;top:50%;left:0;right:0;text-align:center;font-size:1.8em;font-weight:300;margin-top:-0.7em}.lmetorrent-section .card-more.focus .card-more__box::after{content:'';position:absolute;top:-0.5em;left:-0.5em;right:-0.5em;bottom:-0.5em;border:.3em solid #fff;-webkit-border-radius:1.4em;border-radius:1.4em;z-index:-1}.lmetorrent-section .card-more--fixed-size{display:block}@media screen and (min-width:64em){body.size--bigger .lmetorrent-section .card-more{font-size:1.14em}}\n </style>\n ");
// Header template
Lampa.Template.add('lmetorrent_header', "<div class=\"lmetorrent-header__data lmetorrent-header__update simple-button selector\">Update</div>\n <div class=\"lmetorrent-header__data lmetorrent-header__space\">Free space: {space}</div>\n ");
// List item template
Lampa.Template.add('lmetorrent_item', "<div class=\"card selector lmetorrent-item\">\n <div class=\"lmetorrent-item__data lmetorrent-item__name\">{name}</div>\n <div class=\"lmetorrent-item__data lmetorrent-item__state\">{state}</div>\n <div class=\"lmetorrent-item__data lmetorrent-item__progress\">{size} / {completed}</div>\n </div>");
// Card item template
Lampa.Template.add('lmetorrent_item__card', "<div class=\"card card--category selector layer--visible layer--render\">\n <div class=\"card__view\">\n <img src=\"{image}\" data-src=\"{image_src}\" alt=\"{title}\" class=\"card__img\">\n <div class=\"card__icons\">\n <div class=\"card__icons-inner\">\n </div>\n </div>\n <div class=\"lmetorrent_card__state\">{state}</div>\n <div class=\"lmetorrent_card__size\">{size}</div>\n <div class=\"lmetorrent_card__completed\" data-completed=\"{data-completed}\">{completed}</div>\n </div>\n </div>");
}
/**
* 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();
}
})();