Greasy Fork

来自缓存

Greasy Fork is available in English.

DLSite Price History

Remembers and displays the prices you have seen for listings.

当前为 2024-02-24 提交的版本,查看 最新版本

// ==UserScript==
// @name         DLSite Price History
// @namespace    V.L
// @version      0.1.0
// @description  Remembers and displays the prices you have seen for listings.
// @author       Valerio Lyndon
// @match        https://www.dlsite.com/*
// @run-at       document-start
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.addStyle
// @license      MIT
// ==/UserScript==

GM.addStyle(`
.vl-phist {
	max-width: 200px;
	padding: 0 6px;
	border-radius: 2px;
	color: #fff;
	font-size: 11px;
	font-weight: bold;
	line-height: 17px;
	vertical-align: middle;
	box-sizing: border-box;
	text-align: center;
	text-transform: uppercase;
}
.vl-phist.best {
	background: #68e;
}
.vl-phist.worse {
	background: #e69;
}
.cp_work_deals .vl-phist {
	display: inline-block;
	width: 49%;
}
.search_result_img_box_inner .vl-phist {
	margin-top: 5px;
}
`);

class PriceHistory {
	constructor( parent ){
		this.parent = parent || document;
		this.processWorkPage();
		this.processCarousel();
		this.processGrid();
		this.processRecommended();
		this.processFavourites();
	}

	async comparePrice( RJ, currentPrice ){
		let previousBest = await GM.getValue(RJ, -1);
		let best = previousBest;
		if( previousBest < 0 || previousBest > currentPrice ){
			GM.setValue(RJ, currentPrice);
			best = currentPrice;
		}
		return best;
	}

	async insertVisual( currentPrice, lowestPrice, marker, placement = 'afterend' ){
		let alert = document.createElement('div');
		alert.className = 'vl-phist';
		if( lowestPrice >= currentPrice ){
			alert.classList.add('best');
			alert.textContent = `Best price!`;
		}
		if( lowestPrice < currentPrice ){
			alert.classList.add('worse');
			alert.textContent = `Best: ${lowestPrice} JPY`;
		}
		marker.insertAdjacentElement(placement, alert);
	}

	async processWorkPage( ){
		const item = this.parent.querySelector('#work_buy_box_wrapper');
		if( !item ){
			return;
		}

		const RJ = item.dataset.productId;
		if( !RJ ){
			console.log('failed to read RJ number: ', item);
			return;
		}

		let currentPrice = item.querySelector('.work_buy_content .price')?.firstChild?.textContent;
		if( currentPrice === undefined ){
			console.log('failed to read price of '+RJ);
			return;
		}
		currentPrice = parseInt(currentPrice.replaceAll(/\D+/g, ''));

		const lowestPrice = await this.comparePrice(RJ, currentPrice);

		// janky timeout because DLSite removes and replaces some of the children here and I can't be fucked to write a mutation observer for this
		setTimeout(()=>{
			const marker = item.querySelector('.work_buy_body:has(.price)');
			this.insertVisual( currentPrice, lowestPrice, marker );
		}, 300);
	}

	async processCarousel( ){
		const listings = this.parent.querySelectorAll('.cp_work_item');
		for( let item of listings ){
			const RJ = item.dataset.workno;
			if( !RJ ){
				console.log('failed to read RJ number: ', item);
				continue;
			}

			let currentPrice = item.querySelector('.cp_work_price')?.firstChild?.textContent;
			if( currentPrice === undefined ){
				console.log('failed to read price of '+RJ);
				continue;
			}
			currentPrice = parseInt(currentPrice.replaceAll(/\D+/g, ''));

			const lowestPrice = await this.comparePrice(RJ, currentPrice);

			const marker = item.querySelector('.cp_work_deals span:last-of-type') || item.querySelector('.cp_work_value');
			this.insertVisual( currentPrice, lowestPrice, marker );
		}
	}

	async processGrid( ){
		const listings = this.parent.querySelectorAll('.search_result_img_box_inner');
		for( let item of listings ){
			let RJ = item.getElementsByTagName('dt')?.[0]?.id;
			RJ = typeof RJ === 'string' ? RJ.substring(6) : undefined;
			if( !RJ ){
				console.log('failed to read RJ number: ', item);
				continue;
			}

			let currentPrice = item.querySelector('.work_price')?.firstChild?.textContent;
			if( currentPrice === undefined ){
				console.log('failed to read price of '+RJ);
				continue;
			}
			currentPrice = parseInt(currentPrice.replaceAll(/\D+/g, ''));

			const lowestPrice = await this.comparePrice(RJ, currentPrice);

			const marker = item.querySelector('.work_deals');
			this.insertVisual( currentPrice, lowestPrice, marker );
		}
	}

	async processRecommended( ){
		const listings = this.parent.querySelectorAll('.recommend_list .swiper-slide');
		for( let item of listings ){
			const RJ = item.dataset.prod;
			if( !RJ ){
				console.log('failed to read RJ number: ', item);
				continue;
			}

			let currentPrice = item.querySelector('.work_price')?.firstChild?.textContent;
			if( currentPrice === undefined ){
				console.log('failed to read price of '+RJ);
				continue;
			}
			currentPrice = parseInt(currentPrice.replaceAll(/\D+/g, ''));

			const lowestPrice = await this.comparePrice(RJ, currentPrice);

			const marker = item.querySelector('.recommend_work_item div:nth-last-of-type(2)');
			this.insertVisual( currentPrice, lowestPrice, marker );
		}
	}

	async processFavourites( ){
		const listings = this.parent.querySelectorAll('._favorite_item');
		for( let item of listings ){
			let RJ = item.querySelector('.work_thumb a')?.id;
			RJ = typeof RJ === 'string' ? RJ.substring(6) : undefined;
			if( !RJ ){
				console.log('failed to read RJ number: ', item);
				continue;
			}

			let currentPrice = item.querySelector('.work_price')?.firstChild?.textContent;
			if( currentPrice === undefined ){
				console.log('failed to read price of '+RJ);
				continue;
			}
			currentPrice = parseInt(currentPrice.replaceAll(/\D+/g, ''));

			const lowestPrice = await this.comparePrice(RJ, currentPrice);

			const marker = item.querySelector('.work_price_wrap');
			this.insertVisual( currentPrice, lowestPrice, marker );
		}
	}
}

document.addEventListener('DOMContentLoaded', ()=>{
	new PriceHistory();

	document.querySelectorAll('.recommend_list').forEach((targetNode)=>{
		const observer = new MutationObserver((mutationsList, observer)=>{
			new PriceHistory(targetNode);
			observer.disconnect();
		});
		observer.observe(targetNode, { childList: true });
	});
});