您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds movie plots and ratings to the Ville de Luxembourg Cinematheque website
// ==UserScript== // @name Cinematheqular // @namespace https://github.com/harristom // @version 2025-06-01 // @description Adds movie plots and ratings to the Ville de Luxembourg Cinematheque website // @author https://github.com/harristom // @match https://www.vdl.lu/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // ==/UserScript== (function () { 'use strict'; function formatRtRating(rating) { const FRESH_THRESHOLD = 60; return (parseInt(rating) >= FRESH_THRESHOLD ? '🍅 ' : '🍏 ') + rating; } function cleanTitle(title) { // Remove ending brackets from the title (often used to indicate the showing is part of a season) title = title.trim(); return encodeURIComponent(title.replaceAll(/ \(.*$/gi, '')); } function agendaPage(omdbApiKey) { const RATINGS_CLASS_NAME = 'cinematheqular-ratings'; GM_addStyle(` /* Hide the categories as it will be "Cinema" on all and the language tags are always wrong */ .media-inner .media-category { display: none; } .${RATINGS_CLASS_NAME} { color: #4A4A4A; font-weight: 400; } .media-image figure[data-content] { position: relative; &::after { position: absolute; inset: 0px; color: white; background: linear-gradient(black, transparent); padding: 3px; content: attr(data-content); font-size: 0.85em; line-height: 1.2; transition: 0.2s; opacity: 0; mask: linear-gradient(black 80%, transparent); } .media-link:hover &::after { opacity: 1; } } `); const movieEls = document.querySelectorAll('.media-inner'); for (const movieEl of movieEls) { const titleEl = movieEl.querySelector('.media-title'); if (!titleEl) continue; let title = titleEl.textContent; title = cleanTitle(title); fetch(`https://www.omdbapi.com/?apikey=${omdbApiKey}&type=movie&t=${title}`) .then(response => response.json()) .then(result => { if (result.Error) return; const tooltip = `${result.Title} (${result.Year}) ${result.Director}`; // Add plot const imgEl = movieEl.querySelector('.media-image figure'); if (imgEl) { imgEl.dataset.content = result.Plot; imgEl.title = tooltip; } // Add ratings const rtRating = result.Ratings?.find(r => r.Source == 'Rotten Tomatoes')?.Value; if (rtRating) { const ratingEl = document.createElement('p'); ratingEl.className = RATINGS_CLASS_NAME; ratingEl.textContent = formatRtRating(rtRating); ratingEl.title = tooltip; titleEl.append(ratingEl); } }) .catch(error => console.log('error', error)); } } function detailPage(omdbApiKey) { const DETAILS_CLASS_NAME = 'cinematheqular-details'; GM_addStyle(` .${DETAILS_CLASS_NAME} { border-radius: 7px; border: 2px solid #E4E4E4; padding: 10px 20px 20px 20px; margin-bottom: 20px; } .${DETAILS_CLASS_NAME}__disclaimer { text-transform: uppercase; margin-block: 0px 10px; font-size: 0.8rem; opacity: 0.8; } .${DETAILS_CLASS_NAME}__wrapper { display: flex; justify-content: center; align-items: start; flex-wrap: wrap; gap: 20px; } .${DETAILS_CLASS_NAME}__poster { display: block; object-fit: contain; overflow: hidden; flex: 1 100px; max-width: 200px; border-radius: 3px; } .${DETAILS_CLASS_NAME}__data { flex: 3 300px; } .${DETAILS_CLASS_NAME}__title { font-size: 1.8em; margin-bottom: 5px; } .${DETAILS_CLASS_NAME}__director { margin-top: 0px; margin-bottom: 5px; color: #b6b6b6; font-weight: 600; } .${DETAILS_CLASS_NAME}__ratings { margin-top: 0px; margin-bottom: 10px; } .${DETAILS_CLASS_NAME}__plot { margin: 0px; } `); let titleEl = document.querySelector('.block-page-title'); if (!titleEl) return; const title = cleanTitle(titleEl.textContent); fetch(`https://www.omdbapi.com/?apikey=${omdbApiKey}&type=movie&plot=full&t=${title}`) .then(response => response.json()) .then(result => { if (result.Error) return; const detailsEl = document.createElement('div'); detailsEl.className = DETAILS_CLASS_NAME; detailsEl.innerHTML = ` <p class="${DETAILS_CLASS_NAME}__disclaimer"><small>Added by <a href="https://github.com/harristom/cinematheqular">Cinematheqular</a> — please check the rest of the listing to ensure the information is correct</small></p> <div class="${DETAILS_CLASS_NAME}__wrapper"> <img src="" alt="" class="${DETAILS_CLASS_NAME}__poster"> <div class="${DETAILS_CLASS_NAME}__data"> <h2 class="${DETAILS_CLASS_NAME}__title"></h2> <p class="${DETAILS_CLASS_NAME}__director"></p> <p class="${DETAILS_CLASS_NAME}__ratings"></p> <p class="${DETAILS_CLASS_NAME}__plot"></p> </div> </div> `; detailsEl.querySelector(`.${DETAILS_CLASS_NAME}__poster`).src = result.Poster; detailsEl.querySelector(`.${DETAILS_CLASS_NAME}__title`).textContent = `${result.Title} (${result.Year})`; const rtRating = result.Ratings.find(r => r.Source == 'Rotten Tomatoes')?.Value; if (rtRating) detailsEl.querySelector(`.${DETAILS_CLASS_NAME}__ratings`).textContent = formatRtRating(rtRating); detailsEl.querySelector(`.${DETAILS_CLASS_NAME}__plot`).textContent = result.Plot; detailsEl.querySelector(`.${DETAILS_CLASS_NAME}__director`).textContent = result.Director; document.querySelector('.event-full .node-content .container .content')?.prepend(detailsEl); }) .catch(error => console.log('error', error)); } async function getOmdbApiKey() { const key = GM_getValue('key', null); if (key) { return fetch('https://www.omdbapi.com/?apikey=' + key).then(r => r.ok && key); } } function showApiKeyPrompt() { const KEY_POPUP_CLASS_NAME = 'cinematheqular-popup'; GM_addStyle(` .${KEY_POPUP_CLASS_NAME} { position: fixed; bottom: 15px; width: 100%; } .${KEY_POPUP_CLASS_NAME}__wrapper { display: block; margin: 0 auto; background-color: white; width: fit-content; min-width: 250px; padding: 10px; border-radius: 30px; box-shadow: 0px 2px 8px rgba(0,0,0,0.15); } .${KEY_POPUP_CLASS_NAME}__form { display: flex; align-items: center; background: #F3F3F3; border-radius: 20px; height: 30px; padding: 0px 5px 0px 0px; } .${KEY_POPUP_CLASS_NAME}__input { flex-grow: 1; height: 100%; background: transparent; padding: 0px 3px 0px 20px; border-radius: 20px 0px 0px 20px; &:focus { outline: none !important; } .${KEY_POPUP_CLASS_NAME}__form:has(&:focus) { outline: solid 1px; } } .${KEY_POPUP_CLASS_NAME}__save { cursor: pointer; background: #34B47D; color: #ffffff; border-radius: 50%; height: 20px; width: 20px; display: grid; place-content: center; } `); const popupEl = document.createElement('div'); popupEl.className = KEY_POPUP_CLASS_NAME; popupEl.innerHTML = ` <div class="${KEY_POPUP_CLASS_NAME}__wrapper"> <form class="${KEY_POPUP_CLASS_NAME}__form"> <input type="text" name="key" placeholder="Enter your OMDB API Key" class="${KEY_POPUP_CLASS_NAME}__input"> <button class="${KEY_POPUP_CLASS_NAME}__save">+</button> </form> </div> `; document.body.append(popupEl); popupEl.querySelector(`.${KEY_POPUP_CLASS_NAME}__form`).addEventListener('submit', e => { e.preventDefault(); const key = e.currentTarget.querySelector('[name=key]').value.trim(); GM_setValue('key', key); location.reload(); }); } function isAgendaPage() { return /\/cinematheque\/(?:film-programme|programm|agenda)$/.test(location.href); } function isDetailPage() { return true && // Matches the URL pattern for an event page /^https:\/\/www\.vdl\.lu\/.*?\/(?:kalender|agenda|whats-on)\//.test(location.href) && // Event location is Cinematheque document.querySelector('.infos-inner .place strong')?.textContent.trim().startsWith('Cinémathèque'); } function isMainPage() { return /\/cinematheque$/.test(location.href); } async function auth(callback) { const key = await getOmdbApiKey(); if (key) { if (typeof callback == 'function') callback(key); } else { showApiKeyPrompt(); } } function route() { if (isAgendaPage()) { console.log('agenda'); auth(agendaPage); } else if (isDetailPage()) { console.log('detail'); auth(detailPage); } else if (isMainPage()) { console.log('main'); auth(); } } route(); // FOR DEBUGGING ONLY function GM_clearValues() { for (const value of GM_listValues()) { GM_deleteValue(value); location.reload(); } } unsafeWindow.GM_clearValues ??= GM_clearValues; })();