NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name ShikiAirTime // @version 2.11 // @description Добавляет на главную страницу блок "Ваше аниме", в котором отображается таймер до выхода новых серий онгоингов пользователя (заменяет собой блок "Сейчас выходит") // @author AniOleg // @match https://shikimori.one/* // @match https://shikimori.org/* // @match https://shikimori.me/* // @match http://shikimori.one/* // @match http://shikimori.org/* // @match http://shikimori.me/* // @icon https://www.google.com/s2/favicons?domain=shikimori.me // @homepageURL https://openuserjs.org/scripts/AniOleg/ShikiAirTime // @updateURL https://openuserjs.org/meta/AniOleg/ShikiAirTime.meta.js // @license MIT // @grant none // ==/UserScript== //НАСТРАИВАЕМЫЕ ПАРАМЕТРЫ const cGap = 11.4, //отступ между обложками аниме (px) cGridItemWidth = 96, //ширина обложки аниме (px) clOngoing = 'GreenYellow', //цвет линии просматриваемого онгоинга clOngoingLaze = 'red', //цвет линии просматриваемого онгоинга (есть отставание по сериям) clOngoingPlanned = 'orange', //цвет линии запланированного онгоинга bSize = 2, //толщина линии подчёркивания тайтла (px) cTooltip = true, //включить отображение карточек при наведении на обложку аниме showAiredAnime = true, //отображать уже вышедшее аниме, или аниме для которого неизвестна дата выхода эпизода showPlannedAirAnime = false, //отображать онгоинги из списка "Запланировано" finalEpisodeDate = true, //подсчитывать предполагаемую дату окончания онгоинга и отображать во всплывающей карточке (при наведении) gDebul = false; //выводить отладочную информацию в консоль DevTools // const host = location.protocol + '//' + location.host; const lang_data = {ru: {header: 'Ваше аниме', episode: 'Эпизод', progress: 'Прогресс:', day: 'д', hour: 'ч', min: 'м', emptyList: 'Список аниме пуст', errorCatch: 'При обработке данных произошла ошибка. Попробуйте обновить страницу', episodeIncrementProgress: 'Отправка...', cacheResetAlert: 'Кэш расписания сброшен. Обновите страницу', finalEpisode: 'Финальный эпизод' }, en: {header: 'Your anime', episode: 'Ep', progress: 'Progress:', day: 'd', hour: 'h', min: 'm', emptyList: 'Anime list is empty', errorCatch: 'An error occurred while processing the data. Try refreshing the page', episodeIncrementProgress: 'Sending...', cacheResetAlert: 'Schedule cache was reset. Please, refresh page', finalEpisode: 'Final episode' }, }; var CalendarList = [], AnimeList = [], GlobalServerTime = null, SiteLang, TitleLang; function InitScript(fn) { document.addEventListener('page:load', fn); document.addEventListener('turbolinks:load', fn); if (document.attachEvent ? document.readyState === 'complete' : document.readyState !== 'loading') { fn(); var ctrlDown = false, shiftDown = false, ctrlKey = 17, cmdKey = 91, shiftKey = 16, fKey = 70, prevKey = 0; $(document).keydown(function(e) { if (e.keyCode == ctrlKey || e.keyCode == cmdKey) ctrlDown = true; if (e.keyCode == shiftKey) shiftDown = true; if (ctrlDown && shiftDown && e.keyCode == fKey && location.pathname == '/' && GetUserId() != null) { localStorage.setItem('__AniListCalendar', '[]'); alert(lang_data[SiteLang].cacheResetAlert); if (gDebul) console.log('ShikiAirTime: Кэш календаря сброшен пользователем'); } }).keyup(function(e) { if (e.keyCode == ctrlKey || e.keyCode == cmdKey) ctrlDown = false; if (e.keyCode == shiftKey) shiftDown = false; }); } else { document.addEventListener('DOMContentLoaded', fn); } } function InitListFetch() { if (localStorage.getItem('__AniListCalendar') == null) { localStorage.setItem('__AniListCalendar', '[]'); } if (location.pathname == '/' && GetUserId() != null) { GlobalServerTime = GetServerTime(); SiteLang = $('body')[0].attributes['data-locale'].nodeValue; TitleLang = $('body')[0].attributes['data-localized_names'].nodeValue; if (gDebul) console.log('ShikiAirTime: Запрос списка пользователя...'); InitListGrid(); fetch(host + '/api/users/' + GetUserId() + '/anime_rates?status=watching&limit=5000', { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', } }) .then(r => r.json()) .then(data => { AnimeList = data; if (gDebul) console.log('ShikiAirTime: Список аниме пользователя получен'); //2.5 if (showPlannedAirAnime) { fetch(host + '/api/users/' + GetUserId() + '/anime_rates?status=planned&limit=5000', { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', } }) .then(r => r.json()) .then(data => { if (gDebul) console.log('ShikiAirTime: Список аниме пользователя получен (для опции "Отображать запланированное аниме")'); for (var a = 0; a < data.length; a++) { if (data[a].anime.status == 'ongoing') { AnimeList.push(data[a]); } } InitDataFetch(); }) .catch(error => { if (gDebul) console.log('ShikiAirTime: Ошибка получения списка аниме пользователя (для опции "Отображать запланированное аниме") ' + error); $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].errorCatch + '</span></div>'; }) } else { InitDataFetch(); } }) .catch(error => { if (gDebul) console.log('ShikiAirTime: Ошибка получения списка аниме пользователя ' + error); $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].errorCatch + '</span></div>'; }) } } function InitListGrid() { if (location.pathname == '/') { if ($("._ShikiAirTime__CustomStyles").length < 1) { var d = document.createElement('style'); d.classList = "_ShikiAirTime__CustomStyles"; d.innerHTML = `._ShikiAirTime__CalendarGridEntry .cover .image-decor>.text:before {background: none !important} ._ShikiAirTime__CalendarGridEntry .cover .image-decor>.text {text-align: center !important} ._ShikiAirTime__PreAirDate {display: none !important;} ._ShikiAirTime__PreAirDate.Show {display: block !important;} ._ShikiAirTime__AddEpisode {display: none !important; padding-top: 5px !important; padding-bottom: 5px; font-size: 14px !important;} ._ShikiAirTime__AddEpisode:hover > .plus {font-weight: bold !important;} ._ShikiAirTime__AddEpisode.Show {display: block !important} ._ShikiAirTime__CalendarGridContainer {display: grid; gap: ` + cGap + `px; grid-template-columns: repeat(auto-fill, ` + cGridItemWidth + `px);}`; $('body')[0].appendChild(d); } if ($("._ShikiAirTime__CalendarTooltip").length < 1) { var g = document.createElement('div'); g.classList = '_ShikiAirTime__CalendarTooltip'; g.style = 'display: none; position: absolute; left: 0px; top: 0px; pointer-events: none !important; margin: 0'; g.innerHTML = '<div class="tooltip-inner" style="width: 240px !important; min-height: 50px !important; box-shadow: none !important; margin: 0px !important"></div>'; $('body')[0].appendChild(g); } $('.block2')[0].childNodes[1].style = ''; $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><div class="b-ajax"></div>'; $('.block2')[0].childNodes[1].classList = ''; $('.block2')[0].classList.add("_ShikiAirTime__MainContainer"); $("._ShikiAirTime__MainContainer").find(".subheadline").addClass("_ShikiAirTime__HeaderLinkContainer"); $('.block2')[0].childNodes[0].childNodes[0].childNodes[0].nodeValue = lang_data[SiteLang].header; $('.block2')[0].childNodes[0].childNodes[0].href = GetUserLink() + '/list/anime?mylist=watching,rewatching'; HideCustomTooltip(); } } function InitDataFetch() { try{ AnimeList.sort(compareNames); //2.9 сортируем по названию CalendarList = []; CalendarList = JSON.parse(localStorage.getItem('__AniListCalendar')); var Queue = []; if (CalendarList != null && CalendarList.length > 0) { if (gDebul) console.log('ShikiAirTime: В localStorage обнаружены данные календаря'); for (var a = 0; a < AnimeList.length; a++) { if (AnimeList[a].anime.status == 'ongoing' || AnimeList[a].anime.status == 'anons') { var IsFinded = false; for (var b = CalendarList.length - 1; b >= 0; b--) { //перебор календаря (сохранённого) if (AnimeList[a].anime.id == CalendarList[b].data.Media.idMal) { //2.2 IsFinded = true; if (CalendarList[b].data.Media.nextAiringEpisode.airingAt - GlobalServerTime < 0) { //если у аниме эпизод уже вышел, то обновляем информацию CalendarList.splice(b, 1); Queue.push(AnimeList[a].anime.id); } } } if (!IsFinded) //если не нашли нужного аниме, то запрашиваем инфу Queue.push(AnimeList[a].anime.id); } } } else { //если календарь не был сохранён, то запрашиваем инфу по всем своим онгоингам if (gDebul) console.log('ShikiAirTime: Запрос новых данных по всему списку...') CalendarList = []; if (AnimeList.length > 0) { for (var i = 0; i < AnimeList.length; i++) { if (AnimeList[i].anime.status == 'ongoing' || AnimeList[i].anime.status == 'anons') { Queue.push(AnimeList[i].anime.id); } } } } if (Queue.length > 0) { GrabAniList(Queue, 1); } else { CalendarList.sort(compareNumeric); DrawListGrid(); } } catch(e) { if (gDebul) console.log('ShikiAirTime: Ошибка ' + e + '. Кэш календаря будет сброшен') $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].errorCatch + '</span></div>'; localStorage.setItem('__AniListCalendar', '[]'); } } function GrabAniList(Queue, Page) { const ApiUrl = 'https://graphql.anilist.co'; const QueryStruct = ` query($idMal_in: [Int], $page: Int) { Page(page: $page) { media(idMal_in: $idMal_in) { idMal nextAiringEpisode { airingAt episode } } pageInfo { perPage hasNextPage total } } }`; var xhr = new XMLHttpRequest(); xhr.open('POST', ApiUrl); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { data = JSON.parse(xhr.responseText); for (var i = 0; i < data.data.Page.media.length; i++) { if (data.data.Page.media[i].nextAiringEpisode != null) { CalendarList.push({data: {Media:data.data.Page.media[i]}}); } } localStorage.setItem('__AniListCalendar', JSON.stringify(CalendarList)); //сохраняем календарь в локальное хранилище if (data.data.Page.pageInfo.hasNextPage == true) { GrabAniList(Queue, Page + 1) } else { CalendarList.sort(compareNumeric); DrawListGrid(); } } }; var data = JSON.stringify({ query: QueryStruct, variables: {idMal_in: Queue} }); xhr.send(data); } function DrawListGrid() { if (location.pathname == '/') { var tInner = ''; var allCount = 0; var findedArray = []; for (var a = 0; a < CalendarList.length; a++) { for (var b = 0; b < AnimeList.length; b++) { if (CalendarList[a].data.Media.idMal == AnimeList[b].anime.id && (AnimeList[b].anime.status == 'ongoing' || AnimeList[b].anime.status == 'anons')) { findedArray.push(b); var tTime = CalendarList[a].data.Media.nextAiringEpisode.airingAt - GlobalServerTime; if (tTime > -60) { //придерживаем серию ещё минуту после её выхода allCount++; var bColor = AnimeList[b].episodes + 1 < CalendarList[a].data.Media.nextAiringEpisode.episode ? clOngoingLaze : clOngoing; if (AnimeList[b].episodes == 0) bColor = clOngoingPlanned; tInner += ` <article class="b-catalog_entry _ShikiAirTime__CalendarGridEntry" itemtype="http://schema.org/Movie" style="border-bottom: ` + bSize + `px solid ` + bColor + `; position: relative" title_name="` + AnimeList[b].anime.name.replace(/["]/g, '"') + `" title_ru="` + AnimeList[b].anime.russian.replace(/["]/g, '"') + `" ` + `episodes_watched="` + AnimeList[b].episodes + `" episodes="` + AnimeList[b].anime.episodes + `" episodes_aired="` + (CalendarList[a].data.Media.nextAiringEpisode.episode != null ? (CalendarList[a].data.Media.nextAiringEpisode.episode - 1) : 10000) + `" next_episode_airing_at="` + (CalendarList[a].data.Media.nextAiringEpisode.airingAt != null ? (CalendarList[a].data.Media.nextAiringEpisode.airingAt - 1) : 0) +`" anime_rate_id="` + AnimeList[b].id + `" clOngoing="` + clOngoing + `" clOngoingLaze="` + clOngoingLaze + `" is_ongoing=true> <a class="cover anime-tooltip-processed" href="` + host + AnimeList[b].anime.url + `"> <span class="image-decor"> <span class="image-cutter _ShikiAirTime__CalendatGridEntryImageContainer"> <img alt="` + (TitleLang == 'ru' ? AnimeList[b].anime.russian.replace(/["]/g, '"') : AnimeList[b].anime.name.replace(/["]/g, '"')) + `" src="` + AnimeList[b].anime.image.preview + `"> </span> <div class="text _ShikiAirTime__PreAirDate Show">` + lang_data[SiteLang].episode + ` ` + CalendarList[a].data.Media.nextAiringEpisode.episode + `<br>` + CalcEndTime(tTime) + `</div> <div class="text _ShikiAirTime__AddEpisode">` + AnimeList[b].episodes + ` <span class="plus">+</span></div> </span> </a> </article> `; } } } } if (showAiredAnime) { for (var c = 0; c < AnimeList.length; c++) { if (findedArray.indexOf(c) == -1) { allCount++; //2.7 var episodes_aired = 0; if (AnimeList[c].anime.status == 'ongoing') { episodes_aired = AnimeList[c].anime.episodes_aired == null ? 0 : AnimeList[c].anime.episodes_aired; } else { episodes_aired = AnimeList[c].anime.episodes == 0 ? '?' : AnimeList[c].anime.episodes; } tInner += ` <article class="b-catalog_entry _ShikiAirTime__CalendarGridEntry" itemtype="http://schema.org/Movie" style=" position: relative" title_name="` + AnimeList[c].anime.name.replace(/["]/g, '"') + `" title_ru="` + AnimeList[c].anime.russian.replace(/["]/g, '"') + `" ` + `episodes_watched="` + AnimeList[c].episodes + `" episodes="` + AnimeList[c].anime.episodes + `" episodes_aired="` + episodes_aired + `" anime_rate_id="` + AnimeList[c].id + `" clOngoing="` + clOngoing + `" clOngoingLaze="` + clOngoingLaze + `" is_ongoing=false> <a class="cover anime-tooltip-processed" data-delay="150" href="` + host + AnimeList[c].anime.url + `"> <span class="image-decor"> <span class="image-cutter"> <img alt="` + (TitleLang == 'ru' ? AnimeList[c].anime.russian.replace(/["]/g, '"') : AnimeList[c].anime.name.replace(/["]/g, '"')) + `" src="` + AnimeList[c].anime.image.preview + `"> </span> <div class="text _ShikiAirTime__AddEpisode">` + AnimeList[c].episodes + ` <span class="plus">+</span></div> </span> </a> </article> `; } } } if (allCount > 0) { $('.block2')[0].childNodes[1].classList = '_ShikiAirTime__CalendarGridContainer'; $('.block2')[0].childNodes[1].innerHTML = tInner; } else { $('.block2')[0].childNodes[1].innerHTML = '<div style="text-align: center"><span>' + lang_data[SiteLang].emptyList + '</span></div>'; } //очистка завершенных онгоингов из кэша календаря for (var x = CalendarList.length - 1; x >= 0; x--) { var IsFinded = false; for (var d = 0; d < AnimeList.length; d++) { if (CalendarList[x].data.Media.idMal == AnimeList[d].anime.id) { IsFinded = true; } } if (!IsFinded) { CalendarList.splice(x, 1); } } localStorage.setItem('__AniListCalendar', JSON.stringify(CalendarList)); //сохраняем календарь в локальное хранилище if (cTooltip) { var elements = $('._ShikiAirTime__CalendarGridEntry'); for (var i = 0; i < elements.length; i++) { $(elements[i].childNodes[1].childNodes[1]).hover( function (e) { ShowCustomTooltip(e.target); $(e.target.parentElement.parentElement).find('._ShikiAirTime__AddEpisode').addClass('Show'); $(e.target.parentElement.parentElement).find('._ShikiAirTime__PreAirDate').removeClass('Show'); }, function (e) { HideCustomTooltip(); $(e.target.parentElement.parentElement).find('._ShikiAirTime__AddEpisode').removeClass('Show'); $(e.target.parentElement.parentElement).find('._ShikiAirTime__PreAirDate').addClass('Show'); } ) } } $('._ShikiAirTime__AddEpisode').click(function (e) { e.preventDefault(); incEpisode($(e.target.parentElement.parentElement).find('._ShikiAirTime__AddEpisode')[0].parentElement.parentElement.parentElement.getAttribute('anime_rate_id'), $(e.target.parentElement.parentElement).find('._ShikiAirTime__AddEpisode')[0]); }) } } function ShowCustomTooltip(e) { var AnimeElement = e.tagName == 'IMG' ? e.parentElement.parentElement.parentElement.parentElement : e.parentElement.parentElement.parentElement; var TooltipElement = $('._ShikiAirTime__CalendarTooltip')[0]; TooltipElement.style.display = 'block'; TooltipElement.style.top = AnimeElement.getBoundingClientRect().y + document.documentElement.scrollTop + 'px'; TooltipElement.childNodes[0].innerHTML = (TitleLang == 'ru' ? AnimeElement.attributes.title_ru.nodeValue : AnimeElement.attributes.title_name.nodeValue) + '<br><br>'; var EpisodesBehind = AnimeElement.attributes.episodes_aired.nodeValue - AnimeElement.attributes.episodes_watched.nodeValue; if (EpisodesBehind > 0) { TooltipElement.childNodes[0].innerHTML += (SiteLang == 'ru' ? 'Отставание на ' + EpisodesBehind + ' ' + GetLocalizedEpisodeHint(EpisodesBehind) : EpisodesBehind + ' episode' + (EpisodesBehind > 1 ? 's' : '') + ' behind') + '<br><br>'; } TooltipElement.childNodes[0].innerHTML += lang_data[SiteLang].progress + ' ' + AnimeElement.attributes.episodes_watched.nodeValue + '/' + (AnimeElement.attributes.episodes.nodeValue != 0 ? AnimeElement.attributes.episodes.nodeValue : '?'); if (AnimeElement.getBoundingClientRect().x + AnimeElement.getBoundingClientRect().width + 2 + TooltipElement.childNodes[0].getBoundingClientRect().width + 10 < $(window).width()) { TooltipElement.style.left = AnimeElement.getBoundingClientRect().x + AnimeElement.getBoundingClientRect().width + 2 + 'px'; } else { TooltipElement.style.left = AnimeElement.getBoundingClientRect().x - 2 - TooltipElement.childNodes[0].getBoundingClientRect().width + 'px'; } //2.8 if (finalEpisodeDate && AnimeElement.attributes.episodes.nodeValue != 0 && AnimeElement.attributes.is_ongoing.nodeValue == "true") { var endDate = new Date( ( GlobalServerTime + (60 * 60 * 24 * 7 * (AnimeElement.attributes.episodes.nodeValue - AnimeElement.attributes.episodes_aired.nodeValue - 1) + parseInt(AnimeElement.attributes.next_episode_airing_at.nodeValue) - GlobalServerTime) ) * 1000 ); TooltipElement.childNodes[0].innerHTML += "<br><br>" + lang_data[SiteLang].finalEpisode + " " + endDate.getDate() + "." + (endDate.getMonth() + 1).toString().padStart(2, "0") + "." + endDate.getFullYear(); } } function GetLocalizedEpisodeHint(e) { var variants = ['эпизод', 'эпизода', 'эпизодов']; e = Math.abs(e) % 100; var n1 = e % 10; if (e > 10 && e < 20) { return variants[2]; } if (n1 > 1 && n1 < 5) { return variants[1]; } if (n1 == 1) { return variants[0]; } return variants[2]; } function HideCustomTooltip() { $('._ShikiAirTime__CalendarTooltip')[0].style.display = 'none'; } function GetUserId() { return JSON.parse($('body')[0].attributes['data-user'].value).id; } function GetUserLink() { return JSON.parse($('body')[0].attributes['data-user'].value).url; } function GetServerTime() { try { return new Date($('body')[0].attributes['data-server_time'].nodeValue).getTime() / 1000; } catch(e) { if (gDebul) console.log('ShikiAirTime: Ошибка запроса серверного времени'); return null; } } function compareNumeric(a, b) { //сортировка AniList по времени до выхода эпизода a = a.data.Media.nextAiringEpisode.airingAt; b = b.data.Media.nextAiringEpisode.airingAt; if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; } function compareNames(a, b) { //2.9 сортировка списка аниме по имени (для неонгоингов или без даты след. серии) a = (TitleLang == 'ru' ? a.anime.russian : a.anime.name); b = (TitleLang == 'ru' ? b.anime.russian : b.anime.name); if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; } function CalcEndTime(tUTime) { var t = parseInt(tUTime); if (t<60) { return '<1' + lang_data[SiteLang].min; } else { var days = parseInt(t / 86400); t = t - (days * 86400); var hours = parseInt(t / 3600); t = t - (hours * 3600); var minutes = parseInt(t / 60); var content = ''; if (days) { content += days + lang_data[SiteLang].day; } if (hours > 0) { if (hours || days) { if (content) { content += ' '; } content += hours + lang_data[SiteLang].hour; } } if (minutes) { if (content) { content += ' '; } content += minutes + lang_data[SiteLang].min; } return content; } } function incEpisode(rate_id, addEpisodeElement) { var prevHTML = addEpisodeElement.innerHTML; addEpisodeElement.innerHTML = lang_data[SiteLang].episodeIncrementProgress; fetch(host + '/api/v2/user_rates/' + rate_id + '/increment', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', } }) .then(r => r.json()) .then(data => { if (gDebul) console.log('ShikiAirTime: Количество просмотренных эпизодов увеличено на 1'); addEpisodeElement.innerHTML = data.episodes + ' <span class="plus">+</span>'; addEpisodeElement.parentElement.parentElement.parentElement.setAttribute('episodes_watched', data.episodes); HideCustomTooltip(); if (addEpisodeElement.parentElement.parentElement.parentElement.getAttribute('is_ongoing') == 'true') { var bColor = data.episodes < addEpisodeElement.parentElement.parentElement.parentElement.getAttribute('episodes_aired') ? clOngoingLaze : clOngoing; addEpisodeElement.parentElement.parentElement.parentElement.style.borderBottom = bSize + `px solid ` + bColor; } if (addEpisodeElement.parentElement.parentElement.parentElement.getAttribute('episodes') > 0 && addEpisodeElement.parentElement.parentElement.parentElement.getAttribute('episodes') == data.episodes) { addEpisodeElement.parentElement.parentElement.parentElement.remove(); } }) .catch(error => { if (gDebul) console.log('ShikiAirTime: Ошибка увеличения кол-ва эпизодов'); addEpisodeElement.innerHTML = prevHTML; }) } InitScript(InitListFetch)