diff --git a/js/fuck-jr-central-oshi-tabi-speedtest/README.md b/js/fuck-jr-central-oshi-tabi-speedtest/README.md new file mode 100644 index 0000000..b6a93ec --- /dev/null +++ b/js/fuck-jr-central-oshi-tabi-speedtest/README.md @@ -0,0 +1,62 @@ +# fuck-jr-central-oshi-tabi-speedtest 去他妈的JR东海/东日本 "推し旅" 测速 + +> 本项目修改自 [kiritoxkiriko/fuck-jr-central-oshi-tabi-speedtest](https://github.com/kiritoxkiriko/fuck-jr-central-oshi-tabi-speedtest) + +## 简介 +众所周知,JR 东海/东日本经常推出和热门 IP 联动的 "推し旅" 活动。 +这些活动往往需要乘坐新干线进行测速来领取特典,比如 [JR東海×「BanG Dream! 10th ANNIVERSARY」](https://recommend.jr-central.co.jp/oshi-tabi/bang-dream-10th/)、 +[ブルーアーカイブ × JR東海 推し旅 名古屋](https://oshi-tabi.voistock.com/2605bluearchive/)。 +本脚本用来快速绕过这个测速。 + +

+ +

+ +## 原理 +活动页通过浏览器的 `navigator.geolocation` API 获取你的**速度**和**位置**,并校验位置是否落在一组预设的矩形区域(`area`)内。 +原项目针对的 `recommend.jr-central.co.jp` 旧活动是在**前端**直接判定的,所以只要 hook `navigator.geolocation.watchPosition`, +让它返回一个**固定速度**和一个**范围内随机经纬度**即可绕过。 +(`oshi-tabi.voistock.com` 的活动则把采样点上传到**服务端**判定,详见下方「服务端测速」一节;但数据源同样是 `navigator.geolocation`,所以 hook 的思路通用。) + +不过 voistock 的 ブルアカ × 名古屋 活动除了速度,还会判定**方向**: +它会比较连续位置点相对目标车站(名古屋 `[35.17099309, 136.8815566]`)的运动方向, +入口 `名古屋行き` 要求逐步**靠近**名古屋(`towards`),`名古屋帰り` 要求逐步**远离**(`away`)。 +旧脚本返回的“东京矩形内随机点”没有稳定路线,连续采样的方向来回跳变,会触发: + +``` +進行方向が逆(反対方向)です。正しい方向の新幹線で再度お試しください。 +``` + +因此本脚本改为**沿东海道新干线真实途经点的折线轨迹模拟移动**: + +- 内置东京→名古屋的途经点折线,按真实时间推进位置(`已行驶距离 = 经过秒数 × 速度`), + 保证上报的 `speed` 字段与相邻采样点算出的“距离/时间”速度自洽,且落在活动页要求的 `150–360 km/h` 区间内。 +- 根据相邻点的方位角填入 `heading`(不再是 `null`)。 +- 同时覆写 `watchPosition`、`getCurrentPosition`、`clearWatch`,覆盖页面可能用到的多种采样方式。 +- 右下角提供方向选择悬浮面板(`名古屋行き` / `名古屋帰り`),切换即从对应起点重新出发; + `towards` 时到名古屋距离单调递减,`away` 时单调递增。 + +可调参数集中在脚本顶部(`SPEED_KMH`、`SAMPLE_INTERVAL_MS`、途经点 `ROUTE_TO_NAGOYA`、`ENABLE_NIGHT_BYPASS` 等), +不同活动页如有差异可自行调整。 + +## 服务端测速与接口营业时间 +`oshi-tabi.voistock.com` 上的 ブルアカ × 名古屋 活动(第 1、2 弾共用同一套 `scrt_measure.js`)的测速判定在**服务端**: +浏览器只负责用 `navigator.geolocation` 采集 GPS 点、成批 `POST` 到 `/api/measure/start`、`/api/measure/point(s)`, +最终由服务器返回通过/失败。 +(这跟原项目针对的 `recommend.jr-central.co.jp` 旧活动不是同一套实现,后者据原项目描述为纯前端判定。) + +好消息是 - 服务器仍然是**按你上传的 GPS 轨迹**判定的,并不需要真实乘车凭证或额外认证, +所以本脚本的轨迹模拟**同样有效**,方向选对即可通过。 + +需要注意的一个坑是 **接口营业时间**:测速 API 在**深夜会关闭**,此时请求会直接返回 + +```json +{ "status": "unavailable", "message": "このAPIは深夜の間はご利用いただけません。", "available_from": "05:55 JST" } +``` + +控制台对应 `[MeasureClient] Error: 503 error`。**这跟脚本无关**,是接口本身按时间关闭。处理方式: + +- 正常情况下,**等日本时间 05:55 之后再测**即可。 +- 夜间调试时,脚本顶部的 `ENABLE_NIGHT_BYPASS`(默认开启)会在 `document-start` 阶段写入 + `localStorage.devMode = '1'`,让页面启用其内置的夜间测试 token 以绕开营业时间限制。 + 这只绕开“营业时间”,token 由 JR 维护、随时可能失效;不需要时把它设为 `false` 即可。 diff --git a/js/fuck-jr-central-oshi-tabi-speedtest/fuck-jr-central-oshi-tabi-speedtest.js b/js/fuck-jr-central-oshi-tabi-speedtest/fuck-jr-central-oshi-tabi-speedtest.js new file mode 100644 index 0000000..d4d0aa5 --- /dev/null +++ b/js/fuck-jr-central-oshi-tabi-speedtest/fuck-jr-central-oshi-tabi-speedtest.js @@ -0,0 +1,218 @@ +// ==UserScript== +// @name fuck-jr-central-oshi-tabi-speedtest +// @namespace https://recommend.jr-central.co.jp/oshi-tabi/ +// @version 1.0 +// @description 去他妈的JR东海/东日本 推し旅 活动测速(沿新干线轨迹模拟,支持方向判定) +// @author Stardream +// @match https://oshi-tabi.voistock.com/* +// @match https://recommend.jr-central.co.jp/oshi-tabi/* +// @grant none +// @run-at document-start +// @license MIT +// ==/UserScript== + +(function () { + 'use strict'; + + // ===== 夜间测试开关 ===== + // 测速接口在深夜会关闭(503 / available_from 05:55 JST)。 + // 页面内联脚本会在 devMode 下设置 window.NIGHT_BYPASS_TOKEN 以绕开夜间窗口。 + // 本脚本 @run-at document-start 先于该内联脚本执行,所以这里提前置位即可生效。 + // 注意:这只是绕开“营业时间”,且 token 由 JR 维护,随时可能失效;白天测试无需此项。 + const ENABLE_NIGHT_BYPASS = true; + if (ENABLE_NIGHT_BYPASS) { + try { + if (localStorage.getItem('devMode') !== '1') { + localStorage.setItem('devMode', '1'); + } + } catch (e) { /* localStorage 不可用时忽略 */ } + } + + // ===== 可配置项 ===== + // 模拟速度,单位 km/h(活动页要求落在 150-360 之间) + const SPEED_KMH = 250; + // 成功回调的采样间隔(毫秒) + const SAMPLE_INTERVAL_MS = 1000; + // GPS 精度(米) + const ACCURACY_M = 10; + // 默认行进方向:'towards' = 名古屋行き(驶向名古屋),'away' = 名古屋帰り(驶离名古屋) + let DIRECTION = 'towards'; + + const SPEED_MPS = (SPEED_KMH * 1000) / 3600; + + // ===== 东海道新干线 东京→名古屋 主要途经点(名古屋为终点)===== + // 沿真实线路排列,保证“驶向名古屋”时到名古屋的距离单调递减。 + const ROUTE_TO_NAGOYA = [ + { lat: 35.681236, lon: 139.767125 }, // 東京 + { lat: 35.628471, lon: 139.738760 }, // 品川 + { lat: 35.507871, lon: 139.617495 }, // 新横浜 + { lat: 35.256293, lon: 139.155720 }, // 小田原 + { lat: 35.103156, lon: 139.078000 }, // 熱海 + { lat: 35.126363, lon: 138.911040 }, // 三島 + { lat: 35.142395, lon: 138.663320 }, // 新富士 + { lat: 34.971401, lon: 138.389500 }, // 静岡 + { lat: 34.838074, lon: 138.130700 }, // 掛川付近 + { lat: 34.703608, lon: 137.734900 }, // 浜松 + { lat: 34.762960, lon: 137.381900 }, // 豊橋 + { lat: 34.962145, lon: 137.081050 }, // 三河安城 + { lat: 35.170993, lon: 136.881557 } // 名古屋(目标点) + ]; + + // ===== 几何工具 ===== + const R = 6371000; // 地球半径(米) + const toRad = (d) => (d * Math.PI) / 180; + const toDeg = (r) => (r * 180) / Math.PI; + + function haversine(a, b) { + const dLat = toRad(b.lat - a.lat); + const dLon = toRad(b.lon - a.lon); + const lat1 = toRad(a.lat); + const lat2 = toRad(b.lat); + const h = + Math.sin(dLat / 2) ** 2 + + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2; + return 2 * R * Math.asin(Math.sqrt(h)); + } + + // 计算从 a 指向 b 的方位角(0-360,正北为0,顺时针) + function bearing(a, b) { + const lat1 = toRad(a.lat); + const lat2 = toRad(b.lat); + const dLon = toRad(b.lon - a.lon); + const y = Math.sin(dLon) * Math.cos(lat2); + const x = + Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon); + return (toDeg(Math.atan2(y, x)) + 360) % 360; + } + + // 在两点之间按比例 t 线性插值(短距离下足够精确) + function lerp(a, b, t) { + return { + lat: a.lat + (b.lat - a.lat) * t, + lon: a.lon + (b.lon - a.lon) * t + }; + } + + // ===== 预计算某方向路线的累计距离 ===== + function buildRoute(points) { + const cum = [0]; + for (let i = 1; i < points.length; i++) { + cum.push(cum[i - 1] + haversine(points[i - 1], points[i])); + } + return { points, cum, total: cum[cum.length - 1] }; + } + + const routeTowards = buildRoute(ROUTE_TO_NAGOYA); + const routeAway = buildRoute([...ROUTE_TO_NAGOYA].reverse()); + + function activeRoute() { + return DIRECTION === 'away' ? routeAway : routeTowards; + } + + // 给定沿路线已行驶的距离(米),返回当前坐标与航向 + function pointAtDistance(route, dist) { + const { points, cum, total } = route; + const d = Math.max(0, Math.min(dist, total)); + let i = 0; + while (i < cum.length - 2 && cum[i + 1] < d) i++; + const segLen = cum[i + 1] - cum[i] || 1; + const t = (d - cum[i]) / segLen; + const pos = lerp(points[i], points[i + 1], t); + const head = bearing(points[i], points[i + 1]); + return { pos, head }; + } + + // ===== 轨迹状态 ===== + let startTime = null; // 本次测速的起始时间戳 + + function ensureStarted() { + if (startTime === null) startTime = Date.now(); + } + + function currentCoords() { + ensureStarted(); + const elapsedSec = (Date.now() - startTime) / 1000; + const traveled = elapsedSec * SPEED_MPS; + const { pos, head } = pointAtDistance(activeRoute(), traveled); + return { + latitude: pos.lat, + longitude: pos.lon, + accuracy: ACCURACY_M, + altitude: null, + altitudeAccuracy: null, + heading: head, + speed: SPEED_MPS // 单位 m/s + }; + } + + function createPosition() { + return { + coords: currentCoords(), + timestamp: Date.now() + }; + } + + // ===== 覆写 geolocation API ===== + const geo = navigator.geolocation; + + geo.watchPosition = function (success, error, options) { + // 每次开始监听都重置轨迹,从线路起点出发 + startTime = Date.now(); + if (typeof success === 'function') success(createPosition()); + const id = setInterval(() => { + if (typeof success === 'function') success(createPosition()); + }, SAMPLE_INTERVAL_MS); + return id; + }; + + geo.clearWatch = function (watchId) { + clearInterval(watchId); + }; + + geo.getCurrentPosition = function (success, error, options) { + if (typeof success === 'function') success(createPosition()); + }; + + // ===== 方向选择悬浮面板 ===== + function injectPanel() { + if (document.getElementById('fjct-panel')) return; + const panel = document.createElement('div'); + panel.id = 'fjct-panel'; + panel.style.cssText = [ + 'position:fixed', 'right:12px', 'bottom:12px', 'z-index:2147483647', + 'background:rgba(20,20,28,0.92)', 'color:#fff', 'font:12px/1.5 sans-serif', + 'padding:10px 12px', 'border-radius:10px', 'box-shadow:0 4px 16px rgba(0,0,0,.4)', + 'min-width:150px' + ].join(';'); + panel.innerHTML = + '
推し旅 测速模拟
' + + '' + + '' + + '
'; + const mount = () => { + (document.body || document.documentElement).appendChild(panel); + panel.querySelectorAll('input[name="fjct-dir"]').forEach((r) => { + r.addEventListener('change', (e) => { + DIRECTION = e.target.value; + startTime = Date.now(); // 切换方向后重新出发 + updateStatus(); + }); + }); + updateStatus(); + }; + if (document.body) mount(); + else document.addEventListener('DOMContentLoaded', mount); + } + + function updateStatus() { + const el = document.getElementById('fjct-status'); + if (!el) return; + const dir = DIRECTION === 'away' ? '驶离名古屋' : '驶向名古屋'; + el.textContent = `${dir} · ${SPEED_KMH}km/h`; + } + + injectPanel(); +})();