This commit is contained in:
Yorgei
2026-01-18 14:29:12 +10:00
commit e23ac642a7
21 changed files with 3370 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
name: Update Data
on:
schedule:
- cron: '0 */6 * * *'
workflow_dispatch:
jobs:
update-data:
runs-on: ubuntu-latest
container:
image: nikolaik/python-nodejs:python3.12-nodejs22-slim
steps:
- name: Install system dependencies
run: |
apt-get update
apt-get install -y git
- name: Checkout repository
uses: actions/checkout@v3
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Run download script
run: python download_data.py
- name: Check for changes and commit
run: |
git config --global user.name 'Gitea Actions'
git config --global user.email 'actions@noreply.gitea.io'
# Check if new_data.json has changed
if [[ -n $(git status -s chrome-extension/new_data.json) ]]; then
echo "Changes detected in new_data.json"
git add chrome-extension/new_data.json
git commit -m "Auto-update new_data.json"
git push
else
echo "No changes detected"
fi

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
response.html

18
README.md Normal file
View File

@@ -0,0 +1,18 @@
# RuneScape Wiki Ely Prices
This extension integrates Ely.gg price data directly into the official RuneScape Wiki, allowing players to view real-time market information seamlessly while browsing data.
## Description
Enhance your RuneScape Wiki experience with live pricing data. This extension fetches the latest street prices from Ely.gg and displays them right next to the item title on the Wiki page. It helps you stay informed about the current market value, whether you are checking for an upgrade or planning your next flip.
## Features
* **Live Price Display**: Shows the most recent transaction price, including whether it was an Instant Buy (inb) or Instant Sell (ins), and the date of the transaction.
* **Sales History Popup**: Click on the price to open a details popup showing the last 10 recorded sales for that item.
* **Seamless Integration**: Designed to look optimal on the Wiki without cluttering the interface.
* **Custom Data Source**: Includes an options menu to configure a custom data URL if needed.
## Privacy
This extension does not collect any personal data. It only fetches public price information corresponding to the Wiki page you are currently viewing.

37
build_release.py Normal file
View File

