Premier.one – Enhanced [Ath]

Premier.one enhancements: fixed time range filtering, IMDB and Kinopoisk ratings in lists, better episode titles, expanded lists with direct links, extra filters etc.

  1. // ==UserScript==
  2. // @name Premier.one – Enhanced [Ath]
  3. // @name:ru Premier.one – Улучшенный [Ath]
  4. // @name:uk Premier.one – Покращений [Ath]
  5. // @name:be Premier.one – Удасканалены [Ath]
  6. // @name:bg Premier.one – Подобрен [Ath]
  7. // @name:tt Premier.one – Яхшыртылган [Ath]
  8. // @name:sl Premier.one – Izboljšan [Ath]
  9. // @name:sr Premier.one – Poboljšan [Ath]
  10. // @name:ka Premier.one – გაუმჯობესებული [Ath]
  11. // @description Premier.one enhancements: fixed time range filtering, IMDB and Kinopoisk ratings in lists, better episode titles, expanded lists with direct links, extra filters etc.
  12. // @description:ru Улучшения для Premier.one: исправление фильтрации по годам, рейтинги Кинопоиска и IMDB в списках, полные заголовки эпизодов, раскрытые списки с прямыми ссылками, дополнительные фильтры
  13. // @description:uk Покращення для Premier.one: виправлення фільтрації за роками, рейтинги Кінопоіска та IMDB у списках, повні назви епізодів, розгорнуті списки з прямими посиланнями, додаткові фильтри
  14. // @description:be Удасканаленні для Premier.one: выпраўленне фільтрацыі па гадах, рэйтынгі Кінапоіска і IMDB у спісах, поўныя назвы эпізодаў, раскрытыя спісы з простымі спасылкамі, дадатковыя фільтры
  15. // @description:bg Подобрения за Premier.one: коригиране на филтрирането по времеви диапазони, рейтинги на Кинопоиск и IMDB в списъците, по-добри заглавия на епизодите, разширени списъци с директни връзки, допълнителни филтри и т.н.
  16. // @description:tt Premier.one өчен яхшыртулар: еллар буенча фильтрлауны төзәтү, Кинопоиск һәм IMDB рейтинглары исемлектә, эпизодларның яхшыртылган исемнәре, туры сылтамалар белән киңәйтелгән исемлекләр, өстәмә фильтрлар һ.б.
  17. // @description:sl Izboljšave za Premier.one: popravki filtriranja po časovnih obdobjih, ocene IMDB in Kinopoisk v seznamih, boljši naslovi epizod, razširjeni seznami z neposrednimi povezavami, dodatni filtri itd.
  18. // @description:sr Poboljšanja za Premier.one: ispravka filtriranja po godinama, rejtinzi Kinopoiska i IMDB u listama, bolji naslovi epizoda, proširene liste sa direktnim linkovima, dodatni filteri itd.
  19. // @description:ka Premier.one-ის გაუმჯობესებები: დროის დიაპაზონის ფილტრაციის გასწორება, კინოპოისკისა და IMDB-ს რეიტინგები სიებში, უკეთესი ეპიზოდების სათაურები, გაფართოებული სიები პირდაპირი ბმულებით, დამატებითი ფილტრები და სხვა.
  20. // @namespace athari
  21. // @author Athari (https://github.com/Athari)
  22. // @copyright © Prokhorov ‘Athari’ Alexander, 2024–2025
  23. // @license MIT
  24. // @homepageURL https://github.com/Athari/AthariUserJS
  25. // @supportURL https://github.com/Athari/AthariUserJS/issues
  26. // @version 1.1.4
  27. // @icon https://www.google.com/s2/favicons?sz=64&domain=premier.one
  28. // @match https://premier.one/*
  29. // @match https://rutube.ru/*
  30. // @grant unsafeWindow
  31. // @grant GM_getValue
  32. // @grant GM_setValue
  33. // @grant GM_getResourceText
  34. // @grant GM_getResourceURL
  35. // @grant GM_info
  36. // @run-at document-start
  37. // @require https://cdn.jsdelivr.net/npm/string@3.3.3/dist/string.min.js
  38. // @require https://cdn.jsdelivr.net/npm/@athari/monkeyutils@0.3.1/monkeyutils.u.min.js
  39. // @resource script-urlpattern https://cdn.jsdelivr.net/npm/urlpattern-polyfill/dist/urlpattern.js
  40. // @tag athari
  41. // ==/UserScript==
  42.  
  43. const { assignDeep, delay, waitFor, matchLocation, adjustUrlSearch, overrideFetch, setElementTagName, ress, scripts, els, opts, props } =
  44. //require("../@athari-monkeyutils/monkeyutils.u");
  45. athari.monkeyutils;
  46.  
  47. (async () => {
  48. 'use strict'
  49.  
  50. const premierHost = "premier.one";
  51. const res = ress(), script = scripts(res);
  52. const el = els(document, {
  53. fakeLinks: "div[to]",
  54. seasons: ".w-show-card-seasons-and-series__tabs", episodes: ".w-show-card-seasons-and-series__slide:not(.ath-linkified)",
  55. main: ".l-main", btnCancelNext: ".f-player-recommendation__cancel",
  56. });
  57.  
  58. S.extendPrototype();
  59. Object.assign(globalThis, globalThis.URLPattern ? null : await script.urlpattern);
  60.  
  61. const extraComplexFilters = [
  62. {
  63. nameForClient: "Советские Мультфильмы", nameForClientEn: "Soviet Cartoons", sendUrl: 'soviet-toons',
  64. values: [
  65. { inputFilter: 'countries', inputFilterValues: "SU" },
  66. { inputFilter: 'types', inputFilterValues: 'movie' },
  67. { inputFilter: 'genres', inputFilterValues: 'multfilmy' },
  68. ],
  69. },
  70. {
  71. nameForClient: "Советские Фильмы", nameForClientEn: "Soviet", sendUrl: 'soviet-films',
  72. values: [
  73. { inputFilter: 'countries', inputFilterValues: "SU" },
  74. { inputFilter: 'types', inputFilterValues: 'movie' },
  75. ],
  76. },
  77. {
  78. nameForClient: "Российские Мультфильмы", nameForClientEn: "Russian Cartoons", sendUrl: 'russia-toons',
  79. values: [
  80. { inputFilter: 'countries', inputFilterValues: "RU" },
  81. { inputFilter: 'types', inputFilterValues: 'movie' },
  82. { inputFilter: 'genres', inputFilterValues: 'multfilmy' },
  83. ],
  84. },
  85. /*{
  86. activeValue: true,
  87. nameForClient: "Пост-совковые Мультфильмы", nameForClientEn: "Post-USSR Cartoons", sendUrl: 'ex-ussr-toons',
  88. values: [
  89. { inputFilter: 'countries', inputFilterValues: "BY,KZ,PL,SK,UA,CZ,RU" },
  90. { inputFilter: 'types', inputFilterValues: 'movie' },
  91. { inputFilter: 'genres', inputFilterValues: 'multfilmy' },
  92. ],
  93. },*/
  94. ];
  95.  
  96. const matchPremierApi = (url, pathname) =>
  97. matchLocation(premierHost, { pathname: pathname.replace(/\/api\//, "/{uma-}?api/") }, url);
  98.  
  99. const matchPremierApiMap = (url, data, map) => {
  100. let murl;
  101. for (const [ pathname, proc ] of Object.entries(map))
  102. if ((murl = matchPremierApi(url, pathname)) != null)
  103. return proc(data, murl);
  104. };
  105.  
  106. const fixSeoTitle = o => {
  107. const seoTitle = o.seoTemplate.seoTitle;
  108. const cleanTitle = seoTitle.includes("{{episode}}")
  109. ? "{{title_name}} s{{season}}e{{episode}}"
  110. : seoTitle.includes("{{season}}")
  111. ? "{{title_name}} s{{season}}"
  112. : seoTitle.includes("{{title_name}}")
  113. ? "{{title_name}}"
  114. : seoTitle;
  115. assignDeep(o, {
  116. seoTemplate: {
  117. seoH1: cleanTitle,
  118. seoTitle: cleanTitle,
  119. },
  120. });
  121. };
  122.  
  123. const fixPlayerTitle = o => {
  124. if (o.season > 0 && o.episode > 0 && o.description.length < 100)
  125. o.title_for_player += `: ${o.description}`;
  126. return o;
  127. };
  128.  
  129. const addRatingsToPosters = async (movies) => {
  130. const elPostersList = await waitFor(() => document.querySelector(`.e-poster-list:has(a[data-id='${movies.at(-1).objectId}'])`), 20000);
  131. if (elPostersList == null)
  132. return;
  133. for (let elPoster of elPostersList.children) {
  134. const movie = movies.filter(m => m.objectId == elPoster.dataset.id)[0];
  135. if (movie == null)
  136. continue;
  137. elPoster.querySelector(".e-rating")?.remove();
  138. const htmlRating = (rating, type) => rating[type] == 0 ? "" : /*html*/`
  139. <div class="e-rating e-rating--${type}">
  140. <div class="a-icon e-rating__icon"><i class="a-icons icon-mono--rating--${type}"></i></div>
  141. <span class="e-rating__value font-poster-badge-cc">${rating[type].toFixed(1)}</span>
  142. </div>`;
  143. elPoster.querySelector(".e-poster__play-icon").insertAdjacentHTML('afterEnd', /*html*/`
  144. <div class="ath-poster-ratings">
  145. ${htmlRating(movie.rating, 'kinopoisk')}
  146. ${htmlRating(movie.rating, 'imdb')}
  147. </div>`);
  148. elPoster.title =
  149. `${movie.name} (${movie.genres?.map(g => g.name).join(", ")}) ${movie.age_restriction}\n\n` +
  150. `${movie.description}\n\n` +
  151. `КП: ${movie.rating.kinopoisk.toFixed(1)}   IMDB: ${movie.rating.imdb.toFixed(1)}`;
  152. }
  153. };
  154.  
  155. let trackInfo = {}, trackInfo2 = {};
  156.  
  157. overrideFetch(unsafeWindow, {
  158. fakeResponse: ({ url }, output) =>
  159. matchPremierApiMap(url, null, {
  160. "/api/play/access/:videoId": (_, murl) =>
  161. output.json({ id: murl.videoId })
  162. }),
  163. modifyRequestUrl: ({ url }) =>
  164. matchPremierApiMap(url, null, {
  165. "/catalog/:version/tv": () =>
  166. adjustUrlSearch(url, { per_page: 100 }),
  167. }),
  168. //modifyRequestJson: ({ url }, json) => {},
  169. modifyResponseJson: ({ url }, json) =>
  170. matchPremierApiMap(url, json, {
  171. "/app/v:version/page/info": (json) =>
  172. (fixSeoTitle(json.result), json),
  173. /*"/app/v:version/show/:videoSlug/metainfo": (json) =>
  174. assignDeep(json, {
  175. result: {
  176. slogan: "Информация должна быть свободной!",
  177. accessibility: 'free',
  178. has_allow_download: true,
  179. restriction_notices: [],
  180. },
  181. }),*/
  182. "/catalog/v:version/filters": (json) => {
  183. const years = json.result.filter(f => f.typeFilter == 'select' && f.name == 'years')[0];
  184. if (years == null)
  185. return;
  186. const yearRangeSwap = 2000, yearRangeMin = 1930, yearRangeLength = 5;
  187. let iyear = 1;
  188. const getFilterYearValue = syear => ({
  189. nameValue: syear, title: syear, titleEn: syear, //sendUrl: syear,
  190. multiselect: true, otherParam: false, activeValue: true,
  191. numberValue: ++iyear,
  192. });
  193. years.values = [ years.values[0] ];
  194. for (let year = new Date().getFullYear(); year >= yearRangeSwap; year--)
  195. years.values.push(getFilterYearValue(`${year}`));
  196. for (let toYear = yearRangeSwap - 1; toYear > yearRangeMin; toYear -= yearRangeLength)
  197. years.values.push(getFilterYearValue(`${toYear - yearRangeLength + 1}-${toYear}`));
  198. return json;
  199. },
  200. "/catalog/v:version/complex-filters": (json) => {
  201. json.result = [
  202. ...extraComplexFilters,
  203. ...json.result.filter(f => !f.nameForClient.includes("20") && f.sendUrl != 'RU'),
  204. ];
  205. let ifilter = 0;
  206. for (let filter of json.result)
  207. Object.assign(filter, { number: ++ifilter, activeValue: true });
  208. return json;
  209. },
  210. "/catalog/:version/tv": (json) => {
  211. const movies = json.result?.items;
  212. if (movies != null)
  213. addRatingsToPosters(movies);
  214. },
  215. "/api/metainfo/tv/:videoSlug/video/{/*}?": (json) => {
  216. if (json.results?.[0]?.title_for_player != null)
  217. for (let r of json.results)
  218. fixPlayerTitle(r);
  219. return json;
  220. },
  221. "/api/view_history/": (json) => {
  222. if (json.results?.[0]?.video != null) {
  223. for (let r of json.results) {
  224. fixPlayerTitle(r.video);
  225. r.title_for_player = r.video.title_for_player;
  226. }
  227. }
  228. return json;
  229. },
  230. "/api/play/trackinfo/:trackId/": (json) => {
  231. trackInfo = structuredClone(json);
  232. console.debug("info1", { trackInfo: structuredClone(trackInfo), title: el.tag.title?.innerText });
  233. return fixPlayerTitle(json);
  234. },
  235. "/api/video/:videoId": (json) => {
  236. trackInfo2 = structuredClone(json);
  237. console.debug("info2", { trackInfo2: structuredClone(trackInfo2), title: el.tag.title?.innerText });
  238. return fixPlayerTitle(json);
  239. },
  240. })
  241. });
  242.  
  243. (await el.wait.tag.head).insertAdjacentHTML('beforeEnd', /*html*/`
  244. <style>
  245. @media (min-width: 1600px) {
  246. .w-video-section__slide--horizontal {
  247. max-width: calc(20% - 1rem);
  248. }
  249. }
  250. .m-select__options-list--rows {
  251. display: flex;
  252. flex-flow: column wrap;
  253. max-height: 29rem !important;
  254. }
  255. .e-content-filters__list .m-select:nth-child(5) .m-select__dropdown { /* plots */
  256. width: auto !important;
  257. max-width: 50rem;
  258. }
  259. .m-select__option {
  260. padding: 0 !important;
  261. label {
  262. padding: .75rem 1rem;
  263. }
  264. }
  265. .m-section:not(.w-promo-slider) {
  266. .m-section__content {
  267. .m-slider {
  268. .m-slider__wrapper {
  269. display: flex;
  270. flex-flow: row wrap;
  271. transform: none !important;
  272. }
  273. .m-slider__button-prev,
  274. .m-slider__button-next {
  275. display: none;
  276. }
  277. }
  278. }
  279. }
  280. .ath-poster-ratings {
  281. position: absolute;
  282. inset: auto .5rem .5rem auto;
  283. display: flex;
  284. flex-flow: row;
  285. gap: .5rem;
  286. .e-rating {
  287. position: static !important;
  288. padding: 0 .25rem !important;
  289. .a-icon i {
  290. font-size: inherit;
  291. width: auto;
  292. height: auto;
  293. }
  294. }
  295. }
  296. a.ath-episode-link {
  297. position: absolute;
  298. inset: 0;
  299. }
  300. </style>`);
  301.  
  302. const linkifyEpisodes = () => {
  303. let murl = null;
  304. for (let elFakeLink of el.all.fakeLinks) {
  305. elFakeLink.setAttribute('href', elFakeLink.getAttribute('to'));
  306. setElementTagName(elFakeLink, 'A');
  307. }
  308. if (el.seasons != null) {
  309. const propSeasons = props(el.seasons);
  310. const [ seasonsCount, currentSeason ] = [ propSeasons['--m-tabs-items-count'], propSeasons['--m-tabs-active-index'] ];
  311. if (seasonsCount > 0 && (murl = matchPremierApi(location, "/show/:videoSlug{/*}?")) != null) {
  312. for (let elEpisode of el.all.episodes) {
  313. const episodeIndex = +elEpisode.getAttribute('index') + 1;
  314. elEpisode.classList.add('ath-linkified');
  315. elEpisode.querySelector(".e-poster").insertAdjacentHTML('beforeEnd', /*html*/`
  316. <a class="ath-episode-link" href="/show/${murl.videoSlug}/season/${currentSeason}/episode/${episodeIndex}"></a>`);
  317. }
  318. }
  319. }
  320. };
  321.  
  322. for (let ims = 0; ims += 100; await delay(100)) {
  323. if (ims % 1000 == 0) {
  324. linkifyEpisodes();
  325. }
  326. if (ims % 200 == 0) {
  327. el.main?.removeAttribute('inert');
  328. el.btnCancelNext?.click();
  329. }
  330. }
  331. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址