您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Generate a list of everything you watched or read on Natively this year
当前为
// ==UserScript== // @name Natively Recap // @namespace https://learnnatively.com // @description Generate a list of everything you watched or read on Natively this year // @author araigoshi // @version 1.0.0 // @match https://learnnatively.com/* // @license MIT // @grant none // ==/UserScript== (async function () { 'use strict'; const CSS_TEXT = ` .recap-hide-ui { #main-nav { display: none; } .body-footer { display: none; } .content-wrapper { padding-top: 0; } } #recap-takeover { display: grid; grid-template-columns: auto; justify-content: center; padding: 1em; header { display: flex; gap: 2em; align-items: center; margin: 5px 0; h2 { font-size: var(24px); } p { margin-bottom: 0; } } .recap-body { padding-top: 1em; display: grid; width: fit-content; grid-template-columns: repeat(10, 150px); justify-content: center; grid-gap: 0.5em; } figure { margin: 0; text-align: center; } .item-manga { background: #f7eafa; } .item-novel { background: #d9c1de; } .item-anime { background: #c7dec1; } .item-movie { background: #bce3b2; } figure img { height: 200px; aspect-ratio: keep; } span.series-progress { font-size: 10px; white-space: pre; } p.dates { font-size: 10px; } } `; let state = { startDate: new Date("2024-01-01"), endDate: new Date("2024-12-31"), }; function loadStylesheet() { console.log('Loading recap stylesheet'); if (document.getElementById('recap-styles') === null) { let styleSheet = document.createElement('style'); styleSheet.id = 'recap-styles'; styleSheet.textContent = CSS_TEXT; document.head.appendChild(styleSheet); } } function buildRequest(libraryType) { return { "page": 1, "numOfPages": 1, "totalCount": 0, "itemType": "", "genre": "", "q": "", "bookProviders": "", "watchProviders": "", "location": "", "minLevel": "", "maxLevel": "", "levelFilter": "", "bookType": "", "tagFilters": "", "loading": false, "onlyFree": false, "excludeTemporaryRatings": false, "excludeLibrary": false, "wanikani": false, "mobileFilter": false, "tags": "", "includeSpoilers": false, "includeMinorElements": false, "includeMajorElementsOnly": false, "itemTypes": [], "genreOptions": [], "openSeries": [], "error": "", "libraryType": libraryType, "sort": "-recent", "subs": "", "favorite": false, "resultActive": null, "listFilter": null, "lists": [], "customTags": "", "collapseSeries": false, "pageSize": 50, "needsBackendSearch": false }; } function isItemInRange(startDate, endDate) { return function (item) { const itemFinishedUnixDate = item.dateFinishedData?.timestampSeconds; if (!itemFinishedUnixDate) { return false; } const itemFinishedDate = new Date(itemFinishedUnixDate * 1000); return startDate <= itemFinishedDate && itemFinishedDate <= endDate; } } function jsToIsoDate(jsDate) { return jsDate.toISOString().substring(0, 10); } function transformItemDate(item, key) { if (!item[key]) { return 'Unknown'; } const unixDate = item[key].timestampSeconds; const jsDate = new Date(unixDate * 1000); return jsToIsoDate(jsDate); } function generateItemSummary(item, itemType, seriesCache) { return { 'image': item.item.image?.url, 'seriesPos': item.item.seriesOrder || 1, 'seriesLength': item.item.seriesId ? seriesCache[item.item.seriesId].numOfItems : 1, 'name': item.item.title, 'started': transformItemDate(item, 'dateStartedData'), 'finished': transformItemDate(item, 'dateFinishedData'), 'type': itemType, } } function itemHtmlSnippet(parsedItem) { return ` <figure class="item-${parsedItem['type']}"> <img src="${parsedItem['image']}" /> <figcaption> ${parsedItem['name']} <span class="series-progress">(${parsedItem['seriesPos']} / ${parsedItem['seriesLength']})</span> <p class="dates"><span class="started">${parsedItem['started']}</span> - <span class="ended">${parsedItem['finished']}</span><p> </figcaption> </figure> ` } function generateItemList(nativelyData, itemType, nativelyItemTypes) { const itemTypes = nativelyItemTypes ? nativelyItemTypes : [itemType]; const dateFilter = isItemInRange(state.startDate, state.endDate); const typeFilter = (item) => itemTypes.includes(item.item.mediaType); const items = nativelyData.results.filter(typeFilter).filter(dateFilter); return items.map(item => generateItemSummary(item, itemType, nativelyData.seriesCache)); } async function fetchLibrary(libraryType) { const csrfToken = document.querySelector('meta[name=csrf-token]').getAttribute('content'); const response = await fetch('/api/item-library-search-api/araigoshi/', { method: 'POST', headers: { 'content-type': 'application/json', 'X-CSRFToken': csrfToken, }, body: JSON.stringify(buildRequest(libraryType)), }); return response.json(); } async function loadData() { if (!state.rawBookData) { console.log('Loading data'); state.rawBookData = await fetchLibrary('books'); } if (!state.rawVideoData) { state.rawVideoData = await fetchLibrary('videos'); } const manga = generateItemList(state.rawBookData, 'manga'); const novels = generateItemList(state.rawBookData, 'novel', ['Light novel', 'light_novel', 'childrens_book', 'novel']) const movies = generateItemList(state.rawVideoData, 'movie'); const anime = generateItemList(state.rawVideoData, 'movie', ['tv_season']); const parsedData = [...manga, ...novels, ...movies, ...anime]; parsedData.sort((a, b) => a.finished.localeCompare(b.finished)); console.log('Loaded item data'); console.log(parsedData); return parsedData; } async function generateRecapBody() { const data = await loadData(); const htmlComponents = data.map(itemHtmlSnippet); return htmlComponents.join('\n'); } async function updateRecapBody() { let recapBodies = document.querySelectorAll('.recap-body'); let bodyContent = await generateRecapBody(); recapBodies.forEach(body => body.innerHTML = bodyContent); } function addLink() { const navbar = document.querySelector('.navbar-nav'); const lastLink = document.querySelector('.last-link'); const recapLink = document.createElement('a'); recapLink.textContent = 'Recap'; recapLink.classList.add('nav-link'); recapLink.href = '#recap'; recapLink.addEventListener('click', showRecap); navbar.insertBefore(recapLink, lastLink); } function createRecapElement(elementType, id) { const recapEl = document.createElement(elementType); recapEl.id = id; recapEl.innerHTML = ` <header> <h2>Natively Recap</h2> <button class="hide-natively-ui">Show/Hide Natively UI</button> <p>Date range</p> <input type="date" id="recap-start-date" value="${jsToIsoDate(state.startDate)}" /> <input type="date" id="recap-end-date" value="${jsToIsoDate(state.endDate)}" /> </header> <main class="recap-body"> </main> `; recapEl.querySelector('.hide-natively-ui').addEventListener('click', (evt) => { document.body.classList.toggle('recap-hide-ui'); }); recapEl.querySelector('#recap-start-date').addEventListener('change', (evt) => { state.startDate = new Date(evt.target.valueAsNumber); updateRecapBody(); }); recapEl.querySelector('#recap-end-date').addEventListener('change', (evt) => { state.endDate = new Date(evt.target.valueAsNumber); updateRecapBody(); }); document.body.appendChild(recapEl); return recapEl; } function showRecap() { const recapEl = createRecapElement('div', 'recap-takeover'); document.querySelector('.content-wrapper').replaceChildren(recapEl); updateRecapBody(); } function init() { loadStylesheet(); addLink(); window.RECAP_STATE = state; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址