@@ -0,0 +1,37 @@
import os
import zipfile
import shutil
import json
def create_zip(source_dir, output_filename, manifest_filename='manifest.json'):
with zipfile.ZipFile(output_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(source_dir):
for file in files:
if file.startswith('manifest') and file.endswith('.json'):
continue
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, source_dir)
zipf.write(file_path, arcname)
manifest_path = os.path.join(source_dir, manifest_filename)
zipf.write(manifest_path, 'manifest.json')
def main():
if os.path.exists('release'):
shutil.rmtree('release')
os.makedirs('release')
# Userscript
shutil.copy('userscript.js', 'release/ely-userscript.user.js')
# Chrome Extension (Manifest V3)
create_zip('chrome-extension', 'release/ely-extension-chrome.zip', 'manifest-v3.json')
# Firefox Extension (Manifest V2)
create_zip('chrome-extension', 'release/ely-extension-firefox.zip', 'manifest.json')
print("Build complete in /release folder.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,35 @@
// CORS
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'fetchPrice') {
fetch(`https://www.ely.gg/chart/${request.itemId}/prices`)
.then(response => response.json())
.then(data => sendResponse({ success: true, data }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'fetchData') {
fetch(request.url)
.then(response => response.json())
.then(data => sendResponse({ success: true, data }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
}
});
//Firefox compatibility
if (typeof browser !== 'undefined') {
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'fetchPrice') {
fetch(`https://www.ely.gg/chart/${request.itemId}/prices`)
.then(response => response.json())
.then(data => sendResponse({ success: true, data }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'fetchData') {
fetch(request.url)
.then(response => response.json())
.then(data => sendResponse({ success: true, data }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
}
});
}

257
chrome-extension/content.js Normal file
View File

@@ -0,0 +1,257 @@
(async function() {
const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
try {
const getStorage = (key) => {
return new Promise((resolve) => {
const api = (typeof chrome !== 'undefined' && chrome.storage) ? chrome : browserAPI;
if (api.storage && api.storage.sync) {
api.storage.sync.get([key], (result) => {
resolve(result ? result[key] : null);
});
} else {
resolve(null);
}
});
};
const sendMessage = (message) => {
return new Promise((resolve) => {
const api = (typeof chrome !== 'undefined' && chrome.runtime) ? chrome : browserAPI;
try {
api.runtime.sendMessage(message, (response) => {
// Chrome callback
if (api.runtime.lastError) {
resolve(null);
} else {
resolve(response);
}
});
} catch(e) {
if (browserAPI && browserAPI.runtime && browserAPI.runtime.sendMessage) {
browserAPI.runtime.sendMessage(message).then(resolve).catch(() => resolve(null));
} else {
resolve(null);
}
}
});
};
const customUrl = await getStorage('customDataUrl');
let itemData;
if (customUrl) {
const response = await sendMessage({
action: 'fetchData',
url: customUrl
});
if (response && response.success) {
itemData = response.data;
} else {
console.error('Failed to fetch custom data, falling back to local');
const localResponse = await fetch(browserAPI.runtime.getURL('new_data.json'));
itemData = await localResponse.json();
}
} else {
const response = await fetch(browserAPI.runtime.getURL('new_data.json'));
itemData = await response.json();
}
const pageTitleElement = document.querySelector('.mw-page-title-main');
if (!pageTitleElement) return;
const originalCursor = pageTitleElement.style.cursor;
const pageTitle = pageTitleElement.textContent.trim();
let itemId = null;
if (itemData[pageTitle]) {
itemId = itemData[pageTitle];
} else {
const urlPath = window.location.pathname;
const urlTitle = urlPath.replace('/w/', '').replace(/_/g, ' ');
if (itemData[urlTitle]) {
itemId = itemData[urlTitle];
} else {
for (const [itemName, id] of Object.entries(itemData)) {
const itemNameBase = itemName.split('(')[0].trim();
if (itemName.toLowerCase() === pageTitle.toLowerCase() ||
itemName.toLowerCase() === urlTitle.toLowerCase() ||
itemNameBase.toLowerCase() === pageTitle.toLowerCase() ||
itemNameBase.toLowerCase() === urlTitle.toLowerCase()) {
itemId = id;
break;
}
}
}
}
if (itemId) {
try {
const response = await browserAPI.runtime.sendMessage({
action: 'fetchPrice',
itemId: itemId
});
if (response.success && response.data.items && response.data.items.length > 0) {
const lastItem = response.data.items[response.data.items.length - 1];
const price = lastItem.price.toLocaleString();
const date = new Date(lastItem.date).toLocaleDateString();
const saleType = lastItem.purchase;
var saleTypeShort = saleType;
if (saleType.toLowerCase() === "sold") {
saleTypeShort = "inb"
} else if (saleType.toLowerCase() === "bought") {
saleTypeShort = "ins"
}
const priceDisplay = document.createElement('span');
priceDisplay.textContent = ` (${price} gp - ${saleTypeShort} - ${date})`;
priceDisplay.style.color = '#5a8c5a';
priceDisplay.style.fontWeight = 'bold';
priceDisplay.style.fontSize = '0.9em';
pageTitleElement.appendChild(priceDisplay);
}
} catch (priceError) {
console.error('Error fetching price data:', priceError);
const idDisplay = document.createElement('span');
idDisplay.textContent = ` (ID: ${itemId}) | Failed to get price.`;
idDisplay.style.color = '#5a8c5a';
idDisplay.style.fontWeight = 'bold';
idDisplay.style.fontSize = '0.9em';
pageTitleElement.appendChild(idDisplay);
}
pageTitleElement.style.cursor = 'pointer';
pageTitleElement.addEventListener('click', async () => {
try {
const response = await sendMessage({
action: 'fetchPrice',
itemId: itemId
});
if (response.success && response.data.items && response.data.items.length > 0) {
showSalesPopup(response.data.items, itemId);
}
} catch (error) {
console.error('Error fetching sales data:', error);
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
}
});
}
} catch (error) {
console.error('Error loading item data:', error);
}
function showSalesPopup(items, itemId) {
const existingPopup = document.getElementById('ely-sales-popup');
if (existingPopup) {
existingPopup.remove();
}
const popup = document.createElement('div');
popup.id = 'ely-sales-popup';
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.backgroundColor = '#fff';
popup.style.border = '1px solid #ccc';
popup.style.borderRadius = '8px';
popup.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)';
popup.style.padding = '20px';
popup.style.zIndex = '10000';
popup.style.maxWidth = '500px';
popup.style.width = '90%';
popup.style.maxHeight = '70vh';
popup.style.overflowY = 'auto';
const header = document.createElement('h3');
header.textContent = 'Recent Sales';
header.style.marginTop = '0';
header.style.marginBottom = '15px';
header.style.color = '#333';
popup.appendChild(header);
const salesList = document.createElement('div');
salesList.style.marginBottom = '20px';
const recentSales = items.slice(-10).reverse();
recentSales.forEach(sale => {
const saleItem = document.createElement('div');
saleItem.style.display = 'flex';
saleItem.style.justifyContent = 'space-between';
saleItem.style.padding = '8px 0';
saleItem.style.borderBottom = '1px solid #eee';
const price = document.createElement('span');
price.textContent = `${parseInt(sale.price).toLocaleString()} gp`;
price.style.fontWeight = 'bold';
const date = document.createElement('span');
date.textContent = new Date(sale.date).toLocaleDateString();
date.style.color = '#666';
const type = document.createElement('span');
type.textContent = sale.purchase.toLowerCase() === 'sold' ? 'inb' : 'ins';
type.style.color = sale.purchase === 'sold' ? '#d9534f' : '#5cb85c';
type.style.fontWeight = 'bold';
saleItem.appendChild(price);
saleItem.appendChild(type);
saleItem.appendChild(date);
salesList.appendChild(saleItem);
});
popup.appendChild(salesList);
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'space-between';
buttonContainer.style.marginTop = '10px';
const elyButton = document.createElement('button');
elyButton.textContent = 'Go to Ely';
elyButton.style.backgroundColor = '#5a8c5a';
elyButton.style.color = 'white';
elyButton.style.border = 'none';
elyButton.style.padding = '8px 16px';
elyButton.style.borderRadius = '4px';
elyButton.style.cursor = 'pointer';
elyButton.addEventListener('click', () => {
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
popup.remove();
});
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.backgroundColor = '#6c757d';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.padding = '8px 16px';
closeButton.style.borderRadius = '4px';
closeButton.style.cursor = 'pointer';
closeButton.addEventListener('click', () => {
popup.remove();
});
buttonContainer.appendChild(elyButton);
buttonContainer.appendChild(closeButton);
popup.appendChild(buttonContainer);
document.body.appendChild(popup);
document.addEventListener('click', function closePopup(e) {
if (!popup.contains(e.target) && e.target !== pageTitleElement) {
popup.remove();
document.removeEventListener('click', closePopup);
}
});
}
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
chrome-extension/img/64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,38 @@
{
"manifest_version": 2,
"name": "RS Wiki Ely.gg Price Display",
"version": "1.0",
"description": "Displays item IDs from ely.gg on RuneScape Wiki pages",
"permissions": [
"storage",
"https://www.ely.gg/*"
],
"icons": {
"48": "img/64.png",
"96": "img/128.png"
},
"browser_action": {
"default_icon": "img/64.png",
"default_title": "Configure Ely Extension",
"default_popup": "options.html"
},
"background": {
"scripts": [
"background.js"
]
},
"content_scripts": [
{
"matches": [
"https://runescape.wiki/*"
],
"js": [
"content.js"
],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
"new_data.json"
]
}

View File

@@ -0,0 +1,51 @@
{
"manifest_version": 3,
"name": "RS Wiki Ely.gg Price Display",
"version": "1.0",
"description": "Displays item IDs from ely.gg on RuneScape Wiki pages",
"permissions": [
"storage"
],
"host_permissions": [
"https://www.ely.gg/*",
"http://*/*",
"https://*/*"
],
"icons": {
"48": "img/64.png",
"96": "img/128.png"
},
"action": {
"default_icon": "img/64.png",
"default_title": "Configure Ely Extension",
"default_popup": "options.html"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": [
"https://runescape.wiki/*"
],
"js": [
"content.js"
],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
{
"resources": [
"new_data.json"
],
"matches": [
"https://runescape.wiki/*"
]
}
],
"options_ui": {
"page": "options.html",
"open_in_tab": false
}
}

View File

@@ -0,0 +1,44 @@
{
"manifest_version": 2,
"name": "RS Wiki Ely.gg Price Display",
"version": "1.0",
"description": "Displays item IDs from ely.gg on RuneScape Wiki pages",
"permissions": [
"storage",
"https://www.ely.gg/*",
"http://*/*",
"https://*/*"
],
"icons": {
"48": "img/64.png",
"96": "img/128.png"
},
"browser_action": {
"default_icon": "img/64.png",
"default_title": "Configure Ely Extension",
"default_popup": "options.html"
},
"background": {
"scripts": [
"background.js"
]
},
"content_scripts": [
{
"matches": [
"https://runescape.wiki/*"
],
"js": [
"content.js"
],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
"new_data.json"
],
"options_ui": {
"page": "options.html",
"open_in_tab": false
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<title>Ely Wiki Extension Options</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
min-width: 300px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
margin-bottom: 10px;
}
button {
padding: 8px 16px;
background-color: #5a8c5a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #487048;
}
.status {
margin-top: 10px;
font-size: 0.9em;
color: green;
}
.note {
font-size: 0.8em;
color: #666;
margin-top: 20px;
}
</style>
</head>
<body>
<label for="dataUrl">Custom Data JSON URL:</label>
<input type="text" id="dataUrl" placeholder="Leave empty to use default local data">
<button id="save">Save</button>
<div id="status" class="status"></div>
<div class="note">
Leave empty to use the bundled extension data. <br>
Should link to the raw json file of items.
</div>
<script src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
document.getElementById('save').addEventListener('click', () => {
const dataUrl = document.getElementById('dataUrl').value.trim();
chrome.storage.sync.set({ customDataUrl: dataUrl }, () => {
const status = document.getElementById('status');
status.textContent = 'Options saved.';
setTimeout(() => {
status.textContent = '';
}, 2000);
});
});
document.addEventListener('DOMContentLoaded', () => {
chrome.storage.sync.get(['customDataUrl'], (items) => {
if (items.customDataUrl) {
document.getElementById('dataUrl').value = items.customDataUrl;
}
});
});

61
download_data.py Normal file
View File

@@ -0,0 +1,61 @@
import requests
import re
import json
import ast
from bs4 import BeautifulSoup
def extract_data_from_page(url):
response = requests.get(url)
response.raise_for_status()
with open("response.html", "w", encoding="utf-8") as f:
f.write(response.text)
soup = BeautifulSoup(response.text, 'html.parser')
script_tags = soup.find_all('script')
for script in script_tags:
if script.string:
lines = script.string.split('\n')
for line in lines:
if re.match(r'^\s*data\s*=', line):
match = re.search(r'data\s*=\s*(.+)', line)
if match:
data_str = match.group(1).rstrip(';').strip()
try:
data = ast.literal_eval(data_str)
return data
except (ValueError, SyntaxError):
continue
raise ValueError("Data object not found in script tags")
if __name__ == "__main__":
url = "https://ely.gg"
data = extract_data_from_page(url)
new_data = {}
replacement_map = {
'greater chain codex': 'Greater Chain ability codex',
'Fractured Armadyl Symbol (Kerapac)': 'Fractured Armadyl Symbol',
'Fractured Stabilization Gem (Kerapac)': 'Fractured Stabilisation Gem',
'Loved Up Walk Override': 'Loved Up Walk Override Token',
'Mizyuyari': 'Mizuyari',
'O lantern title scroll': "'o'-lantern' title scroll",
'OG Gem Cape Token': 'Gem cape token',
'Robin': 'Robin (item)',
'Red Santa Hat': 'Santa Hat',
"One of the many title scroll": "'O ne of the many' title scroll",
"Party Title Scroll": "'Party' title scroll",
}
for item in data:
if 'inverted' in item['value'].lower():
item['value'] = item['value'].replace('(120)', 'token')
for original_key, replacement_value in replacement_map.items():
if item['value'].lower() == original_key.lower():
item['value'] = replacement_value
break
new_data[item['value'].strip()] = item['id']
with open('chrome-extension/new_data.json', 'w') as f:
json.dump(new_data, f, indent=4)

1090
new_data.json Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,258 @@
// ==UserScript==
// @name RuneScape Wiki Ely Prices
// @namespace https://ely.gg/
// @version 1.0.1
// @description Show Ely prices on RuneScape Wiki pages
// @match https://runescape.wiki/w/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect www.ely.gg
// @connect git.yorgei.dev
// @connect *
// @downloadURL https://git.yorgei.dev/yorgei/rs-wiki-ely/raw/branch/main/script.user.js
// ==/UserScript==
(async function () {
const defaultUrl = 'https://git.yorgei.dev/yorgei/rs-wiki-ely/raw/branch/main/new_data.json';
const dataUrl = GM_getValue('elyDataUrl', defaultUrl);
GM_registerMenuCommand('Set Custom Data URL', () => {
const currentUrl = GM_getValue('elyDataUrl', defaultUrl);
const newUrl = prompt('Enter custom data JSON URL:', currentUrl);
if (newUrl) {
GM_setValue('elyDataUrl', newUrl);
alert('Data URL updated! Refresh the page to apply changes.');
}
});
function getCacheBustingUrl(url) {
return `${url}?t=${Date.now()}`;
}
function gmFetchJson(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: res => {
try {
const json = JSON.parse(res.responseText);
resolve(json);
} catch (e) {
reject(e);
}
},
onerror: err => reject(err)
});
});
}
async function fetchPriceData(itemId) {
const url = `https://www.ely.gg/chart/${itemId}/prices`;
const data = await gmFetchJson(url);
return data;
}
try {
const pageTitleElement = document.querySelector('.mw-page-title-main');
if (!pageTitleElement) return;
const originalCursor = pageTitleElement.style.cursor;
const itemData = await gmFetchJson(getCacheBustingUrl(dataUrl));
const pageTitle = pageTitleElement.textContent.trim();
const urlPath = window.location.pathname;
const urlTitle = urlPath.replace('/w/', '').replace(/_/g, ' ');
console.log(pageTitle, urlTitle, urlPath);
let itemId = null;
if (itemData[pageTitle]) {
itemId = itemData[pageTitle];
} else if (itemData[urlTitle]) {
itemId = itemData[urlTitle];
} else {
for (const [itemName, id] of Object.entries(itemData)) {
const itemNameBase = itemName.split('(')[0].trim();
const lowerItem = itemName.toLowerCase();
const lowerBase = itemNameBase.toLowerCase();
const lowerTitle = pageTitle.toLowerCase();
const lowerUrlTitle = urlTitle.toLowerCase();
if (
lowerItem === lowerTitle ||
lowerItem === lowerUrlTitle ||
lowerBase === lowerTitle ||
lowerBase === lowerUrlTitle
) {
itemId = id;
break;
}
}
}
if (!itemId) return;
try {
const priceData = await fetchPriceData(itemId);
if (!priceData.items || !priceData.items.length) return;
const lastItem = priceData.items[priceData.items.length - 1];
const price = Number(lastItem.price).toLocaleString();
const date = new Date(lastItem.date).toLocaleDateString();
const saleType = String(lastItem.purchase || '').toLowerCase();
let saleTypeShort = saleType;
if (saleType === 'sold') {
saleTypeShort = 'inb';
} else if (saleType === 'bought') {
saleTypeShort = 'ins';
}
const priceDisplay = document.createElement('span');
priceDisplay.textContent = ` (${price} gp - ${saleTypeShort} - ${date})`;
priceDisplay.style.color = '#5a8c5a';
priceDisplay.style.fontWeight = 'bold';
priceDisplay.style.fontSize = '0.9em';
pageTitleElement.appendChild(priceDisplay);
pageTitleElement.style.cursor = 'pointer';
pageTitleElement.addEventListener('click', async () => {
try {
const latestPriceData = await fetchPriceData(itemId);
if (latestPriceData.items && latestPriceData.items.length > 0) {
showSalesPopup(latestPriceData.items, itemId, pageTitleElement, originalCursor);
} else {
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
}
} catch (e) {
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
}
});
} catch (e) {
const idDisplay = document.createElement('span');
idDisplay.textContent = ` (ID: ${itemId}) | Failed to get price.`;
idDisplay.style.color = '#5a8c5a';
idDisplay.style.fontWeight = 'bold';
idDisplay.style.fontSize = '0.9em';
pageTitleElement.appendChild(idDisplay);
}
} catch (e) {
console.error('Error loading Ely userscript data:', e);
}
function showSalesPopup(items, itemId, pageTitleElement, originalCursor) {
const existingPopup = document.getElementById('ely-sales-popup');
if (existingPopup) {
existingPopup.remove();
}
const popup = document.createElement('div');
popup.id = 'ely-sales-popup';
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.backgroundColor = '#fff';
popup.style.border = '1px solid #ccc';
popup.style.borderRadius = '8px';
popup.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)';
popup.style.padding = '20px';
popup.style.zIndex = '10000';
popup.style.maxWidth = '500px';
popup.style.width = '90%';
popup.style.maxHeight = '70vh';
popup.style.overflowY = 'auto';
const header = document.createElement('h3');
header.textContent = 'Recent Sales';
header.style.marginTop = '0';
header.style.marginBottom = '15px';
header.style.color = '#333';
popup.appendChild(header);
const salesList = document.createElement('div');
salesList.style.marginBottom = '20px';
const recentSales = items.slice(-10).reverse();
recentSales.forEach(sale => {
const saleItem = document.createElement('div');
saleItem.style.display = 'flex';
saleItem.style.justifyContent = 'space-between';
saleItem.style.padding = '8px 0';
saleItem.style.borderBottom = '1px solid #eee';
const price = document.createElement('span');
price.textContent = `${parseInt(sale.price, 10).toLocaleString()} gp`;
price.style.fontWeight = 'bold';
const type = document.createElement('span');
const purchaseLower = String(sale.purchase || '').toLowerCase();
type.textContent = purchaseLower === 'sold' ? 'inb' : 'ins';
type.style.color = purchaseLower === 'sold' ? '#d9534f' : '#5cb85c';
type.style.fontWeight = 'bold';
const date = document.createElement('span');
date.textContent = new Date(sale.date).toLocaleDateString();
date.style.color = '#666';
saleItem.appendChild(price);
saleItem.appendChild(type);
saleItem.appendChild(date);
salesList.appendChild(saleItem);
});
popup.appendChild(salesList);
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'space-between';
buttonContainer.style.marginTop = '10px';
const elyButton = document.createElement('button');
elyButton.textContent = 'Go to Ely';
elyButton.style.backgroundColor = '#5a8c5a';
elyButton.style.color = 'white';
elyButton.style.border = 'none';
elyButton.style.padding = '8px 16px';
elyButton.style.borderRadius = '4px';
elyButton.style.cursor = 'pointer';
elyButton.addEventListener('click', () => {
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
popup.remove();
pageTitleElement.style.cursor = originalCursor;
});
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.backgroundColor = '#6c757d';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.padding = '8px 16px';
closeButton.style.borderRadius = '4px';
closeButton.style.cursor = 'pointer';
closeButton.addEventListener('click', () => {
popup.remove();
pageTitleElement.style.cursor = originalCursor;
});
buttonContainer.appendChild(elyButton);
buttonContainer.appendChild(closeButton);
popup.appendChild(buttonContainer);
document.body.appendChild(popup);
function closePopupOnOutsideClick(e) {
if (!popup.contains(e.target) && e.target !== pageTitleElement) {
popup.remove();
document.removeEventListener('click', closePopupOnOutsideClick);
pageTitleElement.style.cursor = originalCursor;
}
}
document.addEventListener('click', closePopupOnOutsideClick);
}
})();

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
requests
beautifulsoup4

258
userscript.js Normal file
View File

@@ -0,0 +1,258 @@
// ==UserScript==
// @name RuneScape Wiki Ely Prices
// @namespace https://ely.gg/
// @version 1.0.1
// @description Show Ely prices on RuneScape Wiki pages
// @match https://runescape.wiki/w/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect www.ely.gg
// @connect git.yorgei.dev
// @connect *
// @downloadURL https://git.yorgei.dev/yorgei/rs-wiki-ely/raw/branch/main/script.user.js
// ==/UserScript==
(async function () {
const defaultUrl = 'https://git.yorgei.dev/yorgei/rs-wiki-ely/raw/branch/main/new_data.json';
const dataUrl = GM_getValue('elyDataUrl', defaultUrl);
GM_registerMenuCommand('Set Custom Data URL', () => {
const currentUrl = GM_getValue('elyDataUrl', defaultUrl);
const newUrl = prompt('Enter custom data JSON URL:', currentUrl);
if (newUrl) {
GM_setValue('elyDataUrl', newUrl);
alert('Data URL updated! Refresh the page to apply changes.');
}
});
function getCacheBustingUrl(url) {
return `${url}?t=${Date.now()}`;
}
function gmFetchJson(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: res => {
try {
const json = JSON.parse(res.responseText);
resolve(json);
} catch (e) {
reject(e);
}
},
onerror: err => reject(err)
});
});
}
async function fetchPriceData(itemId) {
const url = `https://www.ely.gg/chart/${itemId}/prices`;
const data = await gmFetchJson(url);
return data;
}
try {
const pageTitleElement = document.querySelector('.mw-page-title-main');
if (!pageTitleElement) return;
const originalCursor = pageTitleElement.style.cursor;
const itemData = await gmFetchJson(getCacheBustingUrl(dataUrl));
const pageTitle = pageTitleElement.textContent.trim();
const urlPath = window.location.pathname;
const urlTitle = urlPath.replace('/w/', '').replace(/_/g, ' ');
console.log(pageTitle, urlTitle, urlPath);
let itemId = null;
if (itemData[pageTitle]) {
itemId = itemData[pageTitle];
} else if (itemData[urlTitle]) {
itemId = itemData[urlTitle];
} else {
for (const [itemName, id] of Object.entries(itemData)) {
const itemNameBase = itemName.split('(')[0].trim();
const lowerItem = itemName.toLowerCase();
const lowerBase = itemNameBase.toLowerCase();
const lowerTitle = pageTitle.toLowerCase();
const lowerUrlTitle = urlTitle.toLowerCase();
if (
lowerItem === lowerTitle ||
lowerItem === lowerUrlTitle ||
lowerBase === lowerTitle ||
lowerBase === lowerUrlTitle
) {
itemId = id;
break;
}
}
}
if (!itemId) return;
try {
const priceData = await fetchPriceData(itemId);
if (!priceData.items || !priceData.items.length) return;
const lastItem = priceData.items[priceData.items.length - 1];
const price = Number(lastItem.price).toLocaleString();
const date = new Date(lastItem.date).toLocaleDateString();
const saleType = String(lastItem.purchase || '').toLowerCase();
let saleTypeShort = saleType;
if (saleType === 'sold') {
saleTypeShort = 'inb';
} else if (saleType === 'bought') {
saleTypeShort = 'ins';
}
const priceDisplay = document.createElement('span');
priceDisplay.textContent = ` (${price} gp - ${saleTypeShort} - ${date})`;
priceDisplay.style.color = '#5a8c5a';
priceDisplay.style.fontWeight = 'bold';
priceDisplay.style.fontSize = '0.9em';
pageTitleElement.appendChild(priceDisplay);
pageTitleElement.style.cursor = 'pointer';
pageTitleElement.addEventListener('click', async () => {
try {
const latestPriceData = await fetchPriceData(itemId);
if (latestPriceData.items && latestPriceData.items.length > 0) {
showSalesPopup(latestPriceData.items, itemId, pageTitleElement, originalCursor);
} else {
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
}
} catch (e) {
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
}
});
} catch (e) {
const idDisplay = document.createElement('span');
idDisplay.textContent = ` (ID: ${itemId}) | Failed to get price.`;
idDisplay.style.color = '#5a8c5a';
idDisplay.style.fontWeight = 'bold';
idDisplay.style.fontSize = '0.9em';
pageTitleElement.appendChild(idDisplay);
}
} catch (e) {
console.error('Error loading Ely userscript data:', e);
}
function showSalesPopup(items, itemId, pageTitleElement, originalCursor) {
const existingPopup = document.getElementById('ely-sales-popup');
if (existingPopup) {
existingPopup.remove();
}
const popup = document.createElement('div');
popup.id = 'ely-sales-popup';
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.backgroundColor = '#fff';
popup.style.border = '1px solid #ccc';
popup.style.borderRadius = '8px';
popup.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)';
popup.style.padding = '20px';
popup.style.zIndex = '10000';
popup.style.maxWidth = '500px';
popup.style.width = '90%';
popup.style.maxHeight = '70vh';
popup.style.overflowY = 'auto';
const header = document.createElement('h3');
header.textContent = 'Recent Sales';
header.style.marginTop = '0';
header.style.marginBottom = '15px';
header.style.color = '#333';
popup.appendChild(header);
const salesList = document.createElement('div');
salesList.style.marginBottom = '20px';
const recentSales = items.slice(-10).reverse();
recentSales.forEach(sale => {
const saleItem = document.createElement('div');
saleItem.style.display = 'flex';
saleItem.style.justifyContent = 'space-between';
saleItem.style.padding = '8px 0';
saleItem.style.borderBottom = '1px solid #eee';
const price = document.createElement('span');
price.textContent = `${parseInt(sale.price, 10).toLocaleString()} gp`;
price.style.fontWeight = 'bold';
const type = document.createElement('span');
const purchaseLower = String(sale.purchase || '').toLowerCase();
type.textContent = purchaseLower === 'sold' ? 'inb' : 'ins';
type.style.color = purchaseLower === 'sold' ? '#d9534f' : '#5cb85c';
type.style.fontWeight = 'bold';
const date = document.createElement('span');
date.textContent = new Date(sale.date).toLocaleDateString();
date.style.color = '#666';
saleItem.appendChild(price);
saleItem.appendChild(type);
saleItem.appendChild(date);
salesList.appendChild(saleItem);
});
popup.appendChild(salesList);
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'space-between';
buttonContainer.style.marginTop = '10px';
const elyButton = document.createElement('button');
elyButton.textContent = 'Go to Ely';
elyButton.style.backgroundColor = '#5a8c5a';
elyButton.style.color = 'white';
elyButton.style.border = 'none';
elyButton.style.padding = '8px 16px';
elyButton.style.borderRadius = '4px';
elyButton.style.cursor = 'pointer';
elyButton.addEventListener('click', () => {
window.open(`https://www.ely.gg/view_item/${itemId}`, '_blank');
popup.remove();
pageTitleElement.style.cursor = originalCursor;
});
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.backgroundColor = '#6c757d';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.padding = '8px 16px';
closeButton.style.borderRadius = '4px';
closeButton.style.cursor = 'pointer';
closeButton.addEventListener('click', () => {
popup.remove();
pageTitleElement.style.cursor = originalCursor;
});
buttonContainer.appendChild(elyButton);
buttonContainer.appendChild(closeButton);
popup.appendChild(buttonContainer);
document.body.appendChild(popup);
function closePopupOnOutsideClick(e) {
if (!popup.contains(e.target) && e.target !== pageTitleElement) {
popup.remove();
document.removeEventListener('click', closePopupOnOutsideClick);
pageTitleElement.style.cursor = originalCursor;
}
}
document.addEventListener('click', closePopupOnOutsideClick);
}
})();