- 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
8274 lines
306 KiB
JavaScript
8274 lines
306 KiB
JavaScript
(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();
|
||
}
|
||
|
||
})();
|