Last active
April 15, 2020 13:04
-
-
Save kosorin/17750e5281f03942712c6022158b190a to your computer and use it in GitHub Desktop.
SerialZone.cz UserScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name SerialZone.cz PLUS | |
// @namespace https://www.serialzone.cz/ | |
// @version 3.1.1 | |
// @author David Kosorin | |
// @match https://www.serialzone.cz/* | |
// @grant none | |
// ==/UserScript== | |
try { | |
console.log("========== SerialZone.cz <PLUS> ========== BEGIN"); | |
(function () { | |
'use strict'; | |
const classPrefix = "szp-"; | |
/** | |
* @typedef {number} SeriesId | |
*/ | |
/** | |
* @readonly | |
* @enum {number} | |
*/ | |
const SeriesState = { | |
None: 0, | |
Watch: 1 << 1, | |
Follow: 1 << 2, | |
Love: 1 << 3, | |
Ignore: 1 << 4, | |
}; | |
/** | |
* @readonly | |
* @enum {number} | |
*/ | |
const ActionType = { | |
Watch: 1, | |
Follow: 2, | |
Love: 3, | |
Ignore: 4, | |
}; | |
class ActionData { | |
/** | |
* @type {Test} | |
*/ | |
action; | |
/** | |
* @type {string} | |
*/ | |
icon; | |
/** | |
* @type {string} | |
*/ | |
title; | |
/** | |
* @param {string} action | |
* @param {string} icon | |
* @param {string} title | |
*/ | |
constructor(action, icon, title) { | |
this.action = action; | |
this.icon = icon; | |
this.title = title; | |
} | |
} | |
class ActionButton { | |
/** | |
* @type {ActionType} | |
*/ | |
type; | |
/** | |
* @type {SeriesId} | |
*/ | |
seriesId; | |
/** | |
* @type {boolean} | |
*/ | |
confirmAction; | |
/** | |
* @type {function (ActionButton): void} | |
*/ | |
onAction; | |
/** | |
* @type {boolean} | |
*/ | |
currentState; | |
/** | |
* @type {*} | |
*/ | |
data; | |
/** | |
* @type {ActionData} | |
*/ | |
get currentData() { | |
return this.data[this.currentState]; | |
} | |
/** | |
* @type {HTMLElement} | |
*/ | |
element; | |
/** | |
* @param {ActionType} type | |
* @param {SeriesId} seriesId | |
* @param {boolean} initialState | |
* @param {*} data | |
* @param {function} [onAction] | |
* @param {boolean} [confirmAction] | |
*/ | |
constructor(type, seriesId, initialState, data, onAction, confirmAction) { | |
let root = document.createElement("span"); | |
let link = document.createElement("a"); | |
link.setAttribute("href", "#"); | |
link.addEventListener("click", e => { | |
e.preventDefault(); | |
if (this.currentState && this.confirmAction && !confirm(this.currentData.title + "?")) { | |
return; | |
} | |
$.ajax({ | |
type: "POST", | |
url: "https://www.serialzone.cz/ajax/moje-serialy/", | |
data: { | |
serial: this.seriesId, | |
akce: this.currentData.action, | |
}, | |
cache: false, | |
}).done(() => { | |
this.toggleState(); | |
if (this.onAction) { | |
this.onAction(this); | |
} | |
}); | |
}); | |
root.appendChild(link); | |
this.type = type; | |
this.seriesId = parseInt(seriesId); | |
this.confirmAction = confirmAction; | |
this.onAction = onAction; | |
this.data = data; | |
this.element = root; | |
this.setState(initialState); | |
} | |
/** | |
* @param {boolean} state | |
* @param {boolean} [visible] | |
* @returns {void} | |
*/ | |
setState(state, visible) { | |
this.currentState = state; | |
let link = this.element.firstChild; | |
link.innerHTML = this.currentData.icon; | |
link.setAttribute("title", this.currentData.title); | |
link.setAttribute("alt", this.currentData.title); | |
if (visible !== undefined) { | |
show(this.element, visible); | |
} | |
} | |
/** | |
* @returns {void} | |
*/ | |
toggleState() { | |
this.setState(!this.currentState); | |
} | |
} | |
function show(element, value) { | |
element.style.display = value ? "block" : "none"; | |
} | |
function getUserUrl() { | |
let root = document.querySelector("img.user-ico-user"); | |
if (!root) { | |
return null; | |
} | |
return root.parentNode.href; | |
} | |
function processMenu(userUrl) { | |
function normalizeUrl(url, originUrl, append) { | |
url = url.replace(originUrl, ""); | |
url = url.substring(0, url.length - 1); | |
if (append) { | |
url = url + "/" + append | |
} | |
return url; | |
} | |
let menu = document.querySelector("ul.menu"); | |
if (!menu) { | |
return; | |
} | |
if (!userUrl) { | |
let originUrl = location.origin + "/"; | |
let links = menu.querySelectorAll("li.fl-left > a"); | |
for (let i = 0; i < links.length; i++) { | |
let link = links[i]; | |
let current = normalizeUrl(link.href, originUrl); | |
switch (current) { | |
case "serialy": | |
case "clanky": | |
case "forum": | |
break; | |
default: | |
link.parentNode.remove(); | |
break; | |
} | |
} | |
return; | |
} | |
function setLinkStyle(link) { | |
link.style.fontWeight = "bolder"; | |
} | |
function setOtherLinkStyle(link) { | |
link.style.fontSize = "smaller"; | |
} | |
function addLink(url, text, beforeLink, afterLink) { | |
let link = document.createElement("a"); | |
link.innerHTML = text; | |
link.setAttribute("href", url); | |
if (location.href == url) { | |
link.setAttribute("class", "navactive"); | |
} | |
setLinkStyle(link); | |
let item = document.createElement("li"); | |
item.setAttribute("class", "fl-left"); | |
item.appendChild(link); | |
if (beforeLink) { | |
menu.insertBefore(item, beforeLink.parentNode); | |
} else if (afterLink) { | |
menu.insertBefore(item, afterLink.parentNode.nextSibling); | |
} else { | |
menu.appendChild(item); | |
} | |
return link; | |
} | |
let serialsLink; | |
let calendarLink; | |
let watchlistLink; | |
let originUrl = location.origin + "/"; | |
let links = menu.querySelectorAll("li.fl-left > a"); | |
for (let i = 0; i < links.length; i++) { | |
let link = links[i]; | |
let current = normalizeUrl(link.href, originUrl); | |
switch (current) { | |
case "serialy": | |
setOtherLinkStyle(link); | |
serialsLink = link; | |
break; | |
case "kalendar": | |
setLinkStyle(link); | |
calendarLink = link; | |
break; | |
case "watchlist": | |
setLinkStyle(link); | |
watchlistLink = link; | |
break; | |
case "hry": | |
case "playlist": | |
case "timeline": | |
case normalizeUrl(userUrl, originUrl, "moje"): | |
show(link.parentNode, false); | |
break; | |
default: | |
setOtherLinkStyle(link); | |
break; | |
} | |
} | |
calendarLink.parentNode.remove(); | |
watchlistLink.parentNode.remove(); | |
menu.appendChild(calendarLink.parentNode); | |
menu.appendChild(watchlistLink.parentNode); | |
let sawLink = addLink(userUrl + "rozkoukane/", "Rozkoukané", null, watchlistLink); | |
let favoritesLink = addLink(userUrl + "moje/", "Oblíbené", null, sawLink); | |
let wannaSeeLink = addLink(userUrl + "chci-videt/", "Chci vidět", null, favoritesLink); | |
} | |
function processCalendar() { | |
let root = document.querySelector("#calendar"); | |
if (!root) { | |
return; | |
} | |
let today = root.querySelector("td.linked-day"); | |
if (!today) { | |
return; | |
} | |
today.style.border = "2px solid red"; | |
} | |
function processWatchlist(userUrl) { | |
if (!userUrl) { | |
return; | |
} | |
let root = document.querySelector("#watchlist"); | |
if (!root) { | |
return; | |
} | |
/** | |
* @readonly | |
* @enum {string} | |
*/ | |
const GroupId = { | |
Bind: "V poslední době odškrtávané", | |
Follow: "Sledované", | |
Love: "Zamilované", | |
Ignore: "Ignorované", | |
}; | |
/** | |
* @readonly | |
* @enum {SeriesState} | |
*/ | |
const GroupSeriesState = { | |
[GroupId.Bind]: SeriesState.None, | |
[GroupId.Follow]: SeriesState.Follow, | |
[GroupId.Love]: SeriesState.Follow | SeriesState.Love, | |
[GroupId.Ignore]: SeriesState.Follow | SeriesState.Ignore, | |
}; | |
/** | |
* @readonly | |
* @enum {number} | |
*/ | |
const CategoryId = { | |
Other: 1, | |
Follow: 2, | |
Ignore: 3, | |
/** | |
* @param {SeriesState} seriesState | |
* @returns {CategoryId} | |
*/ | |
getFromSeriesState(seriesState) { | |
if (!seriesState) { | |
return CategoryId.Other; | |
} | |
if ((seriesState & SeriesState.Ignore) != 0) { | |
return CategoryId.Ignore; | |
} else if ((seriesState & SeriesState.Watch) != 0) { | |
return CategoryId.Other; | |
} else { | |
return CategoryId.Follow; | |
} | |
} | |
}; | |
/** | |
* @readonly | |
* @enum {string} | |
*/ | |
const CategoryTitle = { | |
[CategoryId.Other]: "Ostatní", | |
[CategoryId.Follow]: "Sledované", | |
[CategoryId.Ignore]: "Ignorované", | |
}; | |
class Category { | |
/** | |
* @type {number} | |
*/ | |
categoryId; | |
/** | |
* @type {string} | |
*/ | |
title; | |
/** | |
* @type {HTMLElement} | |
*/ | |
element; | |
/** | |
* @type {HTMLElement} | |
*/ | |
seriesElement; | |
/** | |
* @param {number} categoryId | |
*/ | |
constructor(categoryId) { | |
this.categoryId = categoryId; | |
this.title = CategoryTitle[categoryId]; | |
let seriesElementClass = classPrefix + "category-series"; | |
this.element = $.parseHTML(` | |
<div> | |
<div class="list-h list-h-lightblue mto20 mbo10" data-kategorie-m="${this.categoryId}"> | |
<div class="lhhead">${this.title}</div> | |
<div class="cleaner"></div> | |
</div> | |
<div class="${seriesElementClass}"> | |
</div> | |
</div> | |
`)[1]; | |
this.seriesElement = this.element.querySelector("." + seriesElementClass); | |
} | |
} | |
class Series { | |
/** | |
* @type {SeriesId} | |
*/ | |
seriesId; | |
/** | |
* @type {SeriesState} | |
*/ | |
state; | |
/** | |
* @type {CategoryId} | |
*/ | |
get categoryId() { | |
return CategoryId.getFromSeriesState(this.state); | |
} | |
/** @type {Episode} */ | |
episode; | |
buttons; | |
/** | |
* @type {HTMLElement} | |
*/ | |
element; | |
/** | |
* @type {HTMLElement} | |
*/ | |
buttonsElement; | |
/** | |
* @param {SeriesId} seriesId | |
* @param {HTMLElement} element | |
*/ | |
constructor(seriesId, element) { | |
this.seriesId = seriesId; | |
this.buttonsElement = document.createElement("div"); | |
this.buttonsElement.classList.add(classPrefix + "buttons"); | |
this.buttonsElement.setAttribute("style", "float: right; width: 48px;"); | |
this.element = element; | |
this.element.querySelector("div.watchlist-content > div").parentNode.appendChild(this.buttonsElement); | |
this.updateEpisode(); | |
} | |
/** | |
* @param {Series} a | |
* @param {Series} b | |
* @returns {number} | |
*/ | |
static compare(a, b) { | |
let result = 0; | |
// Kategorie | |
result = a.categoryId - b.categoryId; | |
if (result) { | |
return result; | |
} | |
// Epizoda | |
result = Episode.compare(a.episode, b.episode); | |
if (result) { | |
return result; | |
} | |
return result; | |
} | |
/** | |
* @returns {void} | |
*/ | |
updateEpisode() { | |
this.episode = new Episode(this.seriesId, this.element); | |
this.updateOpacity(); | |
} | |
/** | |
* @param {boolean} [opaque] | |
* @returns {void} | |
*/ | |
updateOpacity(opaque) { | |
this.element.style.opacity = (opaque || (this.episode && this.episode.isReady)) ? 1 : 0.4; | |
} | |
/** | |
* @param {Category} category | |
* @returns {void} | |
*/ | |
onRender(category) { | |
this.element.dataset.kategorie = category.categoryId; | |
this.createButtons(); | |
this.element.addEventListener("mouseover", () => this.updateOpacity(true)); | |
this.element.addEventListener("mouseout", () => this.updateOpacity(false)); | |
this.element.addEventListener("DOMSubtreeModified", e => { | |
for (let target = e.target; target && target != this.element; target = target.parentNode) { | |
if (target.matches(".eh" + this.seriesId)) { | |
this.updateEpisode(); | |
break; | |
} | |
} | |
}, false); | |
} | |
/** | |
* @returns {void} | |
*/ | |
createButtons() { | |
if (this.buttons) { | |
return; | |
} | |
this.buttons = {}; | |
this.buttons[ActionType.Love] = new ActionButton(ActionType.Love, this.seriesId, (this.state & SeriesState.Love) != 0, { | |
[false]: new ActionData("love", "🤍", "Do zamilovaných"), | |
[true]: new ActionData("unlove", "❤️", "Odebrat ze zamilovaných"), | |
}) | |
this.buttonsElement.appendChild(this.buttons[ActionType.Love].element); | |
this.buttons[ActionType.Ignore] = new ActionButton(ActionType.Ignore, this.seriesId, (this.state & SeriesState.Ignore) != 0, { | |
[false]: new ActionData("ignore", "👁️", "Do ignorovaných"), | |
[true]: new ActionData("unignore", "🔕", "Odebrat z ignorovaných"), | |
}); | |
this.buttonsElement.appendChild(this.buttons[ActionType.Ignore].element); | |
this.buttons[ActionType.Follow] = new ActionButton(ActionType.Follow, this.seriesId, (this.state & SeriesState.Follow) != 0, { | |
[false]: new ActionData("pridat", "➕", "Přidat do sledovaných"), | |
[true]: new ActionData("odebrat", "➖", "Odebrat ze sledovaných"), | |
}, button => { | |
this.buttons[ActionType.Love].setState(false, button.currentState); | |
this.buttons[ActionType.Ignore].setState(false, button.currentState); | |
}, true); | |
this.buttonsElement.appendChild(this.buttons[ActionType.Follow].element); | |
if (!this.buttons[ActionType.Follow].currentState) { | |
this.buttons[ActionType.Love].setState(false, false); | |
this.buttons[ActionType.Ignore].setState(false, false); | |
} | |
} | |
} | |
class Episode { | |
/** | |
* @type {SeriesId} | |
*/ | |
seriesId; | |
/** | |
* @type {number} | |
*/ | |
serie; | |
/** | |
* @type {number} | |
*/ | |
episode; | |
/** | |
* @type {string} | |
*/ | |
title; | |
/** | |
* @type {Date} | |
*/ | |
date; | |
/** | |
* @type {boolean} | |
*/ | |
get isReady() { | |
if (!this.date) { | |
return false; | |
} | |
const today = new Date().setHours(0, 0, 0, 0); | |
return this.date <= today; | |
} | |
/** | |
* @param {SeriesId} seriesId | |
* @param {HTMLElement} element | |
*/ | |
constructor(seriesId, element) { | |
this.seriesId = seriesId; | |
// Serie + Episode | |
let serieEpisodeParagraph = element.querySelector("div.watchlist-epnr > div").innerText; | |
if (serieEpisodeParagraph.match(/^\s*$/)) { | |
return null; | |
} | |
[this.serie, this.episode] = serieEpisodeParagraph.split("×").map(x => parseInt(x)); | |
// Title | |
this.title = element.querySelector("div.watchlist-content a.dfl2-name").innerText; | |
// LikeBox | |
let likeBoxElement = element.querySelector("div.likebox"); | |
if (likeBoxElement) { | |
likeBoxElement.remove(); | |
} | |
// Date | |
try { | |
let dateElement = element.querySelectorAll("div.watchlist-epname div.wep-name")[1]; | |
let { groups: { day, month, year } } = /:\s*((?<day>\d+)\.\s*(?<month>\d+)\.\s*(?<year>\d+))/.exec(dateElement.innerText); | |
this.date = new Date(year, month - 1, day); | |
dateElement.querySelector("span.greentext,span.redtext").parentNode.classList.remove("font11"); | |
} catch (error) { | |
this.date = null; | |
} | |
// Voting button | |
for (const button of element.querySelectorAll("div.wlp-in > a")) { | |
if (button.getAttribute("href") == "#nic" && this.isReady) { | |
button.classList.add("opacity40"); | |
button.style.margin = "0"; | |
button.style.marginLeft = "44px"; | |
} else { | |
button.remove(); | |
} | |
} | |
} | |
/** | |
* @param {Episode} a | |
* @param {Episode} b | |
* @returns {number} | |
*/ | |
static compare(a, b) { | |
const maxDate = new Date(8640000000000000); | |
let result = 0; | |
// Datum | |
result = (a.date || maxDate) - (b.date || maxDate); | |
if (result) { | |
return result; | |
} | |
// Název | |
result = a.title.localeCompare(b.title); | |
return result; | |
} | |
} | |
class Watchlist { | |
/** | |
* | |
*/ | |
series; | |
/** | |
* | |
*/ | |
categories; | |
/** | |
* @type {HTMLElement} | |
*/ | |
element; | |
/** | |
* @param {HTMLElement} element | |
*/ | |
constructor(element) { | |
this.element = element; | |
this.series = {}; | |
let parseGroupId = function (element) { | |
return element.querySelector("div.lhhead").innerHTML; | |
}; | |
let parseSeriesId = function (element) { | |
return parseInt(element.querySelector("div.wlp-in").dataset["sid"]); | |
}; | |
let currentGroupId; | |
for (let element of this.element.children) { | |
if (element.classList.contains("list-h")) { | |
currentGroupId = parseGroupId(element); | |
} else if (element.classList.contains("def-list2-sz3")) { | |
let seriesId = parseSeriesId(element); | |
let series = this.series[seriesId]; | |
if (!series) { | |
series = new Series(seriesId, element); | |
this.series[seriesId] = series; | |
} | |
series.state |= GroupSeriesState[currentGroupId]; | |
} | |
} | |
} | |
/** | |
* @returns {void} | |
*/ | |
render() { | |
this.categories = {}; | |
this.element.innerHTML = ""; | |
this.element.classList.add("mbo40"); | |
this.renderCategory(CategoryId.Other); | |
this.renderCategory(CategoryId.Follow); | |
this.renderCategory(CategoryId.Ignore); | |
this.renderFilter(); | |
} | |
/** | |
* @param {CategoryId} categoryId | |
* @returns {void} | |
*/ | |
renderCategory(categoryId) { | |
/** @type {[Series]} */ | |
let categorySeries = Object.values(this.series).filter(x => x.categoryId == categoryId).sort(Series.compare); | |
if (categorySeries.length == 0) { | |
return; | |
} | |
let category = new Category(categoryId); | |
this.categories[categoryId] = category; | |
this.element.appendChild(category.element); | |
for (const series of categorySeries) { | |
series.onRender(category); | |
category.seriesElement.appendChild(series.element); | |
} | |
} | |
/** | |
* @returns {void} | |
*/ | |
renderFilter() { | |
let filterIgnoredElementClass = classPrefix + "filter-ignored"; | |
let filterElement = $.parseHTML(` | |
<div class="list-h list-h-grey mto10"> | |
<div class="check-head"> | |
<a href="#" class="filtr-videni ${filterIgnoredElementClass}">zobrazit ignorované <div class="ch-uncheck" id="filter-ignored"></div></a> | |
</div> | |
<div class="cleaner"></div> | |
</div> | |
`)[1]; | |
this.element.parentNode.insertBefore(filterElement, this.element); | |
let filterIgnoredElement = filterElement.querySelector("." + filterIgnoredElementClass); | |
filterIgnoredElement.addEventListener("click", e => { | |
e.preventDefault(); | |
let checkElement = filterIgnoredElement.querySelector("div"); | |
checkElement.classList.toggle("ch-check"); | |
checkElement.classList.toggle("ch-uncheck"); | |
let showIgnored = checkElement.classList.contains("ch-check"); | |
this.filter(CategoryId.Ignore, showIgnored); | |
}); | |
this.filter(CategoryId.Ignore, false); | |
} | |
/** | |
* @param {CategoryId} categoryId | |
* @param {boolean} value | |
* @returns {void} | |
*/ | |
filter(categoryId, value) { | |
show(this.categories[categoryId].element, value); | |
} | |
} | |
new Watchlist(root).render(); | |
} | |
let userUrl = getUserUrl(); | |
processMenu(userUrl); | |
processCalendar(); | |
processWatchlist(userUrl); | |
})(); | |
} catch (e) { | |
console.log("========== SerialZone.cz <PLUS> ========== ERROR"); | |
console.log(e); | |
} finally { | |
console.log("========== SerialZone.cz <PLUS> ========== END"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment