Kinorium.com enhancements: user collections usability, links to extra streaming providers, native lazy loading of images etc.
// ==UserScript== // @name Kinorium.com – Enhanced [Ath] // @name:ru Kinorium.com – Улучшенный [Ath] // @name:uk Kinorium.com – Покращений [Ath] // @name:be Kinorium.com – Удасканалены [Ath] // @name:bg Kinorium.com – Подобрен [Ath] // @name:tt Kinorium.com – Яхшыртылган [Ath] // @name:sl Kinorium.com – Izboljšan [Ath] // @name:sr Kinorium.com – Poboljšan [Ath] // @name:ka Kinorium.com – გაუმჯობესებული [Ath] // @description Kinorium.com enhancements: user collections usability, links to extra streaming providers, native lazy loading of images etc. // @description:ru Улучшения для Kinorium.com: удобство работы с пользовательскими коллекциями, ссылки на дополнительные онлайн-кинотеатры, нативная ленивая загрузка изображений и т.д. // @description:uk Покращення для Kinorium.com: зручність роботи з користувацькими колекціями, посилання на додаткові онлайн-кінотеатри, нативна лінива завантаження зображень тощо. // @description:be Удасканаленні для Kinorium.com: зручнасць працы з карыстальніцкімі калекцыямі, спасылкі на дадатковыя анлайн-кінатэатры, натыўная ленівая загрузка здымкаў і г.д. // @description:bg Подобрения за Kinorium.com: удобство при работата с потребителски колекции, връзки към допълнителни онлайн кинотеатри, нативно мързеливо зареждане на изображения и т.н. // @description:tt Kinorium.com өчен яхшыртулар: кулланучы коллекцияләре белән эш итү җиңеллеге, өстәмә онлайн-кинотеатрларга сылтамалар, туган ленивая загрузка изображений һ.б. // @description:sl Izboljšave za Kinorium.com: uporabnost uporabniških zbirk, povezave do dodatnih spletnih kinodvoran, domače leno nalaganje slik itd. // @description:sr Poboljšanja za Kinorium.com: upotrebljivost korisničkih kolekcija, linkovi ka dodatnim online bioskopima, nativno lenjo učitavanje slika itd. // @description:ka Kinorium.com-ის გაუმჯობესება: მომხმარებლის კოლექციების გამოყენების მარტივება, დამატებითი ონლაინ-კინოთეატრების ბმულები, ნატიური ზარმაცი იმიჯების ჩატვირთვა და სხვა. // @namespace athari // @author Athari (https://github.com/Athari) // @copyright © Prokhorov ‘Athari’ Alexander, 2024–2025 // @license MIT // @homepageURL https://github.com/Athari/AthariUserJS // @supportURL https://github.com/Athari/AthariUserJS/issues // @version 1.6.0 // @icon https://www.google.com/s2/favicons?sz=64&domain=kinorium.com // @match https://*.kinorium.com/* // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_getResourceText // @grant GM_getResourceURL // @grant GM_info // @grant GM_registerMenuCommand // @run-at document-start // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/string.min.js // @require https://cdn.jsdelivr.net/npm/@athari/[email protected]/monkeyutils.u.min.js // @resource script-microdata https://cdn.jsdelivr.net/npm/@cucumber/[email protected]/dist/esm/src/index.min.js // @resource script-urlpattern https://cdn.jsdelivr.net/npm/urlpattern-polyfill/dist/urlpattern.js // @resource font-neucha-latin https://fonts.gstatic.com/s/neucha/v17/q5uGsou0JOdh94bfvQlt.woff2 // @resource img-cinema-default https://images.kinorium.com/web/vod/vod_channels.svg // @resource img-cinema-rezka https://rezka.ag/templates/hdrezka/images/hdrezka-logo.png // @resource img-cinema-mults https://mults.info/img/logo.png // @resource img-cinema-kinobox data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32" viewBox="0 0 16 32"><polygon points="3,11 10,16 3,21" fill="%23eee" stroke="%23eee" stroke-width="4" stroke-linejoin="round" /></svg> // @tag athari // ==/UserScript== (async () => { 'use strict' const { assignDeep, delay, waitForDocumentReady, h, u, f, utf8ToWin1251, toUrl, matchLocation, download, attempt, throwError, overrideProperty, overrideXmlHttpRequest, reviveConsole, wrapElement, ress, scripts, els, opts } = //require("../@athari-monkeyutils/monkeyutils.u"); // TODO athari.monkeyutils; const hostKinorium = "*\.kinorium\.com"; const res = ress(), script = scripts(res); const eld = doc => els(doc, { dlgCollections: ".collectionWrapper.collectionsWindow", collectionCaches: ".collection_cache", lstCollection: ".collectionList, .filmList, .statuses", athMovieUserList: ".ath-movie-ulist", lazyImages: "img[data-preload], img[src*='/img/blank'][style^='background:']", lstCinemaButtons: ".film-page__buttons-cinema", item: ".item", itemComment: ".statusText", itemInfo: ".info", statusWidget: ".statusWidget", athItemComment: ".ath-item-status", athItemCommentRating: ".ath-item-status-rating", lnkTrailer: ".trailers__list .trailers__link, .trailer.item.video", mnuUser: ".userMenu", lstSites: ".sites_page .sites", lnkSiteDataHref: ".sites_page .sites a[href='#']:is([data-original-url], [data-url])", btnDelSite: ".sites_page .sites .delReport", filmLeftPanel: ".film-page_leftContent", }), el = eld(document); const ctls = ctl => els(ctl, { ctlMovieItem: ".item.movie", ctlColItemSpan: "span:is(.title, .icon, .cnt)", ctlColItemIcon: ".collectionList span.icon", checkbox: "input[type=checkbox]", }); const opt = opts({ listUserCollections: true, iconifyUserCollections: true, addExtraCinemaSources: true, commentsBelowRatings: true, directLinksToTrailers: true, addExternalLinks: true, nativeLazyImages: true, }); const strs = { en: { listUserCollections: "List user collections", iconifyUserCollections: "Iconify user collections", addExtraCinemaSources: "Add extra cinema sources", nativeLazyImages: "Native lazy images", commentsBelowRatings: "Comments below ratings", directLinksToTrailers: "Direct links to videos", addExternalLinks: "External movie links in sidebar", watchMovieOn: "watch “%0%” on %1%", }, ru: { listUserCollections: "Список коллекций юзера", iconifyUserCollections: "Иконки у коллекций юзера", addExtraCinemaSources: "Дополнительные кинотеатры", nativeLazyImages: "Нативные ленивые картинки", commentsBelowRatings: "Комментарии под оценками", directLinksToTrailers: "Прямые ссылки на видео", addExternalLinks: "Внешние ссылки на фильм сбоку", watchMovieOn: "смотреть «%0%» на %1%", }, uk: { listUserCollections: "Список колекцій користувача", iconifyUserCollections: "Іконки у колекціях користувача", addExtraCinemaSources: "Додаткові кінотеатри", nativeLazyImages: "Нативні ліниві зображення", commentsBelowRatings: "Коментарі під оцінками", directLinksToTrailers: "Прямі посилання на відео", addExternalLinks: "Зовнішні посилання на фільм збоку", watchMovieOn: "дивитися «%0%» на %1%", }, }; const op = {}; const { log: consoleLog } = unsafeWindow.console; unsafeWindow.console.log = (...args) => args[0]?.includes?.("Не пиши парсер") ? throwError("#### you") : consoleLog(...args); const consoleRevived = reviveConsole(unsafeWindow); overrideXmlHttpRequest(unsafeWindow, { on: { load: async (_, load) => { load?.(); await delay(0); if (!op.isInitialized) return; op.doCommentsBelowRatings(); op.doListUserCollections(); op.doNativeLazyImages(); }, }, }); await consoleRevived; S.extendPrototype(); Object.assign(globalThis, globalThis.URLPattern ? null : await script.urlpattern); console.debug("GM info", GM_info); //GM_registerMenuCommand("Config", e => alert(e), { accessKey: 'a', title: "Config Enhancer" }); //overrideProperty(unsafeWindow, 'loadedTimestamp', v => (v.setFullYear(3000), v)); console.log(await waitForDocumentReady()); const { USER_ID: userId, PRO: userPro } = unsafeWindow; const language = { ua: 'uk' }[unsafeWindow.LANGUAGE] ?? unsafeWindow.LANGUAGE; const str = strs[language] ?? strs.en; let murl = null; el.tag.head.insertAdjacentHTML('beforeEnd', /*html*/` <style> @font-face { font-family: 'Neucha'; font-style: normal; font-weight: 400; font-display: block; src: url(${res.font.neucha.latin.url}) format('woff2'); } body { --ath-header-color-gray: #000c; --ath-text-color-gray-light: #777; &.body-dark { --ath-header-color-gray: #fffc; --ath-text-color-gray-light: #bbb; } } .ath-movie-ulist { display: block; margin: 4rem 40rem 4rem 0rem; font-size: 14rem; color: #555; .body-dark & { color: #aaa; } .filmList__item-wrap-title & { margin: 4rem 80rem 4rem 110rem; } a { color: inherit !important; font-size: 14rem; text-decoration: inherit; &:hover { color: #f53 !important; } } } .collectionWrapper.collectionsWindow .collectionList span.icon { background: none; text-align: center; font-size: 20rem; line-height: 32rem; opacity: 0.8; } .film-page__buttons-cinema li { vertical-align: top; } .ath-cinema:not(#\0) { width: 88px; height: 32px; text-align: center; color: #eee; background-color: #444; border-radius: 2rem; a:has(> &) { text-decoration: none !important; } &.ath-cinema-rezka { background: #282828 url(${res.img.cinema.rezka.data}) no-repeat 2px 3px / 83px 57px; filter: brightness(1.8) blur(0.5px); } &.ath-cinema-mults { background: #444 url(${res.img.cinema.mults.data}) no-repeat center 5px / 100px 24px; filter: grayscale(1); } &.ath-cinema-reyohoho { font: 600 22rem / 32px Neucha, Impact, sans-serif; letter-spacing: 1px; &::after { content: "ReYohoho"; } } &.ath-cinema-kinobox { font: 600 18rem / 32px Arial, sans-serif; &::before { content: ""; display: inline-block; width: 16px; height: 32px; vertical-align: top; background: url('${res.img.cinema.kinobox.url}') no-repeat center center; } &::after { content: "Kinobox"; display: inline; } } &.ath-cinema-kinogo { font: 600 19rem / 32px Times New Roman, sans-serif; letter-spacing: 1px; &::after { content: "KINOGO"; } } } .trailers__item { display: flex; flex-flow: column; .away-transparency { display: none; } } .trailer.item.video { height: auto !important; } .ath-trailer-title, .ath-trailer-link { display: block; max-width: 260rem; padding: 5rem 0 0 0; font-size: 13rem; text-decoration: inherit; color: #777; .body-dark & { color: #999; } .trailer.item.video & { padding: 3rem 5rem; } } .ath-trailer-title { font-weight: 500; } .item .info .ath-item-status { display: grid; grid-template-columns: 32rem 1fr; gap: 20rem; min-height: 32rem; margin: 8rem 0; .ath-item-status-rating { margin: -10rem auto 0; .statusWidget { margin: 0; width: auto; min-width: 32rem; } } .statusText { display: block; align-self: end; margin: 0; font-size: 15rem; line-height: 1.3; color: var(--ath-text-color-gray-light); } } .film-page_leftContent { .sites { .sites-page__title-group { margin: 15rem 0 8rem 0; font-size: 18rem; color: var(--ath-header-color-gray); } .sites-pages__item { margin: 6rem 0; a { font-size: 14rem; color: #21b0d0; text-decoration: none; &:hover { color: #ff5032; } .sites_page__flag { margin: 0 7rem 0 0; } img { display: inline; width: 16rem; height: 16rem; margin: 0 5rem 0 0; vertical-align: middle; } } } } } .ath-menu-options { padding: 0 5rem; &:hover { background: rgba(33, 176, 208, .25); } .title { max-width: 240rem !important; } label { white-space: nowrap; } } </style>`); (op.addOptionsMenu = () => attempt("add options menu", () => { const chks = []; const tplCheckbox = (id) => ( chks.push({ id }), /*html*/` <label class="title"> <input type="checkbox" id="ath-${id}" ${opt[id] ? 'checked' : ""}> ${str[id]} </label>`); const userScriptStr = (prop, def) => GM_info.script[`${prop}_i18n`]?.[language] ?? GM_info.script[prop] ?? def; el.mnuUser.insertAdjacentHTML('beforeEnd', /*html*/` <li class="settings settings_border ath-menu-options"> <span class="icon"></span> <div class="title"> <label title="${h(userScriptStr('description'))}">${h(userScriptStr('name'))} ${GM_info.script.version}</label> ${tplCheckbox('listUserCollections')} ${tplCheckbox('iconifyUserCollections')} ${tplCheckbox('addExtraCinemaSources')} ${tplCheckbox('commentsBelowRatings')} ${tplCheckbox('directLinksToTrailers')} ${tplCheckbox('addExternalLinks')} ${tplCheckbox('nativeLazyImages')} </div> </li>`); for (let chk of chks) el.id[`ath-${chk.id}`].onchange = (e) => opt[chk.id] = e.target.checked; }))(); let userCollections = []; (op.doListUserCollections = () => attempt("list user collections under movie title", () => { for (let elwCache of el.wrap.all.collectionCaches) { const cache = JSON.parse(elwCache.self.dataset.cache); console.debug("collection cache", cache); let { items: movies, ulist: collections } = cache; userCollections = /*userCollections.concat(collections)*/collections; const elwLstCollection = elwCache.wrap.parent.lstCollection; if (!opt.listUserCollections || movies == null || elwLstCollection == null || elwLstCollection.athMovieUserList != null) return; for (let [ itemId, uids ] of Object.entries(movies)) { const itemUlist = uids .map(uid => collections.filter(l => l.ulist_id == uid)[0]) .sort((a, b) => a.sequence - b.sequence) .map(u => /*html*/` <a href="/user/${userId}/collection/movie/${u.ulist_id}/" >${u.icon != null ? `${u.icon} ` : ""}${u.title.replace(/[^\p{L}\p{N} -]/ug, '').trim()}</a>`); for (let elTitle of elwLstCollection.self.querySelectorAll(`:is(.movie-title__text, .link-info-movie-type-film)[data-id="${itemId}"]`)) (elTitle.closest("h3") ?? elTitle.closest("a")).insertAdjacentHTML('afterEnd', /*html*/` <div class="ath-movie-ulist">${itemUlist.length > 0 ? itemUlist.join(", ") : "—"}<div>`); } } console.info("user collections", userCollections); }))(); (op.doNativeLazyImages = () => attempt("switch to native lazy loading of images", () => { if (!opt.nativeLazyImages) return; for (let elImage of el.all.lazyImages) assignDeep(elImage, { loading: 'lazy', style: { opacity: 1 }, src: elImage.dataset.preload ?? elImage.style.backgroundImage?.replace(/url\("?(.*?)"?\)/, "$1"), }); }))(); (op.doAddExtraCinemaSources = () => attempt("add extra external links", async () => { if ((murl = matchLocation(hostKinorium, { pathname: "/:movieId/" })) != null) { if (!opt.addExtraCinemaSources) return; const { microdata/*, microdataAll*/ } = await script.microdata; const movie = microdata("http://schema.org/Movie", document); const titleRu = movie.name; const titleRu1251 = utf8ToWin1251(titleRu.replace(/ё/gi, "е").toLowerCase(), { encode: true }); const titleOrig = movie.alternativeHeadline?.length > 0 ? movie.alternativeHeadline : titleRu; const cinemas = [ { id: 'rezka', name: "HDRezka", url: `https://rezka.ag/search/?do=search&subaction=search&q=${u(titleOrig)}` }, { id: 'reyohoho', name: "ReYohoho", url: `https://reyohoho.github.io/reyohoho/#search=${u(titleOrig)}` }, { id: 'mults', name: "Mults", url: `https://mults.info/mults/?wp=1&wd=1&s=${titleRu1251}` }, { id: 'kinobox', name: "Kinobox", url: `https://kinohost.web.app/search?query=${u(titleOrig)}` }, { id: 'kinogo', name: "Kinogo", url: `https://kinogo.fun/search/${u(titleRu)}` }, ]; el.lstCinemaButtons.insertAdjacentHTML('beforeEnd', cinemas.map(c => /*html*/` <li> <a title='${h(f(str.watchMovieOn, movie.name, c.name))}' href="${h(c.url)}" target="_top"> <div class="ath-cinema ath-cinema-${c.id}"></div> </a> </li>`).join("")); } }))(); (op.doAddExternalLinks = () => attempt("add external links", async () => { if ((murl = matchLocation(hostKinorium, { pathname: "/:movieId/" })) != null) { if (!opt.addExternalLinks) return; const sitesEl = eld(await download(`/${murl.movieId}/sites/`, 'html')); for (const elLink of sitesEl.all.lnkSiteDataHref) elLink.href = elLink.dataset.originalUrl ?? elLink.dataset.url; for (const btnDelSite of sitesEl.all.btnDelSite) btnDelSite.remove(); el.filmLeftPanel.insertAdjacentElement('beforeEnd', sitesEl.lstSites); } }))(); (op.doCommentsBelowRatings = () => attempt("show comments after user comments", () => { if (!opt.commentsBelowRatings) return; for (const elwComment of el.wrap.all.itemComment) { const elwItem = elwComment.wrap.parent.item; if (elwItem.athItemComment != null) continue; elwItem.itemInfo.insertAdjacentHTML('beforeEnd', /*html*/` <div class="ath-item-status"> <div class="ath-item-status-rating"></div> </div>`); elwItem.athItemCommentRating.insertAdjacentElement('beforeEnd', elwItem.all.statusWidget.at(-1)); elwItem.athItemComment.insertAdjacentElement('beforeEnd', elwComment.self); } }))(); (op.doDirectLinksToTrailers = () => attempt("add direct trailer links", () => { if (!opt.directLinksToTrailers) return; for (let lnkTrailer of el.all.lnkTrailer) { const elLnkTrailer = els(lnkTrailer); const trailer = { ...lnkTrailer.dataset, ...elLnkTrailer.tag.img?.dataset }; const trailerUrl = toUrl(trailer.video); const trailerTitle = { "www.youtube.com": "YouTube", }[trailerUrl.hostname] ?? "Video"; const trailerUrlHref = trailerUrl.toString() .replace("https://www.youtube.com/embed/", "https://www.youtube.com/watch?v="); if (lnkTrailer.classList.contains('item')) wrapElement(lnkTrailer, 'DIV', { copyAttrs: true }); lnkTrailer.insertAdjacentHTML('beforeEnd', /*html*/` <span class="ath-trailer-title">${h(lnkTrailer.title)}</span>`); lnkTrailer.insertAdjacentHTML('afterEnd', /*html*/` <a class="ath-trailer-link" href="${h(trailerUrlHref)}">${h(trailerTitle)}</a>`); } }))(); (op.iconifyCollections = (force = false) => { if (!opt.iconifyUserCollections) return; const dlgCollections = el.dlgCollections; if (dlgCollections == null || (!force && dlgCollections.classList.contains('ath-iconified'))) return; dlgCollections.classList.add('ath-iconified'); const ctl = ctls(dlgCollections); let i = 0; for (let icon of ctl.all.ctlColItemIcon) icon.innerText = userCollections[i++].icon; })(); [ 'click', 'mouseup' ].forEach(e => document.addEventListener(e, async e => { console.debug("document event", e.type, e.target, e); const ctl = ctls(e.target); const pctl = ctls(e.target.parentElement); // Collection list checkbox if (e.type == 'click' && ctl.is.ctlColItemSpan && pctl.is.ctlMovieItem) { e.preventDefault(); pctl.checkbox.click(); } await delay(0); if (e.type == 'mouseup') { op.iconifyCollections(true); } })); op.isInitialized = true; for (;;) { attempt("iconify user collections popup", () => { op.iconifyCollections(); }); await delay(200); } })();