// ==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);
};
}
})();
Donate for the site OpenUserJS
Are you sure you want to go to an external site to donate a monetary value?
WARNING: Some countries laws may supersede the payment processors policy such as the GDPR and PayPal. While it is highly appreciated to donate, please check with your countries privacy and identity laws regarding privacy of information first. Use at your utmost discretion.