NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name HJ Admin Toolkit // @namespace http://tampermonkey.net/ // @version 0.6.4 // @description A suite of functions aimed at automating the admin process in the HANDJOB thread // @author HJ-OTMOP // @license MIT // @updateURL https://openuserjs.org/meta/HJ-OTMOP/HJ_Admin_Toolkit.meta.js // @downloadURL https://openuserjs.org/install/HJ-OTMOP/HJ_Admin_Toolkit.user.js // @copyright 2018 - 2021, HJ-OTMOP (https://openuserjs.org/users/HJ-OTMOP) // @icon https://ptpimg.me/6uob9q.png // @match https://passthepopcorn.me/*threadid=13617* // @match https://passthepopcorn.me/upload.php* // @match https://passthepopcorn.me/torrents.php* // @require http://code.jquery.com/jquery-3.3.1.min.js // @connect imdb.com // @connect handjob-ptp.xyz // @run-at document-end // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // ==/UserScript== // Changelog 0.6.4: // - Shifted to using subdomain, rather than folder for API // Changelog 0.6.3: // - Script will now auto-logout when receiving a signal to do so from the API // Changelog 0.6.2: // - Fix for HTML inside mediainfo scan issues not rendering on the recent group encodes window // - Switched the order of the 'Show/Hide Info Panel' and 'Scan Storage Management' buttons in the recent group encodes window // - Added a title to be the default recent encodes status message // - In the authorisation checker, forum posts can now be viewed inline by clicking their row // Changelog 0.6.1: // - Hovering over the recent encodes action buttons will now display their function in the status display // Changelog 0.6.0: // - Completely re-written "Check Recent Encodes" functionality and UI // - New Material Design-inspired UI // - Live recent encode data is now pulled from handjob-ptp.xyz // - Automated login creates an API session for any PTP user // - API now handles provided data, allowing for faster browsing of recent uploads // - Easy pagination links allow you to quickly traverse results // - Easier search filter allow you to search by title, filename, director, rank and username // - Scan data now persists across sessions until manually cleared // - Authorisation Checker now has increased accuracy (function () { 'use strict'; const PROD_ROOT = 'https://api.handjob-ptp.xyz'; const DEV_ROOT = 'http://localhost:9090'; const ROOT = PROD_ROOT; const PTP = 'https://passthepopcorn.me'; let _access_token, groupEncodes, _AntiCsrfToken, _user = {}, _logged_in = false, _defaultRecentEncodesMessage = 'Recent HANDJOB Group Encodes'; const userNode = document.querySelector('#header #userinfo a.user-info-bar__link'); _user.username = userNode.textContent.trim(); _user.userid = parseInt(userNode.href.split('id=').pop(), 10); _AntiCsrfToken = document.body.getAttribute('data-anticsrftoken'); const accessToken = GM_getValue('accessToken'); if (accessToken) { _logged_in = true; _access_token = accessToken; } async function logoutFromAPI() { ADMIN_ajaxRequest({ url: ROOT + '/auth/logout', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: `Bearer ${_access_token}`, }, }) .then((res) => { GM_deleteValue('accessToken'); location.reload(); }) .catch(console.log); } function getLoginCredentials(ctx) { let stepsTotal = 6, stepsComplete = 0, progress = 0; ctx.inputs.progress.style.width = '0%'; ctx.inputs.progress.classList.add('active'); const cb = () => { ctx.inputs.progress.classList.remove('active'); ctx.inputs.progress.removeEventListener('transitionend', cb); }; const progressStep = () => { stepsComplete++; progress = Math.ceil((100 / stepsTotal) * stepsComplete); ctx.inputs.progress.style.width = progress + 1 + '%'; }; return new Promise(async (resolve, reject) => { const data = { username: _user.username, userid: _user.userid }; // 1. Get a verification token from the API ctx.scanning = 'Logging in to API'; const { verificationToken } = (await ADMIN_ajaxRequest({ url: ROOT + '/auth/verification-token', method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, data: JSON.stringify(data), }).catch((e) => reject())) || { verificationToken: null }; progressStep(); if (!verificationToken) return reject(); // 2. Find a recent forum post by the user to use for verifying token const recentPostsURL = `${PTP}/userhistory.php?action=posts&userid=${_user.userid}&showunread=0&group=0`; const recentPostsRaw = await ADMIN_ajaxRequest({ url: recentPostsURL, }).catch((e) => reject()); progressStep(); const recentPosts = parseHTMLtoDOM(recentPostsRaw); const postForVerification = Array.from(recentPosts.querySelectorAll('div.forum-post')).pop(); const postForVerificationNode = postForVerification.querySelector( '.forum-post__heading > span:first-child > a' ); const threadId = parseInt(postForVerificationNode.href.match(/(?:threadid=)(\d+)/)[1], 10); const postId = parseInt(postForVerificationNode.href.match(/(?:postid=)(\d+)/)[1], 10); if (!threadId || !postId) return reject(); // 3. Retrieve the content of the post from PTP so it can be reverted afterwards const postContent = await ADMIN_ajaxRequest({ url: `${PTP}/forums.php?action=get_post&post=${postId}`, }).catch((e) => reject()); progressStep(); if (!postContent) return reject(); // 4. Add the verification token and POST the updated post content to PTP ctx.scanning = 'Verifying login details'; const newPostContent = postContent + `\n\n[size=1][hide= ]${verificationToken}[/hide][/size]`; const formData = new FormData(); formData.append('AntiCsrfToken', _AntiCsrfToken); formData.append('body', newPostContent); formData.append('post', postId); formData.append('key', '1'); const postForVerificationEditURL = `${PTP}/forums.php?action=takeedit`; const postEdited = await ADMIN_ajaxRequest({ method: 'POST', data: formData, url: postForVerificationEditURL, }).catch((e) => reject()); progressStep(); // 5. Send a GET request to the API with the information about where to find the verification token const { accessToken } = (await ADMIN_ajaxRequest({ url: `${ROOT}/auth/verification-token/${threadId}/${postId}/${verificationToken}`, headers: { Accept: 'application/json', }, }).catch((e) => reject())) || { accessToken: null }; progressStep(); if (!accessToken) return reject(); // 6. Revert the edits to the user's forum post formData.delete('body'); formData.append('body', postContent); const postReverted = await ADMIN_ajaxRequest({ method: 'POST', data: formData, url: postForVerificationEditURL, }).catch((e) => reject()); progressStep(); GM_setValue('accessToken', accessToken); _access_token = accessToken; _logged_in = true; ctx.scanning = 'Login complete. Welcome ' + _user.username; ctx.inputs.progress.addEventListener('transitionend', cb); // 7. Profit resolve(); }); } // ## Start Section for functions for 'Check Recent HANDJOB Encodes' function ## class GroupEncodes { // TODO List // 1.) Method to clear saved scans constructor() { this.style(); this.json = null; this.pagination = { json: null, }; this.columns = [ 'title', 'uploaded', 'codec', 'source', 'res', 'filename', 'member', 'rank', 'auth', 'scan', ]; this.elements(); this.framework(); this.loader = null; this.loadingCallback = null; this._scanning = null; this._showInfo = false; this._loading = false; this._orderBy = GM_getValue('groupEncodesOrderBy', 'uploaded_at'); this._asc = GM_getValue('groupEncodesAsc', false); this._batchActive = false; } /* ************************************************ GETTERS AND SETTERS ************************************************ */ /** * Return list of DB results filtering to show only those without a MI scan status * @memberof GroupEncodes * @returns { Object[] } */ get filtered() { return this.json.filter((item) => item.scanStatus === ''); } /** * Is the Mi scanner batch process running? * @memberof GroupEncodes * @returns { Boolean } */ get batchActive() { return this._batchActive; } /** * Scan batch scanner status and activate side-effects * @memberof GroupEncodes */ set batchActive(v) { this._batchActive = v; // Show the progress bar depending on value this.inputs.progress.classList.toggle('active', v); const progressNode = document.querySelector('.progress-outer'); const cb = () => { progressNode.removeEventListener('transitionend', cb); progressNode.querySelector('.progress-inner').style.width = '0%'; }; if (v === false) { progressNode.addEventListener('transitionend', cb); } progressNode.classList.toggle('active', v); // Start or stop the batch scanner depending on value if (v === true) this.batchMiScan(); } /** * The column by which database results should be ordered * @memberof GroupEncodes * @returns { string } */ get orderBy() { return this._orderBy; } /** * Set and save to GM localStorage * @memberof GroupEncodes */ set orderBy(v) { GM_setValue('groupEncodesOrderBy', v); this._orderBy = v; } /** * Order database results by ascending order * @memberof GroupEncodes * @returns { boolean } */ get asc() { return this._asc; } /** * Set and save to GM localStorage * @memberof GroupEncodes */ set asc(v) { GM_setValue('groupEncodesAsc', v); this._asc = v; } /** * Show the loader in the information window * @memberof GroupEncodes * @returns { boolean } */ get loading() { return this._loading; } /** * Set loader status and activate side-effects * @memberof GroupEncodes */ set loading(v) { this._loading = v; if (v === false) { // Callback to ensure loader gracefully fades out before removing it from the DOM this.loader.addEventListener('transitionend', () => { this.loader.parentNode.removeChild(this.loader); // Run the callback, usually to populate the container that the loader was showing in if (this.loadingCallback) this.loadingCallback(); // Remove loader HTMLElement from class object this.loader = null; }); // Start the transition to remove loader this.loader.classList.remove('show'); } } /** * Show the information window: used for scan results and authorisation searches * @memberof GroupEncodes * @returns { boolean } */ get showInfo() { return this._showInfo; } /** * Set information window status and activate side-effects * @memberof GroupEncodes */ set showInfo(v) { this._showInfo = v; const cb = () => { const viewer = document.querySelector('#forum-post-viewer'); if (viewer) document.querySelector('#forum-post-viewer').innerHTML = ''; this.framework.info.removeEventListener('transitionend', cb); }; // Show the window based on value this.framework.info.addEventListener('transitionend', cb); this.framework.info.classList.toggle('show', v); // Change the button's icon depending on value if (v === false) { this.controls.info.querySelector('i').textContent = 'unfold_more'; } else { this.controls.info.querySelector('i').textContent = 'unfold_less'; } } /** * The text to show in the status bar * @memberof GroupEncodes * @returns { string } */ get scanning() { return this._scanning; } /** * Set and transition to new status text * @memberof GroupEncodes */ set scanning(v) { if (v === this._scanning) return false; this._scanning = v; // Callback: Set new status text and fade-in const cb = () => { this.inputs.display.removeEventListener('transitionend', cb); this.inputs.display.innerHTML = v; this.inputs.display.style.opacity = 1; }; // Fade-out the existing status text this.inputs.display.addEventListener('transitionend', cb); if (this.inputs.display.style.opacity == 0) cb(); else this.inputs.display.style.opacity = 0; } /* ************************************************ CLICK HANDLERS ************************************************ */ /** * The central click-handler function. All clicks inside the display are routed here * @param { EventObject } ev - the click event object * @memberof GroupEncodes */ click(ev) { // If clicked element has a ripple class, activate it if (ev.target.classList.contains('hj-ripple')) this.ripple(ev); // Close the modal if (ev.target.classList.contains('hj-close')) this.hide(); // Clear the scan storage cache else if (ev.target.classList.contains('hja__cache-button')) this.clearScanCache(ev.target); // Toggle the information window display else if (ev.target.classList.contains('hj-close-info')) this.showInfo = !this.showInfo; // Toggle the status of the batch scanner // See "set batchActive()" for side-effects. else if (ev.target.classList.contains('controls')) this.batchActive = !this.batchActive; // Load information regarding an upload's authorisation check else if (ev.target.classList.contains('hj-auth-check')) this.showAuthorisation(ev.target.closest('tr').getAttribute('data-row-id')); // Change tab inside an Authorisation Check display else if (ev.target.classList.contains('tablinks')) this.changeTab(ev); // Handle links for the DB pagination else if (ev.target.classList.contains('group-encodes-pagination')) this.paginationLink(ev); // Handle links for the DB results ordering column / order else if (ev.target.classList.contains('group-encodes--header-sort')) this.sortingLink(ev); // Load mediainfo scan information for an upload else if (ev.target.classList.contains('hj-scan-view')) this.showMediainfoScan(ev.target.closest('tr').dataset.rowId); // Inside MI Scan info window, handle the refresh of a MI scan else if (ev.target.classList.contains('hj-refresh-scan-button')) this.refreshScan(ev); // Logout of HANDJOB API session else if (ev.target.classList.contains('hja__logout')) this.logout(); // Authorisation check forum post links else if (ev.target.closest('.hja__group-encodes--forum-message-row')) this.loadForumPost(ev); // Results row click, create a mediainfo scan else if (ev.target.closest('.hja__group-encodes--scan-row')) this.singleMiScan(ev); } logout() { this.hide(); logoutFromAPI(); } // Show the Group Encodes modal window async show() { // Activate PTP's body scroll lock document.body.classList.add('lightbox__scroll-lock'); this.container.classList.add('show'); if (!this.json && _logged_in === true) { const container = Create('div.hja__group-encodes--loader-container'); const loader = container.appendChild(Create('div.loader.show')); this.framework.content.appendChild(container); await this.load(); } else if (!this.json) { const container = Create('div.hja__group-encodes--login-container'); container.innerHTML = `<h3>Login required</h3> <div>HANDJOB group encode records are now stored on a separate API which requires: <ol><li>Verifying that you're a member of PTP</li><li>Confirming your PTP username</li></ol> None of your PTP credentials are ever sent to the API server, only your username and userid (via encrypted https). No IP addresses or browser information are logged.</div>`; const loginButton = container.appendChild(Create('button.hj-ripple.primary')); loginButton.textContent = 'Login'; this.framework.content.appendChild(container); loginButton.addEventListener('click', () => { loginButton.disabled = true; getLoginCredentials(this) .then(() => this.load()) .catch((e) => { container.innerHTML = `<h3>Error logging in</h3><div>There was an error logging you into the HANDJOB API. Please try again later.</div><div style="margin-top: 1em;">If the problem persists, contact The Godfather.</div>`; loginButton.disabled = false; container.appendChild(loginButton); }); }); } } // Hide the Group Encodes modal window hide() { // Deactivate PTP's body scroll lock document.body.classList.remove('lightbox__scroll-lock'); this.container.classList.remove('show'); } // Handle alterations to the footer options that require a data reflow editFooterOption(option, ev) { switch (option) { case 'limit': GM_setValue('groupEncodesLimit', ev.target.value); break; } this.load(); } /** * Handle a data request for when the sorting order/column is changed * @param { EventObject } ev - the click event object * @memberof GroupEncodes */ sortingLink(ev) { // Get data value from row const newOrderBy = ev.target.dataset.sort; // If the user clicked the current orderBy col, reverse the sort order if (this.orderBy == newOrderBy) this.asc = !this.asc; // Else set a new orderBy column else this.orderBy = newOrderBy; // Send new load request and refresh page display this.load({ page: this.pagination.json.page }); } /** * Send a load request when a page link is clicked * @param { EventObject } ev - The click event object * @memberof GroupEncodes */ paginationLink(ev) { const page = ev.target.getAttribute('data-page'); this.load({ page }); } /** * Adds a ripple effect to buttons/links with a class of "hj-ripple" * @param { EventObject } e - The event object * @memberof GroupEncodes */ ripple(e) { const el = e.target; const remove = (node) => (node ? node.parentElement.removeChild(node) : null); remove(el.querySelector('.ripple')); const pos = el.getBoundingClientRect(); let X = e.pageX - pos.left; let Y = e.pageY - pos.top; let rippleDiv = document.createElement('div'); rippleDiv.classList.add('ripple'); rippleDiv.setAttribute('style', 'top:' + Y + 'px; left:' + X + 'px;'); let customColor = el.getAttribute('data-ripple-color'); if (customColor) rippleDiv.style.background = customColor; el.appendChild(rippleDiv); rippleDiv.addEventListener('animationend', () => { remove(rippleDiv); }); } /** * Clear mediainfo scans from GM localStorage by scan timestamp * @param { HTMLElement } target - the button which was clicked * @memberof GroupEncodes */ clearScanCache(target) { const age = target.dataset.cacheClearTime; let cutoff; switch (age) { case 'all': cutoff = Date.now(); break; case '30': cutoff = Date.now() - 1000 * 60 * 60 * 24 * 30; break; case '15': cutoff = Date.now() - 1000 * 60 * 60 * 24 * 15; break; default: cutoff = 0; } const values = GM_listValues(); const filtered = values && Array.isArray(values) === true ? values.filter((item) => /^torrentScan_/i.test(item) === true) : []; const toDelete = filtered.filter((item) => { const scan = GM_getValue(item); const timestamp = scan.timestamp; return !timestamp || timestamp < cutoff; }); toDelete.forEach((scan) => { GM_deleteValue(scan); }); this.scanning = `Deleted ${toDelete.length} scans from storage`; this.load({ page: this.pagination.json.page }); } /* ************************************************ AJAX FUNCTIONS ************************************************ */ /** * Send a new results request to the DB * @param { object } input - Single object input * @param { number } input.page - The page of results to request from the DB * @returns Promise<void> * @memberof GroupEncodes */ load({ page = 1 } = {}) { const filter = this.inputs.filter.value || ''; const limit = GM_getValue('groupEncodesLimit') || 25; const orderBy = GM_getValue('groupEncodesOrderBy') || 'uploaded_at'; const asc = GM_getValue('groupEncodesAsc') || false; const order = asc === true ? 'asc=1&' : ''; return new Promise((resolve, reject) => { ADMIN_ajaxRequest({ url: `${ROOT}/group-encodes?${order}orderBy=${orderBy}&limit=${limit}&page=${page}&filter=${filter}`, headers: { Accept: 'application/json', Authorization: `Bearer ${_access_token}`, }, }) .then((result) => { this.json = result.data; this.pagination.json = result.pagination; // Update the table display this.update(); // Remove the loader spinner from the filter input container this.inputs.filterInputContainer.classList.remove('loading'); resolve(); }) .catch((err) => { console.log(err); }); }); } /* ************************************************ FRAMEWORK SETUP ************************************************ */ // Set up the basic container and click handler elements() { this.container = document.querySelector('#group-encodes'); this.container.addEventListener('click', this.click.bind(this), { passive: true }); } // Set up the initial framework required to display all information framework() { this.framework = {}; this.controls = {}; this.inputs = {}; this.framework.status = Create('div.hja__group-encodes--status'); this.framework.info = Create('div.hja__group-encodes--info'); this.framework.info.innerHTML = ` <div class="hja__group-encodes--info--scan-container"> <div class="hja__group-encodes--scan-results hj-card"> <header style="background-color: #1091ec !important">Group Encodes: Upload Information Window</header> <section> <div style="text-align: center; width: 100%;"> Use the action buttons in the 'AUTH' and 'SCAN' columns of the table to show - where available - information in this window. </div> </section> </div> </div>`; this.framework.content = Create('div.hja__group-encodes--content'); this.framework.footer = Create('footer.hja__group-encodes--footer'); this.status(); this.footer(); Object.values(this.framework).forEach((item) => { this.container.appendChild(item); }); } hover(target, show) { const title = target.dataset.title; if (show === true) this.scanning = title; else this.scanning = _defaultRecentEncodesMessage; } // Set up the status bar status() { const appActions = Create('div.hja__recent-encodes--app-actions'); const logout = appActions.appendChild(Create('button.hj-ripple.icon.warning.hja__logout')); logout.innerHTML = "<i class='material-icons'>power_settings_new</i>"; logout.setAttribute('data-title', 'Logout from HANDJOB API'); this.controls.logout = logout; const storageSpeedDial = appActions.appendChild(Create('div.speed-dial-container')); storageSpeedDial.innerHTML = ` <div class="speed-dial-activator"> <button class="icon primary" data-title="Remove saved mediainfo scans"> <i class="material-icons open">sd_card</i> <i class="material-icons close">close</i> </button> </div> <div class="speed-dial-menu-items"> <button class="icon accent hja__cache-button" data-label="Delete scans aged 60+ days" data-cache-clear-time="60" > <i class="material-icons">hourglass_full</i> </button> <button class="icon accent hja__cache-button" data-label="Delete scans aged 30+ days" data-cache-clear-time="30" > <i class="material-icons">hourglass_bottom</i> </button> <button class="icon accent hja__cache-button" data-label="Delete scans aged 15+ days" data-cache-clear-time="15" > <i class="material-icons">hourglass_top</i> </button> <button class="icon accent hja__cache-button" data-label="Delete all scans" data-cache-clear-time="all"> <i class="material-icons">delete</i> </button> </div>`; const infoControl = appActions.appendChild(Create('button.hj-ripple.icon.primary.hj-close-info')); infoControl.innerHTML = "<i class='material-icons'>unfold_more</i>"; infoControl.setAttribute('data-title', 'Show/hide information panel'); this.controls.info = infoControl; const status = Create('div.hja__group-encodes--status-container'); status.innerHTML = `<div class="progress-outer"> <div class="progress-inner"> <div id="scanner-display" class="progress-label">${_defaultRecentEncodesMessage}</div> <div class="controls controls-start" data-title="Start batch mediainfo scan"> <i class="material-icons">play_circle_outline</i> </div> <div class="controls controls-stop" data-title="Stop batch mediainfo scan"> <i class="material-icons">pause_circle_outline</i> </div> </div> </div>`; const spacer = Create('div'); this.inputs.display = status.querySelector('#scanner-display'); this.inputs.progress = status.querySelector('.progress-inner'); const close = Create('button.hj-ripple.icon.error.hj-close'); close.setAttribute('data-ripple-color', '#F44336'); close.setAttribute('data-title', 'Close recent encodes window'); close.innerHTML = "<i class='material-icons'>close</i>"; [...Array.from(appActions.querySelectorAll('button:not(.hja__cache-button)')), close].forEach((item) => { item.addEventListener('mouseenter', (ev) => this.hover(ev.target, true)); item.addEventListener('mouseleave', (ev) => this.hover(ev.target, false)); }); [appActions, status, close].forEach((x) => { this.framework.status.appendChild(x); }); } // Set up the table footer footer() { const limit = GM_getValue('groupEncodesLimit') || 25; const limitOptions = [ { value: 10 }, { value: 25 }, { value: 50 }, { value: 75 }, { value: 100 }, { value: 250 }, { value: 500 }, { value: 0, text: 'All' }, ]; const limitOptionsHTML = limitOptions.reduce((acc, current) => { acc += `<option value="${current.value}"`; acc += limit == current.value ? ' selected>' : '>'; acc += current.text ? current.text : current.value; acc += '</option>'; return acc; }, ''); // Limit select input section const limitNode = Create('div.hja__group-encodes--footer-limit'); const limitSelect = Create('select.select-css'); const limitSelectLabel = Create('label'); limitSelectLabel.setAttribute('for', 'limit-select'); limitSelectLabel.style.marginRight = '0.5em'; limitSelectLabel.style.whiteSpace = 'nowrap'; limitSelectLabel.textContent = 'Results per page: '; limitSelect.name = 'limit-select'; limitSelect.innerHTML = limitOptionsHTML; limitSelect.addEventListener('change', (ev) => this.editFooterOption('limit', ev)); limitNode.appendChild(limitSelectLabel); limitNode.appendChild(limitSelect); // Search filter input section const filterNode = Create('div.hja__group-encodes--footer-filter'); const filterLabel = filterNode.appendChild(Create('label')); const filterInputContainer = Create('div.hja__group-encodes--footer-filter-input-container'); const filterPrepend = filterInputContainer.appendChild(Create('div.input-prepend')); filterPrepend.innerHTML = `<i class="material-icons">search</i>`; const filterInput = filterInputContainer.appendChild(Create('input')); filterNode.appendChild(filterInputContainer); filterInput.setAttribute('type', 'text'); filterInput.name = 'filter'; filterLabel.setAttribute('for', 'filter'); filterLabel.style.marginRight = '0.5em'; filterLabel.textContent = 'Filter: '; this.pagination.links = Create('div.hja__group-encodes--footer-pagination-links'); // Create a debounced function bound to this instance for handling search filter input const loadBound = this.load.bind(this); const fn = debounce(loadBound, 500); this.inputs.filter = filterInput; this.inputs.filterInputContainer = filterInputContainer; this.inputs.filter.addEventListener('input', () => { filterInputContainer.classList.add('loading'); fn(); }); // Add all elements to the footer container [limitNode, filterNode, this.pagination.links].forEach((item) => this.framework.footer.appendChild(item)); } // Set up and refresh the table headers / Show sort arrows where applicable headers(node) { this.columns.forEach((label) => { const col = Create('th.hja__group-encodes--col'); switch (label) { case 'auth': col.classList.add('col-centre'); break; case 'uploaded': col.classList.add('group-encodes--header-sort'); col.setAttribute('data-sort', 'uploaded_at'); break; case 'member': col.classList.add('group-encodes--header-sort'); col.setAttribute('data-sort', 'username'); break; case 'rank': col.classList.add('group-encodes--header-sort'); col.setAttribute('data-sort', 'rank_order'); break; case 'scan': break; default: col.classList.add('group-encodes--header-sort'); col.setAttribute('data-sort', label); } const divNode = col.appendChild(Create('div')); const labelNode = divNode.appendChild(Create('span')); labelNode.textContent = label; if (this.orderBy === col.dataset.sort) { const arrow = Create('i.material-icons'); arrow.textContent = this.asc == true ? 'arrow_drop_up' : 'arrow_drop_down'; divNode.appendChild(arrow); } node.appendChild(col); }); } /* ************************************************ MI SCANNER ************************************************ */ /** * Pause execution of the script for a speicied amount of time, used to increase delay between Ajax calls * @param { number } ms - Number of milliseconds to pause for * @returns { Promise<null> } * @memberof GroupEncodes */ delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Start the batch MI Scanner process * @memberof GroupEncodes * @returns { void } */ async batchMiScan() { const total = this.filtered.length; let complete = 0, progress = 0; this.inputs.progress.style.width = '0%'; if (total === 0) { this.scanning = 'No uploads left to scan from this page'; this.batchActive = false; } else { this.inputs.progress.classList.add('active'); for (let [idx, upload] of this.json.entries()) { // If the upload has already been scanned, skip to the next one if (upload.scanStatus) continue; // If batch scan has been turned off, cancel loop if (this.batchActive !== true) break; this.scanning = 'Scanning: ' + upload.filename; await this.singleMiScan(undefined, idx); // Update progress display information complete++; progress = Math.ceil((100 / total) * complete); this.inputs.progress.style.width = progress + 1 + '%'; // Pause execution to prevent spamming of PTP's servers await this.delay(1000); } const cb = () => { this.inputs.progress.classList.remove('active'); this.inputs.progress.removeEventListener('transitionend', cb); }; this.inputs.progress.addEventListener('transitionend', cb); if (total === complete) this.scanning = 'Scanning complete'; else this.scanning = 'Scanning cancelled'; this.batchActive = false; } } /** * Called from the MI scan information window, update scan information with a new scan * @param {*} ev * @memberof GroupEncodes * @returns { void } */ async refreshScan(ev) { const rowId = ev.target.dataset.rowid; const row = this.framework.content.querySelector(`[data-row-id="${rowId}"].hja__group-encodes--scan-row`); // Reset the scan status of the row to enable it to be updated row.setAttribute('data-scan', ''); await this.singleMiScan(undefined, rowId); // Update the information window this.showMediainfoScan(rowId); // Update the status display this.scanning = 'Scanning complete'; } async singleMiScan(ev, setRow = null) { // Get the DOM row element const row = setRow === null ? ev.target.closest('.hja__group-encodes--scan-row') : this.framework.content.querySelector(`[data-row-id="${setRow}"].hja__group-encodes--scan-row`); const rowId = row.dataset.rowId; const scanStatus = row.dataset.scan; // If the row has already been scanned, abort the process here if (scanStatus && scanStatus !== '') return false; // If called from click handler, run a pass of the progress bar loader if (setRow === null) this.inputs.progress.classList.add('indeterminate'); const { groupid, torrentid, filename } = this.json[rowId]; const scanRaw = await this.scanMediainfo({ groupid, torrentid, filename }); // Batch: will animate a flash of colour on the row that's just been scanned if (setRow !== null) row.classList.add('complete'); // Filter out the extraneous information from the MI Scanner, reducing size to save output const scan = scanRaw ? this.cleanScan(scanRaw) : 'deleted'; // Save scan data to GM's localStorage GM_setValue('torrentScan_' + torrentid, scan); // Update scan status in the DOM row const info = scan === 'deleted' ? 'deleted' : scan.errors > 0 ? 'error' : scan.warnings > 0 ? 'warning' : scan.advisories > 0 ? 'advisory' : 'pass'; row.setAttribute('data-scan', info); this.json[rowId].scanStatus = info; const cb = () => { this.inputs.progress.removeEventListener('animationiteration', cb); this.inputs.progress.classList.remove('indeterminate'); this.scanning += ' ... Complete'; }; if (setRow === null) this.inputs.progress.addEventListener('animationiteration', cb); } /** * Remove extraneous scan information from object, required to allow saving to localStorage * @param { object } scanRaw - An instance of the Mediainfo Scan class * @returns { object } Minimal scan results * @memberof GroupEncodes */ cleanScan(scanRaw) { const scan = { scans: [], issues: scanRaw.issues, errors: scanRaw.errors, warnings: scanRaw.warnings, advisories: scanRaw.advisories, cover: scanRaw.cover, timestamp: scanRaw.timestamp, }; // Iterate over each mediainfo log scan on the upload page scanRaw.scans.forEach((item) => { // If there was a problem reading the log/scan, skip to the next one if (!item.data || !item.data.filename) return true; // Create a new feedback object const fb = { data: { filename: item.data.filename, metatitle: item.data.metatitle, }, feedback: {}, }; // Iterate over the feedback sections ( e.g. audio / video / x264 / chapters etc. ) for (const [key, value] of Object.entries(item.feedback)) { // Skips the guides property, used in other scripts if (key === 'guides') continue; const i = item.feedback[key]; // If no issues are found in this section, skip to the next one. if (i.errors.length === 0 && i.warnings.length === 0 && i.advisories.length === 0) continue; fb.feedback[key] = {}; if (i.errors.length > 0) fb.feedback[key].errors = i.errors; if (i.warnings.length > 0) fb.feedback[key].warnings = i.warnings; if (i.advisories.length > 0) fb.feedback[key].advisories = i.advisories; } scan.scans.push(fb); }); return scan; } /** * Retrieve MI logs from PTP torrent page and create scans of each * @param { object } input - Single object input * @param { number } input.torrentid - The PTP torrent ID of the upload * @param { number } input.groupid - The PTP group ID of the upload * @param { number } input.filename - The filename of the uploaded torrent * @returns { object } - Scan results object * @memberof GroupEncodes */ scanMediainfo({ torrentid, groupid, filename }) { this.scanning = 'Scanning: ' + truncate({ str: filename, maxLength: 90 }); const url = `${PTP}/torrents.php?id=${groupid}&torrentid=${torrentid}`; return new Promise((resolve, reject) => { ADMIN_ajaxRequest({ url }).then((res) => { const dom = parseHTMLtoDOM(res); const cover = dom.querySelector('div.sidebar .box_albumart img.sidebar-cover-image').src; const el = dom.querySelector('tr#torrent_' + torrentid + ' td > div:last-child'); if (!el) return resolve(null); const scans = []; let warnings = 0, errors = 0, advisories = 0, issues = 0; Array.from(el.querySelectorAll('table.mediainfo')).forEach((mi) => { const miScan = populateMiScan(mi, true); scans.push(miScan); }); if (scans.length === 0) { return resolve(null); } const sections = [ 'filename', 'metatitle', 'x264', 'video', 'audio', 'subtitles', 'chapters', 'misc', ]; for (let scan of scans) { for (let i = 0; i < sections.length; i++) { errors += scan.feedback[sections[i]].errors.length; warnings += scan.feedback[sections[i]].warnings.length; advisories += scan.feedback[sections[i]].advisories.length; } } issues = errors + warnings + advisories; const results = { scans, issues, errors, warnings, advisories, cover, timestamp: Date.now(), }; resolve(results); }); }); } // Load the mediainfo scan results for a row into the information window showMediainfoScan(row) { const json = this.json[row]; const scan = json ? GM_getValue('torrentScan_' + json.torrentid) : 'deleted'; const container = Create('div.hja__group-encodes--info--scan-container'); this.framework.info.innerHTML = ''; this.framework.info.appendChild(container); this.showInfo = true; const scanDisplay = Create('div.hja__group-encodes--scan-results.hj-card'); const summary = () => { let t = []; if (scan.errors > 0) t.push(`${scan.errors} error${scan.errors !== 1 ? 's' : ''}`); if (scan.warnings > 0) t.push(`${scan.warnings} warning${scan.warnings !== 1 ? 's' : ''}`); if (scan.advisories > 0) t.push(`${scan.advisories} advisor${scan.advisories !== 1 ? 'ies' : 'y'}`); if (t.length > 1) { const last = t.pop(); return t.join(', ') + ' and ' + last; } else return t[0]; }; if (json) scanDisplay.innerHTML = `<header>Mediainfo scan results for ${truncate({ str: json.filename, maxLength: 100, split: true, })}<div style="position:absolute;right:20px;"> <button class="hj-ripple icon secondary hj-refresh-scan-button" data-torrentid="${json.torrentid}" data-groupid="${json.groupid}" data-filename="${json.filename}" data-rowid="${row}" > <i class="material-icons" style="color:white;">replay</i> </button> <a href="/torrents.php?id=${json.groupid}&torrentid=${json.torrentid}" target="_blank"> <button class="hj-ripple icon secondary"> <i class="material-icons" style="color:white;">link</i> </button> </a> </div> </header> <section> <div class="flex-col" style="padding-right: 18px;"> <article> <table class="hja__group-encodes--miscan-table"> <tr> <td>Title: </td> <td>${json.title}</td> </tr> <tr> <td>Year: </td> <td>${json.year}</td> </tr> <tr> <td>Resolution: </td> <td>${json.res}</td> </tr> <tr> <td>Uploaded: </td> <td> ${new Date(json.uploaded_at).toLocaleString(undefined, { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', })} </td> </tr> <tr> <td>Uploader: </td> <td>${json.username} – [ ${json.rank_full} ]</td> </tr> <tr> <td class="autofit">Scanned on: </td> <td> ${new Date(scan.timestamp).toLocaleString(undefined, { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', })} </td> </tr> <tr> <td>Summary: </td> <td> ${summary()} </td> </tr> </table> </article> <article id="scan-results"> </article> </div> <aside> <figure> <img class="cover" src="${scan.cover}" /> </figure> </aside> </section>`; else scanDisplay.innerHTML = `<header>This torrent has been deleted</header> <section > <div style="text-align: center; width: 100%;"> This torrent has been deleted from PTP since the previous scan. </div> </section>`; container.appendChild(scanDisplay); const scanNode = container.querySelector('article#scan-results'); if (json) this.insertScanResults(scanNode, scan); setTimeout( () => this.container.scrollTo({ top: 0, behavior: 'smooth', }), 300 ); } // Information Window: add formatted MI scan results to page insertScanResults(container, scan) { const icons = { filename: 'label', metatitle: 'short_text', video: 'theaters', audio: 'volume_up', subtitles: 'subtitles', x264: 'developer_board', misc: 'more_horiz', chapters: 'playlist_play', }; for (let result of scan.scans) { const header = container.appendChild(Create('header.hja__group-encodes--miscan-results--header')); header.innerHTML = result.data.filename; for (let [key, section] of Object.entries(result.feedback)) { const rowClass = section.errors ? '.errors' : section.warnings ? '.warnings' : '.advisories'; const row = container.appendChild( Create('div.hja__group-encodes--miscan-results--section-heading' + rowClass) ); row.innerHTML = `<i class='material-icons'>${icons[key]}</i> <span style="text-transform: capitalize;margin-left:1em;">${key}</span>`; for (let [severity, issues] of Object.entries(section)) { issues.forEach((issue) => { const line = Create('div.hja__group-encodes--miscan-results--section-issue.' + severity); line.innerHTML = issue.output; container.appendChild(line); }); } } } } /* ************************************************ AUTHORISATION WINDOW ************************************************ */ /** * Authorisation Check window, handle tab transitions * @param { EventObject } ev - The click event object * @returns { void } * @memberof GroupEncodes */ changeTab(ev) { const tabID = ev.target.getAttribute('data-tab-id'); let i, tabcontent, tablinks; tabcontent = document.querySelectorAll('.tab-content-item.active'); for (i = 0; i < tabcontent.length; i++) { tabcontent[i].classList.remove('active'); } // Get all elements with class="tablinks" and remove the class "active" tablinks = document.getElementsByClassName('tablinks'); for (i = 0; i < tablinks.length; i++) { tablinks[i].classList.remove('active'); } // Clear the forum post viewer document.querySelector('#forum-post-viewer').innerHTML = ''; // Show the current tab, and add an "active" class to the link that opened the tab document.getElementById(tabID).classList.add('active'); ev.target.classList.add('active'); } // Run a search via PTP's forum search function checkAuth(query = '', user = '') { return new Promise((resolve, reject) => { let url = `/forums.php?action=search&search=${query}&user=${user}&threadid=13617`; ADMIN_ajaxRequest({ url, method: 'GET' }).then((res) => { const results = []; const dom = parseHTMLtoDOM(res); Array.from(dom.querySelectorAll('div.thin > table.table--panel-like tbody tr')).forEach((item) => { const contents = item.querySelector('td:nth-child(2)'); const dateCont = item.querySelector('td:nth-child(3)'); const forumDate = dateCont.querySelector('span.time').getAttribute('title'); const forumLink = contents.querySelector('a.forum-topic__go-to-last-read').href; const postID = parseInt(forumLink.split('#post').pop(), 10); const threadID = parseInt(forumLink.match(/(?:threadid=)(\d+)/i)[1], 10); const forumPost = contents.querySelector('span:nth-child(2)').textContent; const forumAuthor = contents.querySelector('span.last_poster').querySelector('a.username') .textContent; results.push({ forumLink, forumPost, forumAuthor, forumDate, threadID, postID }); }); resolve(results); }); }); } /** * Load an authorisation check into the Information Window * @param { HTMLElement } row - The row of which to show an authorisation check * @returns { void } * @memberof GroupEncodes */ async showAuthorisation(row) { this.framework.info.innerHTML = ''; const container = Create('div.hja__group-encodes--info--auth-container'); const loader = container.appendChild(Create('div.loader.show')); this.framework.info.appendChild(container); this.loader = loader; this.showInfo = true; this.loading = true; const json = this.json[row]; // Search for exact matches by the uploader const a = this.checkAuth(json.filename, json.username); // Search for exact match replies const b = this.checkAuth(json.filename); // Search for potential replies const c = this.checkAuth('@' + json.username); // Search for potential matches by the uploader const d = this.checkAuth('approval', json.username); setTimeout( () => this.container.scrollTo({ top: 0, behavior: 'smooth', }), 300 ); // Send all 4 search requests to PTP and then process results Promise.all([a, b, c, d]).then((results) => { results = this.filterAuthResults(results, json); this.loadingCallback = () => { const resultsDisplay = Create('div.hja__group-encodes--auth-results.hj-card'); const tableHeader = `<table class="hja__group-encodes--auth-table"><thead><tr><th class="hja__group-encodes--col">Author</th><th class="hja__group-encodes--col">Post Date</th><th class="hja__group-encodes--col">Preview</th><th class="table-actions">Link</th></tr></thead><tbody>`; // Function to generate a table for the display of authorisation post results const generateAuthTable = ({ noData, results }) => { let html = tableHeader; if (results.length === 0) { html += `<tr class="hja__group-encodes--row"><td colspan="4" class="hja__group-encodes--col" style="padding-top: 3rem; font-size: 1rem; font-style: italic; text-align: center;">${noData}</td></tr>`; } results.forEach((post) => { html += `<tr class="hja__group-encodes--forum-message-row" data-post-id="${ post.postID }" data-thread-id="${post.threadID}"> <td class="hja__group-encodes--col">${post.forumAuthor}</td> <td class="hja__group-encodes--col" style="white-space: nowrap">${new Date(post.forumDate).toLocaleString()}</td> <td class="hja__group-encodes--col">${post.forumPost}</td> <td class="table-actions"><a href="${ post.forumLink }" target="_blank"><button class="hj-ripple primary icon small"><i class="material-icons">link</i></button></a> </tr>`; }); html += `</tbody></table>`; return html; }; const approvalRequests = () => { return generateAuthTable({ noData: 'No approval requests found', results: results[0] }); }; const potentialApprovalRequests = () => { return generateAuthTable({ noData: 'No potential approval requests found', results: results[3], }); }; const exactReplies = () => { return generateAuthTable({ noData: 'No exact replies found', results: results[1] }); }; const potentialReplies = () => { return generateAuthTable({ noData: 'No potential replies found', results: results[2] }); }; resultsDisplay.innerHTML = ` <header>Authorisation check for ${json.username}'s ${json.res === 'Other' ? 'DVDRip' : json.res} encode of ${ json.title } [${json.year}]</header> <div class="tab-container"> <div class="tab-controls"> <div class="tablinks hj-ripple active" data-tab-id="approval-requests"> <i class="material-icons">speaker_notes</i> <div>Approval Requests</div> </div> <div class="tablinks hj-ripple" data-tab-id="potential-approval-requests"> <i class="material-icons">question_answer</i> <div>Potential Approval Requests</div> </div> <div class="tablinks hj-ripple" data-tab-id="exact-replies"> <i class="material-icons">comment</i> <div>Exact Replies</div> </div> <div class="tablinks hj-ripple" data-tab-id="potential-replies"> <i class="material-icons" style="transform: scaleX(-1)">feedback</i> <div>Potential Replies</div> </div> </div> <div class="tab-content"> <div id="approval-requests" class="tab-content-item active"> ${approvalRequests()} </div> <div id="potential-approval-requests" class="tab-content-item"> ${potentialApprovalRequests()} </div> <div id="exact-replies" class="tab-content-item"> ${exactReplies()} </div> <div id="potential-replies" class="tab-content-item"> ${potentialReplies()} </div> <div id="forum-post-viewer"></div> </div> </div>`; container.appendChild(resultsDisplay); }; // Remove the loader this.loading = false; }); } /** * Loads the clicked forum post in the authorisation checker window * @param { EventObject } ev - The click event object * @memberof GroupEncodes */ async loadForumPost(ev) { if (/button/i.test(ev.target.tagName) === true) return false; const viewer = document.querySelector('#forum-post-viewer'); viewer.innerHTML = ''; const row = ev.target.closest('.hja__group-encodes--forum-message-row'); const postID = row.dataset.postId; const threadID = row.dataset.threadId; const raw = await ADMIN_ajaxRequest({ url: `${PTP}/forums.php?action=viewthread&threadid=${threadID}&postid=${postID}`, }).catch((e) => (this.scanning = 'Error loading forum post')); const page = parseHTMLtoDOM(raw); const postNode = page.querySelector('#post' + postID); const avatar = postNode.querySelector('.forum-post__avatar__image').src; const username = postNode.querySelector('.forum-post__heading a.username').textContent; const relativeTime = postNode.querySelector('.forum-post__heading span.time').textContent; const content = postNode.querySelector('.forum-post__bodyguard').innerHTML; const container = Create('div.hja__group-encodes--forum-post-container.hja__scrollbar'); const header = Create('div.hja__group-encodes--forum-post-header'); const footer = Create('div.hja__group-encodes--forum-post-header'); const authorAndBody = container.appendChild(Create('div.hja__group-encodes--forum-post-author-and-body')); const author = authorAndBody.appendChild(Create('div.hja__group-encodes--forum-post-avatar')); const body = authorAndBody.appendChild(Create('div.hja__group-encodes--forum-post-body')); header.innerHTML = `Posted by ${username}, ${relativeTime} <button id="forum-post-viewer-close" class="hj-ripple icon small"><i class="material-icons">close</i></button>`; const button = header.querySelector('#forum-post-viewer-close'); button.addEventListener('click', () => (viewer.innerHTML = '')); author.innerHTML = `<img src="${avatar}" />`; body.innerHTML = content; viewer.appendChild(header); viewer.appendChild(container); viewer.appendChild(footer); } /** * @description * @param { Object[] } results - Search results from "async showAuthorisation()" * @param { Object } json - JSON data for the current row * @returns { Object[] } - The filtered results Array * @memberof GroupEncodes */ filterAuthResults(results, json) { results.forEach((result) => { result.forEach((post) => { post.forumDate = new Date(post.forumDate + ' UTC').getTime(); }); }); // Filter results for Member's posts const initialPost = results[0] && results[0][0] ? results[0][0].forumDate : json.uploaded_at - 1000 * 60 * 60 * 24 * 14; // Filter results for exact replies results[1] = results[1] .filter((post) => post.forumAuthor !== json.username) .filter((post) => post.postID !== 376903) .slice(0, 10); const matchedPostIDs = results[1].map((post) => post.postID); const matchedRequestIDs = results[0].map((post) => post.postID); // Filter results for potential replies results[2] = results[2] .filter((post) => post.forumAuthor !== json.username) .filter((post) => post.forumDate > initialPost && post.forumDate < json.uploaded_at) .filter((post) => !matchedPostIDs.includes(post.postID)); results[3] = results[3] .filter( (post) => post.forumDate > json.uploaded_at - 1000 * 60 * 60 * 24 * 14 && post.forumDate < json.uploaded_at ) .filter((post) => !matchedRequestIDs.includes(post.postID)) .slice(0, 10); return results; } /* ************************************************ DISPLAY UPDATE ************************************************ */ /** * Updates the main display after a load request * @memberof GroupEncodes */ update() { // A few formatting functions to convert DB output to human const format = { source: (v) => (v === 'bluray' ? 'Blu-Ray' : v === 'dvd' ? 'DVD' : v), check: (v) => v === 'associates' ? '<button class="hj-ripple icon small primary hj-auth-check"><i class="material-icons">fact_check</i></button>' : '', }; const table = Create('table.hja__group-encodes--table'); const thead = table.appendChild(Create('thead')); const header = thead.appendChild(Create('tr.hja__group-encodes--header')); this.headers(header); const tbody = table.appendChild(Create('tbody')); if (this.json.length === 0) { const row = Create('tr.hja__group-encodes--row'); row.innerHTML = `<td colspan="100" style="text-align: center; padding-top: 3em; padding-bottom: 1em;"><em>No encodes found</em></td>`; tbody.appendChild(row); } this.json.forEach((item, idx) => { const scan = GM_getValue('torrentScan_' + item.torrentid); const scanInfo = !scan ? '' : scan === 'deleted' ? 'deleted' : scan.errors > 0 ? 'error' : scan.warnings > 0 ? 'warning' : scan.advisories > 0 ? 'advisory' : 'pass'; item.scanStatus = scanInfo; const row = Create('tr.hja__group-encodes--row.hja__group-encodes--scan-row'); row.setAttribute('data-scan', scanInfo); row.setAttribute('data-row-id', idx); const fullTitle = `${item.title} [${item.year}]`; const truncatedTitle = truncate({ str: item.title, split: true, maxLength: 50, }); const truncatedFilename = truncate({ str: item.filename, maxLength: 60, split: true, }); let html = ''; html += `<td class="hja__group-encodes--col"`; // If the title has been truncated, use a tooltip to display to original title on hover if (truncatedTitle != item.title) html += ` data-tooltip="${fullTitle}">`; else html += '>'; html += `${truncatedTitle} [${item.year}]</td>`; html += `<td class="hja__group-encodes--col">${new Date(item.uploaded_at).toLocaleString()}</td>`; html += `<td class="hja__group-encodes--col">${item.codec}</td>`; html += `<td class="hja__group-encodes--col">${format.source(item.source)}</td>`; html += `<td class="hja__group-encodes--col">${item.res}</td>`; html += `<td class="hja__group-encodes--col hja__group-encodes--col-filename"`; // If the filename has been truncated, use a tooltip to display the original filename on hover if (truncatedFilename != item.filename) html += ` data-tooltip="${item.filename}">`; else html += '>'; html += `<div>${truncatedFilename}</div></td>`; html += `<td class="hja__group-encodes--col">${item.username}</td>`; html += `<td class="hja__group-encodes--col">${item.rank_full}</td>`; html += `<td class="hja__group-encodes--col col-centre py-0">${format.check(item.rank_short)}</td>`; html += `<td class="hja__group-encodes--col hja__group-encodes--col-scan col-centre py-0"> <button class="hj-ripple icon small primary hj-scan-view"> <i></i> </button> </td>`; row.innerHTML = html; tbody.appendChild(row); }); this.framework.content.innerHTML = ''; this.framework.content.appendChild(table); // Update the pagination data and links in the footer this.updatePagination(); } /** * Refresh pagination data and links in the table footer * @memberof GroupEncodes */ updatePagination() { const { start, end, page, totalPages: total } = this.pagination.json; this.pagination.links.innerHTML = ''; this.pagination.total = Create('div.hja__group-encodes--footer-pagination'); this.pagination.total.innerHTML = `${start} to ${end} of ${this.pagination.json.total}`; const pageButton = () => Create('button.hj-ripple.secondary.icon.light.group-encodes-pagination'); const p = page > 1 ? page - 1 : null; const n = page < total ? page + 1 : null; const first = pageButton(); first.innerHTML = `<i class="material-icons">first_page</i>`; first.setAttribute('data-page', 1); if (page === 1) first.setAttribute('disabled', true); const prev = pageButton(); prev.innerHTML = `<i class="material-icons">keyboard_arrow_left</i>`; if (p) prev.setAttribute('data-page', p); else prev.setAttribute('disabled', true); const next = pageButton(); next.innerHTML = `<i class="material-icons">keyboard_arrow_right</i>`; if (n) next.setAttribute('data-page', n); else next.setAttribute('disabled', true); const last = pageButton(); last.innerHTML = `<i class="material-icons">last_page</i>`; last.setAttribute('data-page', total); if (page === total) last.setAttribute('disabled', true); [first, prev, this.pagination.total, next, last].forEach((item) => this.pagination.links.appendChild(item)); } /* ************************************************ CSS ************************************************ */ /** * Inject the styles required for this Class * @memberof GroupEncodes */ style() { const css = ` .hja__group-encodes--footer-limit { display: flex; align-items: center; width: 200px; } .select-css { display: inline-block; font-family: "Roboto", sans-serif; font-weight: 700; color: #444; line-height: 1.3; padding: .3em 1em .2em .8em; width: 100%; max-width: 100%; /* useful when width is set to anything other than 100% */ box-sizing: border-box; margin: 0; border: 1px solid #aaa; box-shadow: 0 1px 0 1px rgba(0,0,0,.04); border-radius: .5em; -moz-appearance: none; -webkit-appearance: none; appearance: none; background-color: #fff; background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), linear-gradient(to bottom, #ffffff 0%,#e5e5e5 100%); background-repeat: no-repeat, repeat; /* arrow icon position (1em from the right, 50% vertical) , then gradient position*/ background-position: right .7em top 50%, 0 0; /* icon size, then gradient */ background-size: .65em auto, 100%; } /* Hide arrow icon in IE browsers */ .select-css::-ms-expand { display: none; } /* Hover style */ .select-css:hover { border-color: #888; } /* Focus style */ .select-css:focus { border-color: #aaa; /* It'd be nice to use -webkit-focus-ring-color here but it doesn't work on box-shadow */ box-shadow: 0 0 1px 3px rgba(59, 153, 252, .7); box-shadow: 0 0 0 3px -moz-mac-focusring; color: #222; outline: none; } /* Set options to normal weight */ .select-css option { font-weight:normal; } /* Support for rtl text, explicit support for Arabic and Hebrew */ *[dir="rtl"] .select-css, :root:lang(ar) .select-css, :root:lang(iw) .select-css { background-position: left .7em top 50%, 0 0; padding: .6em .8em .5em 1.4em; } /* Disabled styles */ .select-css:disabled, .select-css[aria-disabled=true] { color: graytext; background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22graytext%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), linear-gradient(to bottom, #ffffff 0%,#e5e5e5 100%); } .select-css:disabled:hover, .select-css[aria-disabled=true] { border-color: #aaa; } .speed-dial-container button.icon { border-radius: 50%; border: transparent; height: 50px; width: 50px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.15s ease-in; } .speed-dial-container .primary, .hj-close-info.primary { background-color: #1091ec !important; color: white !important; } .warning { transition: background-color 150ms ease-in; background-color: #E65100 !important; color: white !important; } .warning:hover { background-color: #B71C1C !important; } .speed-dial-container .primary:hover, .hj-close-info.primary:hover { background-color: #0D47A1 !important; } .speed-dial-container .accent { background-color: #C2185B !important; color: white !important; } .speed-dial-container .accent:hover { background-color: #880E4F !important; } .speed-dial-container .speed-dial-activator i.open { display: block; } .speed-dial-container:hover .speed-dial-activator i.open { display: none; } .speed-dial-container .speed-dial-activator i.close { display: none; } .speed-dial-container:hover .speed-dial-activator i.close { display: block; } .hj-close { margin-right: 5px; margin-top: 5px; } .hja__recent-encodes--app-actions { padding-left: 10px; display: flex; min-height: 100%; align-items: center; } .hja__recent-encodes--app-actions button { margin-right: 10px; } .speed-dial-container { display: inline-block; position: relative; z-index: 1; } .speed-dial-container > .speed-dial-menu-items { z-index: -1; transform: scale(0) translateY(-25px); transform-origin: top; transition: transform 150ms ease-in; } .speed-dial-container:hover > .speed-dial-menu-items { transform: scale(1); } .speed-dial-menu-items { position: absolute; width: 100%; display: flex; flex-flow: column; align-items: center; } .speed-dial-menu-items button.icon { width: 35px !important; height: 35px !important; margin-top: 5px; position: relative; overflow: visible !important; transition: filter 150ms ease-in; } .speed-dial-menu-items button.icon:hover { filter: drop-shadow(2px 2px 5px #777); } .speed-dial-menu-items button::after { color: rgba(255,255,255,0.9) !important; content: attr(data-label); position: absolute; left: 100%; transform: scaleX(0); transition: transform 150ms ease-in; transform-origin: left; text-transform: none; background: #880E4F; padding: 2px 16px; border-radius: 0 4px 4px 0; z-index: -1; margin-left: -8px; white-space: nowrap; } .speed-dial-menu-items button:hover::after { transform: scaleX(1); } .speed-dial-menu-items .icon i { font-size: 18px; } #group-encodes { overflow-x: hidden; box-sizing: border-box; font-family: "Roboto", sans-serif; color: rgba(0,0,0,0.6); font-size: 14px; position: fixed; width: 100%; height: 100%; background: white; top: 0; left: 0; z-index: 9999; transform-origin: center; transition: all 300ms ease-in-out; } #group-encodes.show { transform: scale(1) !important; opacity: 1 !important; } .hja__group-encodes--scan-results.hj-card { padding-top: 32px; } .flex-col { display: flex; flex-flow: column; width: 100%; } .hja__group-encodes--footer-filter { display: flex; align-items: center; } .hja__group-encodes--footer-filter-input-container { display: flex; align-items: center; border: 1px solid rgba(0,0,0,0.1); border-radius: 4px; height: 30px; transition: background-color 300ms ease-in; } .hja__group-encodes--footer-filter-input-container:focus-within { background-color: rgba(16, 145, 236, 0.1); } .hja__group-encodes--footer-filter-input-container input { border: 0; padding: 2px 6px; background-color: transparent; width: 250px; } .hja__group-encodes--footer-filter-input-container input:focus { outline: 0; } #forum-post-viewer { margin-right: 2rem; padding-top: 2rem; } #forum-post-viewer .mediainfo > tbody > tr, #forum-post-viewer .mediainfo__section > tbody > tr { background-color: #E0F2F1; } #forum-post-viewer a { color: #009688; transition: color 50ms ease-in; } #forum-post-viewer blockquote { border: 1px solid #B2DFDB; border-radius: 4px; background-color: #E0F2F1; -webkit-box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); } #forum-post-viewer a:hover { color: #004D40; } .hja__scrollbar::-webkit-scrollbar { width: 11px; } .hja__scrollbar { scrollbar-width: thin; scrollbar-color: #009688 #B2DFDB; } .hja__scrollbar::-webkit-scrollbar-track { background: #B2DFDB; } .hja__scrollbar::-webkit-scrollbar-thumb { background-color: #009688; border-radius: 6px; border: 3px solid #B2DFDB; } .hja__group-encodes--forum-post-container { display: flex; flex-direction: column; width: 100%; max-height: 700px; overflow-x: auto; border-left: 1px solid #009688; } .hja__group-encodes--forum-post-header { padding: 8px 16px; background-color: #009688; color: white; min-height: 30px; position: relative; } .hja__group-encodes--forum-post-header button { color: white !important; position: absolute !important; right: 10px; top: 50%; transform: translateY(-50%); } .hja__group-encodes--forum-post-author-and-body { display: flex; width: 100%; min-height: 250px; } .hja__group-encodes--forum-post-avatar { min-width: 150px; max-width: 150px; position: absolute; } .hja__group-encodes--forum-post-avatar img { max-width: 100%; } .hja__group-encodes--forum-post-body { padding: 8px 16px; margin-left: 150px; } .input-prepend { width: 30px; height: 100%; display: flex; align-items: center; justify-content: center; background-color: rgba(0,0,0,0.1); color: black; border-radius: 4px 0 0 4px; } .hja__group-encodes--footer-filter-input-container::after { content: ""; position: absolute; border: 4px solid rgba(0,0,0,0.2); border-top: 4px solid #1091ec; /* Blue */ border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; opacity: 0; transition: opacity 150ms ease-in; left: 4px; } .hja__group-encodes--footer-filter-input-container.loading::after { opacity: 1; } .hja__group-encodes--footer-filter-input-container.loading i.material-icons { display: none; } .hja__group-encodes--scan-results section { display: flex; flex-flow: row; justify-content: space-between; padding: 2rem; } .hja__group-encodes--miscan-table td { padding: 4px 8px; } .hja__group-encodes--miscan-table tr:nth-child(odd) { background-color: #F5F5F5; } .hja__group-encodes--miscan-table { width: 100%; } .hja__group-encodes--scan-results section figure img.cover { max-height: 400px; } .hja__group-encodes--scan-results section figure { margin: 0; } .hja__group-encodes--scan-results article { width: 100%; } td.autofit { width: 1px; white-space: nowrap; padding-right: 2rem; } .hja__group-encodes--scan-row.complete { animation-name: row-flash; animation-iteration-count: 1; animation-duration: 0.5s; } @keyframes row-flash { 50% { background-color: #1091ec; color: white; } } [data-scan="deleted"].hja__group-encodes--scan-row, [data-scan="pass"].hja__group-encodes--scan-row, [data-scan="advisory"].hja__group-encodes--scan-row, [data-scan="warning"].hja__group-encodes--scan-row, [data-scan="error"].hja__group-encodes--scan-row { cursor: default !important; } [data-scan=""].hja__group-encodes--scan-row:hover, .hja__group-encodes--auth-table tbody tr:hover { background-color: #FFEBEE !important; } .hja__group-encodes--auth-table tbody tr { transition: background-color 30ms ease-in; cursor: pointer; } [data-scan="deleted"] .hja__group-encodes--col-filename { color: #F44336; position: relative; } [data-scan="deleted"] .hja__group-encodes--col-filename::before { position: absolute; top: 0; left: 0; right: 0; bottom: 0; content: "This torrent has been deleted"; padding: 4px 8px; } [data-scan="deleted"] .hja__group-encodes--col-filename > div { display: none; } [data-scan=""] .hja__group-encodes--col-scan button { display: none !important; } .hja__group-encodes--col-scan button { display: flex; align-items: center; justify-content: center; } .hja__group-encodes--col-scan button i::before { font-family: "Material Icons"; text-transform: none; font-weight: normal; font-style: normal; -moz-font-feature-settings: 'liga'; font-feature-settings: 'liga'; transition: color .15s ease-in-out; } #scanner-display { transition: opacity 100ms ease-in-out; } [data-scan="pass"] .hja__group-encodes--col-scan button, [data-scan="deleted"] .hja__group-encodes--col-scan button { pointer-events: none; } [data-scan="pass"] .hja__group-encodes--col-scan button i::before { content: "\\e5ca"; /* check */ color: #388E3C; } [data-scan="advisory"] .hja__group-encodes--col-scan button i::before { content: "\\e925"; /* pan_tool */ color: #1091ec; } [data-scan="warning"] .hja__group-encodes--col-scan button i::before { content: "\\e645"; /* priority_high */ color: #FF9800; } [data-scan="error"] .hja__group-encodes--col-scan button i::before { content: "\\e14b"; /* block */ color: #F44336; } [data-scan="deleted"] .hja__group-encodes--col-scan button i::before { content: "\\e872"; /* delete */ color: #F44336; } .hja__group-encodes--scan-row { transition: background-color 50ms ease-out; cursor: pointer; } .hja__group-encodes--auth-table { width: 100%; } .hja__group-encodes--auth-table thead { background-color: white; color: #1091ec; } footer.hja__group-encodes--footer { width: 100%; display: flex; flex-flow: row; justify-content: space-between; align-items: center; padding: 1rem 2rem; } .hja__group-encodes--info--auth-container, .hja__group-encodes--info--scan-container, .hja__group-encodes--loader-container, .hja__group-encodes--login-container { padding: 2rem; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; } .hja__group-encodes--login-container { min-height: 50vh; display: flex; flex-flow: column; align-items: center; } .hja__group-encodes--login-container button { margin-top: 2em; } .hja__group-encodes--info--scan-container { max-width: 1300px; margin: 0 auto; } .group-encodes-pagination:disabled { color: rgba(0,0,0,0.2) !important; } .loader { border: 16px solid #f3f3f3; border-radius: 50%; border-top: 16px solid #1091ec; border-bottom: 16px solid #1091ec; width: 120px; height: 120px; -webkit-animation: spin 2s linear infinite; animation: spin 2s linear infinite; opacity: 0; pointer-events: none; transition: opacity 150ms ease-in; margin: 100px 0; } .loader.show { opacity: 1; pointer-events: all; } @keyframes loader-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .hja__group-encodes--info { width: 100%; max-height: 0; overflow: hidden; transition: max-height 300ms ease-in-out; } .group-encodes--header-sort { cursor: pointer; transition: background-color 150ms ease-in; } .group-encodes--header-sort:hover { background-color: rgba(255,255,255,0.3); } .group-encodes--header-sort > div { pointer-events: none; display: flex; align-items: center; } group-encodes--header-sort i { margin-left: 8px; } .hja__group-encodes--info.show { max-height: 1500px; } .table-actions { text-align: center; } tbody .table-actions { padding: 0 2rem; } .hja__group-encodes--status-container { flex: 1; display: flex; justify-content: center; align-items: center; } .group-encodes-pagination { border-radius: 2px !important; } .hja__group-encodes--footer-pagination { margin: 0 1em; } .progress-outer { margin-left: -100px; width: 1000px; border-radius: 4px; min-height: 30px; background-color: #E0F2F1; position: relative; overflow: hidden; } .progress-outer .controls { position: absolute; right: 40px; top: 50%; transform: translateY(-50%); line-height: 0; cursor: pointer; transition: opacity 300ms ease-in; } .progress-outer.active .controls-start { opacity: 0; pointer-events: none; } .progress-outer.active .controls-stop { opacity: 1; } .progress-outer:not(.active) .controls-start { opacity: 1; } .progress-outer:not(.active) .controls-stop { opacity: 0; pointer-events: none; } .progress-inner { height: 30px; } .progress-inner.active { transition: width 1000ms linear; background-image: linear-gradient(to right, #FFCC80 0%, #FFCC80 99%, transparent); width: 0%; } .progress-inner.indeterminate { width: 25%; background-image: linear-gradient(to right, rgba(255,255,255,0), #FFCC80, rgba(255,255,255,0)); animation-name: progress-indeterminate; animation-duration: 1.7s; animation-iteration-count: infinite; } @keyframes progress-indeterminate { from { margin-left: -25%; } to { margin-left: 100%; } } [data-tooltip] { position: relative; } [data-tooltip]::after { content: attr(data-tooltip); position: absolute; top: 0; left: 5px; background-color: #1091ec; color: white; padding: 4px 8px; opacity: 0; transition: opacity 150ms ease-in; transition-delay: 0.5s; border-radius: 4px; white-space: nowrap; box-shadow: 0 3px 5px -1px rgba(0,0,0,.2),0 5px 8px 0 rgba(0,0,0,.14),0 1px 14px 0 rgba(0,0,0,.12) !important; } [data-tooltip]:hover::after { opacity: 1; } .progress-label { color: rgba(0,0,0,0.6); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); white-space: nowrap; } .ripple { position: absolute; background: #fff; border-radius: 50%; width: 5px; height: 5px; animation: rippleEffect ease-in 0.4s 1; opacity: 0; z-index: 9; } .hja__group-encodes--table { width: 100%; } tbody .hja__group-encodes--row:nth-child(even) { background-color: #F5F5F5; } .hja__group-encodes--status { display: flex; justify-content: space-between; flex: 1; min-height: 50px; } .hja__group-encodes--header { background-color: #009688; color: rgba(255,255,255,0.9); } thead .hja__group-encodes--col { text-transform: uppercase; white-space: nowrap; } .hja__group-encodes--col { padding: 4px 8px; } .hja__group-encodes--col.col-centre { text-align: center; } tbody .hja__group-encodes--col { font-size: 0.7rem; } .py-0 { padding-top: 0 !important; padding-bottom: 0 !important; } #group-encodes button { position: relative; overflow: hidden; text-transform: uppercase; display: inline-flex; font-weight: 400; color: #fff; justify-content: center; align-items: center; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-color: transparent; border: 1px solid transparent; border-top-color: transparent; border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; padding: .375rem .75rem; border-radius: .25rem; transition: color .15s ease-in-out, background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; cursor: pointer; } #group-encodes button:disabled { pointer-events: none; } #group-encodes button i, div.hj-ripple i, i.material-icons { pointer-events: none; } #group-encodes button.primary { background-color: #1091ec; color: #fff; } #group-encodes button:hover.primary { background-color: #1976D2; color: #fff; } #group-encodes button:disabled.primary { background-color: #CFD8DC; color: black; } #group-encodes button:not(.icon):hover { color: #fff; background-color: #0069d9; border-color: #0062cc; } #group-encodes button.icon { background-color: transparent; border-color: transparent; border-radius: 50%; color: rgba(0,0,0,0.7); padding: 0; width: 40px; height: 40px; } #group-encodes button.icon.small { width: 22px; height: 22px; } #group-encodes button.icon.small i { font-size: 16px; } #group-encodes button.icon.error:hover { background-color: rgba(255, 82, 82, 0.4); color: #fff; } #group-encodes button.icon.primary:hover { background-color: rgba(25, 118, 210,0.4); color: #fff; } #group-encodes button.icon.primary:hover i::before { color: #fff !important; } #group-encodes button.icon.secondary:hover { background-color: rgba(255, 255, 255,0.2); color: #fff; } #group-encodes button.icon.secondary.light:hover { background-color: rgba(0, 0, 0, 0.1); color: rgba(0,0,0,0.7); } @keyframes rippleEffect { from { transform: scale(1); opacity: 0.4; } to { transform: scale(100); opacity: 0; } } .hja__group-encodes--auth-results, .hja__group-encodes--scan-results { width: 100%; display: flex; flex-flow: column; } .hja__group-encodes--scan-results { margin-top: 1em; } .hja__group-encodes--auth-results > header, .hja__group-encodes--scan-results > header { height: 64px; width: 100%; background-color: #1091ec; color: white; display: flex; justify-content: center; align-items: center; } .hja__group-encodes--scan-results > header { background-color: #673AB7 !important; } .hja__group-encodes--footer-pagination-links { display: flex; flex-flow: row; align-items: center; justify-content: flex-end; } /* Style the tab */ .tab-container { width: 100%; display: flex; flex-flow: row; } .tab-controls { padding: 1rem 2rem; } .tablinks { color: black; position: relative; overflow: hidden; padding: 1rem 2rem; display: flex; flex-flow: column; justify-content: flex-start; align-items: center; cursor: pointer; transition: all 150ms ease-in; border-radius: 4px; } .tablinks div { pointer-events: none; } .tablinks i { margin-bottom: 1rem; } .tablinks:hover { background-color: rgba(0,0,0,0.1); } .tablinks.active { background-color: #1091ec; color: white; } .tab-controls { display: flex; flex-flow: column; } .tab-content { flex: 1; } .tab-content-item:not(.active) { display: none; } .hj-card { display: block; max-width: 100%; outline: none; text-decoration: none; -webkit-transition-property: opacity,-webkit-box-shadow; transition-property: opacity,-webkit-box-shadow; transition-property: box-shadow,opacity; transition-property: box-shadow,opacity,-webkit-box-shadow; overflow-wrap: break-word; position: relative; white-space: normal; -webkit-transition: -webkit-box-shadow .28s cubic-bezier(.4,0,.2,1); transition: -webkit-box-shadow .28s cubic-bezier(.4,0,.2,1); transition: box-shadow .28s cubic-bezier(.4,0,.2,1); transition: box-shadow .28s cubic-bezier(.4,0,.2,1),-webkit-box-shadow .28s cubic-bezier(.4,0,.2,1); will-change: box-shadow; -webkit-box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); border-radius: 4px; position: relative; padding-top: 64px; padding-bottom: 16px; } .hj-card > header { width: 90%; margin-left: 5%; position: absolute; top: -32px; border-radius: 4px; -webkit-transition: -webkit-box-shadow .28s cubic-bezier(.4,0,.2,1); transition: -webkit-box-shadow .28s cubic-bezier(.4,0,.2,1); transition: box-shadow .28s cubic-bezier(.4,0,.2,1); transition: box-shadow .28s cubic-bezier(.4,0,.2,1),-webkit-box-shadow .28s cubic-bezier(.4,0,.2,1); will-change: box-shadow; -webkit-box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); } .hja__group-encodes--miscan-results--header { padding: 8px; background-color: #1091ec; color: white; padding-left: 2em; } .hja__group-encodes--miscan-results--header:first-child { border-radius: 4px 4px 0 0; } .hja__group-encodes--miscan-results--section-heading { margin: 1em 2em; display: flex; align-items: center; padding: 0.5em 1em; } .hja__group-encodes--miscan-results--section-heading.errors { background-color: #C62828; color: white; } .hja__group-encodes--miscan-results--section-heading.warnings { background-color: #EF6C00; color: white; } .hja__group-encodes--miscan-results--section-heading.advisories { background-color: #607D8B; color: white; } .hja__group-encodes--miscan-results--section-issue { position: relative; padding: 0.5em 1em; margin: 1em 2em; } .hja__group-encodes--miscan-results--section-issue::after { font-family: "Material Icons"; font-size: 24px; text-transform: none; position: absolute; top: 50%; right: 10px; transform: translateY(-50%); } .hja__group-encodes--miscan-results--section-issue.warnings::after { content: "warning"; color: #EF6C00; } .hja__group-encodes--miscan-results--section-issue.advisories::after { content: "pan_tool"; color: #607D8B; } .hja__group-encodes--miscan-results--section-issue.errors::after { content: "error_outline"; color: #C62828; } #scan-results { margin-top: 1em; border: 1px solid rgba(0,0,0,0.1); border-radius: 4px; } `; const style = document.createElement('style'); const roboto = document.createElement('link'); const icons = Create('link'); roboto.rel = 'stylesheet'; icons.rel = 'stylesheet'; roboto.href = 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,500;0,700;1,500&display=swap'; icons.href = 'https://fonts.googleapis.com/icon?family=Material+Icons'; style.innerHTML = css; document.head.appendChild(style); document.head.appendChild(roboto); document.head.appendChild(icons); } } function checkRecentUploads() { groupEncodes.show(); } class MessageCentre { constructor() { this.skip = 0; this.script = 'admin'; this.issueCodes = []; } branchOffice(branch, issue, data) { var feedback; var code = issue + '@' + branch; this.issueCodes.push(code); switch (branch) { case 'filename': feedback = this.filenameBranch(issue, data); break; case 'metatitle': feedback = this.metatitleBranch(issue, data); break; case 'x264': feedback = this.x264Branch(issue, data); break; case 'video': feedback = this.videoBranch(issue, data); break; case 'audio': feedback = this.audioBranch(issue, data); break; case 'subtitles': feedback = this.subtitlesBranch(issue, data); break; case 'chapters': feedback = this.chaptersBranch(issue, data); break; case 'misc': feedback = this.miscBranch(issue, data); break; default: feedback = { error: `Couldn't find branch for "${branch}"` }; } feedback.code = code; return feedback; } // use ##source##, ##res## or ##speclink## in link.section for replacement filenameBranch(issue, data) { var severity, message, link, branch = 'filename'; switch (issue) { case 'none': severity = 'errors'; message = 'No filename found'; break; case 'errorParsing': severity = 'errors'; message = 'There was an error processing the filename'; break; case 'extensionAbsent': severity = 'errors'; message = "Couldn't find an '.mkv' filename extension"; break; case 'extensionDupe': severity = 'errors'; message = 'Filename extension was duplicated'; break; case 'groupTagAbsent': severity = 'errors'; message = "Couldn't find properly formatted group tag (.x264-HANDJOB)"; break; case 'groupTagIncorrect': severity = 'errors'; message = "Group tag wasn't formatted properly as '.x264-HANDJOB'"; break; case 'invalidCharacters': severity = 'errors'; message = `Invalid characters detected in filename. Please advise to fix the following: <div class="filename-error">${data}</div>`; break; case 'noRes': severity = 'errors'; message = `No valid resolution tag found. Should include ${data}`; break; case 'noYear': severity = 'errors'; message = 'No year tag was found'; break; case 'reservedFilename': severity = 'errors'; message = `Some operating systems won't allow filenames beginning with '${capFirst( data )}'. Advise an alternative`; break; case 'shutdown': severity = 'errors'; message = 'Multiple errors found in filename'; break; case 'wrongEnding': severity = 'errors'; message = "Filename should always end '.x264-HANDJOB.mkv'"; break; // Warnings case 'codecMismatch': severity = 'warnings'; message = `Codec in filename (${data.filenameCodec}) doesn't match actual audio codec (${data.codec})`; break; case 'noP': severity = 'warnings'; message = "Missing 'p' from resolution"; break; case 'resDVD': severity = 'warnings'; message = 'Included resolution tag in a DVDRip'; break; case 'resFormat': severity = 'warnings'; message = `Improperly formatted resolution tag. "${data.match}" should be written as "${data.res}"`; break; case 'resMismatch': severity = 'warnings'; message = `Resolution tag in the filename (${data.f}) doesn't match detected resolution (${data.d})`; break; case 'resolutionAfterSource': severity = 'warnings'; message = `The resolution tag should come before the source tag`; break; case 'SDTagHDSource': severity = 'warnings'; message = `Encode appears to be from a Blu-Ray source, but has ${data} tag in filename`; break; case 'yearAfterResolution': severity = 'warnings'; message = 'The year tag should come before the resolution one'; break; case 'yearBeforeDVDRip': severity = 'warnings'; message = 'The year tag should come before DVDRip'; break; case 'yearFormatting': severity = 'warnings'; message = "The year isn't formatted properly"; break; // Advisories case 'ac3Included': severity = 'advisories'; message = 'Audio codec should only be included for non-AC3 formats'; break; case 'akaFormatting': severity = 'advisories'; message = 'AKA should be written in uppercase'; break; case 'akaSpelling': severity = 'advisories'; message = 'Potential typo in AKA'; break; case 'blurayFormatting': severity = 'advisories'; message = `BluRay source tag has been incorrectly written ${data}`; break; case 'codecNotIncluded': severity = 'advisories'; message = `Audio codec not present in filename, i.e. '.${data}.x264-HANDJOB.mkv'`; break; case 'extensionCase': severity = 'advisories'; message = "Filename extension should be written in lowercase as '.mkv'"; break; } return { branch, severity, message, link }; } metatitleBranch(issue, data) { var severity, message, link, branch = 'metatitle'; switch (issue) { case 'none': severity = 'errors'; message = 'No metatitle found'; break; case 'filenameMetatitle': severity = 'errors'; message = 'Filename used as metatitle'; break; case 'filenameSyntax': severity = 'errors'; message = 'Filename syntax used'; break; case 'noResolutionTag': severity = 'errors'; message = 'No resolution tag was found'; break; case 'noSourceTag': severity = 'errors'; message = `No source tag (${data}) found`; break; case 'noYear': severity = 'errors'; message = 'No year detected'; break; case 'shutdown': severity = 'errors'; message = 'Multiple errors found in metatitle'; break; // Warnings case 'endHJ': severity = 'warnings'; message = "No 'HJ' group tag detected"; break; case 'generalError': severity = 'warnings'; message = 'Error detected in metatitle formatting'; break; case 'HJFormatting': severity = 'warnings'; message = "' - HJ' tag is formatted incorrectly"; break; case 'noP': severity = 'warnings'; message = "Missing 'p' from resolution"; break; case 'resDVD': severity = 'warnings'; message = `Included a resolution tag (${data}) in a DVDRip`; break; case 'resFormat': severity = 'warnings'; message = `Improperly formatted resolution tag. "${data.match}" should be written as "${data.res}"`; break; case 'resMismatch': severity = 'warnings'; message = `Resolution tag in the metatitle (${data.m}) doesn't match detected resolution (${data.d})`; break; case 'SDTagHDSource': severity = 'warnings'; message = `Encode appears to be from a Blu-Ray source, but has ${data} tag in filename`; break; case 'sourceSpelling': severity = 'warnings'; message = "Source tag is mispelled. Should be 'BluRay'"; break; case 'usedHANDJOB': severity = 'warnings'; message = 'HJ should be used as the group tag'; break; // Advisories case 'afterYearFormatting': severity = 'advisories'; message = `Contains invalid characters after the year tag`; break; case 'audioCodecPresent': severity = 'advisories'; message = "Audio codec shouldn't be included in metatitle"; break; case 'noSquareBrackets': severity = 'advisories'; message = 'Year should be placed inside square brackets'; break; case 'sourceResOrder': severity = 'advisories'; message = 'Source and resolution tags are in the wrong order'; break; case 'x264Found': severity = 'advisories'; message = "x264 shouldn't be included in metatitle"; break; case 'yearMismatch': severity = 'advisories'; message = "Year in metatitle doesn't match the year in filename"; break; } return { branch, severity, message, link }; } x264Branch(issue, data) { var severity, message, link, branch = 'x264'; switch (issue) { case '2passUsed': severity = 'errors'; message = 'CRF must be used for rate control, not 2-pass'; break; case 'codecNotX264': severity = 'errors'; message = 'Codec is not x264'; break; case 'dxvaIncompatible': severity = 'errors'; message = `Encode is not DXVA compatible (${data.join(' / ')})`; break; case 'minimumNotMet': severity = 'errors'; message = `Minimum settings were not met (${data.join(' / ')})`; break; case 'qcompVeryLow': severity = 'errors'; message = 'Q-Comp setting is too low'; break; case 'qcompVeryHigh': severity = 'errors'; message = 'Q-Comp setting is too high'; break; // Warnings case 'aqUnusual': severity = 'warnings'; message = `Aq-strength (${data}) outside of recommended range (0.5 - 1.2)`; break; case 'psyOff': severity = 'warnings'; message = 'Psy Rate-Distortion Optimisation is turned off'; break; case 'psyTrellisHigh': severity = 'warnings'; message = `Psy-trellis is set unusually high at ${data[0]}:<strong>${data[1]}</strong>`; break; case 'psyUnusual': severity = 'warnings'; message = `Unusual setting for Psy-RD (<strong>${data[0]}</strong>:${data[1]})`; break; case 'qcompLow': severity = 'warnings'; message = 'Q-Comp should not generally be set below 0.5'; break; case 'qcompHigh': severity = 'warnings'; message = 'Q-Comp should not generally be set above 0.8'; break; // Advisories case 'mbtreeOn': severity = 'advisories'; message = 'MB-Tree is on. Not advised except for digital animation'; break; } return { branch, severity, message, link }; } videoBranch(issue, data) { var severity, message, link, branch = 'video'; switch (issue) { case 'noVideoSection': severity = 'errors'; message = 'No video section found in log'; break; case '2160pEncode': severity = 'errors'; message = '2160p encodes are not allowed'; break; case 'bitrateUnderSeverely': severity = 'errors'; message = `Video bitrate (${data.bitrate} Kbps) is well below the target range of ${data.text} for this resolution`; break; case 'bitrateOverSeverely': severity = 'errors'; message = `Video bitrate (${data.bitrate} Kbps) is well above the target range of ${data.text} for this resolution`; break; case 'blurayAnamorphic': severity = 'errors'; message = 'Blu-Ray encodes must not use anamorphic display'; break; case 'crfVeryLow': severity = 'warnings'; message = 'CRF is very low and encode is likely bloated'; break; case 'crfVeryHigh': severity = 'warnings'; message = 'CRF is very high. Extreme detail loss is likely'; break; case 'dvdNonAnamorphic': severity = 'errors'; message = 'DVDRip encodes must use anamorphic display'; break; case 'resizedDVDRip': severity = 'errors'; message = `This ${data} DVDRip appears to have been resized`; break; case 'resProgressive': severity = 'errors'; message = 'Encode does not use progressive scan'; break; case 'resWidthOver': severity = 'errors'; message = `Width (${this.data.width}px) is over the maximum for this resolution (${data.width} x ${data.height})`; break; case 'resHeightOver': severity = 'errors'; message = `Height (${this.data.height}px) is over the maximum for this resolution (${data.width} x ${data.height})`; break; case 'resNeitherEqual': severity = 'errors'; message = `Neither width (${this.data.width}px) nor height (${this.data.height}px) equal the maximum for this resolution (${data.width} x ${data.height})`; break; case 'tvAnamorphic': severity = 'errors'; message = 'TVRip / VHSRip encodes must not use anamorphic display'; break; case 'variableFramerate': severity = 'errors'; message = 'Variable frame-rate has been used'; break; // Warnings case 'bitrateMissing': severity = 'warnings'; message = 'Video bitrate missing from log'; break; case 'bitrateUnder': severity = 'warnings'; message = `Video bitrate (${data.bitrate} Kbps) is below the target range of ${data.text} for this resolution`; break; case 'bitrateOver': severity = 'warnings'; message = `Video bitrate (${data.bitrate} Kbps) is above the target range of ${data.text} for this resolution`; break; case 'crfLow': severity = 'warnings'; message = 'CRF is set below 14. This is usually pointless'; break; case 'crfHigh': severity = 'warnings'; message = 'CRF is set above 23. Advise settings for dealing with grain if necessary'; break; // Advisories case 'bitrateUnderSlightly': severity = 'advisories'; message = `Video bitrate (${data.bitrate} Kbps) is slightly below the target range of ${data.text} for this resolution`; break; case 'bitrateOverSlightly': severity = 'advisories'; message = `Video bitrate (${data.bitrate} Kbps) is slightly above the target range of ${data.text} for this resolution`; break; case 'dupeFrames': severity = 'advisories'; message = 'Encode is 29.970 fps, potential dupe frames'; break; case 'fpsUnusual': severity = 'advisories'; message = `Encode's frame-rate is ${this.data.fps} fps. Is this correct?`; break; } return { branch, severity, message, link }; } audioBranch(issue, data) { var severity, message, link, branch = 'audio'; switch (issue) { case 'audioLengthMismatch': severity = 'errors'; // duration and label message = `${data.label} length (${data.duration} mins) doesn't seem to match video's (${this.data.duration} mins)`; break; case 'bitDepthHigh': severity = 'errors'; message = `${data.text} bit depth is ${data.bits}. Is lossless audio available for bit depth reduction?`; break; case 'dtsHDMA': severity = 'errors'; message = `${data}: DTS-HD MA audio. Should be converted to FLAC or encoded as AC3/AAC`; break; case 'dupeEnglish': severity = 'errors'; message = `Detected ${data} English main audio tracks. Is there redundant audio?`; break; case 'lpcmAudio': severity = 'errors'; message = `${data}: (L)PCM audio must be converted to FLAC or encoded as AC3/AAC`; break; case 'missingBitrate': severity = 'errors'; message = `The bitrate for ${data} is missing`; break; case 'noAudio': severity = 'errors'; message = 'No main audio track present'; break; case 'noAudioSection': severity = 'errors'; message = 'No audio section found in log'; break; case 'sampleRateHigh': severity = 'errors'; message = `${data.text} sample rate is ${data.sample} kHz. Is lossless audio available for downsampling?`; break; // Warnings case 'aacCommentary': severity = 'warnings'; message = 'AAC should be used for ' + data; break; case 'aacDVD': severity = 'warnings'; message = 'AAC used for DVDRip. Potentially transcoded audio'; break; case 'aacSurround': severity = 'warnings'; message = data + ' is encoded with AAC, this should only be used for mono and stereo audio tracks'; break; case 'ac3Bitrate': severity = 'warnings'; message = `${data.text} is encoded at ${data.actual} Kbps. Should be ${data.spec} Kbps if lossless source available`; break; case 'defaultCommentary': severity = 'warnings'; message = capFirst(data) + ' should not be set as default'; break; case 'engDefault': severity = 'warnings'; message = 'Original language audio should be default in dual audio encodes'; break; case 'genericCommentary': severity = 'warnings'; message = 'Commentary tracks generally need to be encoded as AAC @ 96 Kbps or less'; break; case 'highCommentary': severity = 'warnings'; message = 'If using CBR, bitrate should generally be 96 Kbps or lower for ' + data; break; case 'multipleAudio': severity = 'warnings'; message = 'More than two main audio tracks detected. Any redundant?'; break; case 'noLanguageTag': severity = 'warnings'; message = `No language tag was found for ${data}`; break; case 'noParticipants': severity = 'warnings'; message = 'Commentary track name should include participants'; break; case 'noTrackName': severity = 'warnings'; message = 'No title detected for ' + data; break; case 'useCommentary': severity = 'warnings'; message = `If applicable, track title '${data}' should start 'Commentary'`; break; // Advisories case 'ac3Stereo': severity = 'advisories'; message = data + ' should be encoded with AAC if the source has lossless audio available'; break; case 'engDubName': severity = 'advisories'; message = "English dubs should ideally be labelled 'English Dub'"; break; case 'mainTitle': severity = 'advisories'; message = `Main audio title (${data}) should be blank`; break; case 'mpegAudio': severity = 'advisories'; message = data + ' seems to use MPEG audio. Any others available?'; break; case 'titleBlank': severity = 'advisories'; message = `Track name for ${data} should be blank`; break; case 'usedAudioCommentary': severity = 'advisories'; message = `Used 'Audio Commentary' rather than 'Commentary' for track title`; break; } return { branch, severity, message, link }; } subtitlesBranch(issue, data) { var severity, message, link, branch = 'subtitles'; switch (issue) { // Warnings case 'defaultYes': severity = 'warnings'; message = `Track '${data.title || data.lang || 'ID: ' + data.id}' is set to 'Default: Yes'`; break; case 'engDefault': severity = 'warnings'; message = 'English subtitles should be set to default for non-English films'; break; case 'forcedFlagNotDefault': severity = 'warnings'; message = `Subtitle track '${ data.title || data.lang || 'ID: ' + data.id }' seems to be for non-English scenes, which should generally be set as 'Default: Yes'`; break; case 'forcedYes': severity = 'warnings'; message = `Subtitle track '${data.title || data.lang || 'ID: ' + data.id}' is set to forced`; break; case 'noDefault': severity = 'warnings'; message = 'English subtitles not set as default for dual audio encode'; break; case 'noLanguageTag': severity = 'warnings'; message = `Track '${data.title || 'ID: ' + data.id}' has no language tag`; break; case 'nonEngDefault': severity = 'warnings'; message = `Track ${data} set as default`; break; // Advisories case 'nameForced': severity = 'advisories'; message = `Track title 'English [non-English scenes]' is prefered over '${data}'`; break; case 'noEngSubs': severity = 'advisories'; message = 'No English subtitles found. Any available?'; break; case 'noLinguisticLabelling': severity = 'advisories'; message = 'For films whose audio has no linguistic content, default subtitles are best labelled with [On-Screen Text]'; break; } return { branch, severity, message, link }; } chaptersBranch(issue, data) { var severity, message, link, branch = 'chapters'; switch (issue) { case 'chapterTimecodes': severity = 'advisories'; message = `Chapter titles ${data} seem to be timecodes, advise to use generics or named ones`; break; case 'noTitles': severity = 'advisories'; message = `Chapter titles ${data} seem to be missing, advise to use generics or named ones`; break; case 'missingTitles': severity = 'advisories'; message = `Some chapter titles are missing`; break; } return { branch, severity, message, link }; } miscBranch(issue, data) { let severity, message, link, branch = 'misc'; switch (issue) { case 'attachments': severity = 'errors'; message = `Attachments detected in this .mkv file. These should be removed before uploading`; break; } return { branch, severity, message, link }; } } // Last updated 2020-04-27 class MediainfoScanner extends MessageCentre { constructor(log) { super(); this.log = log; this.feedback = { filename: { errors: [], warnings: [], advisories: [] }, metatitle: { errors: [], warnings: [], advisories: [] }, video: { errors: [], warnings: [], advisories: [] }, x264: { errors: [], warnings: [], advisories: [] }, audio: { errors: [], warnings: [], advisories: [] }, subtitles: { errors: [], warnings: [], advisories: [] }, chapters: { errors: [], warnings: [], advisories: [] }, misc: { errors: [], warnings: [], advisories: [] }, guides: [], }; this.exceptions = []; this.breakdown(); let valid = this.valid(); if (!valid) return; this.filename(); this.resolution(); this.metatitle(); this.checkDxva(); this.checkMinimum(); this.x264(); this.checkAnamorphic(); this.checkRes(); this.checkFramerate(); this.checkBitrate(); this.sortAudioTracks(); this.checkMainAudio(); this.checkSecondaryAudio(); this.checkChapters(); this.condenseErrors(); } valid() { if (!this.data) return false; else if (!this.data.filename) return false; else if (!this.data.width) return false; else if (!this.data.height) return false; else return true; } getFeedback() { return this.feedback; } generateLink(link) { // Insertion of contextual data into link for (let key in link) { link[key] = link[key].replace(/##source##/g, this.data.source); link[key] = link[key].replace(/##res##/g, this.data.res); link[key] = link[key].replace(/##speclink##/g, this.data.speclink); link[key] = link[key].replace(/##spectext##/g, this.data.spectext); } if (!link.subsection) link.subsection = ''; return ` <button type="button" data-guide="${link.section}" data-subsection="${link.subsection}" class="expand-guide"> <div>${link.text}</div> </button>`; } miParse({ match, log = this.log, singleLine = true, crushOutput = false }) { if (singleLine) match = new RegExp('(?:^' + match + '\\s*:\\s*)(.*$)', 'im'); else if (match === 'General') match = /^General[\w\W]*?(?:\n\n|$)/gi; else if (match === 'Menu') match = /(Menu(\s#\d+)?\n)((\s|\S)*?)(\n\n|$)/gi; else if (match !== 'Video') match = new RegExp('(^' + match + '[\\s\\S]*?^Forced[^\\n]*)', 'gim'); else match = new RegExp('(?:\\n)(' + match + '[\\s\\S]*?)(\\n\\n|$)', 'gi'); let a = log.match(match); try { if (singleLine) a = a[1]; } catch (e) { a = ''; } if (crushOutput && a && singleLine) return a.replace(/\s/g, ''); else return a; } err(branch, issue, data = '') { // messageObject returns object containing: {branch, severity, message, link: {section, subsection, message}} // messageObject.error if problem with retrieving message var output, messageObject, link = ''; if (branch && issue) messageObject = this.branchOffice(branch, issue, data); else console.log('Not Enough data to generate message'); if (this.script === 'member' && messageObject.link) link = this.generateLink(messageObject.link); if (/##button##/.test(messageObject.message)) output = messageObject.message.replace('##button##', link); else output = messageObject.message + link; if (!messageObject.message) console.log(`Error retrieving message for ${issue}@${branch}`); else this.feedback[branch][messageObject.severity].push({ code: messageObject.code, output, data }); } checkSkip() { if (this.skip > 2) return true; else return false; } condenseErrors() { var skip = this.checkSkip(); if (skip) return false; var count = 0; this.issueCodes.map((x) => { // If more than 2 metatitle issues, replace with one error if (x.split('@')[1].search(/metatitle/i) >= 0) count++; }); if (count > 2) { this.feedback.metatitle = this.wipe(); this.err('metatitle', 'shutdown'); } var count = 0; this.issueCodes.map((x) => { // If more than 2 filename issues, replace with one error if (x.split('@')[1].search(/filename/i) >= 0) count++; }); if (count > 2) { this.feedback.filename = this.wipe(); this.err('filename', 'shutdown'); } if (this.data.source === 'dvd') this.filterErrors(['noResolutionTag@metatitle', 'noSourceTag@metatitle']); this.filterErrors(['resMismatch@filename', 'noRes@filename']); this.filterErrors(['multipleAudio@audio', 'dupeEnglish@audio']); this.filterErrors(['noResolutionTag@metatitle', 'noP@metatitle']); this.filterErrors(['resMismatch@metatitle', 'noResolutionTag@metatitle']); this.filterErrors(['aacCommentary@audio', 'highCommentary@audio'], true, ['audio', 'genericCommentary']); } wipe() { return { errors: [], warnings: [], advisories: [] }; } filterErrors(codes, all, cb) { let check = this.checkCombination(codes); if (check && all) this.removeIssue(codes); else if (check) this.removeIssue([codes[0]]); if (check && cb) this.err(cb[0], cb[1]); } checkCombination(issues) { let count = 0; this.issueCodes.map((x) => { for (let issue of issues) { if (x === issue) count++; } }); if (count >= issues.length) return true; else return false; } removeIssue(issues) { let sections = ['errors', 'warnings', 'advisories']; let data = []; for (let i = 0; i < issues.length; i++) { let branch = issues[i].split('@')[1]; for (let x of sections) { this.feedback[branch][x] = this.feedback[branch][x].filter((f) => { if (f.code !== issues[i]) return true; else data.push(f.data); }); } } return data; } arrayCount(array, value) { let reg = new RegExp(value, 'i'); return array.reduce((n, x) => n + (x.search(reg) >= 0), 0); } escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } calculateDuration(string) { let hours = string.match(/(\d{1,})(?:h)/i); hours = hours && hours.length > 0 ? parseInt(hours[1], 10) : 0; let mins = string.match(/(\d{1,})(?:mn)/i); mins = mins && mins.length > 0 ? parseInt(mins[1], 10) : 0; let secs = string.match(/(\d{1,})(?:s)/i); secs = secs && secs.length > 0 ? parseInt(secs[1], 10) : 0; if (secs > 30) mins++; return hours * 60 + mins; } // Run these in the constructor and log with this.err("filename","none",data) breakdown() { var data = {}; data.filename = this.miParse({ match: 'Complete name' }); data.filename = data.filename.replace(/\\/g, '/').split('/').pop(); data.metatitle = this.miParse({ match: 'Movie name' }); data.primaries = this.miParse({ match: 'Color primaries' }).replace(/\W/g, '').toLowerCase(); if (data.primaries.search(/601ntsc/i) >= 0 || /470systemm/i.test(data.primaries) === true) { data.source = 'dvd'; data.region = 'ntsc'; } else if (data.primaries.search(/601pal/i) >= 0 || /systemi/i.test(data.primaries) === true) { data.source = 'dvd'; data.region = 'pal'; } else data.source = 'bluray'; try { let general = this.miParse({ match: 'General', singleLine: false })[0].trim(); data.general = { uniqueID: this.miParse({ match: 'Unique ID', log: general }), format: this.miParse({ match: 'Format', log: general }), application: this.miParse({ match: 'Writing application', log: general }), library: this.miParse({ match: 'Writing library', log: general }), attachments: this.miParse({ match: 'Attachments', log: general }) || '', }; } catch (e) { console.log(e); } try { data.video = this.miParse({ match: 'Video', singleLine: false })[0]; } catch (e) { this.err('video', 'noVideoSection'); this.exceptions.push({ stack: e.stack, message: 'Error finding valid video section' }); return false; } if (data.general && data.general.attachments != '') this.err('misc', 'attachments'); data.bitrate = this.miParse({ match: 'Bit rate', log: data.video, crushOutput: true }); data.bitrate = data.bitrate.search(/mb/i) >= 0 ? parseFloat(data.bitrate) * 1000 : parseFloat(data.bitrate); data.scan = this.miParse({ match: 'Scan type', log: data.video }); data.duration = (() => { let string = this.miParse({ match: 'Duration', log: data.video }); return this.calculateDuration(string); })(); data.width = parseInt(this.miParse({ match: 'Width', log: data.video, crushOutput: true }), 10); data.height = parseInt(this.miParse({ match: 'Height', log: data.video, crushOutput: true }), 10); data.aspect = this.miParse({ match: 'Display aspect ratio', log: data.video, crushOutput: true }).split( ':' ); if (!data.aspect[1]) data.aspect[1] = 1; data.frm = this.miParse({ match: 'Frame rate mode', log: data.video }).toLowerCase(); data.library = this.miParse({ match: 'Writing library', log: data.video }); data.fps = parseFloat(this.miParse({ match: 'Frame rate', log: data.video, crushOutput: true })); data.settings = this.miParse({ match: 'Encoding settings', log: data.video }).split('/'); try { data.audio = this.miParse({ match: 'Audio', singleLine: false }) || []; } catch (e) { this.err('audio', 'noAudioSection'); } data.subtitles = this.miParse({ match: 'Text', singleLine: false }) || []; data.chapters = this.miParse({ match: 'Menu', singleLine: false }) || []; for (let i = 0; i < data.audio.length; i++) { let audio = data.audio[i], obj = {}; obj.id = parseInt(this.miParse({ match: 'ID', log: audio }), 10); obj.title = this.miParse({ match: 'Title', log: audio }); obj.format = obj.fullFormat = this.miParse({ match: 'Format', log: audio, crushOutput: true }); obj.format = obj.format.replace(/\W/, ''); obj.comp = this.miParse({ match: 'Compression mode', log: audio }); obj.bitrate = this.miParse({ match: 'Bit rate', log: audio }); obj.bitrate = obj.bitrate.search(/kb/i) >= 0 ? parseFloat(obj.bitrate.replace(/[^0-9.]/g, '')) : null; obj.bits = parseInt(this.miParse({ match: 'Bit depth', log: audio }), 10) || 16; obj.channels = parseInt(this.miParse({ match: 'Channel\\(s\\)', log: audio, crushOutput: true }), 10); obj.duration = (() => { let string = this.miParse({ match: 'Duration', log: audio }); return this.calculateDuration(string); })(); obj.sample = this.miParse({ match: 'Sampling rate', log: audio, crushOutput: true }); obj.sample = obj.sample.search(/k/i) < 0 ? parseFloat(obj.sample) / 1000 : parseFloat(obj.sample); obj.lang = this.miParse({ match: 'Language', log: audio }); obj.default = this.miParse({ match: 'Default', log: audio }); obj.default = obj.default.search(/yes/i) >= 0 ? true : false; data.audio[i] = obj; } for (let i = 0; i < data.subtitles.length; i++) { let subs = data.subtitles[i]; let obj = {}; obj.id = parseInt(this.miParse({ match: 'ID', log: subs }), 10); obj.title = this.miParse({ match: 'Title', log: subs }); obj.format = this.miParse({ match: 'Format', log: subs }); obj.lang = this.miParse({ match: 'Language', log: subs }); obj.default = this.miParse({ match: 'Default', log: subs }); obj.default = obj.default.search(/yes/i) >= 0 ? true : false; obj.forced = this.miParse({ match: 'Forced', log: subs }); obj.forced = obj.forced.search(/yes/i) >= 0 ? true : false; data.subtitles[i] = obj; } for (let i = 0; i < data.chapters.length; i++) { let chap = data.chapters[i]; chap = chap.replace(/Menu(\s#\d+)?\n/i, '').trim(); chap = chap.split('\n'); chap = chap.map((x) => { let a = x.split(/\s+:(?:[^0-9]*?:)?/); if (!a[1]) a[1] = ''; if (a[0]) return { time: a[0], title: a[1] }; }); data.chapters[i] = chap; } var settingsObj = {}; for (let i = 0; i < data.settings.length; i++) { try { let setting = data.settings[i].split('='); let key = setting[0].replace(/[^a-zA-Z0-9]/g, ''); let value = setting[1].trim(); if (key.search(/(aq|crf(?!max)|psyrd|qcomp|ipratio|pbratio)/i) >= 0) value = value.replace(/,/g, '.'); settingsObj[key] = value; } catch (e) { continue; } } data.settings = settingsObj; this.data = data; if (this.data.library.search(/x264/i) < 0) { this.err('x264', 'codecNotX264'); this.skip += 5; } } filename() { var skip = this.checkSkip(); if (skip) return false; let f = this.data.filename; if (!f) { this.err('filename', 'none'); return false; } var illegal = ['aux', 'com', 'lpt', 'con', 'nul', 'prn']; for (let i = 0; i < illegal.length; i++) { var regex; if (illegal[i] === 'com' || illegal[i] === 'lpt') { for (let a = 1; a < 10; a++) { regex = new RegExp('^' + illegal[i] + a + '\\.', 'i'); if (regex.test(f)) this.err('filename', 'reservedFilename', illegal[i] + a); } } else { regex = new RegExp('^' + illegal[i] + '\\.', 'i'); if (regex.test(f)) this.err('filename', 'reservedFilename', illegal[i]); } } if (f.search(/remux/i) >= 0) this.skip += 5; try { var a = f.match(/[^a-zA-z\d.-]/gi); if (a) { a = f.replace(/([^a-zA-Z0-9-.])/g, "<span class='filename-typo'>$1</span>"); this.err('filename', 'invalidCharacters', a); a = ''; } this.data.filenameYear = f.match(/^.*(?=19|20)(\d{4}).*$/i); this.data.filenameYear = this.data.filenameYear && this.data.filenameYear.length > 0 ? this.data.filenameYear.pop() : ''; if (this.data.filenameYear < 0) this.err('filename', 'noYear'); else if (f.search(/\.(?=19|20)(\d{4})\./i) < 0) this.err('filename', 'yearFormatting'); if (f.search(/\.aka\./i) >= 0 && f.search(/\.AKA\./) < 0) this.err('filename', 'akaFormatting'); if (f.search(/\.a(?![kK])[a-zA-Z]a\./i) >= 0) this.err('filename', 'akaSpelling'); a = f.match(/dvdrip.\d{4}/gi); if (a) this.err('filename', 'yearBeforeDVDRip'); let yearPos = f.search(/\.(?=19|20)(\d{4})\./i); let sourcePos = f.search(/blu(.{0,3})ray/i); let resolutionPos = f.search(/\d{3,4}p/i); if (sourcePos >= 0 && resolutionPos > sourcePos) this.err('filename', 'resolutionAfterSource'); if (resolutionPos >= 0 && yearPos > resolutionPos) this.err('filename', 'yearAfterResolution'); if (f.search(/ac3/i) >= 0) this.err('filename', 'ac3Included'); a = f.match(/blu(.*?)ray/gi); if (a) a = `as '${a[0]}'`; if (f.replace(/[^0-9a-zA-Z]/g, '').search(/blu(.*?)ray/i) >= 0 && f.search(/BluRay/) < 0) { this.err('filename', 'blurayFormatting', a); a = ''; } if (f.search(/\.mkv$/i) >= 0 && f.search(/\.mkv$/) < 0) this.err('filename', 'extensionCase'); else if (f.search(/\.mkv$/) < 0) this.err('filename', 'extensionAbsent'); else if (f.match(/mkv/gi).length >= 2) this.err('filename', 'extensionDupe'); else if (f.replace(/[^a-zA-Z0-9]/g, '').search(/x264handjob/i) >= 0 && f.search(/x264-HANDJOB/) < 0) this.err('filename', 'groupTagIncorrect'); else if (f.search(/x264-HANDJOB/) < 0) this.err('filename', 'groupTagAbsent'); else if (f.search(/x264-HANDJOB\.mkv$/gi) < 0) this.err('filename', 'wrongEnding'); } catch (e) { this.err('filename', 'errorParsing'); this.exceptions.push({ stack: e.stack, message: 'Error during parsing of filename section' }); console.log(e); } } metatitle() { var skip = this.checkSkip(); let failed = false, general; if (skip) return false; try { let m = this.data.metatitle; if (!m) { this.err('metatitle', 'none'); return true; } else if ( m == this.data.filename || m.search(this.data.filename) >= 0 || this.data.filename.replace(/\.mkv/i, '') === m ) { this.err('metatitle', 'filenameMetatitle'); return true; } else if (m.search(/\s/) < 0) { this.err('metatitle', 'filenameSyntax'); return true; } general = /\[\d{4}\](\s[A-Z].*[a-z])?(\s\d{3,4}p\s[a-zA-Z]{5,7}|\s\d{3,4}p\s[A-Z]{2}-[A-Z]{3}|\s[a-zA-Z]{5,7})\s-\sHJ$/g.test( m ); this.data.metatitleYear = m.match(/^.*(?=19|20)(\d{4}).*$/i); this.data.metatitleYear = this.data.metatitleYear && this.data.metatitleYear.length > 0 ? this.data.metatitleYear.pop() : ''; if ( this.data.metatitleYear && this.data.filenameYear && this.data.metatitleYear !== this.data.filenameYear ) this.err('metatitle', 'yearMismatch'); else if (this.data.metatitleYear < 0) this.err('metatitle', 'noYear'); else if (m.search(/\[\d{4}\]/) < 0) this.err('metatitle', 'noSquareBrackets'); let ex = m.match(/\[\d{4}\]\s([^a-zA-Z0-9])/); if (ex && ex.length > 0) this.err('metatitle', 'afterYearFormatting', ex[1]); if (m.search(/(?:\[|\]\(\))(?:.*?)(flac|dts|aac|ac3|mp3|mp2|mpeg|eac3)/gi) >= 0) this.err('metatitle', 'audioCodecPresent'); if ( this.data.source === 'bluray' && m.replace(/[^a-zA-Z0-9]/g, '').search(/bluray/i) < 0 && m.replace(/[^a-zA-Z0-9]/g, '').search(/hdtv/i) < 0 && m.replace(/[^a-zA-Z0-9]/g, '').search(/hddvd/i) < 0 ) { this.err('metatitle', 'noSourceTag', 'BluRay'); } else if (m.replace(/[^a-zA-Z0-9]/g, '').search(/blueray/i) > 0) this.err('metatitle', 'sourceSpelling', 'BluRay'); else if ( this.data.source === 'dvd' && this.data.filename.search(/(tvrip|vhsrip)/i) < 0 && m.search(/dvdrip/i) < 0 ) { this.err('metatitle', 'noSourceTag', 'DVDRip'); } else if ( this.data.source === 'dvd' && this.data.filename.search(/(tvrip|vhsrip)/i) > 0 && m.search(/(tvrip|vhsrip)/i) < 0 ) { this.err('metatitle', 'noSourceTag', 'TVRip / VHSRip'); } if (this.data.source === 'bluray' && m.search(/\d{3,4}p/i) < 0) this.err('metatitle', 'noResolutionTag'); if (this.data.source === 'bluray' && m.search(/ray[\W]*\d{3,4}/i) >= 0) { let match = m.match(/blu[\W\w]*ray[\W]*\d{3,4}p/gi); this.err('metatitle', 'sourceResOrder', match); } if (m.search(/x264/i) >= 0) this.err('metatitle', 'x264Found'); if (m.search(/HANDJOB$/i) >= 0) this.err('metatitle', 'usedHANDJOB'); else if (m.search(/\s*-\s*hj\s*$/i) >= 0 && m.search(/ - HJ$/) < 0) this.err('metatitle', 'HJFormatting'); else if (m.search(/\s*-\s*hj$/i) < 0) this.err('metatitle', 'endHJ'); } catch (e) { failed = true; } if (failed === true) this.err('metatitle', 'generalError'); if ( general === false && this.feedback.metatitle.errors.length == 0 && this.feedback.metatitle.warnings.length == 0 && this.feedback.metatitle.advisories.length == 0 ) this.err('metatitle', 'generalError'); } checkDxva() { var skip = this.checkSkip(); if (skip) return false; if (!this.data.settings.ref) return false; var pass = true, failings = []; let ref = parseInt(this.data.settings.ref, 10); let vbv = parseFloat(this.data.settings.vbvmaxrate); var maxRef = Math.floor( 8388608 / ([Math.ceil(this.data.width / 16) * 16] * [Math.ceil(this.data.height / 16) * 16]) ); if (this.data.settings.analyse != '0x3:0x133' && this.data.settings.analyse != '0x3:0x113') { pass = false; failings.push(`Analyse not 0x3:0x133 or 0x3:0x113`); } if (ref > maxRef) { pass = false; failings.push( `Ref: ${this.data.settings.ref}. Maximum reference frames for this resolution is ${maxRef}` ); } if (vbv > 62500) { pass = false; failings.push(`Vbv_Maxrate: ${vbv.toLocaleString()} Kbps. Maximum is 50,000 Kbps`); } if (!pass) { this.err('x264', 'dxvaIncompatible', failings); } } checkMinimum() { var skip = this.checkSkip(); if (skip) return false; try { var pass = true, issues = []; if (parseInt(this.data.settings.cabac, 10) != 1) { pass = false; issues.push('CABAC not 1'); } if (this.data.settings.analyse != '0x3:0x133' && this.data.settings.analyse != '0x3:0x113') { pass = false; issues.push('Analyse not 0x3:0x133 or 0x3:0x113'); } if (parseInt(this.data.settings.subme, 10) < 7) { pass = false; issues.push('Subme less than 7'); } if (this.data.settings.me.search(/umh|tesa|esa/) < 0) { pass = false; issues.push('Me is not UMH, ESA, or TESA'); } if (parseInt(this.data.settings.trellis, 10) != 2) { pass = false; issues.push('Trellis not 2'); } let deblock = this.data.settings.deblock.split(':'); deblock.map((x) => parseInt(x, 10)); if (deblock[0] != 1 || deblock[1] >= 0 || deblock[2] >= 0) { pass = false; issues.push('Deblock values not less than zero'); } if (this.data.settings.rc.search(/2pass|crf/) < 0) { pass = false; issues.push('Rate control not 2-pass or CRF'); } if ( this.data.source == 'dvd' || this.data.source === 'tv' || this.data.source === 'vhs' || this.data.res == '480p' || this.data.res == '576p' ) { if (parseInt(this.data.settings.bframes, 10) < 5) { pass = false; issues.push('Used fewer than 5 bframes'); } if (parseInt(this.data.settings.ref, 10) < 9) { pass = false; issues.push('Used fewer than 9 reference frames'); } if (parseInt(this.data.settings.merange, 10) < 16) { pass = false; issues.push('Me-range less than 16'); } else if (parseInt(this.data.settings.merange, 10) < 24 && this.data.settings.me != 'tesa') { pass = false; issues.push('Me-range less than 24 without me:TESA'); } } else if (this.data.res == '720p') { if (parseInt(this.data.settings.merange, 10) < 16) { pass = false; issues.push('Me-range less than 16'); } if (parseInt(this.data.settings.ref, 10) < 8) { pass = false; issues.push('Used fewer than 8 reference frames'); } if (parseInt(this.data.settings.bframes, 10) < 5) { pass = false; issues.push('Used fewer than 5 bframes'); } } else if (this.data.res == '1080p') { if (parseInt(this.data.settings.merange, 10) < 16) { pass = false; issues.push('Me-range less than 16'); } if (parseInt(this.data.settings.ref, 10) < 3) { pass = false; issues.push('Used fewer than 3 reference frames'); } if (parseInt(this.data.settings.bframes, 10) < 3) { pass = false; issues.push('Used fewer than 3 bframes'); } } else { console.log('Error checking minimum settings.'); } if (!pass) { this.err('x264', 'minimumNotMet', issues); } return true; } catch (e) { this.err('x264', 'codecNotX264'); return false; } } x264() { var skip = this.checkSkip(); if (skip) return false; if (parseFloat(this.data.settings.qcomp) <= 0.3) this.err('x264', 'qcompVeryLow'); else if (parseFloat(this.data.settings.qcomp) <= 0.5) this.err('x264', 'qcompLow'); else if (parseFloat(this.data.settings.qcomp) > 0.95) this.err('x264', 'qcompVeryHigh'); else if (parseFloat(this.data.settings.qcomp) > 0.8) this.err('x264', 'qcompHigh'); if (this.data.settings.mbtree != '0') this.err('x264', 'mbtreeOn'); if (this.data.settings.rc.search(/2pass/) >= 0) this.err('x264', '2passUsed'); if (parseFloat(this.data.settings.crf) < 12) this.err('video', 'crfVeryLow'); else if (parseFloat(this.data.settings.crf) < 14) this.err('video', 'crfLow'); else if (parseFloat(this.data.settings.crf) > 26) this.err('video', 'crfVeryHigh'); else if (parseFloat(this.data.settings.crf) > 23) this.err('video', 'crfHigh'); if (this.data.settings.psy != '1') this.err('x264', 'psyOff'); var psy = this.data.settings.psyrd.split(':'); if (parseFloat(psy[0]) < 0.8 || parseFloat(psy[0]) > 1.2) { this.err('x264', 'psyUnusual', psy); } if (parseFloat(psy[1]) > 0.25) this.err('x264', 'psyTrellisHigh', psy); let aq = parseFloat(this.data.settings.aq.split(':')[1]); if (aq < 0.5 || aq > 1.2) this.err('x264', 'aqUnusual', aq); if (this.data.frm.search(/variable/i) >= 0) this.err('video', 'variableFramerate'); } checkAnamorphic() { var skip = this.checkSkip(); if (skip) return false; var anamorphic = false; var anaWidth = Math.ceil( (this.data.height / parseFloat(this.data.aspect[1])) * parseFloat(this.data.aspect[0]) ); var margin = (this.data.width / 100) * 3; if (anaWidth < this.data.width - margin || anaWidth > this.data.width + margin) anamorphic = true; this.data.anamorphic = anamorphic; var fname = this.data.filename.replace(/[^a-zA-Z0-9.]/g, ''); if (fname.search(/(tvrip|vhsrip)/i) >= 0 && anamorphic) this.err('video', 'tvAnamorphic'); else if (this.data.source == 'dvd' && fname.search(/(tvrip|vhsrip)/i) < 0 && !anamorphic) this.err('video', 'dvdNonAnamorphic'); else if (this.data.source == 'bluray' && anamorphic) this.err('video', 'blurayAnamorphic'); } resolution() { var skip = this.checkSkip(); if (skip) return false; // Check width and height against this.data.res: over, under, neither equal // Check progressive scan var resolutions = ['DVDRip', 'VHSRip', 'TVRip', '480p', '576p', '720p', '1080p']; var resString = () => { let temp = this.data.source === 'bluray' ? resolutions.splice(3) : resolutions.splice(0, 3); let x = temp.pop(); let y = temp.join(', '); return y + ' or ' + x; }; // ## SECTION FOR FILENAME ## var regex, regex2, filenameRes = {}, metatitleRes = {}, detectedRes; switch (this.data.source) { case 'dvd': // Iterate through resolutions resolutions.map((x) => { // Ensure that numbered resolution isn't included in a DVDRip. regex = new RegExp(parseInt(x, 10), 'i'); if (parseInt(x, 10) && this.data.filename.search(regex) >= 0) this.err('filename', 'resDVD'); // Capture filename resolution if (!isNaN(x.charAt(0))) return true; // Skip the following for Blu-Ray resolutions regex = new RegExp(x.replace(/[^a-zA-Z0-9]/g, ''), 'i'); if (this.data.filename.match(regex)) { filenameRes.match = this.data.filename.match(regex)[0]; // Match and save filename res filenameRes.actual = x; } // Test if filename resolution is formatted properly if (this.data.filename.search(x) < 0 && this.data.filename.search(regex) >= 0) this.err('filename', 'resFormat', { res: x, match: filenameRes.match }); }); break; case 'bluray': resolutions.map((x) => { if (isNaN(x.charAt(0))) { // If the iterated resolution starts with a letter... regex = new RegExp(x.replace(/[^a-zA-Z0-9]/g, ''), 'i'); // Check that an SD tag hasn't been included if (this.data.filename.search(regex) >= 0 && this.data.filename.search(/hdtv/i) < 0) this.err('filename', 'SDTagHDSource', x); return true; } else { regex = new RegExp(x.replace(/[^a-zA-Z0-9]/g, ''), 'i'); regex2 = new RegExp(x.replace(/[^0-9]/g, '')); if (this.data.filename.match(regex)) { filenameRes.match = this.data.filename.match(regex)[0]; // Match and save filename res filenameRes.actual = x; if (filenameRes.match !== x) this.err('filename', 'resFormat', { res: x, match: filenameRes.match }); } else if (this.data.filename.match(regex2)) { filenameRes.match = this.data.filename.match(regex2)[0]; // Match and save filename res filenameRes.actual = x; if (filenameRes.match !== x) this.err('filename', 'noP', x); } } }); break; default: } if (!filenameRes.match) this.err('filename', 'noRes', resString()); // ## SECTION FOR METATITLE ## switch (this.data.source) { case 'dvd': resolutions.map((x) => { // Ensure that numbered resolution isn't included in a DVDRip. regex = new RegExp(parseInt(x, 10), 'i'); if (parseInt(x, 10) && this.data.metatitle.search(regex) >= 0) this.err('metatitle', 'resDVD', x); if (!isNaN(x.charAt(0))) return true; regex = new RegExp(x.replace(/[^a-zA-Z0-9]/g, ''), 'i'); if (regex.test(this.data.metatitle)) { metatitleRes.match = this.data.metatitle.match(regex)[0]; // Match and save metatitle res metatitleRes.actual = x; } // Test if filename resolution is formatted properly if ( this.data.metatitle.search(x) < 0 && this.data.metatitle.search(regex) >= 0 && filenameRes.actual === metatitleRes.actual ) { this.err('metatitle', 'resFormat', { res: x, match: metatitleRes.match }); } }); break; case 'bluray': resolutions.map((x) => { if (isNaN(x.charAt(0))) { // If the iterated resolution starts with a letter... regex = new RegExp(x.replace(/[^a-zA-Z0-9]/g, ''), 'i'); // Check that an SD tag hasn't been included if (this.data.metatitle.search(regex) >= 0 && this.data.metatitle.search(/hdtv/i) < 0) this.err('metatitle', 'SDTagHDSource', x); return true; } // If the iterated resolution starts with a number else { regex = new RegExp(x.replace(/[^a-zA-Z0-9]/g, ''), 'i'); regex2 = new RegExp(x.replace(/[^0-9]/g, '')); if (regex.test(this.data.metatitle)) { metatitleRes.match = this.data.metatitle.match(regex)[0]; // Match and save metatitle res metatitleRes.actual = x; if (metatitleRes.match !== x) this.err('metatitle', 'resFormat', { res: x, match: metatitleRes.match }); } else if (regex2.test(this.data.metatitle)) { metatitleRes.match = this.data.metatitle.match(regex2)[0]; // Match and save metatitle res metatitleRes.actual = x; if (metatitleRes.match !== x) this.err('metatitle', 'noP', x); } } }); break; default: } if (this.data.metatitle && !metatitleRes.match) this.err('metatitle', 'noResolutionTag', resString()); this.data.res = filenameRes.actual ? filenameRes.actual : metatitleRes.actual; if (this.data.res) this.data.res = !isNaN(this.data.res.charAt(0)) ? this.data.res : ''; if (this.data.scan.search(/progressive/i) < 0) this.err('video', 'resProgressive'); // ## Detected Resolution Section ## var w = this.data.width, h = this.data.height; if (this.data.source === 'bluray') { if (w === 854 || h === 480) detectedRes = '480p'; else if (w === 1024 || h === 576) detectedRes = '576p'; else if (w === 1280 || h === 720) detectedRes = '720p'; else if (w === 1920 || h === 1080) detectedRes = '1080p'; else if (w === 3840 || h === 2160) detectedRes = '2160p'; else if (w <= 939 && h <= 528) detectedRes = '480p'; else if (w <= 1152 && h <= 648) detectedRes = '576p'; else if (w <= 1600 && h <= 900) detectedRes = '720p'; else if (w <= 2880 && h <= 1620) detectedRes = '1080p'; else detectedRes = '2160p'; if (detectedRes === '2160p') this.err('video', '2160pEncode'); } else { if (this.data.region === 'ntsc') { if (w > 720 || h > 480) this.err('video', 'resizedDVDRip', 'NTSC'); } else if (this.data.region === 'pal') { if (w > 720 || h > 576) this.err('video', 'resizedDVDRip', 'PAL'); } } if (detectedRes && detectedRes !== filenameRes.actual) this.err('filename', 'resMismatch', { f: filenameRes.match, d: detectedRes }); if (detectedRes && this.data.metatitle && detectedRes !== metatitleRes.actual) this.err('metatitle', 'resMismatch', { m: metatitleRes.match, d: detectedRes }); this.data.res = detectedRes || ''; // After this.data.res is set... if (this.data.filename.search(/(vhsrip|[^hd]tvrip)/i) >= 0) { this.data.speclink = 'vhstv'; this.data.spectext = 'VHSRip / TVRip'; } else if (this.data.region) { this.data.speclink = this.data.region + 'dvd'; this.data.spectext = this.data.region.toUpperCase() + ' DVD'; } else { this.data.speclink = this.data.res; this.data.spectext = this.data.res + ' Blu-Ray'; } } // This function verifies the width and height settings of the encode. Comes later // 96px - 144px - 360px checkRes() { var skip = this.checkSkip(); if (skip) return false; let wd = this.data.width, ht = this.data.height; let specs = [ { type: '480p', width: 854, height: 480 }, { type: '576p', width: 1024, height: 576 }, { type: '720p', width: 1280, height: 720 }, { type: '1080p', width: 1920, height: 1080 }, ]; if (this.data.source == 'bluray') { for (var x of specs) { if (this.data.res == x.type) { if (wd > x.width) this.err('video', 'resWidthOver', x); if (ht > x.height) this.err('video', 'resHeightOver', x); if (wd < x.width && ht < x.height && x.type != '1080p') this.err('video', 'resNeitherEqual', x); } } } } checkFramerate() { var skip = this.checkSkip(); if (skip) return false; if (this.data.fps == 29.97) this.err('video', 'dupeFrames'); else if ( this.data.fps != 25 && this.data.fps != 24 && this.data.fps != 23.976 && this.data.frm.search(/constant/i) >= 0 ) { this.err('video', 'fpsUnusual'); } } checkBitrate() { var skip = this.checkSkip(); if (skip) return false; var min, max; var bitrate = this.data.bitrate; var insert, buttins; if (!bitrate) { this.err('video', 'bitrateMissing'); return true; } var specs = [ { t: '576p', min: 2000, max: 4000 }, { t: '720p', min: 5000, max: 7000 }, { t: '1080p', min: 8000, max: 12000 }, ]; if (this.data.source == 'dvd' || this.data.source == 'vhs' || this.data.res == '480p') { min = 1500; max = 2500; } else { for (let x of specs) { if (this.data.res == x.t) { min = x.min; max = x.max; } } } var text = min.toLocaleString() + ' Kbps - ' + max.toLocaleString() + ' Kbps'; var buffer = (20 / 100) * bitrate; var smbuffer = (5 / 100) * bitrate; var sendObj = { bitrate: bitrate.toLocaleString(), text }; if (bitrate < min && bitrate > min - smbuffer) this.err('video', 'bitrateUnderSlightly', sendObj); else if (bitrate < min && bitrate > min - buffer) this.err('video', 'bitrateUnder', sendObj); else if (bitrate < min - buffer) this.err('video', 'bitrateUnderSeverely', sendObj); else if (bitrate > max && bitrate < max + smbuffer) this.err('video', 'bitrateOverSlightly', sendObj); else if (bitrate > max && bitrate < max + buffer) this.err('video', 'bitrateOver', sendObj); else if (bitrate > max + buffer) this.err('video', 'bitrateOverSeverely', sendObj); } sortAudioTracks() { var skip = this.checkSkip(); if (skip) return false; var audio = { main: [], secondary: [], score: [] }; for (var x of this.data.audio) { if (this.data.audio.length === 1) x.text = 'Main audio track'; else if (!x.title && x.default && !x.lang) x.text = 'Main audio track'; else if (x.title) x.text = `Audio track '${x.title}'`; else if (x.lang) x.text = `Audio track '${x.lang}'`; else x.text = `Audio track 'ID: ${x.id}'`; if (x.duration < this.data.duration - 2 || x.duration > this.data.duration + 2) this.err('audio', 'audioLengthMismatch', { duration: x.duration, label: x.text }); if (!x.lang && x.title.search(/(score|isolated)/i) < 0) this.err('audio', 'noLanguageTag', x.text); if (x.bits > 16) this.err('audio', 'bitDepthHigh', { text: x.text, bits: x.bits }); if (x.sample > 48) this.err('audio', 'sampleRateHigh', { text: x.text, sample: x.sample.toFixed(1) }); if (x.format.search(/dts/i) >= 0 && x.comp.search(/lossless/i) >= 0) this.err('audio', 'dtsHDMA', x.text); if (x.format.search(/pcm/i) >= 0) this.err('audio', 'lpcmAudio', x.text); if (x.title.search(/comment/i) >= 0) audio.secondary.push(x); else if (x.title.search(/(score|isolated)/i) >= 0) audio.score.push(x); else if (!x.default && x.bitrate <= 100) audio.secondary.push(x); else audio.main.push(x); } this.data.audio = audio; } checkAudioBitrate(input) { // check if bitrate is detectable... if not return an error if (!input.bitrate) { this.err('audio', 'missingBitrate', decapFirst(input.text)); return true; } if (input.default) { let match, codecs = ['aac', 'dts', 'flac', 'pcm'], d = false; // Iterate through each of the non-AC3 codecs for (let x of codecs) { // Create a case-insentive regex with the codec let reg = new RegExp(x, 'i'); // If the filename contains the codec and the audio track format doesn't contain the codec, run this block if (this.data.filename.search(reg) >= 0 && input.format.search(reg) < 0) { this.err('filename', 'codecMismatch', { filenameCodec: x.toUpperCase(), codec: input.fullFormat, }); d = true; } } try { match = input.format.match(/aac|dts|flac|pcm/i); if (match && match.length > 0) { let reg = new RegExp(match[0], 'i'); if (this.data.filename.search(reg) < 0 && !d) this.err('filename', 'codecNotIncluded', match[0].toUpperCase()); } } catch (e) { console.log(e); } } if (this.data.source == 'dvd' && input.format.search(/aac/i) >= 0) this.err('audio', 'aacDVD'); else if (input.format.search(/mpeg/i) >= 0) this.err('audio', 'mpegAudio', input.text); else if (this.data.source == 'bluray' && input.channels <= 2 && input.format.search(/ac3/i) >= 0) this.err('audio', 'ac3Stereo', input.text); else if (this.data.source == 'bluray' && input.channels > 2 && input.format.search(/aac/i) >= 0) this.err('audio', 'aacSurround', input.text); // Bitrate check for AC3 audio if (input.format.search(/ac3/i) >= 0 && this.data.source == 'bluray' && input.channels >= 4) { var spec = 0; if (this.data.res == '480p' || this.data.res == '576p') spec = 448; else if (this.data.res == '720p' || this.data.res == '1080p') spec = 640; if (input.bitrate < spec || input.bitrate > spec) this.err('audio', 'ac3Bitrate', { text: input.text, actual: input.bitrate, spec }); } } checkMainAudio() { var skip = this.checkSkip(); if (skip) return false; var audio = this.data.audio; this.data.subtitles.map((x) => { if (x.forced) this.err('subtitles', 'forcedYes', { lang: x.lang, title: x.title, id: x.id }); if (!x.lang) this.err('subtitles', 'noLanguageTag', { title: x.title, id: x.id }); if ( x.title.search(/forced/i) >= 0 || x.title.replace(/[^a-zA-Z]/, '').search(/nonenglishscene/i) >= 0 ) { if (x.default === false) this.err('subtitles', 'forcedFlagNotDefault', { lang: x.lang, title: x.title, id: x.id }); } }); switch (true) { case audio.main.length === 0: this.err('audio', 'noAudio'); break; case audio.main.length === 1: let m = audio.main[0]; if (m.title.search(/(stereo|mono|surround)/i) >= 0) this.err('audio', 'mainTitle', m.title); // Section for single audio, English language films if (m.lang.search(/english/i) >= 0) { this.data.subtitles.map((x) => { if (x.default && x.title.search(/non(.*?)eng|forced/i) < 0) this.err('subtitles', 'defaultYes', { lang: x.lang, title: x.title, id: x.id }); if (x.title.search(/forced/i) >= 0 && x.lang.search(/english/i) >= 0) this.err('subtitles', 'nameForced', x.title); }); } // Section for single audio films with no linguistic content else if (m.lang.search(/zxx/i) >= 0) { this.data.subtitles.map((x) => { if (x.default && x.title.search(/screen/i) < 0) this.err('subtitles', 'noLinguisticLabelling'); }); } // Section for single audio, non-English language films else { var english = false, def = false; this.data.subtitles.map((x) => { if (x.default && x.lang.search(/english/i) < 0) this.err('subtitles', 'nonEngDefault', x.lang); else if (x.default && x.lang.search(/english/i) >= 0) { english = true; def = true; } else if (!x.default && x.lang.search(/english/i) >= 0) english = true; }); if (!def && english) this.err('subtitles', 'engDefault'); else if (!english && m.lang) this.err('subtitles', 'noEngSubs'); } this.checkAudioBitrate(m); break; case audio.main.length > 2: this.err('audio', 'multipleAudio'); // No break here, needs to filter through to next condition for 2+ audio tracks default: var langs = [], defSub = false, engsub = false, dualAudio = false; audio.main.map((x) => { langs.push(x.lang); }); var engTracks = this.arrayCount(langs, 'english'); if (langs.length > engTracks) dualAudio = true; if (engTracks > 1) this.err('audio', 'dupeEnglish', engTracks); audio.main.map((x) => { this.checkAudioBitrate(x); if (x.default && x.lang && x.lang.search(/english/i) >= 0 && dualAudio) this.err('audio', 'engDefault'); if (x.lang.search(/english/i) >= 0 && x.title.search(/dub/i) < 0 && dualAudio) this.err('audio', 'engDubName'); if ( x.lang.search(/english/i) < 0 && x.default && x.title.search(/(stereo|mono|surround)/i) >= 0 ) this.err('audio', 'titleBlank', x.title); }); for (let i = 0; i < this.data.subtitles.length; i++) { if (this.data.subtitles[i].default) defSub = true; if (this.data.subtitles[i].lang.search(/English/i) >= 0) engsub = true; if ( this.data.subtitles[i].default && this.data.subtitles[i].lang && this.data.subtitles[i].lang.search(/English/i) < 0 ) { this.err('subtitles', 'nonEngDefault', this.data.subtitles[i].lang); } } if (!engsub && dualAudio) this.err('subtitles', 'noEngSubs'); else if (!defSub && dualAudio) this.err('subtitles', 'noDefault'); break; } } checkSecondaryAudio() { var skip = this.checkSkip(); if (skip) return false; var audio = this.data.audio, temp; for (var s of audio.secondary) { var tg = audio.secondary.length > 1 ? 'commentary track (ID: ' + s.id + ')' : 'commentary track'; if (!s.title) { tg = 'secondary audio track (ID: ' + s.id + ')'; this.err('audio', 'noTrackName', tg); } if (s.format.search(/aac/i) < 0) this.err('audio', 'aacCommentary', tg); if (s.bitrate >= 100) this.err('audio', 'highCommentary', tg); if (s.title.search(/comment/i) < 0) this.err('audio', 'useCommentary', tg); temp = s.title.replace(/[^0-9a-zA-z]/g, '').toLowerCase(); if (temp == 'commentary' || temp == 'audiocommentary') this.err('audio', 'noParticipants', tg); else if (temp.search(/audiocommentary/i) >= 0) this.err('audio', 'usedAudioCommentary', tg); if (s.default) this.err('audio', 'defaultCommentary', tg); } } checkChapters() { var skip = this.checkSkip(); if (skip) return false; for (let i = 0; i < this.data.chapters.length; i++) { var tg = this.data.chapters.length > 1 ? '(chapter set #' + [i + 1] + ')' : ''; var tc = 0, bk = 0; var cSet = this.data.chapters[i]; cSet.map((x) => { if (x.time && x.title.search(/[a-zA-Z]/g) < 0 && x.title.search(/[0-9]/g) >= 0) tc++; else if (x.time.search(/\d{2}:\d{2}/i) >= 0 && x.title.search(/[a-zA-Z0-9]/g) < 0) bk++; }); if (tc == cSet.length) this.err('chapters', 'chapterTimecodes', tg); else if (bk == cSet.length) this.err('chapters', 'noTitles', tg); else if (bk > 0) this.err('chapters', 'missingTitles', tg); } } } var urls = { guide: `${PTP}/wiki.php?action=article&id=263` }; var timers = GM_getValue('HJA_timers') || { guide: 0 }; var settings = { cache: 1440, script: 'admin' }; var imgs = { sourceInfo: 'https://ptpimg.me/30bg01.png', handjobBadge: 'https://ptpimg.me/sb54rt.png', settingsIcon: 'https://ptpimg.me/20hlgj.png', miScan: 'https://ptpimg.me/bw61y4.png', appearanceIcon: 'https://ptpimg.me/j7p346.png', }; var envs = { hash: location.hash.replace('#', ''), path: window.location.pathname, }; envs.page = currentPage(); var css = {}; var cssDarkStyle = { text: '#c5c5c5', text2: '#c5c5c5', background: '#34495e', header: '#3d678c', //"#4682b4", passed: '#90ee90', error: '#cd5c5c', warning: '#e08f2b', advisory: '#e8e66c', pageBackground: '#050505', pageBackground2: '#555555', offset: '#f3f3f3', }; var cssLightStyle = { text: '#313131', // For mediainfo scan boxes text2: '#313131', // For everything else background: '#cfe0e8', header: '#87bdd8', passed: '#2cbb39', error: '#cd5c5c', warning: '#e08f2b', advisory: '#5f9ea0', pageBackground: '#fefefe', pageBackground2: '#f5f5dc', offset: '#030303', }; var entityMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', }; darkLight(); injectStylesheet(); switch (envs.page) { case 'upload': settings.fn = settingsModule(); settings.fn.update('miscan'); break; case 'torrent': torrentPageScan(); break; default: injectHJAElements(); activateListeners(); replaceUserLinks(); showEncodeCount(); activateAltLinks(); sourceInfoScan(); loadHandjobGuide(); imageDimensions(); groupEncodes = new GroupEncodes(); settings.fn.update('miscan'); settings.fn.update('borders'); scrollToAnchor(envs.hash); break; } function injectHJAElements() { const lightboxNode = Create('div#admin-lightbox'); lightboxNode.innerHTML = `<div id="admin-lightbox-container"> <div id="admin-lightbox-content"> <div id="admin-lightbox-exit">X</div> </div> <div id="admin-lightbox-alert"></div> </div>`; document.body.appendChild(lightboxNode); const groupEncodesNode = Create('div#group-encodes'); groupEncodesNode.style.transform = 'scale(0)'; groupEncodesNode.style.opacity = 0; document.body.appendChild(groupEncodesNode); const toolbarIcon = Create('div#handjob-bbcode-icon.bbcode-toolbar__button{title}Expand HANDJOB Toolbar'); const toolbarNode = Create('div#handjob-bbcode-select'); toolbarNode.innerHTML = ` <div id="handjob-bbcode-settings" class="bbcode-toolbar__button" title="Toolbar Settings"></div> <div id="handjob-settings-window"> <div class="settings-option" title="Automatically scan mediainfo logs posted into the HANDJOB forum"><input type="checkbox" id="switch1" name="switch1" class="switch" data-option="miscan" /><label for="switch1">MI Autoscan</label></div> <div class="settings-option" title="Change the username links on forum posts from profile links to uploaded HANDJOBs"><input type="checkbox" id="switch2" name="switch2" class="switch" data-option="encodelinks" /><label for="switch2">Encode Links</label></div> <div class="settings-option" title="Display HANDJOB encode count when hovering over username in forum post"><input type="checkbox" id="switch3" name="switch3" class="switch" data-option="encodecount" /><label for="switch3">Encode Count</label></div> <div class="settings-option" title="When enabled, clicking an avatar in the forum will insert an @username link in the quickpost input box"><input type="checkbox" id="switch4" name="switch4" class="switch" data-option="avatarat" /><label for="switch4">Avatar @</label></div> <div class="settings-option" title="Borders will be added to any detected screenshots posted. Click the eye to edit border style"><input type="checkbox" id="switch5" name="switch5" class="switch" data-option="borders" /><label for="switch5">Img Borders</label><div class="appearance-setting" title="Change Border Appearance"><img src="${imgs.appearanceIcon}"></div> <div class="appearance-extension-outer"><div class="appearance-extension-inner"> <div id="appearance-extension-border-width-container"> <div>Border Width:</div> <div> <select id="appearance-extension-border-width" class="borderStyle"> <option>1</option> <option>2</option> <option>3</option> <option>4</option> <option>5</option> </select> </div> </div> <div id="appearance-extension-border-colour-container"> <div>Colour:</div> <div><input type="color" class="borderStyle" id="appearance-extension-border-colour" value="#ffffff"></div> </div> </div></div> </div> <div class="settings-option" title="Clicking the 'Reply to Approval Request' button will generate a quote rather than an @username link in the quickpost message box"><input type="checkbox" id="switch6" name="switch6" class="switch" data-option="quotereplies" /><label for="switch6">Quote Replies</label></div> </div> </div>`; const target = document.querySelector('#Bbcode_Toolbar'); const targetBefore = target.querySelector('div[style*="clear: both"]'); target.insertBefore(toolbarIcon, targetBefore); target.insertBefore(toolbarNode, targetBefore); const linkboxNode = document.querySelector('div#content div.thin > div.linkbox:first-of-type'); const recentEncodesNode = Create('a#checkUploadsLink.linkbox__link{href}null'); recentEncodesNode.innerHTML = '[Check Recent HANDJOB Encodes]'; linkboxNode.appendChild(recentEncodesNode); settings.fn = settingsModule(); } function darkLight() { let bk = document.getElementsByClassName('forum_post'); if (bk.length === 0) bk = document.getElementsByClassName('panel'); bk = window .getComputedStyle(bk[0], null) .getPropertyValue('background-color') .replace(/[^0-9,]/gi, '') .split(','); var rgb = { r: parseInt(bk[0]), g: parseInt(bk[1]), b: parseInt(bk[2]) }; var RsRGB, GsRGB, BsRGB, R, G, B; RsRGB = rgb.r / 255; GsRGB = rgb.g / 255; BsRGB = rgb.b / 255; if (RsRGB <= 0.03928) { R = RsRGB / 12.92; } else { R = Math.pow((RsRGB + 0.055) / 1.055, 2.4); } if (GsRGB <= 0.03928) { G = GsRGB / 12.92; } else { G = Math.pow((GsRGB + 0.055) / 1.055, 2.4); } if (BsRGB <= 0.03928) { B = BsRGB / 12.92; } else { B = Math.pow((BsRGB + 0.055) / 1.055, 2.4); } var luma = 0.2126 * R + 0.7152 * G + 0.0722 * B; if (luma >= 0.5) css = cssLightStyle; else css = cssDarkStyle; } function activateListeners() { // Set listener for the Admin Toolkit settings menu document.querySelector('#handjob-bbcode-settings').addEventListener('click', function () { $('#handjob-settings-window').fadeToggle('fast'); $('.appearance-extension-outer').slideUp('fast'); }); // Set listener for the Admin Toolkit toolbar $('#handjob-bbcode-icon').on('click', function () { toggleToolbar(this); }); // Set listener for border settings document.querySelector('.appearance-setting').addEventListener('click', function () { $('.appearance-extension-outer').slideToggle('fast'); }); // Set listener for the Admin Toolkit 'Check Recent HANDJOB encodes' display document.querySelector('#checkUploadsLink').addEventListener('click', function () { checkRecentUploads(); }); } function toggleToolbar(element) { var reset = false, toggleWidth, newWidth = 220; if ($(element).prop('title') === 'Expand HANDJOB Toolbar') { $(element).prop('title', 'Close HANDJOB Toolbar'); } else { $(element).prop('title', 'Expand HANDJOB Toolbar'); $('#handjob-settings-window').fadeOut('fast'); $('.appearance-extension-outer').slideUp('fast'); reset = true; } if ($('#handjob-bbcode-contest-start').length >= 1) newWidth += 25; if ($('#handjob-bbcode-settings').length >= 1) newWidth += 25; toggleWidth = $('#handjob-bbcode-select').width() === 0 ? newWidth : '0px'; $('#handjob-bbcode-select').animate({ width: toggleWidth }, function () { if (reset) resetElements(); }); } // ## End Section for functions for 'Check Recent HANDJOB Encodes' function ## function torrentPageScan() { let html = ` | <a class="linkbox__link torrents_scan_mediainfo" href="#">Scan Mediainfo log(s)</a>`; $('tr.torrent_info_row div.linkbox').append(html); $('.torrents_scan_mediainfo').on('click', function (e) { e.preventDefault(); var el = $(this).closest('div.linkbox').siblings('div.movie-page__torrent__panel'); $(el) .find('table.mediainfo') .each(function () { var element = $(this); populateMiScan(element); }); }); } function scrollToAnchor(aid) { if (!aid) return true; else if (aid === 'quickpost') return true; var timer; var errors = 0; var checker = function () { let ele = $('#HJM_loading'); let status = $('#HJM_loading').val(); if (ele.length === 0 && errors < 5) { errors++; } else if (status != 'active') { dropAnchor(); clearInterval(timer); } }; function dropAnchor() { var aTag = $("div[id='" + aid + "']"); $('html,body').animate({ scrollTop: aTag.offset().top }, 'slow'); } timer = setInterval(checker, 100); } function lightboxBorder() { var style = GM_getValue('borderStyle'); $('#lightbox').find('img').css('border', `${style.wid}px solid ${style.colour}`).css('margin', '2px'); } function addBorders(bdStatus) { var style = GM_getValue('borderStyle'); $('blockquote').each(function () { if ($(this).find('img').not('img[src*=static]').not('.view_post').length >= 3) { $(this) .find('img') .not('.view_post') .each(function () { if (bdStatus) { $(this).removeAttr('style'); $(this).css('border', `${style.wid}px solid ${style.colour}`).css('margin', '5px 0px'); $(this).on('click', function () { lightboxBorder(); }); } else { $(this).removeAttr('style').off('click'); } }); } }); } // Adds arrow enabling a quick reply to approval requests with a mediainfo log pasted function addMediainfoClick(element) { if (envs.page || $(element).closest('#quickpostform').length > 0) return false; var elApp = $(element).find('tbody tr td:nth-child(3)'); $(elApp).css('position', 'relative'); $(elApp).prepend('<span class="replyApp" title="Reply to Approval Request">⤵</span>'); $(elApp) .find('span.replyApp') .on('click', function () { var insert; var quotereplies = GM_getValue('quotereplies'); var filename = $(element).prev('a').text(); var heading = $(element).closest('div.forum_post').find('div.forum-post__heading'); var username = $(heading).find('a.username').data('username'); var postId = $(heading).find('a.forum-post__id').text().replace('#', ''); var threadId = $(heading) .find('a.forum-post__id') .attr('href') .match(/(?:threadid=)(\d{1,})/i)[1]; if (quotereplies === true) { insert = `[quote=${username}:f${threadId}:${postId}]Approval request for ${filename}[/quote]\n`; } else { insert = '@' + username + ' – ' + filename + ' '; } insertQuickpostText(insert); }); } function displayMiScan() { function setObserver(element) { var elementNode = document.getElementById(element.replace(/[^a-zA-Z0-9]/i, '')); var observer = new MutationObserver(function (mutations) { disconnectObs(); if (mutations[0].attributeName === 'class' && !$(element).hasClass('hidden')) { $(element + ' table.mediainfo').each(function () { if ( $(this) .prev('a') .text() .search(/handjob/i) >= 0 && $(this).siblings('div.mediainfo-feedback').length === 0 ) { populateMiScan(this); } }); } }); var observerConfig = { attributes: true, attributeOldValue: true, }; observer.observe(elementNode, observerConfig); function disconnectObs() { observer.disconnect(); } } $('#post_preview').on('click', function () { setObserver('#quickreplypreview'); }); $('#preview_button').on('click', function () { setObserver('#release_desc_preview'); }); $('.forum-post__bodyguard') .children('table.mediainfo') .each(function () { var feedback = populateMiScan(this); }); $('.forum-post__bodyguard').each(function () { var replyArr = []; $(this) .find('span.replyApp') .each(function () { replyArr.push($(this).closest('table').prev('a').text()); }); if (replyArr.length > 1) { var ibox = ''; var parent = $(this).closest('div.forum_post').find('div.forum-post__heading'); addIconSpacer($(this).closest('.forum-post__body').find('.forum-post__bodyguard').get(0)); var threadId = $(parent) .find('a.forum-post__id') .attr('href') .match(/(?:threadid=)(\d{1,})/i)[1]; var username = $(parent).find('a.username').data('username'); var postId = $(parent).find('a.forum-post__id').text().replace('#', ''); if ($(this).closest('.forum-post__body').find('.source-info').length > 0) ibox = ' ibox'; var insert = `<div data-username="${username}" data-filenames="${replyArr.join( '##' )}" data-threadid="${threadId}" data-postid="${postId}" class="mi-reply-all${ibox}" title="Reply to All">⤵</div>`; if (envs.page !== 'upload') $(this).after(insert); } }); $('div.mi-reply-all').on('click', function () { var insert; var quotereplies = GM_getValue('quotereplies'); let username = $(this).data('username'); let postId = $(this).data('postid'); let threadId = $(this).data('threadid'); let filenames = $(this).data('filenames').split('##'); filenames = filenames.filter(function (item, pos) { return filenames.indexOf(item) == pos; }); // filenames = filenames.map((x) => "[b]" + x + "[/b] — "); // filenames = filenames.join('\n[*] '); if (quotereplies === true) { insert = `[quote=${username}:f${threadId}:${postId}]Approval requests for:\n[indent]${filenames.join( '\n' )}[/indent][/quote]`; insert += filenames.map((x) => `[i]${x}[/i]\n[*] `).join('\n'); } else { insert = '@' + username + '\n[*] ' + filenames.map((x) => '[i]' + x + '[/i] — ').join('\n[*] ') + ' '; } insertQuickpostText(insert); }); showAllMediainfoScans(); } function showAllMediainfoScans() { $('div.mediainfo-feedback').addClass('log-visible'); } function populateMiScan(element, inline = false) { var miScan = new MediainfoScanner($(element).next().text()); if (inline) return miScan; else renderMiScanResults(miScan, element); } function renderMiScanResults(miScan, el) { if (miScan.skip >= 2) return false; var sections = ['filename', 'metatitle', 'x264', 'video', 'audio', 'subtitles', 'chapters', 'misc']; var html = "<div class='mediainfo-feedback-table'>"; var fill = false, insert = ''; if (envs.page === 'upload') insert = ' upload-mediainfo'; for (let x = 0; x < sections.length; x++) { var m = sections[x] === 'metatitle' && miScan.data && miScan.data.metatitle ? " (<span class='lowercase'>" + miScan.data.metatitle + '</span>)' : ''; if ( miScan.feedback[sections[x]].errors.length > 0 || miScan.feedback[sections[x]].warnings.length > 0 || miScan.feedback[sections[x]].advisories.length > 0 ) { html += `<div class="mediainfo-feedback-header">${capFirst(sections[x])}${m}</div>`; fill = true; } if (miScan.feedback[sections[x]].errors.length > 0) { miScan.feedback[sections[x]].errors.map((f) => { html += `<div class="mediainfo-feedback-issue mediainfo-feedback-error">${f.output}</div>`; }); } if (miScan.feedback[sections[x]].warnings.length > 0) { miScan.feedback[sections[x]].warnings.map((f) => { html += `<div class="mediainfo-feedback-issue mediainfo-feedback-warning">${f.output}</div>`; }); } if (miScan.feedback[sections[x]].advisories.length > 0) { miScan.feedback[sections[x]].advisories.map((f) => { html += `<div class="mediainfo-feedback-issue mediainfo-feedback-advisory">${f.output}</div>`; }); } } html += '</div>'; if (!fill) { html = ''; $(el).prev('a').addClass('forum-scan-passed'); } else { $(el).next('blockquote').after(`<div class="mediainfo-feedback${insert}">${html}</div>`); } addMediainfoClick(el); if (envs.page === 'upload') $('.upload-mediainfo').slideDown('slow'); } function resetMiScan() { $('.forum-scan-passed').removeClass('forum-scan-passed'); $('.mediainfo-feedback').slideUp(); } function imageDimensions() { $('img.bbcode__image') .one('load', function () { let wd = this.naturalWidth || this.width; let ht = this.naturalHeight || this.height; $(this).attr('title', wd + ' x ' + ht); }) .each(function () { if (this.complete) { $(this).trigger('load'); } }); } function checkTimer(last, interval) { interval = interval * 60 * 1000; var current = new Date().getTime(); return current > last + interval ? true : false; } function loadHandjobGuide() { var updateCache = checkTimer(timers.guide, settings.cache); var cache = GM_getValue('HJA_cache_guide'); if (updateCache || !cache) { ADMIN_ajaxRequest({ url: urls.guide, method: 'GET' }).then((res) => { timers.guide = new Date().getTime(); GM_setValue('HJA_cache_guide', res); GM_setValue('HJA_timers', timers); populateHandjobGuide(res); }); } else { populateHandjobGuide(GM_getValue('HJA_cache_guide')); } } function populateHandjobGuide(raw) { Array.prototype.clean = function () { return this.filter(((set) => (f) => !set.has(f.link) && set.add(f.link))(new Set())); }; var opt_out = '</optgroup>'; var tp = function (x) { return '<optgroup label="#">'.replace('#', x); }; var select_in = '<select><option value="" disabled selected>-- The HANDJOB Guide --</option>', select_out = '</select>'; var spec_in = tp('Quick Reference'); var sbs_in = tp('Step-by-Step Guides'); var faq_in = tp('Frequently Asked Questions'); var faq_video_in = tp('FAQ -> Video'); var faq_audio_in = tp('FAQ -> Audio'); var faq_subtitles_in = tp('FAQ -> Subtitles'); var faq_other_in = tp('FAQ -> Other'); var faq_approval_in = tp('FAQ -> Approval'); var spec = [], sbs = [], faq = {}; faq.video = []; faq.audio = []; faq.subtitles = []; faq.other = []; faq.approval = []; $('a', raw).each(function () { let href = $(this).attr('href'); href = href.split('#')[1] || null; if (href) { var title = $(this).text().trim().replace(/\:$/, ''); var link = urls.guide + '#' + href; if (href.match(/^spec/)) { spec.push({ link: link, title: title }); } else if (href.match(/^sbs/)) { sbs.push({ link: link, title: title }); } else if (href.match(/^faq/)) { var subsec = $(this) .closest('div.bbcode_indent') .prev('span') .find('strong') .text() .trim() .toLowerCase(); faq[subsec].push({ link: link, title: title }); } } }); var base = 'The HANDJOB Guide -> '; var ov = function (arr, txt) { return arr.clean().map(function (x) { return `<option value="[url=${x.link}]${base}${txt} --> ${x.title}[/url]">${x.title}</option>`; }); }; spec = ov(spec, 'Quick Reference'); sbs = ov(sbs, 'Step-by-Step Guides'); faq.video = ov(faq.video, 'FAQ -> Video'); faq.audio = ov(faq.audio, 'FAQ -> Audio'); faq.subtitles = ov(faq.subtitles, 'FAQ -> Subtitles'); faq.other = ov(faq.other, 'FAQ -> Other'); faq.approval = ov(faq.approval, 'FAQ -> Approval'); var element = select_in.concat( spec_in, spec.join('\n'), opt_out, sbs_in, sbs.join('\n'), opt_out, faq_video_in, faq.video.join('\n'), opt_out, faq_audio_in, faq.audio.join('\n'), opt_out, faq_subtitles_in, faq.subtitles.join('\n'), opt_out, faq_other_in, faq.other.join('\n'), opt_out, faq_approval_in, faq.approval.join('\n'), opt_out, select_out ); $('#handjob-bbcode-select').prepend(element); handjobGuideEventListener(); } function handjobGuideEventListener() { $('#handjob-bbcode-select > select').on('change', function () { var txtToAdd = $(this).val(); insertQuickpostText(txtToAdd); }); } function resetElements() { $('#handjob-bbcode-select > select').prop('selectedIndex', 0); } function replaceUserLinks() { var cb = function (child) { let userid = child.getAttribute('href').split('=').pop(); let username = child.firstChild.nodeValue; child.dataset.username = username; child.dataset.userid = userid; }; domIterate('.forum-post__heading', '.username', cb); settings.fn.update('encodelinks'); } function settingsModule() { var option, optionValue; var settingsDefaults = { quotereplies: false, encodecount: true, borders: true, miscan: true, encodelinks: false, avatarat: true, borderStyle: { wid: 1, colour: '#ffffff' }, }; if (!envs.page) { $('.switch').each(function () { option = $(this).data('option'); var optionValue = GM_getValue(option); if (typeof optionValue !== 'boolean') { optionValue = settingsDefaults[option]; GM_setValue(option, optionValue); } if (optionValue === true) $(this).prop('checked', true); if (optionValue === true && option === 'borders') $('.appearance-setting').fadeIn('fast'); }); $('.switch').on('change', function () { option = $(this).data('option'); optionValue = GM_getValue(option); if ($(this).is(':checked')) { GM_setValue(option, true); updateSetting(option); if (option === 'borders') $('.appearance-setting').fadeIn('fast'); } else { GM_setValue(option, false); updateSetting(option); if (option === 'borders') $('.appearance-setting').fadeOut('fast'); } }); $('.borderStyle').on('change', function () { var wid = $('#appearance-extension-border-width').val(); var colour = $('#appearance-extension-border-colour').val(); GM_setValue('borderStyle', { wid, colour }); }); var bs = GM_getValue('borderStyle'); if (!GM_getValue('borderStyle')) { bs = {}; bs.wid = settingsDefaults.borderStyle.wid; bs.colour = settingsDefaults.borderStyle.colour; GM_setValue('borderStyle', bs); } $('#appearance-extension-border-colour').val(bs.colour); $('#appearance-extension-border-width').val(bs.wid); } function updateSetting(option) { switch (option) { case 'quotereplies': if (GM_getValue(option) === true) { } else { } break; case 'encodecount': if (GM_getValue(option) === true) { $('.newtooltip_text').addClass('show'); } else { $('.newtooltip_text').removeClass('show'); } break; case 'borders': var borders = GM_getValue(option); addBorders(borders); break; case 'bordersStyle': break; case 'avatarat': if (GM_getValue(option) === true) $('.forum-post__avatar img').attr('title', 'Reply to User'); else $('.forum-post__avatar img').attr('title', null); break; case 'encodelinks': var cb; var activate = function () { cb = function (child) { let userid = child.dataset.userid; let newlink = '/torrents.php?type=uploaded&filelist=handjob&userid=' + userid; child.setAttribute('href', newlink); return true; }; domIterate('.forum-post__heading', '.username', cb); }; var deactivate = function () { cb = function (child) { let userid = child.dataset.userid; let newlink = '/user.php?id=' + userid; child.setAttribute('href', newlink); return true; }; domIterate('.forum-post__heading', '.username', cb); }; if (GM_getValue(option) === true) activate(); else deactivate(); break; case 'miscan': if (GM_getValue(option) === true) displayMiScan(); else resetMiScan(); break; } } return { update: updateSetting }; } function showEncodeCount() { $('.forum-post__heading a.username').append( '<div class="newtooltip_text"><div class="loading"><div class="loading-bar"></div><div class="loading-bar"></div><div class="loading-bar"></div><div class="loading-bar"></div></div></div>' ); settings.fn.update('encodecount'); var timer; var delay = 200; $('.forum-post__heading a.username').hover(function () { if (GM_getValue('encodecount') === true) { let el = $(this); let userid = $(this).attr('data-userid'); let href = '/torrents.php?type=uploaded&filelist=handjob&userid=' + userid; if (!$(this).hasClass('newtooltip')) { $(this).addClass('newtooltip'); var options = { method: 'GET', url: href }; ADMIN_ajaxRequest(options) .then((raw) => { var paranoid; if ($('h2.page__title', raw).text() === 'Error 403') paranoid = 'I am paranoid!'; var username = $(el).text().trim(); var count = $('span.search-form__footer__results', raw) .text() .replace('Results', '') .trim(); if (!count) { count = 0; } $('a.username:contains(' + username + ')').each(function () { var tiptext; if (paranoid) tiptext = paranoid; else tiptext = count + ' HANDJOB encodes'; if (count == '1') tiptext = tiptext.slice(0, -1); $(this) .find('.newtooltip_text .loading') .fadeOut('fast', 'linear', function () { $(this).closest('.newtooltip_text').html(tiptext); }); }); }) .catch(function (err) { console.log(err); }); } } }); } function activateAltLinks() { settings.fn.update('avatarat'); $('.forum-post__avatar img').on('click', function () { if (GM_getValue('avatarat') === true) { let link = $(this).closest('.forum_post').find('.forum-post__heading a.username').attr('data-username'); insertQuickpostText('@' + link + ' – '); } }); } function sourceInfoScan() { var sources = ['DVD5', 'DVD9', 'BD25', 'BD50', 'Remux']; var count = 1; for (let i = 0; i < sources.length; i++) { $('a[href*="torrents.php"]:contains(' + sources[i] + ')').each(function () { if ($(this).closest('blockquote').length === 0) { var ptpLink = $(this).attr('href'); ptpLink = '/' + ptpLink.replace(/^\//, ''); let temp = $(this).closest('.forum-post__body'); if (temp.attr('data-sourcelink')) temp.attr('data-sourcelink', temp.attr('data-sourcelink') + '~~' + ptpLink); else { $(this) .closest('.forum-post__body') .append( "<div title='Show info on scanned sources' class='source-info' id='source-info-" + count + "'><img src='" + imgs.sourceInfo + "' style='width:100%;height:100%;'></div><div class='source-info-box' id='source-info-box-" + count + "'></div>" ); $('#source-info-' + count).attr('data-sourceinfo', ptpLink); $(this) .closest('.forum-post__body') .attr('data-sourcelink', ptpLink) .css({ position: 'relative' }); addIconSpacer($(this).closest('.forum-post__body').find('.forum-post__bodyguard').get(0)); } count++; } }); } activateSourceInfo(); } function addIconSpacer(el) { if (el.getElementsByClassName('icon-spacer').length === 0) $(el).prepend('<div class="icon-spacer"> </div>'); else $(el).find('.icon-spacer').css('width', '42px'); } function activateSourceInfo() { $('.source-info').on('click', function () { if (!$(this).next('.source-info-box').hasClass('loaded')) { $('.lightbox').addClass('handjobSourceImage'); $(this).addClass('spinning-info'); var thisId = $(this).attr('id'); thisId = '#source-info-box-' + thisId.replace(/\D/g, ''); let ptpLinks = $(this).closest('.forum-post__body').attr('data-sourcelink'); ptpLinks = ptpLinks.split('~~'); var unique_links = []; $.each(ptpLinks, function (i, el) { if ($.inArray(el, unique_links) === -1) unique_links.push(el); }); gatherSourceInfo(thisId, unique_links); } else { $(this).next('.source-info-box').slideToggle(); $('.lightbox').toggleClass('handjobSourceImage'); } }); $(document).click(function (event) { if ( !$(event.target).closest('.source-info-box').length && !$(event.target).closest('.source-info').length && !$(event.target).closest('#lightbox__shroud').length && !$(event.target).closest('img').length ) { if ($('.source-info-box').is(':visible')) { $('.source-info-box').slideUp(); $('.lightbox').removeClass('handjobSourceImage'); } } if ( !$(event.target).closest('#handjob-settings-window').length && !$(event.target).closest('#handjob-bbcode-settings').length ) { if ($('#handjob-settings-window').is(':visible')) { $('#handjob-settings-window').fadeOut('fast'); } } }); } function gatherSourceInfo(el, links) { var promises = []; var newinput = []; for (var i = 0; i < links.length; i++) { let options = { method: 'GET', url: links[i] }; var reqPromise = new Promise(function (resolve, reject) { ADMIN_ajaxRequest(options) .then((raw) => { var res = {}, imdbId; try { imdbId = $('#imdb-title-link', raw).attr('href').split('/title/')[1].replace(/\//g, ''); } catch (e) { imdbId = null; } try { res.co = $('div#movieinfo.panel .panel__body', raw) .find('strong:contains("Country:")') .parent() .text() .split(':')[1]; } catch (e) { res.co = null; } try { res.lang = $('div#movieinfo.panel .panel__body', raw) .find('strong:contains("Language:")') .parent() .text() .split(':')[1]; } catch (e) { res.lang = null; } res.fileinfo = $('#PermaLinkedTorrentToggler', raw).text(); var torrentRow = $('#PermaLinkedTorrentToggler', raw).closest('tr').next('tr.torrent_info_row'); res.runtime = $('div#movieinfo.panel .panel__body', raw) .find('strong:contains("Runtime:")') .parent() .text() .split(':')[1]; try { $(torrentRow) .find('a[onclick^="BBCode.MediaInfoToggleShow"]') .each(function () { if ($(this).text().search(/.ifo/i) >= 0) return true; res.audio = $(this) .next('table') .find('.mediainfo__section__caption:contains("Audio")') .next('tbody') .text() .split(/#\d+:/g); }); } catch (e) { res.audio = null; } res.images = []; $(torrentRow) .find('.bbcode__image') .each(function () { res.images.push($(this).attr('src')); }); res.year = $('h2.page__title', raw).text().split('[')[1].split(']')[0]; res.title = $('h2.page__title', raw).text().split('[')[0]; if (!imdbId) { var addhtml = populateSourceInfoBox(res); newinput.push(addhtml); return resolve(); } var imdbReq = { url: 'https://www.imdb.com/title/' + imdbId + '/technical?ref_=tt_dt_spec', method: 'GET', }; ADMIN_ajaxRequest(imdbReq) .then((imdbRes) => { res.aspect = $('td.label:contains("Aspect Ratio")', imdbRes) .next('td') .text() .trim() .replace(/\)/g, ')<br>'); var addhtml = populateSourceInfoBox(res); newinput.push(addhtml); resolve(); }) .catch((err) => console.log(err)); }) .catch((err) => console.log(err)); }); promises.push(reqPromise); } Promise.all(promises).then(function () { $(el).html(newinput.join('')).addClass('loaded').slideDown(); $('.source-info').removeClass('spinning-info'); }); } function populateSourceInfoBox(data) { let skip = false; let html = `<div class="source-info-header">${data.title} [${data.year}] – ${data.fileinfo}</div> <div class="source-info-section"><div class="source-info-section-content">`; if (!data.co && !data.lang && !data.runtime && !data.aspect) { skip = true; html += "<div style='width:100%;text-align:center'><em>No information on this source available</em></div>"; } else html += `<table>`; html += data.co ? '<tr><td>Country: </td><td>' + data.co + '</td></tr>' : ''; html += data.lang ? '<tr><td>Language: </td><td>' + data.lang + '</td></tr>' : ''; html += data.runtime ? '<tr><td>Runtime: </td><td>' + data.runtime + '</td></tr>' : ''; html += data.aspect ? "<tr style='vertical-align:top'><td>Aspect Ratio: </td><td>" + data.aspect + '</td></tr>' : ''; if (!skip) html += '</table>'; html += '</div></div>'; if (data.images.length > 0) { html += `<div class="source-info-section"><div class="source-info-section-header">Source Screens</div> <div class="source-info-section-content" style="text-align: center;">`; for (let i = 0; i < data.images.length; i++) { if (i > 5) break; html += `<img class="bbcode__image handjobSourceImage" onclick="lightbox.init(this,500)" alt="${ data.images[i] }" src="${data.images[i]}" title="Source Image ${ i + 1 }" style="max-height:50px;border:1px solid white">`; } html += '</div></div>'; } if (data.audio.length > 0) { html += `<div class="source-info-section"><div class="source-info-section-header">Audio Tracks</div>`; html += `<div class="source-info-section-content">`; for (let i = 0; i < data.audio.length; i++) { html += `<div class="source-info-section-content-item">${data.audio[i]}</div>`; } html += '</div></div>'; } return html; } function insertQuickpostText(insert) { let el; if ($('textarea[id^="editbox"]').length == 1) { el = $('textarea[id^="editbox"]').get(0).id; } else el = 'quickpost'; var caretPos = document.getElementById(el).selectionStart; var caretEnd = document.getElementById(el).selectionEnd; var textAreaTxt = $('#' + el).val(); $('#' + el).val(textAreaTxt.substring(0, caretPos) + insert + textAreaTxt.substring(caretEnd)); $('#' + el).focus(); document.getElementById(el).selectionStart = caretPos + insert.length; document.getElementById(el).selectionEnd = caretPos + insert.length; } function ADMIN_ajaxRequest(options) { return new Promise((resolve, reject) => { let local = new Date(); console.log('ADMIN_ajaxRequest() to ' + options.url + ' @ ' + local.toUTCString()); GM_xmlhttpRequest({ enctype: options.enc, headers: options.headers, method: options.method, data: options.data, url: options.url, onload: function (res) { // Callback for a JSON request, used for the HANDJOB-API if (options.headers && options.headers.Accept === 'application/json') { // If something goes wrong, try once more to login if (res.status !== 200) { console.log(res); return reject('Failed login'); } else { const json = JSON.parse(res.responseText); if (json.accessToken && json.loggedOut !== true) { _access_token = json.accessToken; GM_setValue('accessToken', json.accessToken); } else if (json.loggedOut === true) { GM_deleteValue('accessToken'); } resolve(json); } } // All other types of requests used by the script else resolve(res.responseText); }, onerror: function (e) { console.log(e); reject(e); }, }); }); } function currentPage() { if (envs.path.search(/upload/i) >= 0) return 'upload'; else if (envs.path.search(/torrents.php/i) >= 0) return 'torrent'; else return ''; } function injectStylesheet() { var inject = `<style> div#Bbcode_Toolbar { position: relative; } #admin-lightbox-title { position: absolute; width: 100%; height: 40px; display: flex; align-items: center; justify-content: center; top: 0; font-weight: bold; font-size: 1.1rem; } #handjob-bbcode-icon { background-color: #ddd; background-image: url(${imgs.handjobBadge}); background-repeat: no-repeat; background-position: center; background-size: cover; cursor: pointer; } #handjob-bbcode-select > select { width: 200px; margin: 0 10px; } .loading {position: relative;} .loading-bar { display: inline-block; width: 4px; height: 0.8em; border-radius: 3px; animation: loading 1s ease-in-out infinite; } .loading-bar:nth-child(1) { background-color: #035B99; animation-delay: 0; } .loading-bar:nth-child(2) { background-color: #1091EC; animation-delay: 0.09s; } .loading-bar:nth-child(3) { background-color: #3BA3ED; animation-delay: .18s; } .loading-bar:nth-child(4) { background-color: #66B6EE; animation-delay: .27s; } @keyframes loading{ 0%{transform: scale(1);} 20% {transform:scale(1, 2.2);} 40% {transform: scale(1);} } #handjob-bbcode-select{ background:#ddd; width:0px; height:24px; float:left; overflow-x:hidden; display:flex; align-items:center; } input.switch:empty{margin-left: -9999px;} input.switch:empty ~ label{ position: relative; float: left; line-height: 19px; text-indent: 48px; margin: 9px 0; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } input.switch:empty ~ label:before, input.switch:empty ~ label:after{ position: absolute; display: block; top: 0; bottom: 0; left: 0; content: " "; width: 43px; background-color: #c33; border-radius: 4px; box-shadow: inset 0 3px 0 rgba(0,0,0,0.3); -webkit-transition: all 150ms ease-in; transition: all 150ms ease-in; } input.switch:empty ~ label:after { width: 17px; top: 1px; bottom: 1px; margin-left: 1px; background-color: #fff; border-radius: 2px; box-shadow: inset 0 -2px 0 rgba(0,0,0,0.2); } input.switch:checked ~ label:before {background-color: #393;} input.switch:checked ~ label:after {margin-left: 25px;} #handjob-settings-window{ font-size: 12px; font-family:Helvetica; font-weight:bold; color:#111; text-transform:capitalize; padding:10px; display:none; position: absolute; width: 160px; height: 240px; background: #ddd; border: 2px solid black; right: 35px; top: -230px; } #handjob-bbcode-settings { background-color:#ddd; background-image:url(${imgs.settingsIcon}); background-repeat:no-repeat; background-position:center; background-size:cover; cursor:pointer } .mediainfo-feedback-table { width:100%; } span.lowercase { text-transform: none; } .feedback-advisories { background:#d9edf7; border:1px solid #bce8f1; color:#31708f; } .feedback-warnings { background:#fcf8e3; border:1px solid #faebcc; color:#8a6d3b; } .feedback-errors { background:#f2dede; border:1px solid #ebccd1; color:#a94442; } .mediainfo-feedback-issue, .inline-mi-scan-issue { color: ${css.text}; margin-left: 20px; padding: 10px 10px 10px 20px; } .mediainfo-feedback-issue { position: relative; } .mediainfo-feedback-issue::after { position: absolute; right: 10px; top: 50%; opacity: 0; transform: translateY(-50%); transition: opacity 150ms ease-in-out; } .mediainfo-feedback-issue:hover::after { opacity: 0.5; } .mediainfo-feedback-error:after { color: ${css.error}; content: "Error"; } .mediainfo-feedback-warning:after { color: ${css.warning}; content: "Warning"; } .mediainfo-feedback-advisory:after { color: ${css.advisory}; content: "Advisory"; } .mediainfo-feedback-advisory, .inline-advisory { border-left: 8px solid ${css.advisory}; } .mediainfo-feedback-warning, .inline-warning { border-left: 8px solid ${css.warning}; } .mediainfo-feedback-error, .inline-error { border-left: 8px solid ${css.error}; } .mediainfo-feedback-header, .inline-mi-header, .source-info-header { color: ${css.text}; text-align:center; padding:7px 0px; font-weight:bold; background: ${css.header}; text-transform: uppercase; } .source-info-box { top: 25px; z-index: 1000; min-width: 500px; background-color: ${css.background}; border: 4px solid ${css.header}; color: #fff; display: none; right: -1px; position: absolute; box-shadow: 0 0 10px 0 #333; } .source-info-section-content { padding: 10px; color: ${css.text}; } .source-info-section-content td { padding: 2px; } .source-info-section-content td:first-child { padding-right: 10px; min-width: 100px; } .source-info-section-header { text-align: left; color: ${css.text}; background: ${css.header}; padding: 1px 20px; } .inline-mi-header span, .source-info-header { text-transform: none !important; } .inline-mi-newfile { background: ${css.header}; padding: 7px 0px; display: flex; align-items: center; color: ${css.text}; margin-left: -4px; width: calc(100% + 8px); border-style: dotted; border-color: ${css.text}; border-width: 0 0 1px 0; } .inline-mi-newfile img { height: 1rem; width: auto; margin: 0 5px; } .mediainfo-feedback { background: ${css.background}; margin-top: 5px; padding: 0; font-weight: bold; font-family: "helvetica neue", helvetica, tahoma, sans-serif; transition: max-height 0.5s cubic-bezier(0, 1, 0, 1); } .upload-mediainfo { display: none; } .mediainfo-feedback.log-visible { visibility: visible; } .lightbox.handjobSourceImage img { border:1px solid white; } .tooltip-loader { margin:0 auto; border: 6px solid #f3f3f3; border-top: 6px solid #3498db; border-radius: 50%; width: 50px; height: 50px; animation: spin 2s linear infinite; } .spinning-info img { animation: spin 2s linear infinite; } @keyframes spin { 0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);} } .newtooltip .newtooltip_text { min-height:2em; top: -5px; left: calc(100% + 10px); } .newtooltip .newtooltip_text:not(.inline-mi-tooltip)::after { z-index:9999; content: " "; position: absolute; top: 50%; right: 100%; margin-top: -13px; border-width: 13px; border-style: solid; border-color: transparent ${css.header} transparent transparent; } .newtooltip {position:relative} .newtooltip_text { pointer-events: none; opacity : 0; transition:opacity 150ms ,min-height 150ms; position:absolute; z-index:9999; width: 200px; background-color: ${css.background}; border:2px solid ${css.header}; color: ${css.text}; text-align: center; padding: 5px 0; } .newtooltip_text.inline-mi-tooltip { text-align: left; width: 500px; top: 1rem; left: -1px; background: ${css.background}; padding: 0; border: 4px solid ${css.header}; border-radius: 0; } .newtooltip_text.inline-mi-tooltip > .inline-mi-header:first-child, .source-info-box > .source-info-header:first-child { padding-top: 3px; } .newtooltip_text.inline-mi-tooltip.reverse-position { bottom: 1rem !important; top: unset !important; } .newtooltip:hover .newtooltip_text.show { opacity:1.0; min-height:2.1em; } .scan-passed { color: ${css.passed} !important; font-weight: bold; } .forum-scan-passed { color: ${css.passed} !important; } .scan-errors, .inline-mi-scan-errors { color: ${css.error} !important; font-weight: bold; } .scan-warnings, .inline-mi-scan-warnings { color: ${css.warning} !important; font-weight: bold; } .scan-advisories, .inline-mi-scan-advisories { color: ${css.advisory} !important; font-weight: bold; } span.replyApp { cursor:pointer; border-radius: 0px 0px 0px 10px; color: white; background: ${css.header}; padding: 0 10px; position: absolute; right: -1px; top: -1px; transition: filter 150ms ease-in-out; } .icon-spacer { height: 16px; width: 16px; position: relative; float: right; } div.source-info { background: ${css.header}; cursor: pointer; width:26px; height:26px; position:absolute; top: -1px; right: -1px; transition: filter 150ms ease-in-out; padding: 4px; z-index: 1001; } .replyApp + table.mediainfo__section caption { min-width: 100px; } #return-message { display: none; width: 100%; text-align: center; padding: 1rem 0; font-style: italic; color: ${css.header}; font-weight: bold; } div.mi-reply-all { cursor: pointer; position: absolute; top: -1px; right: -1px; width: 26px; height: 26px; background: ${css.header}; font-size: 1.2rem; color: white; text-align: center; line-height: 30px; transition: filter 150ms ease-in-out; } .appearance-setting img { transition: opacity 150ms ease-in-out; opacity: 1; } .appearance-setting:hover img { opacity: 0.5; } div.source-info:hover, div.mi-reply-all:hover, a.handjob-add-member:hover, span.replyApp:hover {filter: brightness(0.6);} div.mi-reply-all.ibox {right: 25px;} div.source-audio-box { display: flex; flex-flow: column; padding: 2px 0px; margin-top: 10px; } div.source-audio-box-title { font-weight: bold; padding: 2px; } div.source-audio-box-item { font-style: italic; } span.superscript { font-size: 0.7em; vertical-align: super; } #admin-lightbox { display: none; position: fixed; width: 100%; height: 100%; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; font-size: 11px; } #admin-lightbox-container { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: rgba(0,0,0,0.8); } #admin-lightbox-content { position: relative; width: 100%; height:100%; overflow-y: auto; background-color: ${css.pageBackground}; padding-top: 35px; } #admin-lightbox-exit, #admin-alert-exit { position: absolute; top: 10px; right: 10px; width: 30px; height: 30px; background: #555; text-align: center; font-size: 1.5rem; font-weight: bold; color: #fff; cursor: pointer; transition: color 150ms ease-in; z-index: 1; } #admin-alert-exit { font-size: 1rem; height: 20px; width: 20px; top: 5px; right: 5px; background: #000; } #admin-alert-exit:hover, #admin-lightbox-exit:hover { color: #ccc; } #admin-lightbox-alert { display: none; position: absolute; background-color: ${css.pageBackground2}; width: 85%; padding: 30px 20px 10px 20px; border: 2px solid #888; max-height: 100%; overflow-y: auto; } .admin-lightbox-table { width: 100%; } .admin-lightbox-table thead { background-color: ${css.header}; text-transform: uppercase; font-weight: bold; color: ${css.text}; } .admin-lightbox-table thead td, .admin-lightbox-table-item td { padding: 5px 10px; vertical-align: top; } .admin-lightbox-table-item:nth-child(even) { background-color: ${css.background}; } .admin-lightbox-table-tab { width: 100%; } #admin-lightbox-table-tab2 { display: none; } .admin-lightbox-table-header { width: 100%; display: flex; flex-flow: row; justify-content: center; } .admin-lightbox-table-header div { padding: 5px 20px; background: ${css.background}; font-weight: bold; color: ${css.offset}; cursor: pointer; } .admin-lightbox-table-header div.active-tab { background: ${css.header}; } .admin-lightbox-table-header div:nth-child(1) { border-radius: 10px 0 0 0; } .admin-lightbox-table-header div:nth-child(2) { border-radius: 0 10px 0 0; } .auth-loader { position: relative; } .auth-loader:before { position: absolute; content: ""; top: -25px; left: 10px; margin:0 auto; border: 5px solid ${css.offset}; border-top: 5px solid #3498db; border-radius: 50%; width: 25px; height: 25px; animation: spin 2s linear infinite; } .admin-lightbox-table-search-header { position: absolute; top: 0; left: 50%; transform: translateX(-50%); font-weight: bold; font-size: 0.8rem; margin-top: 5px; white-space: nowrap; } span.filename-typo { position: relative; } .settings-option { height: 37px; position: relative; } span.filename-typo::before { z-index:9999; content: " "; position: absolute; top: -0.3rem; right: -0.1rem; border-width: 5px; border-style: solid; border-color: ${css.error} transparent transparent transparent; } .appearance-setting { width: 20px; height: 14px; position: absolute; right: -5px; top: 50%; transform: translateY(-50%); display: none; cursor: pointer; } .appearance-setting img { max-width: 100%; height: auto; } .appearance-extension-inner { display: flex; justify-content: space-between; font-size: 0.8em; width: 100%; height: 100%; } .appearance-extension-outer { position: absolute; width: calc(100% + 20px); padding: 4px; z-index: 999999; background: linear-gradient(to bottom, #ddd, #aaa); top: 100%; display: none; margin-left: -10px; padding: 4px 10px; } div.filename-error { width: 100%; text-align: center; margin-top: 0.5rem; font-family: monospace, monospace; line-height: 1.4rem; } </style>`; $('head').append(inject); } Date.prototype.getMonthName = function () { var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; return months[this.getMonth()]; }; function decapFirst(string) { return string.charAt(0).toLowerCase() + string.slice(1); } function capFirst(string) { return string.charAt(0).toUpperCase() + string.slice(1); } function domIterate(parents, child, cb) { var childNode; if (parents.charAt(0) === '.') { parents = document.getElementsByClassName(parents.replace('.', '')); } else if (parents.charAt(0) === '#') { parents = document.getElementById(parents.replace('#', '')); } for (let i = 0; i < parents.length; i++) { if (child.charAt(0) === '.') { childNode = parents[i].getElementsByClassName(child.replace('.', '')); } else if (child.charAt(0) === '#') { childNode = parents[i].getElementById(child.replace('#', '')); } if (childNode.length === 0) continue; cb(childNode[0]); } } function Create(element) { // Creates a DOM element from string (accepts "type#id.class{data}active:true-id:1") let type = element.match(/^\w+[^#.{]?/)[0]; let id = element.match(/#([^.{]*)/) || null; if (id && id.length > 0) id = id[1]; let classes = element.match(/\.([^{#]*)/) || []; if (classes && classes.length > 0) classes = classes[1].split('.'); let data = element.match(/{[0-9a-zA-Z_-]*}[^.{#]*/gi) || []; data = data.map((x) => { let sp = x.replace('{', '').split('}'); return { att: sp[0], val: sp[1] }; }); let el = document.createElement(type); if (id) el.id = id; for (let i = 0; i < classes.length; i++) { el.classList.add(classes[i]); } for (let i = 0; i < data.length; i++) { data[i].val = data[i].val.replace('null', '#'); el.setAttribute(data[i].att, data[i].val); } return el; } function parseHTMLtoDOM(raw) { const parser = new DOMParser(); return parser.parseFromString(raw, 'text/html'); } /** * Abbreviate supplied text string to chosen length * @param {string} str - The string to truncate * @param {number} maxLength - The maximum length allowed before truncating * @param {boolean} split - Place ellipses in the middle of the string * @returns {string} */ function truncate({ str = '', maxLength = 30, split = false } = {}) { if (typeof str !== 'string' || typeof maxLength !== 'number' || typeof split !== 'boolean') { return ''; } // Replace incoming special characters for length check let y = str.replace(/&[^\s;]*;/g, 'a'); // If string is shorter than maxLength, return unchanged if (y.length <= maxLength) return str; // Return string with an ellipsis break else if (split === true) return ( str .substr(0, maxLength / 2) .trim() .replace(/\.$/, '') + ' … ' + str .substr(str.length - maxLength / 2) .trim() .replace(/^\./, '') ); // Return string with an ellipsis at the end else return str.substr(0, maxLength - 1).trim() + '…'; } function debounce(func, wait, immediate) { let timeout; return function () { const context = this, args = arguments; const later = function () { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } })();