From 03c65a2250da7937300be9e033dc229c7a07c1b1 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Thu, 31 Oct 2024 23:19:28 +0200 Subject: [PATCH 01/16] update desc --- plugin.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.json b/plugin.json index 8c4ac52..2f35641 100644 --- a/plugin.json +++ b/plugin.json @@ -4,12 +4,12 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.4", + "version": "1.4.1", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", "synopsis": "UASerials — твої улюблені серіали українською мовою онлайн", "category": "video", - "description": "Твої улюблені серіали українською мовою онлайн", + "description": "Плагін для Movian для відтворення відео з українського онлайн-кінотеатру UASerials.", "homepage": "https://github.com/eclipse7723/movian-plugin-uaserials" } \ No newline at end of file From a8a05238e01ca4c8450e1a3f268769ab7f6e4a03 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Thu, 31 Oct 2024 23:58:21 +0200 Subject: [PATCH 02/16] wip filters - add page before actual :list: to select filter - work in progress on filters - minor fixes --- plugin.json | 2 +- src/movie-parser.js | 95 +++++++++++++++++++++++++++++++++++++++++++-- uaserials.js | 23 ++++++++--- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/plugin.json b/plugin.json index 2f35641..c36614f 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.4.1", + "version": "1.4.2", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 994c858..a08185d 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -34,7 +34,7 @@ function parseCollections(page, href) { var desc = ""; desc += formatInfo("Повна назва: " + formatBold(title)) desc += "\n" + formatInfo("Кількість в цій добірці: " + formatBold(itemCount)); - var desc = new RichText(desc); + desc = new RichText(desc); page.appendItem(PLUGIN.id + ":collection:" + itemHref + ":" + title.replace(":", ""), 'video', { title: title, @@ -72,7 +72,7 @@ function parseMovies(page, href) { desc += "\n" + formatInfo("Кількість: " + formatBold(label2.children[0].textContent)); } - var desc = new RichText(desc); + desc = new RichText(desc); page.appendItem(PLUGIN.id + ":moviepage:" + itemHref + ":" + titleUa.replace(":", " "), 'video', { title: titleUa, @@ -123,7 +123,7 @@ function findSoundsByEpisode(movieData, season, episode) { data.seasons.forEach(function(seasonData) { seasonData.episodes.forEach(function(episodeData) { - if (seasonData.title === season && episodeData.title == episode) { + if (seasonData.title === season && episodeData.title === episode) { sounds = episodeData.sounds; } }); @@ -159,6 +159,93 @@ function parseTvEpisode(page, movieData, season, episode) { }) } +/* filters */ + +function parseFilterQuery(filterData) { + const filterTemplate = "/f/{query}"; + const possibleFilters = ["year", "imdb", "cat", "country", "channel"]; + var queries = []; + + possibleFilters.forEach(function(filterKey) { + if (!filterData.hasOwnProperty(filterKey)) return; + var filterValue = filterData[filterKey]; + + if (filterValue.length === 0) return; + + const query = filterKey + "=" + filterValue.join(";"); + queries.push(query); + }) + + return filterTemplate.replace("{query}", queries.join("/")); +} + +function parseListFilters(page, tag, title) { + var pageId = ":list:"; + + function putItem(name, filterData) { + page.appendItem(PLUGIN.id + pageId + tag + ":" + title + ":" + filterData, "directory", { + title: name + }) + } + + // скачаем страницу и спарсим оттуда фильтры + var doc = fetchDoc(href); + + if (!doc.getElementByClassName("filter-block")) { + throw "Not found filter-block"; + } + + putItem("Усі " + title, "all"); + + page.appendPassiveItem("separator", "", { + title: "Роки" + }); + putItem("2020+", "year=2020;" + new Date().getFullYear().toString()); + putItem("2010-2019", "year=2010;2019"); + putItem("2000-2009", "year=2000;2009"); + putItem("1920-1999", "year=1920;1999"); + + page.appendPassiveItem("separator", "", { + title: "Рейтинг IMDb" + }); + putItem("8+", "imdb=8;10"); + putItem("6+", "imdb=6;10"); + putItem("4-6", "imdb=4;6"); + putItem("0-3", "imdb=0;3"); + + /* создаем список жанров, стран, телеканалов (если есть) */ + + // filter-wrap -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect + var items = doc.getElementById("filter-wrap").children[0].children[1].children[1]; + // inside pairs (select, div), ... We need only `select` items, + // as they contain filter's data (as `option` elements, 1st option always empty) for each key + + items.forEach(function(item) { + if (item.tagName !== "select") return; + const filterKey = item.attributes.getNamedItem('name').value; // api key + const filterName = item.attributes.getNamedItem('data-placeholder').value; // human name + + page.appendPassiveItem("separator", "", { + title: filterName + }); + + const options = item.children; + options.forEach(function(option) { + if (option.tagName !== "option") return; + const filterValue = option.attributes.getNamedItem('value').value; + const filterTitle = option.textContent; + if (filterValue) { + putItem(filterTitle, filterKey + "=" + filterValue); + } + }); + }); + +} + +function appendPossibleFilters(page) { // todo + /* добавляет возможные фильтры в сайд-меню, указанные на странице (года, жанры, страны и так далее) */ +} + /* фильм */ function __parseMovieVideo(page, movieData, videoUrl) { @@ -240,7 +327,7 @@ function createPageLoader(page, searchUrlBuilder, startPageNumber) { return false; } - if (page.entries != expectedEntries) { + if (page.entries !== expectedEntries) { hasNextPage = false; page.loading = false; return false; diff --git a/uaserials.js b/uaserials.js index 5188a91..b483c77 100644 --- a/uaserials.js +++ b/uaserials.js @@ -60,7 +60,7 @@ new page.Route(PLUGIN.id + ":start", function(page) { {name: "Аніме", tag: "/anime"}, ]; categories.forEach(function(data) { - page.appendItem(PLUGIN.id + ":list:" + data.tag + ":" + data.name, "directory", { + page.appendItem(PLUGIN.id + ":pre-list:" + data.tag + ":" + data.name, "directory", { title: data.name }) }) @@ -71,11 +71,24 @@ new page.Route(PLUGIN.id + ":start", function(page) { }); -new page.Route(PLUGIN.id + ":list:(.*):(.*)", function(page, href, title) { +new page.Route(PLUGIN.id + ":pre-list:(.*):(.*)", function(page, tag, title) { setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); + try { + parseListFilters(page, tag); + } catch (e) { + console.log("Error while parsing list filters: " + e); + page.redirect(PLUGIN.id + ":list:" + tag + ":" + title + ":" + "all"); + } +}); + +new page.Route(PLUGIN.id + ":list:(.*):(.*):(.*)", function(page, tag, title, filterQuery) { + setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); + + const query = filterQuery === "all" ? "" : filterQuery; + function generateSearchURL(nextPage) { - return BASE_URL + href + "/page/" + nextPage + "/" + return BASE_URL + tag + query + "/page/" + nextPage + "/" } var loader = createPageLoader(page, generateSearchURL, 1); @@ -86,9 +99,9 @@ new page.Route(PLUGIN.id + ":list:(.*):(.*)", function(page, href, title) { new page.Route(PLUGIN.id + ":collections", function(page) { setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + "Добірки"); - href = BASE_URL + "/collections" + var href = BASE_URL + "/collections" - parseCollections(page, href) + parseCollections(page, href); }); new page.Route(PLUGIN.id + ":collection:(.*):(.*)", function(page, href, title) { From a55e85ff93a2352db84b7e30e3209aa615dda611 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Thu, 31 Oct 2024 23:59:35 +0200 Subject: [PATCH 03/16] fix href --- src/movie-parser.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/movie-parser.js b/src/movie-parser.js index a08185d..0b1446b 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -180,15 +180,14 @@ function parseFilterQuery(filterData) { } function parseListFilters(page, tag, title) { - var pageId = ":list:"; - function putItem(name, filterData) { - page.appendItem(PLUGIN.id + pageId + tag + ":" + title + ":" + filterData, "directory", { + page.appendItem(PLUGIN.id + ":list:" + tag + ":" + title + ":" + filterData, "directory", { title: name - }) + }); } // скачаем страницу и спарсим оттуда фильтры + const href = BASE_URL + tag; var doc = fetchDoc(href); if (!doc.getElementByClassName("filter-block")) { From edc61e9194ff36da86f7c4cb062b0dc9855a6eb5 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Fri, 1 Nov 2024 00:40:04 +0200 Subject: [PATCH 04/16] fix --- src/movie-parser.js | 2 +- uaserials.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/movie-parser.js b/src/movie-parser.js index 0b1446b..2cd2664 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -320,7 +320,7 @@ function createPageLoader(page, searchUrlBuilder, startPageNumber) { try { parseMovies(page, url); } catch (e) { - console.error("loading page " + nextPageNumber + " failed -> " + href + ":" + e) + console.error("loading page " + nextPageNumber + " failed -> " + url + ":" + e) hasNextPage = false; page.loading = false; return false; diff --git a/uaserials.js b/uaserials.js index b483c77..7460400 100644 --- a/uaserials.js +++ b/uaserials.js @@ -85,7 +85,7 @@ new page.Route(PLUGIN.id + ":pre-list:(.*):(.*)", function(page, tag, title) { new page.Route(PLUGIN.id + ":list:(.*):(.*):(.*)", function(page, tag, title, filterQuery) { setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); - const query = filterQuery === "all" ? "" : filterQuery; + const query = filterQuery === "all" ? "" : "/f/" + filterQuery; function generateSearchURL(nextPage) { return BASE_URL + tag + query + "/page/" + nextPage + "/" From c54530a66ac0f04672ae8094ad8c695094d7f120 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Fri, 1 Nov 2024 00:48:47 +0200 Subject: [PATCH 05/16] fix paginator --- plugin.json | 2 +- src/movie-parser.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.json b/plugin.json index c36614f..2367d9c 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.4.2", + "version": "1.4.4", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 2cd2664..b33cac1 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -326,7 +326,7 @@ function createPageLoader(page, searchUrlBuilder, startPageNumber) { return false; } - if (page.entries !== expectedEntries) { + if (parseInt(page.entries) !== expectedEntries) { hasNextPage = false; page.loading = false; return false; From 53fab4a82b6d962e1b1627b670504b4379c48099 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Fri, 1 Nov 2024 01:14:13 +0200 Subject: [PATCH 06/16] wip: add full list of filters for genre, country and company (if possible) ...but I think I'll remove countries and companies, because too many options for single page --- plugin.json | 2 +- src/movie-parser.js | 32 +++++++++++++++++--------------- uaserials.js | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/plugin.json b/plugin.json index 2367d9c..d5e16a6 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.4.4", + "version": "1.4.5", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index b33cac1..42dc5ef 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -30,7 +30,7 @@ function parseCollections(page, href) { const itemHref = data.attributes.getNamedItem('href').value; const itemImg = children[0].attributes.getNamedItem('data-src').value; const itemCount = children[2].textContent; - + var desc = ""; desc += formatInfo("Повна назва: " + formatBold(title)) desc += "\n" + formatInfo("Кількість в цій добірці: " + formatBold(itemCount)); @@ -214,13 +214,12 @@ function parseListFilters(page, tag, title) { /* создаем список жанров, стран, телеканалов (если есть) */ - // filter-wrap -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect + // filter-wrap -> div.filter-box -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect var items = doc.getElementById("filter-wrap").children[0].children[1].children[1]; // inside pairs (select, div), ... We need only `select` items, // as they contain filter's data (as `option` elements, 1st option always empty) for each key - items.forEach(function(item) { - if (item.tagName !== "select") return; + items.getElementByTagName("select").forEach(function(item) { const filterKey = item.attributes.getNamedItem('name').value; // api key const filterName = item.attributes.getNamedItem('data-placeholder').value; // human name @@ -228,14 +227,17 @@ function parseListFilters(page, tag, title) { title: filterName }); - const options = item.children; - options.forEach(function(option) { - if (option.tagName !== "option") return; - const filterValue = option.attributes.getNamedItem('value').value; + item.children.forEach(function(option) { const filterTitle = option.textContent; - if (filterValue) { - putItem(filterTitle, filterKey + "=" + filterValue); + + if (!filterTitle) { return; } + + var filterValue = filterTitle; // in case if api value = title + if (option.attributes.getNamedItem('value')) { + filterValue = option.attributes.getNamedItem('value').value; } + + putItem(filterTitle, filterKey + "=" + filterValue); }); }); @@ -311,12 +313,12 @@ function createPageLoader(page, searchUrlBuilder, startPageNumber) { function loader() { if (!hasNextPage) { return false; } - + page.loading = true; var url = searchUrlBuilder(nextPageNumber); - + const expectedEntries = page.entries + itemsPerPage; - + try { parseMovies(page, url); } catch (e) { @@ -325,13 +327,13 @@ function createPageLoader(page, searchUrlBuilder, startPageNumber) { page.loading = false; return false; } - + if (parseInt(page.entries) !== expectedEntries) { hasNextPage = false; page.loading = false; return false; } - + nextPageNumber++; page.loading = false; return true; diff --git a/uaserials.js b/uaserials.js index 7460400..ed4c4d7 100644 --- a/uaserials.js +++ b/uaserials.js @@ -75,7 +75,7 @@ new page.Route(PLUGIN.id + ":pre-list:(.*):(.*)", function(page, tag, title) { setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); try { - parseListFilters(page, tag); + parseListFilters(page, tag, title); } catch (e) { console.log("Error while parsing list filters: " + e); page.redirect(PLUGIN.id + ":list:" + tag + ":" + title + ":" + "all"); From 5179ca049fc35c7a609bfb4f7e82131f60c56971 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sat, 2 Nov 2024 23:31:40 +0200 Subject: [PATCH 07/16] wip + add list-select page where you can choose: show all or go to filters + improve cdn check + add more comments - remove 'channel' filter from list --- plugin.json | 2 +- src/movie-parser.js | 45 +++++++++++++++++++++++++++++++-------------- uaserials.js | 28 ++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/plugin.json b/plugin.json index d5e16a6..63919a2 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.4.5", + "version": "1.4.6", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 42dc5ef..01bfd8f 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -162,6 +162,8 @@ function parseTvEpisode(page, movieData, season, episode) { /* filters */ function parseFilterQuery(filterData) { + /* makes query string from given filterData object */ + const filterTemplate = "/f/{query}"; const possibleFilters = ["year", "imdb", "cat", "country", "channel"]; var queries = []; @@ -180,9 +182,11 @@ function parseFilterQuery(filterData) { } function parseListFilters(page, tag, title) { + /* creates buttons with filters on page */ + function putItem(name, filterData) { page.appendItem(PLUGIN.id + ":list:" + tag + ":" + title + ":" + filterData, "directory", { - title: name + title: name + " ▶" }); } @@ -194,12 +198,14 @@ function parseListFilters(page, tag, title) { throw "Not found filter-block"; } - putItem("Усі " + title, "all"); + // подгтовленные фильтры page.appendPassiveItem("separator", "", { - title: "Роки" + title: "Рік прем'єри" }); - putItem("2020+", "year=2020;" + new Date().getFullYear().toString()); + const thisYear = new Date().getFullYear().toString() + putItem("Цього року", "year=" + thisYear); + putItem("2020+", "year=2020;" + thisYear); putItem("2010-2019", "year=2010;2019"); putItem("2000-2009", "year=2000;2009"); putItem("1920-1999", "year=1920;1999"); @@ -214,6 +220,8 @@ function parseListFilters(page, tag, title) { /* создаем список жанров, стран, телеканалов (если есть) */ + const allowedFilters = ["genre", "cat", "year", "imdb"] // skip "channel" + // filter-wrap -> div.filter-box -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect var items = doc.getElementById("filter-wrap").children[0].children[1].children[1]; // inside pairs (select, div), ... We need only `select` items, @@ -223,21 +231,22 @@ function parseListFilters(page, tag, title) { const filterKey = item.attributes.getNamedItem('name').value; // api key const filterName = item.attributes.getNamedItem('data-placeholder').value; // human name + if (!allowedFilters.indexOf(filterKey)) { return; } + page.appendPassiveItem("separator", "", { title: filterName }); item.children.forEach(function(option) { - const filterTitle = option.textContent; + const itemName = option.textContent; + if (!itemName) { return; } - if (!filterTitle) { return; } - - var filterValue = filterTitle; // in case if api value = title + var itemValue = itemName; // in case if api value = title if (option.attributes.getNamedItem('value')) { - filterValue = option.attributes.getNamedItem('value').value; + itemValue = option.attributes.getNamedItem('value').value; } - putItem(filterTitle, filterKey + "=" + filterValue); + putItem(itemName, filterKey + "=" + itemValue); }); }); @@ -262,6 +271,7 @@ function __parseMovieVideo(page, movieData, videoUrl) { /* трейлер */ function parseTrailer(page, movieData) { + /* adds trailer button if possible */ movieData.data.forEach(function(data) { if (data.tabName !== "Трейлер") return; @@ -282,10 +292,17 @@ function parseTrailer(page, movieData) { /* видео */ function parseVideoURL(href) { - const cdnSubstring1 = "://tortuga.wtf/"; - const cdnSubstring2 = "://tortuga.tw/"; - if (!href.match(cdnSubstring1) && !href.match(cdnSubstring2)) { - console.error("Unknown CDN url '" + href + "' - url must include '" + cdnSubstring1 + "' or '" + cdnSubstring1 + "'"); + const allowedCDNs = ["://tortuga.wtf/", "://tortuga.tw/"] + + var isValidSource = false; + for (var cdnSubstring of allowedCDNs) { + if (href.match(cdnSubstring)) { + isValidSource = true; + break; + } + } + if (!isValidSource) { + console.error("Unknown CDN url '" + href + "' - url must include one of " + allowedCDNs.join(" or ")) return null; } diff --git a/uaserials.js b/uaserials.js index ed4c4d7..9c985b8 100644 --- a/uaserials.js +++ b/uaserials.js @@ -45,6 +45,7 @@ function logDebug(message) { /* PAGES */ new page.Route(PLUGIN.id + ":start", function(page) { + /* главная страница плагина */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id) page.loading = false; @@ -60,7 +61,7 @@ new page.Route(PLUGIN.id + ":start", function(page) { {name: "Аніме", tag: "/anime"}, ]; categories.forEach(function(data) { - page.appendItem(PLUGIN.id + ":pre-list:" + data.tag + ":" + data.name, "directory", { + page.appendItem(PLUGIN.id + ":list-select:" + data.tag + ":" + data.name, "directory", { title: data.name }) }) @@ -71,7 +72,21 @@ new page.Route(PLUGIN.id + ":start", function(page) { }); -new page.Route(PLUGIN.id + ":pre-list:(.*):(.*)", function(page, tag, title) { +new page.Route(PLUGIN.id + ":list-select:(.*):(.*)", function(page, tag, title) { + /* страница с выбором - все фильмы или фильтровать */ + setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); + + page.appendItem(PLUGIN.id + ":list:" + tag + ":" + title + ":all", "directory", { + title: "Усі " + title + " ▶" + }); + + page.appendItem(PLUGIN.id + ":-filtered:" + tag + ":" + title, "directory", { + title: "Обрати категорію \ рік \ країну \ рейтинг ▶" + }); +}); + +new page.Route(PLUGIN.id + ":list-filtered:(.*):(.*)", function(page, tag, title) { + /* страница с фильтрами */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); try { @@ -83,6 +98,7 @@ new page.Route(PLUGIN.id + ":pre-list:(.*):(.*)", function(page, tag, title) { }); new page.Route(PLUGIN.id + ":list:(.*):(.*):(.*)", function(page, tag, title, filterQuery) { + /* страница со списком фильмов согласно прописанным фильтрам (all для отображения всех) */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); const query = filterQuery === "all" ? "" : "/f/" + filterQuery; @@ -98,6 +114,7 @@ new page.Route(PLUGIN.id + ":list:(.*):(.*):(.*)", function(page, tag, title, fi }); new page.Route(PLUGIN.id + ":collections", function(page) { + /* страница с подборками */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + "Добірки"); var href = BASE_URL + "/collections" @@ -105,6 +122,7 @@ new page.Route(PLUGIN.id + ":collections", function(page) { }); new page.Route(PLUGIN.id + ":collection:(.*):(.*)", function(page, href, title) { + /* страница с фильмами из подборки */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); function generateSearchURL(nextPage) { @@ -118,6 +136,7 @@ new page.Route(PLUGIN.id + ":collection:(.*):(.*)", function(page, href, title) }); new page.Route(PLUGIN.id + ":moviepage:(.*):(.*)", function(page, href, title) { + /* страница с деталями фильма и перехода к просмотру */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title) page.loading = true; @@ -194,6 +213,7 @@ new page.Route(PLUGIN.id + ":moviepage:(.*):(.*)", function(page, href, title) { }); new page.Route(PLUGIN.id + ':play:(.*)', function(page, data) { + /* страница с плеером */ data = JSON.parse(data); setPageHeader(page, "video", PLUGIN.id + " - " + data.title) @@ -215,6 +235,7 @@ new page.Route(PLUGIN.id + ':play:(.*)', function(page, data) { }); new page.Route(PLUGIN.id + ':play-select-sound:(.*):(.*):(.*)', function(page, title, season, episode) { + /* страница с выбором озвучки */ setPageHeader(page, "directory", PLUGIN.id + " - " + title + " - озвучка") parseTvEpisode(page, currentMovieData, season, episode); @@ -222,6 +243,7 @@ new page.Route(PLUGIN.id + ':play-select-sound:(.*):(.*):(.*)', function(page, t }); function setupSearchPage(page, query) { + /* поисковая страница */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id) var searchUrl = BASE_URL + "/index.php?do=search&story=" + query @@ -236,9 +258,11 @@ function setupSearchPage(page, query) { } new page.Route(PLUGIN.id + ":search:(.*)", function(page, query) { + /* страница с поиском внутри плагина */ setupSearchPage(page, query); }); new page.Searcher(PLUGIN.id, PLUGIN_LOGO, function(page, query) { + /* Searcher позволяет из мовиана использовать поиск в этом плагине */ setupSearchPage(page, query); }); \ No newline at end of file From c0785f83b6fa0ff61bf1162e44b8986f20a86554 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sat, 2 Nov 2024 23:33:44 +0200 Subject: [PATCH 08/16] fix --- uaserials.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaserials.js b/uaserials.js index 9c985b8..243609e 100644 --- a/uaserials.js +++ b/uaserials.js @@ -80,7 +80,7 @@ new page.Route(PLUGIN.id + ":list-select:(.*):(.*)", function(page, tag, title) title: "Усі " + title + " ▶" }); - page.appendItem(PLUGIN.id + ":-filtered:" + tag + ":" + title, "directory", { + page.appendItem(PLUGIN.id + ":list-filtered:" + tag + ":" + title, "directory", { title: "Обрати категорію \ рік \ країну \ рейтинг ▶" }); }); From 49fea50307a5a4a5d731a3903f0f548baaab4d5e Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sat, 2 Nov 2024 23:34:59 +0200 Subject: [PATCH 09/16] update version --- plugin.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.json b/plugin.json index 63919a2..18ba65e 100644 --- a/plugin.json +++ b/plugin.json @@ -4,12 +4,12 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.4.6", + "version": "1.5", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", "synopsis": "UASerials — твої улюблені серіали українською мовою онлайн", "category": "video", - "description": "Плагін для Movian для відтворення відео з українського онлайн-кінотеатру UASerials.", + "description": "Movian плагін для відтворення відео з українського онлайн-кінотеатру UASerials.", "homepage": "https://github.com/eclipse7723/movian-plugin-uaserials" } \ No newline at end of file From a747a6117a87db7266120f4636e76e0b0a0d1b88 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sat, 2 Nov 2024 23:44:22 +0200 Subject: [PATCH 10/16] fix --- plugin.json | 2 +- src/movie-parser.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin.json b/plugin.json index 18ba65e..c68fce8 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.5", + "version": "1.5.1", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 01bfd8f..abc8247 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -295,7 +295,8 @@ function parseVideoURL(href) { const allowedCDNs = ["://tortuga.wtf/", "://tortuga.tw/"] var isValidSource = false; - for (var cdnSubstring of allowedCDNs) { + for (var i in allowedCDNs) { + const cdnSubstring = allowedCDNs[i]; if (href.match(cdnSubstring)) { isValidSource = true; break; From b14dc85f540717c05c7cc453c8861a6fc7e59bd6 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sat, 2 Nov 2024 23:52:31 +0200 Subject: [PATCH 11/16] fix + show only 20 channels --- plugin.json | 2 +- src/movie-parser.js | 9 +++++++-- uaserials.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugin.json b/plugin.json index c68fce8..b2a257e 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.5.1", + "version": "1.5.2", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index abc8247..2ebe7d4 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -220,7 +220,8 @@ function parseListFilters(page, tag, title) { /* создаем список жанров, стран, телеканалов (если есть) */ - const allowedFilters = ["genre", "cat", "year", "imdb"] // skip "channel" + const allowedFilters = ["genre", "cat", "year", "imdb", "channel"] // skip "channel" + const maxChannels = 20; // filter-wrap -> div.filter-box -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect var items = doc.getElementById("filter-wrap").children[0].children[1].children[1]; @@ -231,13 +232,16 @@ function parseListFilters(page, tag, title) { const filterKey = item.attributes.getNamedItem('name').value; // api key const filterName = item.attributes.getNamedItem('data-placeholder').value; // human name - if (!allowedFilters.indexOf(filterKey)) { return; } + if (allowedFilters.indexOf(filterKey) === -1) { return; } page.appendPassiveItem("separator", "", { title: filterName }); + var entries = 0; item.children.forEach(function(option) { + if (filterKey === "channel" && entries > maxChannels) { return; } + const itemName = option.textContent; if (!itemName) { return; } @@ -247,6 +251,7 @@ function parseListFilters(page, tag, title) { } putItem(itemName, filterKey + "=" + itemValue); + entries += 1; }); }); diff --git a/uaserials.js b/uaserials.js index 243609e..ecc0dca 100644 --- a/uaserials.js +++ b/uaserials.js @@ -81,7 +81,7 @@ new page.Route(PLUGIN.id + ":list-select:(.*):(.*)", function(page, tag, title) }); page.appendItem(PLUGIN.id + ":list-filtered:" + tag + ":" + title, "directory", { - title: "Обрати категорію \ рік \ країну \ рейтинг ▶" + title: "Обрати категорію / рік / країну / рейтинг ▶" }); }); From e993929433f381c52a36c7db1c61392bba2b79d9 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sun, 3 Nov 2024 00:48:53 +0200 Subject: [PATCH 12/16] improvements + show only featured channels - minor changes --- plugin.json | 2 +- src/movie-parser.js | 53 ++++++++++++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/plugin.json b/plugin.json index b2a257e..e09e8ee 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.5.2", + "version": "1.5.3", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 2ebe7d4..84817e8 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -189,6 +189,11 @@ function parseListFilters(page, tag, title) { title: name + " ▶" }); } + function putSeparator(name) { + page.appendPassiveItem("separator", "", { + title: name + }); + } // скачаем страницу и спарсим оттуда фильтры const href = BASE_URL + tag; @@ -198,21 +203,17 @@ function parseListFilters(page, tag, title) { throw "Not found filter-block"; } - // подгтовленные фильтры + // подготовленные фильтры - page.appendPassiveItem("separator", "", { - title: "Рік прем'єри" - }); const thisYear = new Date().getFullYear().toString() + putSeparator("Рік прем'єри") putItem("Цього року", "year=" + thisYear); putItem("2020+", "year=2020;" + thisYear); putItem("2010-2019", "year=2010;2019"); putItem("2000-2009", "year=2000;2009"); putItem("1920-1999", "year=1920;1999"); - page.appendPassiveItem("separator", "", { - title: "Рейтинг IMDb" - }); + putSeparator("Рейтинг IMDb") putItem("8+", "imdb=8;10"); putItem("6+", "imdb=6;10"); putItem("4-6", "imdb=4;6"); @@ -220,30 +221,31 @@ function parseListFilters(page, tag, title) { /* создаем список жанров, стран, телеканалов (если есть) */ - const allowedFilters = ["genre", "cat", "year", "imdb", "channel"] // skip "channel" - const maxChannels = 20; + const allowedFilters = ["genre", "cat", "year", "imdb"] // skip "channel" from parse // filter-wrap -> div.filter-box -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect var items = doc.getElementById("filter-wrap").children[0].children[1].children[1]; // inside pairs (select, div), ... We need only `select` items, // as they contain filter's data (as `option` elements, 1st option always empty) for each key + var hasChannels = false; + items.getElementByTagName("select").forEach(function(item) { const filterKey = item.attributes.getNamedItem('name').value; // api key const filterName = item.attributes.getNamedItem('data-placeholder').value; // human name - if (allowedFilters.indexOf(filterKey) === -1) { return; } + if (filterKey === "channel") { + hasChannels = true; + } - page.appendPassiveItem("separator", "", { - title: filterName - }); + if (allowedFilters.indexOf(filterKey) === -1) return; + + putSeparator(filterName); var entries = 0; item.children.forEach(function(option) { - if (filterKey === "channel" && entries > maxChannels) { return; } - const itemName = option.textContent; - if (!itemName) { return; } + if (!itemName) return; // empty value var itemValue = itemName; // in case if api value = title if (option.attributes.getNamedItem('value')) { @@ -255,6 +257,23 @@ function parseListFilters(page, tag, title) { }); }); + if (hasChannels) { + // channels with 18 and more movies (some exceptions to young but popular channels) + putSeparator("Телеканал"); + + const popularChannels = [ + "Netflix", "BBC", "Amazon", "Apple TV+", "HBO", "FX", "Hulu", "CBS", "Paramount+", + "Nickelodeon", "AMC", "Disney", "National Geographic", "Fox", "Sci Fi Channel", "DC Universe", + "Discovery", "Showtime", "NBC", "The CW", "ABC", "ITV", "Peacock", "Channel 4", "Cartoon Network", + "Starz", "USA Network", "Sky Atlantic", "Rai 1", "TNT", "Sky1", "Syfy", "Comedy Central", + "ZDF", "TF1", "France 2", "CBC", "YouTube Premium", "1+1" + ] // len = 30 + + popularChannels.forEach(function(channel) { + putItem(channel, "channel=" + channel.replace("+", "ppp")); + }) + } + } function appendPossibleFilters(page) { // todo @@ -335,7 +354,7 @@ function createPageLoader(page, searchUrlBuilder, startPageNumber) { page.entries = 0; function loader() { - if (!hasNextPage) { return false; } + if (!hasNextPage) return false; page.loading = true; var url = searchUrlBuilder(nextPageNumber); From 72b030701ce2565f6d54d5aa074f02f95b0f594e Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sun, 3 Nov 2024 01:03:34 +0200 Subject: [PATCH 13/16] begin popular movies --- plugin.json | 2 +- src/movie-parser.js | 7 +++++++ uaserials.js | 26 +++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/plugin.json b/plugin.json index e09e8ee..7455ccb 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.5.3", + "version": "1.5.4", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 84817e8..5266800 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -280,6 +280,13 @@ function appendPossibleFilters(page) { // todo /* добавляет возможные фильтры в сайд-меню, указанные на странице (года, жанры, страны и так далее) */ } +/* main page list */ + +function parseListFromMain(page, tag, title) { + // todo + throw "not realized yet" +} + /* фильм */ function __parseMovieVideo(page, movieData, videoUrl) { diff --git a/uaserials.js b/uaserials.js index ecc0dca..68de79f 100644 --- a/uaserials.js +++ b/uaserials.js @@ -62,12 +62,12 @@ new page.Route(PLUGIN.id + ":start", function(page) { ]; categories.forEach(function(data) { page.appendItem(PLUGIN.id + ":list-select:" + data.tag + ":" + data.name, "directory", { - title: data.name + title: data.name + " ▶" }) }) page.appendItem(PLUGIN.id + ":collections", "directory", { - title: "Добірки фільмів, серіалів і мультфільмів" + title: "Добірки фільмів, серіалів і мультфільмів" + " ▶" }) }); @@ -77,9 +77,17 @@ new page.Route(PLUGIN.id + ":list-select:(.*):(.*)", function(page, tag, title) setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); page.appendItem(PLUGIN.id + ":list:" + tag + ":" + title + ":all", "directory", { - title: "Усі " + title + " ▶" + title: "Усі " + title.toLowerCase() + " ▶" }); + // todo: show popular movie from main site page + if (service._debug) { + page.appendItem(PLUGIN.id + ":list-main:" + tag + ":" + title, "directory", { + title: "В центрі уваги ▶" + }); + } + + // todo: make own button for each option... page.appendItem(PLUGIN.id + ":list-filtered:" + tag + ":" + title, "directory", { title: "Обрати категорію / рік / країну / рейтинг ▶" }); @@ -97,6 +105,18 @@ new page.Route(PLUGIN.id + ":list-filtered:(.*):(.*)", function(page, tag, title } }); +new page.Route(PLUGIN.id + ":list-main:(.*):(.*)", function(page, tag, title) { // todo + /* страница с фильтрами с главной страницы сайта по тегу */ + setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title + " - В центрі уваги"); + + try { + parseListFromMain(page, tag, title); + } catch (e) { + console.log("Error while parsing list filters: " + e); + page.redirect(PLUGIN.id + ":list:" + tag + ":" + title + ":" + "all"); + } +}); + new page.Route(PLUGIN.id + ":list:(.*):(.*):(.*)", function(page, tag, title, filterQuery) { /* страница со списком фильмов согласно прописанным фильтрам (all для отображения всех) */ setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); From a0c2a18dbbce86161cfd3d6c387243b4f45434fe Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sun, 3 Nov 2024 01:15:48 +0200 Subject: [PATCH 14/16] fix this year filter --- plugin.json | 2 +- src/movie-parser.js | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/plugin.json b/plugin.json index 7455ccb..d6831a8 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.5.4", + "version": "1.5.5", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 5266800..2be72ea 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -207,13 +207,14 @@ function parseListFilters(page, tag, title) { const thisYear = new Date().getFullYear().toString() putSeparator("Рік прем'єри") - putItem("Цього року", "year=" + thisYear); + putItem("Цього року", "year=" + thisYear + ";" + thisYear); putItem("2020+", "year=2020;" + thisYear); putItem("2010-2019", "year=2010;2019"); putItem("2000-2009", "year=2000;2009"); putItem("1920-1999", "year=1920;1999"); putSeparator("Рейтинг IMDb") + putItem("9+", "imdb=9;10"); putItem("8+", "imdb=8;10"); putItem("6+", "imdb=6;10"); putItem("4-6", "imdb=4;6"); @@ -222,6 +223,7 @@ function parseListFilters(page, tag, title) { /* создаем список жанров, стран, телеканалов (если есть) */ const allowedFilters = ["genre", "cat", "year", "imdb"] // skip "channel" from parse + const noGenresCategories = ["Мультфільми", "Мультсеріали"] // filter-wrap -> div.filter-box -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect var items = doc.getElementById("filter-wrap").children[0].children[1].children[1]; @@ -238,8 +240,12 @@ function parseListFilters(page, tag, title) { hasChannels = true; } + // excluded filter if (allowedFilters.indexOf(filterKey) === -1) return; + // probably this filter is useless for this category of movies + if (filterKey === "genre" && noGenresCategories.indexOf(title)) return; + putSeparator(filterName); var entries = 0; @@ -258,16 +264,16 @@ function parseListFilters(page, tag, title) { }); if (hasChannels) { - // channels with 18 and more movies (some exceptions to young but popular channels) putSeparator("Телеканал"); - + + // channels with 18 and more movies (some exceptions to young but popular channels) const popularChannels = [ "Netflix", "BBC", "Amazon", "Apple TV+", "HBO", "FX", "Hulu", "CBS", "Paramount+", "Nickelodeon", "AMC", "Disney", "National Geographic", "Fox", "Sci Fi Channel", "DC Universe", "Discovery", "Showtime", "NBC", "The CW", "ABC", "ITV", "Peacock", "Channel 4", "Cartoon Network", "Starz", "USA Network", "Sky Atlantic", "Rai 1", "TNT", "Sky1", "Syfy", "Comedy Central", "ZDF", "TF1", "France 2", "CBC", "YouTube Premium", "1+1" - ] // len = 30 + ] popularChannels.forEach(function(channel) { putItem(channel, "channel=" + channel.replace("+", "ppp")); From f10d537b210cb4540a9c1f78780982eb0e378c4b Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sun, 3 Nov 2024 01:19:29 +0200 Subject: [PATCH 15/16] disable genre filter for cartoons (useless) --- plugin.json | 2 +- src/movie-parser.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.json b/plugin.json index d6831a8..e424f3e 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.5.5", + "version": "1.5.6", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index 2be72ea..c551e7d 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -222,8 +222,8 @@ function parseListFilters(page, tag, title) { /* создаем список жанров, стран, телеканалов (если есть) */ - const allowedFilters = ["genre", "cat", "year", "imdb"] // skip "channel" from parse - const noGenresCategories = ["Мультфільми", "Мультсеріали"] + const allowedFilters = ["genre", "cat", "year", "imdb"]; // skip "channel" from parse + const noGenresCategories = ["Мультсеріали", "Мультфільми"]; // filter-wrap -> div.filter-box -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect var items = doc.getElementById("filter-wrap").children[0].children[1].children[1]; @@ -244,7 +244,7 @@ function parseListFilters(page, tag, title) { if (allowedFilters.indexOf(filterKey) === -1) return; // probably this filter is useless for this category of movies - if (filterKey === "genre" && noGenresCategories.indexOf(title)) return; + if (filterKey === "genre" && noGenresCategories.indexOf(title) !== -1) return; putSeparator(filterName); From 9501f5744d4c0026c6e2fb94517b28bc6fe1acb3 Mon Sep 17 00:00:00 2001 From: eclipse7723 Date: Sun, 3 Nov 2024 01:42:43 +0200 Subject: [PATCH 16/16] skip list-select for anime, because no filters inside --- plugin.json | 2 +- src/movie-parser.js | 9 +++++---- uaserials.js | 7 +++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/plugin.json b/plugin.json index e424f3e..32feb53 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "id": "UASerials", "file": "uaserials.js", "showtimeVersion": "5", - "version": "1.5.6", + "version": "1.5.7", "author": "eclipse7723", "title": "UASerials", "icon": "logo.svg", diff --git a/src/movie-parser.js b/src/movie-parser.js index c551e7d..43809de 100644 --- a/src/movie-parser.js +++ b/src/movie-parser.js @@ -222,7 +222,7 @@ function parseListFilters(page, tag, title) { /* создаем список жанров, стран, телеканалов (если есть) */ - const allowedFilters = ["genre", "cat", "year", "imdb"]; // skip "channel" from parse + const allowedFilters = ["cat", "year", "imdb"]; // skip "channel" from parse const noGenresCategories = ["Мультсеріали", "Мультфільми"]; // filter-wrap -> div.filter-box -> div{3 div.fb-col} -> 2nd div.fb-col -> div.fb-sect @@ -244,7 +244,7 @@ function parseListFilters(page, tag, title) { if (allowedFilters.indexOf(filterKey) === -1) return; // probably this filter is useless for this category of movies - if (filterKey === "genre" && noGenresCategories.indexOf(title) !== -1) return; + if (filterKey === "cat" && noGenresCategories.indexOf(title) !== -1) return; putSeparator(filterName); @@ -284,12 +284,13 @@ function parseListFilters(page, tag, title) { function appendPossibleFilters(page) { // todo /* добавляет возможные фильтры в сайд-меню, указанные на странице (года, жанры, страны и так далее) */ + throw "not realized yet" } /* main page list */ -function parseListFromMain(page, tag, title) { - // todo +function parseListFromMain(page, tag, title) { // todo + /* добавляет на страницу список фильмов с главной страницы */ throw "not realized yet" } diff --git a/uaserials.js b/uaserials.js index 68de79f..15baf56 100644 --- a/uaserials.js +++ b/uaserials.js @@ -74,6 +74,13 @@ new page.Route(PLUGIN.id + ":start", function(page) { new page.Route(PLUGIN.id + ":list-select:(.*):(.*)", function(page, tag, title) { /* страница с выбором - все фильмы или фильтровать */ + + if (title === "Аніме") { + // отсутствие фильтров + page.redirect(PLUGIN.id + ":list:" + tag + ":" + title + ":" + "all"); + return; + } + setPageHeader(page, DEFAULT_PAGE_TYPE, PLUGIN.id + " - " + title); page.appendItem(PLUGIN.id + ":list:" + tag + ":" + title + ":all", "directory", {