diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 8f412bc..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,51 +0,0 @@ -on: - push: - branches: - - master - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Use Node.js 24 - uses: actions/setup-node@v1 - with: - node-version: 24 - - run: corepack enable - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Build - run: NODE_ENV=production pnpm build - - run: echo `git rev-parse HEAD` > dist/COMMIT - - name: Upload Build Result - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist - deploy: - runs-on: ubuntu-latest - needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - steps: - - name: Download Build Result - uses: actions/download-artifact@v4 - with: - name: dist - path: dist - # upload to dist branch - - uses: actions/checkout@v2 - with: - ref: dist - path: ./dist-branch - - run: rm -rf ./dist-branch/* && cp -r ./dist/* ./dist-branch/ - - run: | - COMMIT=`cat dist/COMMIT` - cd dist-branch - rm COMMIT - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A . - git commit --allow-empty -m "update with $COMMIT" - git push diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 437c2ba..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -dist/ -.rollup.cache/ -tsconfig.tsbuildinfo diff --git a/README.md b/README.md deleted file mode 100644 index 753015f..0000000 --- a/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# userscripts - -:warning: 基本的に他人にとって使い物になるものは Greasy Fork にミラーしているので、そちらからのインストールをおすすめします https://greasyfork.org/ja/users/163419-rinsuki - -ビルド成果物が https://github.com/rinsuki/userscripts/tree/dist にあります - -## How to develop - -needs: ViolentMonkeyが入ったブラウザ, Node.js, pnpm - -1. `pnpm dev` をすると watch & HTTPサーバーが起動します -1. http://localhost:9191/ を開いて開発したい user.js を開くと ViolentMonkey のインストール画面が開くはずです -1. 「このページを閉じるまでローカルファイルの変更を監視する」にチェックを入れてからインストールします -1. そのタブを放っておいてソースコードを変更します -1. 自動でビルドされて数秒後に ViolentMonkey が更新を感知するはずです (ViolentMonkey が更新を取得するとrollupのビルドログのあとにリクエストログが流れます) -1. Have fun! - -:warning: scripts/*/banner.js の更新は感知されないので rollup を手で回すか適当に src/ の下をいじるとかしてください - -## LICENSE - -MIT License. diff --git a/devserver.js b/devserver.js deleted file mode 100644 index 1f0c731..0000000 --- a/devserver.js +++ /dev/null @@ -1,60 +0,0 @@ -// @ts-check -const http = require("http") -const fs = require("fs") - -http.createServer(async (req, res) => { - if (req.method !== "GET") { - res.writeHead(405) - res.end() - return - } - const path = new URL(req.url, `http://example.com`).pathname - if (path === "/") { - console.log("[DEVSERVER]", new Date().toISOString(), req.socket.remoteAddress, req.method, path) - res.setHeader("Content-Type", "text/html") - res.writeHead(200) - res.write("

scripts

simple-scripts

simple-styles

") - res.end() - return - } else if (/^\/(dist|simple-scripts|simple-styles)\/[a-z0-9\.\-]+\.user\.(js|css)$/.test(path)) { - const stat = await fs.promises.stat(__dirname+path) - const etag = `"${stat.mtimeMs}"` - if (req.headers["if-none-match"] === etag) { - res.writeHead(304) - res.end() - return - } - console.log("[DEVSERVER]", new Date().toISOString(), req.socket.remoteAddress, req.method, path) - res.setHeader("ETag", etag) - if (path.endsWith(".js")) { - res.setHeader("Content-Type", "application/javascript") - } else { - res.setHeader("Content-Type", "text/css") - } - res.setHeader("X-Content-Type-Options", "nosniff") - fs.createReadStream(__dirname+path).pipe(res) - return - } - console.log("[DEVSERVER]", new Date().toISOString(), req.socket.remoteAddress, req.method, path) - res.setHeader("Content-Type", "text/plain") - res.writeHead(404) - res.write("Not Found") - res.end() -}).listen(9191, "localhost", () => { - console.log("listen at http://localhost:9191") -}) \ No newline at end of file diff --git a/erogamescape-campaign-histories.user.js b/erogamescape-campaign-histories.user.js new file mode 100644 index 0000000..978d08f --- /dev/null +++ b/erogamescape-campaign-histories.user.js @@ -0,0 +1,135 @@ +// ==UserScript== +// @name ErogameScape: ゲームのセール履歴を追加 +// @description エロゲー批評空間のゲームページに、ゲームのセール(キャンペーン)履歴を表示します。 +// @version 0.1.0 +// @grant none +// @match https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/game.php?* +// @namespace https://rinsuki.net +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// @require https://cdn.jsdelivr.net/npm/@rinsuki/dom-chef@5.1.1/umd.js#sha256-EvGMVNob2jRhcwSo8gGGJp3mZJENNaQwrpYlNRQlUMc=,sha512-b/5dx8A+dc01RsqR2hF2Na/DFwwn64M4PQFka7dXhcAeXjPfpkgWS9YFs1LHPaEckkxAI7ivhVn6i+OrFivnYQ== +// ==/UserScript== + +(function () { + 'use strict'; + + const gameIdPlaceholder = "__GAME_ID__"; + const sql = `SELECT + row_to_json(campaignlist) AS campaign, + row_to_json(campaign_game) AS campaign_game +FROM campaign_game +INNER JOIN campaignlist ON campaign_game.campaign = campaignlist.id +WHERE campaign_game.game = ${gameIdPlaceholder} ORDER BY campaignlist.end_timestamp DESC +-- UserScript: ${GM_info.script.name} ${GM_info.script.version}`; + const domainsToServiceName = { + ".dmm.co.jp": "FANZA", + ".dmm.com": "DMM", + ".dlsite.com": "DLsite", + ".dlaf.jp": "DLsite", + }; + function domainToServiceName(url) { + const parsed = URL.parse(url); + if (parsed == null) + return null; + for (const [domain, serviceName] of Object.entries(domainsToServiceName)) { + if (parsed.hostname.endsWith(domain)) + return serviceName; + if (domain.startsWith(".") && parsed.hostname === domain.slice(1)) + return serviceName; + } + return null; + } + const sectionId = "userscript-erogamescape-campaign-histories"; + const style = `#${sectionId} { + & .current-campaign { + color: red; + font-weight: bolder; + } +}`; + async function main() { + const url = new URL(location.href); + const gameId = url.searchParams.get("game"); + if (gameId == null) + return; + const section = DOMChef.createElement("section", { id: sectionId }, + DOMChef.createElement("h3", null, + "セール履歴 (UserScript: ", + GM_info.script.name, + " ", + GM_info.script.version, + ")"), + DOMChef.createElement("style", null, style)); + const beforeElements = [ + document.querySelector(`#toContents + div[style="margin-bottom: 20px;"]`), + document.getElementById("toContents"), + ]; + for (const el of beforeElements) { + if (el == null) + continue; + el.insertAdjacentElement("afterend", section); + break; + } + fetch("/~ap2/ero/toukei_kaiseki/sql_for_erogamer_form.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `sql=${encodeURIComponent(sql.replace(gameIdPlaceholder, gameId))}`, + }).then(async (r) => { + if (!r.ok) + throw new Error(`HTTP error ${r.status}\n\n${await r.text()}`); + const dom = new DOMParser().parseFromString(await r.text(), "text/html"); + const table = dom.querySelector("#query_result_main > table"); + if (table == null) + throw new Error(`テーブルが見つからない`); + const rows = []; + const keys = []; + for (const rowEl of table.rows) { + console.log(rowEl); + if (rowEl.querySelector("th") != null) { + keys.push(...Array.from(rowEl.cells, c => c.textContent.trim() ?? "")); + continue; + } + const values = Array.from(rowEl.cells, c => c.textContent.trim() ?? ""); + const row = Object.fromEntries(values.map((v, i) => [keys[i], JSON.parse(v)])); + rows.push(row); + } + console.log(rows); + if (rows.length) { + section.append(DOMChef.createElement("table", null, + DOMChef.createElement("tr", null, + DOMChef.createElement("th", null, "終了日時 (たぶんJST)"), + DOMChef.createElement("th", null, "内容"), + DOMChef.createElement("th", null, "キャンペーン名")), + rows.map(row => { + const startDate = new Date(row.campaign.start_timestamp); + const endDate = new Date(row.campaign.end_timestamp); + const isCurrentCampaign = endDate.getTime() > Date.now(); + const serviceName = domainToServiceName(row.campaign_game.url ?? row.campaign.url ?? ""); + const duration = Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); + return DOMChef.createElement("tr", null, + DOMChef.createElement("td", { title: `${row.campaign.start_timestamp} 〜 ${row.campaign.end_timestamp}` }, + DOMChef.createElement("span", { className: isCurrentCampaign ? "current-campaign" : "" }, row.campaign.end_timestamp), + " ", + DOMChef.createElement("small", null, + "(約", + duration, + "日間)")), + DOMChef.createElement("td", null, + DOMChef.createElement("a", { href: row.campaign_game.url ?? undefined }, row.campaign_game.content)), + DOMChef.createElement("td", null, + serviceName && `[${serviceName}] `, + DOMChef.createElement("a", { href: row.campaign.url ?? undefined }, row.campaign.name))); + }))); + } + else { + section.append(DOMChef.createElement("p", null, "セール履歴が見つからなかったか、取得に失敗しました。")); + } + }); + } + main().catch(e => { + console.error(e); + alert(`セール情報の取得に失敗しました:\n${e}\n\nfrom UserScript ${GM_info.script.name} ${GM_info.script.version}`); + }); + +})(); diff --git a/external-globals.js b/external-globals.js deleted file mode 100644 index 1e759ae..0000000 --- a/external-globals.js +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-check - -/** @type {Record} */ -export const table = { - zod: { - var: "Zod", - path: "lib/index.umd.js", - }, - react: { - var: "React", - path: "umd/react.production.min.js", - }, - "react-dom": { - var: "ReactDOM", - path: "umd/react-dom.production.min.js", - }, - "@rinsuki/dom-chef": { - var: "DOMChef", - path: "umd.js", - } -} \ No newline at end of file diff --git a/fix-1password-extension-passkey.user.js b/fix-1password-extension-passkey.user.js new file mode 100644 index 0000000..c66d959 --- /dev/null +++ b/fix-1password-extension-passkey.user.js @@ -0,0 +1,41 @@ +// ==UserScript== +// @name 1Password Extension Passkey Fix +// @namespace rinsuki.net +// @description Treat 1Password extension's response as a instance of window.PublicKeyCredential +// @version 1.0 +// @match https://accounts.nintendo.com/* +// @grant none +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + (() => { + const origPublicKeyCredential = window.PublicKeyCredential; + window.PublicKeyCredential = new Proxy(origPublicKeyCredential, { + get(...args) { + if (args[1] === Symbol.hasInstance) { + return (instance) => { + if (typeof instance !== "object") + return false; + if (instance == null) + return false; + if (!("type" in instance)) + return false; + if (instance.type !== "public-key") + return false; + if (!("id" in instance)) + return false; + if (typeof instance.id !== "string") + return false; + return true; + }; + } + return Reflect.get(...args); + } + }); + })(); + +})(); diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index b413e10..0000000 --- a/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; \ No newline at end of file diff --git a/mastodon-show-act-users.user.js b/mastodon-show-act-users.user.js new file mode 100644 index 0000000..bc26f87 --- /dev/null +++ b/mastodon-show-act-users.user.js @@ -0,0 +1,155 @@ +// ==UserScript== +// @name Mastodon Show act users in /@xxx/12345 +// @namespace https://rinsuki.net +// @version 0.3.2 +// @description Mastodonの投稿詳細画面でBT/favしたユーザー一覧を見れるようにする +// @author rinsuki +// @include /^https:\/\/[^/]*\/@[A-Za-z0-9_]+\/[0-9]+([?#].*)?$/ +// @exclude-match https://*/@*/*/embed +// @exclude-match https://*.tiktok.com/* +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// @require https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js#sha256-2Unxw2h67a3O2shSYYZfKbF80nOZfn9rK/xTsvnUxN0=,sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg== +// @require https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js#sha256-NfT5dPSyvNRNpzljNH+JUuNB+DkJ5EmCJ9Tia5j2bw0=,sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ== +// ==/UserScript== + +(function () { + 'use strict'; + + const User = ({ user }) => { + return React.createElement("div", { style: { backgroundColor: "#282c37", marginBottom: 1, display: "flex" } }, + React.createElement("a", { href: user.url, style: { padding: 10, display: "flex", textDecoration: "none" } }, + React.createElement("img", { src: user.avatar_static, style: { width: 36, height: 36, paddingRight: 10 } }), + React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column" } }, + React.createElement("bdi", { style: { flex: 1, color: "#ffffff" } }, user.display_name), + React.createElement("span", { style: { flex: 1, color: "#ffffff9f" } }, + "@", + user.acct)))); + }; + + const List = ({ type, statusId }) => { + const [loading, setLoading] = React.useState(false); + const [users, setUsers] = React.useState([]); + const [error, setError] = React.useState(undefined); + React.useEffect(() => ((async () => { + setLoading(true); + try { + const res = await fetch(`${location.origin}/api/v1/statuses/${statusId}/${type === "favourite" ? "favourited_by" : "reblogged_by"}`); + const text = await res.text(); + try { + const json = JSON.parse(text); + const error = json.error; + if (error) + throw new Error(`API: ${error}`); + setError(undefined); + setUsers(json); + } + catch (e) { + throw new Error(`HTTP ${res.status} ${res.statusText} - ${text}`); + } + } + catch (e) { + console.log(e); + setError(e); + } + finally { + setLoading(false); + } + })(), undefined), []); + const centeringStyle = { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + height: "100%", + padding: "30px 15px" + }; + if (error) { + return React.createElement("div", { style: centeringStyle }, + React.createElement("span", { style: { color: "hsl(0, 100%, 60%)", whiteSpace: "pre-wrap" } }, error != null && typeof error === "object" && "stack" in error && typeof error.stack === "string" && error.stack)); + } + else if (loading) { + return React.createElement("div", { style: centeringStyle }, + React.createElement("span", null, "Loading...")); + } + else { + return React.createElement("div", null, users.map(user => React.createElement(User, { user: user, key: user.id }))); + } + }; + + const SectionHeader = ({ icon, name }) => { + return React.createElement("div", { style: { + backgroundColor: document.body.style.backgroundColor || "#17191f", + fontSize: 16, + padding: 15, + color: "white", + } }, + React.createElement("i", { className: `fa fa-${icon}`, style: { marginRight: 5 } }), + name); + }; + + (function () { + // Mastodonかどうかを確かめる + const regex = /^\/@[A-Za-z0-9_]+\/(\d+)/; + const matchedUrl = regex.exec(location.pathname); + if (matchedUrl == null) + return; // URLがそれっぽくない + // これログインしてるとひっかかることに気づいたので無効化 + // if (document.querySelector('a[href="https://joinmastodon.org/#getting-started"]') == null) return // Mastodonっぽくなさそう + if (document.querySelector('a[href^="/interact/"]') == null) + return; // 新しいMastodonを使え + const statusId = matchedUrl[1]; + const statusMetaDiv = document.querySelector(".detailed-status__meta"); + if (statusMetaDiv == null) + return; // UIを出すところがないので帰る + const boostButton = parent(document.querySelector('a[href^="/interact/"][href*="type=reblog"] > .detailed-status__reblogs')); + const favButton = parent(document.querySelector('a[href^="/interact/"] > .detailed-status__favorites')); + const isNicoru = favButton && favButton.querySelector(".fa-nicoru--status"); + // ReactでUIを作る + const Root = () => { + const [active, setActive] = React.useState(undefined); + React.useEffect(() => { + if (boostButton) { + boostButton.addEventListener("click", e => { + e.preventDefault(); + e.stopPropagation(); + setActive("reblog"); + }); + } + if (favButton) { + favButton.addEventListener("click", e => { + e.preventDefault(); + e.stopPropagation(); + setActive("favourite"); + }); + } + }, []); + if (active == null) + return React.createElement("div", null, + React.createElement("i", { className: "fa fa-retweet" }), + " か ", + isNicoru ? React.createElement("i", { className: "fa fa-nicoru--status" }) : React.createElement("i", { className: "fa fa-star" }), + " をクリックするとブーストした/ふぁぼったユーザーが表示されます"); + const [name, icon] = { + "favourite": isNicoru ? ["ニコる", "nicoru"] : ["ふぁぼ", "star"], + "reblog": ["ブースト", "retweet"] + }[active]; + return React.createElement("div", { style: { margin: "15px -15px -15px" } }, + React.createElement(SectionHeader, { icon: icon, name: name }), + React.createElement("div", null, + React.createElement(List, { type: active, key: active, statusId: statusId })), + React.createElement(SectionHeader, { icon: "reply", name: "返信" })); + }; + function parent(dom) { + if (dom == null) + return dom; + return dom.parentElement; + } + // Reactをマウント + const myDiv = document.createElement("div"); + ReactDOM.render(React.createElement(Root, null), myDiv); + statusMetaDiv.appendChild(myDiv); + })(); + +})(); diff --git a/mb-artist-credit-splitter-by-ai.user.js b/mb-artist-credit-splitter-by-ai.user.js new file mode 100644 index 0000000..6b98b5a --- /dev/null +++ b/mb-artist-credit-splitter-by-ai.user.js @@ -0,0 +1,173 @@ +// ==UserScript== +// @name MB: Artist Credit Splitter, but Powered by AI +// @namespace https://rinsuki.net/ +// @version 1.1.0 +// @description OpenRouter でいい感じに MusicBrainz のアーティストクレジットを分割します (失敗することもあります) +// @author rinsuki +// @match https://*.musicbrainz.org/* +// @grant GM_getValue +// @grant GM_setValue +// @grant GM.registerMenuCommand +// @grant GM.xmlHttpRequest +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + const SYSTEM_PROMPT = [ + "あなたはアーティストクレジットを分割するプロフェッショナルです。", + "あなたは与えられた文字列を分割し、適切なオブジェクトの配列に変換する必要があります。", + "それぞれのオブジェクトは、`name`と`joinPhrase`の2つのフィールドを持つ必要があります。", + "`name`フィールドにはアーティストの名前を、`joinPhrase`フィールドには次の名前と結合するために使用されるフレーズを含める必要があります。", + "ルール:", + "* 出力は、入力のすべての文字を、そのままの文字で、削除せずに含める必要があります。句読点や括弧も全て含めます (だいたいの場合はそれらは`joinPhrase`に含めることになるでしょう)。文字種を変換してもいけません (例えば `、` を `,` に変換することはできません)。出力が入力と一致しない場合、ユーザーにエラーが表示されます。", + "* 出力は、1つのオブジェクトに付き1人のアーティスト(個人またはグループ)を含める必要があります。つまり、CVクレジットを1つのアーティストオブジェクトに入力することはできません。たとえば、`キャラ (CV. 人物名)`を`name`に設定することは許可されていません。", + "* `joinPharse`に人物名を含めることはできません。CVクレジットなどが含まれる場合は、人物名の部分でオブジェクトを分割し、次のオブジェクトの`name`に代入してください。", + "* 出力はJSON形式をそのまま出力する必要があります (Markdownのコードブロックなどは不要です)。", + ].join("\n"); + const exampleInputs = [ + { role: "user", content: "せるふとぷりん(稲垣好 / 市ノ瀬加那)" }, + { role: "assistant", content: JSON.stringify([{ "name": "せるふ", "joinPhrase": "と" }, { "name": "ぷりん", "joinPhrase": "(" }, { "name": "稲垣好", "joinPhrase": " / " }, { "name": "市ノ瀬加那", "joinPhrase": ")" }]) }, + { role: "user", content: "Triad Primus [渋谷凛(CV:福原綾香)×神谷奈緒(CV:松井恵理子)×北条加蓮(CV:渕上舞)]" }, + { role: "assistant", content: JSON.stringify([ + { "name": "Triad Primus", "joinPhrase": " [" }, + { "name": "渋谷凛", "joinPhrase": "(CV:" }, + { "name": "福原綾香", "joinPhrase": ")×" }, + { "name": "神谷奈緒", "joinPhrase": "(CV:" }, + { "name": "松井恵理子", "joinPhrase": ")×" }, + { "name": "北条加蓮", "joinPhrase": "(CV:" }, + { "name": "渕上舞", "joinPhrase": ")]" }, + ]) }, + ]; + function copiedArtistCreditToPlainString(artistCredit) { + return artistCredit.map(credit => credit.name + credit.joinPhrase).join(""); + } + GM.registerMenuCommand("Split clipboard content (OpenRouter)", () => { + const inputString = localStorage.getItem("copiedArtistCredit"); + if (inputString == null) { + alert("No copied artist credit found."); + return; + } + const inputJSON = JSON.parse(inputString); + const input = copiedArtistCreditToPlainString(inputJSON.names); + let apiKey = GM_getValue("openrouter_api_key"); + if (apiKey == null) { + apiKey = prompt("Please input your OpenRouter API key") ?? undefined; + if (apiKey == null) { + return; + } + GM_setValue("openrouter_api_key", apiKey); + } + const div = document.createElement("div"); + div.style.position = "fixed"; + div.style.top = "0"; + div.style.left = "0"; + div.style.width = "100%"; + div.style.height = "100%"; + div.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + div.style.zIndex = "9999"; + div.style.display = "flex"; + div.style.alignItems = "center"; + div.style.justifyContent = "center"; + div.style.color = "white"; + div.style.fontSize = "20px"; + div.textContent = "Asking to the LLM..."; + document.body.appendChild(div); + fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: prompt("model", "google/gemini-2.0-flash-exp:free"), + temperature: 0, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + ...exampleInputs, + { role: "user", content: input }, + ], + }) + }).then(r => r.json()).then(r => { + div.remove(); + console.log(r); + const generated = JSON.parse(r.choices[0].message.content); + const output = copiedArtistCreditToPlainString(generated); + if (output !== input) { + alert("LLM's output does not match with input. The LLM moment...\n\n" + input + "\n" + output); + return; + } + if (confirm("Done! ... according to the LLM (" + r.model + ").\n\n" + generated.map(a => JSON.stringify(a)).join("\n"))) { + localStorage.setItem("copiedArtistCredit", JSON.stringify({ + names: generated, + })); + } + }); + }); + GM.registerMenuCommand("Split clipboard content (OpenAI)", () => { + const inputString = localStorage.getItem("copiedArtistCredit"); + if (inputString == null) { + alert("No copied artist credit found."); + return; + } + const inputJSON = JSON.parse(inputString); + const input = copiedArtistCreditToPlainString(inputJSON.names); + let apiKey = GM_getValue("openai_api_key"); + if (apiKey == null) { + apiKey = prompt("Please input your OpenAI API key") ?? undefined; + if (apiKey == null) { + return; + } + GM_setValue("openai_api_key", apiKey); + } + const div = document.createElement("div"); + div.style.position = "fixed"; + div.style.top = "0"; + div.style.left = "0"; + div.style.width = "100%"; + div.style.height = "100%"; + div.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + div.style.zIndex = "9999"; + div.style.display = "flex"; + div.style.alignItems = "center"; + div.style.justifyContent = "center"; + div.style.color = "white"; + div.style.fontSize = "20px"; + div.textContent = "Asking to the LLM..."; + document.body.appendChild(div); + fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: prompt("model", "gpt-4.1-mini"), + temperature: 0, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + ...exampleInputs, + { role: "user", content: input }, + ], + }) + }).then(r => r.json()).then(r => { + div.remove(); + console.log(r); + const generated = JSON.parse(r.choices[0].message.content); + const output = copiedArtistCreditToPlainString(generated); + if (output !== input) { + alert("LLM's output does not match with input. The LLM moment...\n\n" + input + "\n" + output); + return; + } + if (confirm("Done! ... according to the LLM (" + r.model + ").\n\n" + generated.map(a => JSON.stringify(a)).join("\n"))) { + localStorage.setItem("copiedArtistCredit", JSON.stringify({ + names: generated, + })); + } + }); + }); + +})(); diff --git a/mb-artist-credit-splitter.user.js b/mb-artist-credit-splitter.user.js new file mode 100644 index 0000000..7dde702 --- /dev/null +++ b/mb-artist-credit-splitter.user.js @@ -0,0 +1,131 @@ +// ==UserScript== +// @name MB: Artist Credit Splitter +// @namespace https://rinsuki.net/ +// @version 1.0.2 +// @description いい感じに MusicBrainz のアーティストクレジットを分割します (失敗することもあります) +// @author rinsuki +// @match https://*.musicbrainz.org/* +// @grant none +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function getReactProps(elem) { + const properties = Object.getOwnPropertyNames(elem); + const name = properties.find(x => x.startsWith("__reactProps$")); + if (name != null) + return elem[name]; + } + + const LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT = "copiedArtistCredit"; + function getArtistCreditClipboard() { + const str = localStorage.getItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT); + if (str == null) + return undefined; + try { + return JSON.parse(str); + } + catch (e) { + console.warn("Failed to parse artist credit clipboard data", e); + return undefined; + } + } + function setArtistCreditClipboard(artistCredit) { + localStorage.setItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT, JSON.stringify(artistCredit)); + } + + function waitDOMByObserve(root, check, options) { + const firstRes = check(); + if (firstRes != null) + return Promise.resolve(firstRes); + return new Promise(resolve => { + const observer = new MutationObserver(() => { + const res = check(); + if (res != null) { + observer.disconnect(); + resolve(res); + } + }); + observer.observe(root, { + childList: true, + subtree: options.subtree + }); + }); + } + + function splitCredit(input) { + const RE = /([  ]*((?:CV|cv)[.:.:] *|[\((]((?:CV|cv)[.:.:] *)?(?=[^)]{3,})|(?<=[^(]{3})[\))]\/?|[、,\//[]\[\]]|(?: & |&)| feat[.: .: ] *)[  ]*)+/g; + const splittedCredits = []; + let lastIndex = 0; + for (const match of input.matchAll(RE)) { + splittedCredits.push([input.slice(lastIndex, match.index), match[0]]); + lastIndex = match.index + match[0].length; + } + if (input.slice(lastIndex).length > 0) + splittedCredits.push([input.slice(lastIndex), ""]); + return splittedCredits; + } + + async function whenBubbleHappens(bubble) { + const buttons = await waitDOMByObserve(bubble, () => bubble.querySelector(".buttons"), { subtree: false }); + const button = document.createElement("button"); + button.type = "button"; + button.style.float = "left"; + button.textContent = "USERJS: Split Automatically"; + button.addEventListener("click", async () => { + const props = getReactProps(bubble); + console.log(props); + if (props == null) + return alert("Failed to get React container"); + const tbody = bubble.querySelector("tbody"); + if (tbody == null) + return alert("Failed to get tbody"); + const dispatch = props.children[0].props.children.props.dispatch; + dispatch({ type: "copy" }); + await new Promise(resolve => requestAnimationFrame(resolve)); + const currentCredit = getArtistCreditClipboard()?.names.map(name => name.name + (name.joinPhrase ?? "")).join("") ?? ""; + const splittedCredits = splitCredit(currentCredit); + if (!confirm("次のように指定します。よろしいですか?\n\n" + JSON.stringify(splittedCredits, null, 4))) + return; + // localStorage.removeItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT) + // let p = waitLocalStorage(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT) + // props.copyArtistCredit() + // await p + // const stubArtistCredit = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT)!) + setArtistCreditClipboard({ names: splittedCredits.map(([name, joinPhrase], i) => { + return { + joinPhrase, + name, + artist: null, + // artist: { + // entityType: "artist", + // uniqueID: stubArtistCredit[i].artist.uniqueID, + // name, + // } + }; + }) }); + dispatch({ type: "paste" }); + alert("finish!"); + }); + buttons.appendChild(button); + } + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (!(node instanceof HTMLElement)) + continue; + if (node.dataset.floatingUiPortal == null) + continue; + const bubble = node.querySelector("#artist-credit-bubble"); + if (bubble != null && bubble instanceof HTMLElement) + whenBubbleHappens(bubble); + } + } + }); + observer.observe(document.body, { childList: true, subtree: false }); + +})(); diff --git a/mb-auto-set-media-type-by-url-rels.user.js b/mb-auto-set-media-type-by-url-rels.user.js new file mode 100644 index 0000000..0369ce4 --- /dev/null +++ b/mb-auto-set-media-type-by-url-rels.user.js @@ -0,0 +1,158 @@ +// ==UserScript== +// @name MB: Automatically Set Media Type by URL Relationships +// @description Automatically set the media type based on the relationships of release's URL. +// @namespace https://rinsuki.net +// @author rinsuki +// @grant none +// @match https://*.musicbrainz.org/release/add* +// @match https://*.musicbrainz.org/release/*/edit* +// @exclude-match https://*.musicbrainz.org/release/*/edit-relationships* +// @run-at document-idle +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + //#region node_modules/.pnpm/weight-balanced-tree@0.16.0/node_modules/weight-balanced-tree/src/utility/getSliceArgs.js + // @flow strict + + /*:: + import type {ImmutableTree} from '../types.js'; + */ + + function getSliceArgs( + tree/*: ImmutableTree */, + start/*: number | void */, + end/*: number | void */, + )/*: {+actualStart: number, +actualEnd: number} */ { + const size = tree.size; + let actualStart = Math.trunc(+start); + let actualEnd = end === undefined ? size : Math.trunc(+end); + if (Number.isNaN(actualStart)) { + actualStart = 0; + } + if (Number.isNaN(actualEnd)) { + actualEnd = 0; + } + actualStart = actualStart < 0 + ? Math.max(size + actualStart, 0) + : Math.min(actualStart, size); + actualEnd = actualEnd < 0 + ? Math.max(size + actualEnd, 0) + : Math.min(actualEnd, size); + return {actualStart, actualEnd}; + } + + //#endregion + + //#region node_modules/.pnpm/weight-balanced-tree@0.16.0/node_modules/weight-balanced-tree/src/iterate.js + // @flow strict + + + function* iterate/*::*/( + tree/*: ImmutableTree*/, + start/*:: ?: number */ = 0, + end/*:: ?: number */ = tree.size, + )/*: Generator*/ { + const {actualStart, actualEnd} = getSliceArgs(tree, start, end); + if (actualStart >= actualEnd) { + return; + } + + let stack/*: Stack | null */ = null; + let cursor/*: ImmutableTree*/ = tree; + let index = actualStart; + + while (cursor.size !== 0) { + const leftSize = cursor.left.size; + if (index < leftSize) { + stack = {node: cursor, next: stack}; + cursor = cursor.left; + } else if (index === leftSize) { + break; + } else { + index -= (leftSize + 1); + cursor = cursor.right; + } + } + + for (index = actualStart; index < actualEnd; index++) { + /*:: invariant(cursor.size !== 0); */ + yield cursor.value; + /*:: invariant(cursor.size !== 0); */ + cursor = cursor.right; + while (cursor.size !== 0) { + stack = {node: cursor, next: stack}; + cursor = cursor.left; + } + if (stack === null) { + break; + } + cursor = stack.node; + stack = stack.next; + } + } + + //#endregion + + const LINK_TYPE_GID_RELEASE_FREE_STREAMING = "08445ccf-7b99-4438-9f9a-fb9ac18099ee"; + const LINK_TYPE_GID_RELEASE_PAID_STREAMING = "320adf26-96fa-4183-9045-1f5f32f833cb"; + const LINK_TYPE_GID_RELEASE_PAID_DOWNLOAD = "98e08c20-8402-4163-8970-53504bb6a1e4"; + + const MEDIUM_FORMAT_DIGITAL_RELEASE = "12"; + + function isMBWithReleaseEditor(mb) { + return mb.releaseEditor !== undefined; + } + + const DIGITAL_MEDIA_TYPES = [ + LINK_TYPE_GID_RELEASE_FREE_STREAMING, + LINK_TYPE_GID_RELEASE_PAID_DOWNLOAD, + LINK_TYPE_GID_RELEASE_PAID_STREAMING, + ]; + function estimateMediumType(MB) { + for (const url of iterate(MB.releaseEditor.externalLinksData())) { + for (const rel of url.relationships) { + if (rel.linkTypeID == null) + continue; + const gid = MB.linkedEntities.link_type[rel.linkTypeID]?.gid; + if (DIGITAL_MEDIA_TYPES.includes(gid)) { + return MEDIUM_FORMAT_DIGITAL_RELEASE; + } + } + } + } + function doIt(medium, type) { + if (medium.formatID()?.length < 1) { + medium.formatID(type); + } + } + function main() { + const MB = window.MB; + if (!isMBWithReleaseEditor(MB)) + return console.log("You are not on the release editor page."); + MB.releaseEditor.rootField.release().mediums.subscribe(m => { + const type = estimateMediumType(MB); + if (type == null) + return; + for (const medium of m) { + doIt(medium, type); + } + }); + MB.releaseEditor.activeTabID.subscribe(tabID => { + if (tabID !== "#tracklist") + return; + const type = estimateMediumType(MB); + if (type == null) + return; + for (const medium of MB.releaseEditor.rootField.release().mediums()) { + doIt(medium, type); + } + }); + } + main(); + +})(); diff --git a/mb-clear-tracklist-artist.user.js b/mb-clear-tracklist-artist.user.js new file mode 100644 index 0000000..f5345ec --- /dev/null +++ b/mb-clear-tracklist-artist.user.js @@ -0,0 +1,40 @@ +// ==UserScript== +// @name MB: Clear Tracklist Artist +// @namespace https://rinsuki.net +// @match https://*.musicbrainz.org/release/add +// @match https://*.musicbrainz.org/release/*/edit +// @grant none +// @author rinsuki +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function isMBWithReleaseEditor(mb) { + return mb.releaseEditor !== undefined; + } + + function main() { + const tracklist = document.getElementById("tracklist"); + if (tracklist == null) + return; + const button = document.createElement("button"); + button.textContent = "Clear Tracklist Artist"; + button.addEventListener("click", () => { + const MB = window.MB; + if (!isMBWithReleaseEditor(MB)) + return alert("You are not on the release editor page."); + if (!confirm("Are you sure you want to clear the tracklist artist?")) + return; + for (const track of MB.releaseEditor.rootField.release().allTracks()) { + track.artistCredit({ names: [] }); + } + }); + tracklist.prepend(button); + } + main(); + +})(); diff --git a/mb-copy-recording-rels-from-other-release.user.js b/mb-copy-recording-rels-from-other-release.user.js new file mode 100644 index 0000000..574030d --- /dev/null +++ b/mb-copy-recording-rels-from-other-release.user.js @@ -0,0 +1,176 @@ +// ==UserScript== +// @name MB: [WIP] Copy Recording Relationships from Other Release +// @version 0.1.0 +// @grant none +// @namespace https://rinsuki.net +// @author rinsuki +// @match https://*.musicbrainz.org/release/*/edit-relationships* +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// @require https://cdn.jsdelivr.net/npm/@rinsuki/dom-chef@5.1.1/umd.js#sha256-EvGMVNob2jRhcwSo8gGGJp3mZJENNaQwrpYlNRQlUMc=,sha512-b/5dx8A+dc01RsqR2hF2Na/DFwwn64M4PQFka7dXhcAeXjPfpkgWS9YFs1LHPaEckkxAI7ivhVn6i+OrFivnYQ== +// ==/UserScript== + +(function () { + 'use strict'; + + class HTTPError extends Error { + response; + text; + constructor(response, text) { + super(`HTTP-${response.status}: ${text}`); + this.response = response; + this.text = text; + } + } + async function fetchOkOrThrow(...args) { + const res = await fetch(...args); + if (!res.ok) { + throw new HTTPError(res, await res.text()); + } + return res; + } + + //#region node_modules/.pnpm/typedbrainz@0.3.0/node_modules/typedbrainz/lib/index.js + // SPDX-License-Identifier: MIT + function isReleaseRelationshipEditor(relationshipEditor) { + return relationshipEditor.state?.entity.entityType === "release"; + } + + //#endregion + + /** @jsx h */ + + let abortController = null; + const elm = DOMChef.h("div", null, + DOMChef.h("details", { style: { border: "1px solid #ccc", margin: "1em 0", padding: "0.5em" }, open: true }, + DOMChef.h("summary", { style: { marginBottom: "0.5em" } }, + DOMChef.h("h2", { style: { display: "inline" } }, "Copy Recording Relationships from Other Release")), + DOMChef.h("form", { action: "javascript:", onSubmit: e => { + e.preventDefault(); + const input = e.currentTarget.querySelector("input[type=text]"); + if (input == null) + return; + const mbid = (input.value.match(/(?:^|\/release\/)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i) ?? [])[1]; + if (!mbid) { + alert("Invalid MBID or URL"); + return; + } + if (abortController) { + abortController.abort(); + } + abortController = new AbortController(); + fetchReleaseAndShowCopyUI(mbid, abortController, e.currentTarget).catch(err => { + console.error(err); + alert(err.message); + }); + } }, + DOMChef.h("input", { type: "text", placeholder: "Release MBID or URL", size: 40 }), + DOMChef.h("input", { type: "submit", value: "Load" })))); + async function fetchReleaseAndShowCopyUI(mbid, ac, form) { + const resRaw = await fetchOkOrThrow(`/ws/js/entity/${mbid}`).catch(e => { throw new Error(`failed to fetch release: ${e.message}`, { cause: e }); }); + const resJSON = await resRaw.json().catch(e => { throw new Error(`failed to fetch release (parsing json): ${e.message}`, { cause: e }); }); + console.log(resJSON); + const elm = DOMChef.h("div", null, + DOMChef.h("h3", null, + "Copy from ", + resJSON.name), + DOMChef.h("form", { action: "javascript:", onSubmit: e => { + e.preventDefault(); + const input = e.currentTarget.querySelector("select"); + if (input == null) + return; + const mediumid = parseInt(input.value, 10); + if (!Number.isSafeInteger(mediumid)) { + alert("Invalid medium ID"); + return; + } + fetchMediumAndShowCopyUI(resJSON.gid, mediumid, ac, e.currentTarget).catch(err => { + console.error(err); + alert(err.message); + }); + } }, + resJSON.mediums + ? DOMChef.h("div", null, + DOMChef.h("select", { size: Math.max(Math.min(2, resJSON.mediums?.length), 5) }, resJSON.mediums.map(medium => { + return DOMChef.h("option", { value: medium.id }, + medium.position, + ". ", + medium.format?.name ?? "(unknown)", + medium.name.length ? ": " + medium.name : ""); + }))) + : "(no mediums)", + DOMChef.h("input", { type: "submit", value: "Load" }))); + if (ac.signal.aborted) + return; + form.nextElementSibling?.remove(); + form.insertAdjacentElement("afterend", elm); + elm.querySelector("select")?.focus(); + } + async function fetchMediumAndShowCopyUI(releasegid, mediumid, ac, form) { + const resRaw = await fetchOkOrThrow(`/ws/js/medium/${mediumid}?inc=recordings+rels`).catch(e => { throw new Error(`failed to fetch medium: ${e.message}`, { cause: e }); }); + const srcMedia = await resRaw.json().catch(e => { throw new Error(`failed to fetch medium (parsing json): ${e.message}`, { cause: e }); }); + const elm = DOMChef.h("div", null, + DOMChef.h("button", { onClick: () => { + const { MB } = window; + if (MB == null) + return; + const relEditor = MB.relationshipEditor; + if (MB.tree == null || !isReleaseRelationshipEditor(relEditor)) + return; + const dstMedia = MB.tree.iterate(relEditor.state.mediums).toArray()[0]; + let copiedSomeRels = false; + for (let i = 0; i < dstMedia[0].tracks.length; i++) { + const srcRec = srcMedia.tracks[i].recording; + const dstRec = dstMedia[0].tracks[i].recording; + for (const rel of srcRec.relationships) { + copiedSomeRels = true; + relEditor.dispatch({ + type: "update-relationship-state", + sourceEntity: dstRec, + oldRelationshipState: null, + newRelationshipState: { + _lineage: [], + _original: null, + editsPending: false, + _status: 1, + entity0: rel.backward ? rel.target : dstRec, + entity1: rel.backward ? dstRec : rel.target, + entity0_credit: rel.entity0_credit, + entity1_credit: rel.entity1_credit, + begin_date: rel.begin_date, + end_date: rel.end_date, + ended: rel.ended, + attributes: MB.tree.fromDistinctAscArray(rel.attributes.map(attribute => { + const type = MB.linkedEntities.link_attribute_type[attribute.type.gid]; + return { + ...attribute, + type, + typeID: type.id, + }; + })), + id: relEditor.getRelationshipStateId(null), + linkTypeID: rel.linkTypeID, + linkOrder: rel.linkOrder, + }, + batchSelectionCount: undefined, + creditsToChangeForSource: "", + creditsToChangeForTarget: "", + }); + } + } + if (copiedSomeRels) { + let editNote = relEditor.state.editNoteField.value; + if (editNote.length) + editNote += "\n"; + editNote += `Relationships Copied from https://musicbrainz.org/release/${releasegid}/disc/${srcMedia.position} (a.k.a. https://musicbrainz.org/medium/${srcMedia.gid} )`; + relEditor.dispatch({ type: "update-edit-note", editNote }); + } + } }, "Copy")); + if (ac.signal.aborted) + return; + form.nextElementSibling?.remove(); + form.insertAdjacentElement("afterend", elm); + } + document.querySelector("#content > div.tabs")?.insertAdjacentElement("afterend", elm); + +})(); diff --git a/mb-copy-recording-rels-to-karaoke.user.js b/mb-copy-recording-rels-to-karaoke.user.js new file mode 100644 index 0000000..61ab2ca --- /dev/null +++ b/mb-copy-recording-rels-to-karaoke.user.js @@ -0,0 +1,252 @@ +// ==UserScript== +// @name MB: Copy Recording Relationships to Karaoke/Edited Recordings +// @description Copy recording-{artist, work, etc...} relationships to karaoke/edited recordings with one button! +// @version 0.4.0 +// @grant none +// @namespace https://rinsuki.net +// @author rinsuki +// @match https://*.musicbrainz.org/release/*/edit-relationships* +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// @require https://cdn.jsdelivr.net/npm/@rinsuki/dom-chef@5.1.1/umd.js#sha256-EvGMVNob2jRhcwSo8gGGJp3mZJENNaQwrpYlNRQlUMc=,sha512-b/5dx8A+dc01RsqR2hF2Na/DFwwn64M4PQFka7dXhcAeXjPfpkgWS9YFs1LHPaEckkxAI7ivhVn6i+OrFivnYQ== +// ==/UserScript== + +(function () { + 'use strict'; + + //#region node_modules/.pnpm/typedbrainz@0.3.0/node_modules/typedbrainz/lib/index.js + // SPDX-License-Identifier: MIT + function isReleaseRelationshipEditor(relationshipEditor) { + return relationshipEditor.state?.entity.entityType === "release"; + } + + //#endregion + + async function resolveRecordingsRelationships(recordings, parent) { + // とりあえず愚直な実装 (TODO: appearsOnを使ってmediumで引くとかもできるはず) + const progressBar = document.createElement("progress"); + progressBar.max = recordings.length; + progressBar.value = 0; + parent.appendChild(progressBar); + recordings.sort((a, b) => { + if (a.relationships != null && b.relationships == null) + return -1; + if (a.relationships == null && b.relationships != null) + return 1; + return 0; + }); + for (const recording of recordings) { + if (recording.relationships != null) { + progressBar.value += 1; + continue; + } + const res = await fetch("/ws/js/entity/" + recording.gid + "?inc=rels"); + if (!res.ok) { + alert(`Failed to fetch recording ${recording.gid}: ${res.status} ${await res.text()}`); + } + const freshRecording = await res.json(); + Object.assign(recording, { + relationships: freshRecording.relationships, + }); + progressBar.value += 1; + } + progressBar.remove(); + } + + /** @jsx h */ + + const KARAOKE_REL_LINK_TYPE_ID = 226; // gid: 39a08d0e-26e4-44fb-ae19-906f5fe9435d + const EDITS_REL_LINK_TYPE_ID = 309; // gid: ce01b3ac-dd47-4702-9302-085344f96e84 + const WORK_REL_LINK_TYPE_ID = 278; // gid: a3005666-a872-32c3-ad06-98af558e99b0 + const WORK_REL_KARAOKE_LINK_ATTR_TYPE_ID = 1261; // gid: 3d984f6e-bbe2-4620-9425-5f32e945b60d + const WORK_REL_PARTIAL_LINK_ATTR_TYPE_ID = 579; // gid: d2b63be6-91ec-426a-987a-30b47f8aae2d + async function doIt(button, doReverse) { + const MB = window.MB; + if (MB == null) + return alert("MB global not found"); + if (MB.tree == null) + return alert("MB.tree not found"); + const editor = MB.relationshipEditor; + if (editor == null || !isReleaseRelationshipEditor(editor)) + return alert("Relationship editor not found"); + const currentRecordings = new Map(); + const dstRecordingStates = new Map(); + for (const medium of MB.tree.iterate(editor.state.mediums)) { + for (const track of MB.tree.iterate(medium[1])) { + currentRecordings.set(track.recording.gid, track.recording); + dstRecordingStates.set(track.recording.gid, track); + } + } + const recToRecRelationships = new Map(); + for (const dstRecording of Array.from(/* currentRecordings はループ内で変化するので Array.from で確定させる */ currentRecordings.values())) { + console.log("dstrec", dstRecording); + const srcRecordingRels = [ + ...dstRecording.relationships.filter(rel => KARAOKE_REL_LINK_TYPE_ID === rel.linkTypeID && (doReverse ? rel.entity0_id : rel.entity1_id) === dstRecording.id), + ...dstRecording.relationships.filter(rel => EDITS_REL_LINK_TYPE_ID === rel.linkTypeID && (doReverse ? rel.entity1_id : rel.entity0_id) === dstRecording.id), + ]; + for (const srcRecordingRel of srcRecordingRels) { + const srcRecording = srcRecordingRel.target; + if (srcRecording.entityType !== "recording") + continue; // ? + let obj = recToRecRelationships.get(`${dstRecording.gid}:${srcRecording.gid}`); + if (obj == null) { + obj = { + srcRecordingId: srcRecording.gid, + dstRecordingId: dstRecording.gid, + karaoke: false, + partial: false, + }; + if (!currentRecordings.has(srcRecording.gid)) + currentRecordings.set(srcRecording.gid, srcRecording); + recToRecRelationships.set(`${dstRecording.gid}:${srcRecording.gid}`, obj); + } + if (srcRecordingRel.linkTypeID === KARAOKE_REL_LINK_TYPE_ID) { + obj.karaoke = true; + } + else if (srcRecordingRel.linkTypeID === EDITS_REL_LINK_TYPE_ID) { + obj.partial = true; + } + } + } + await resolveRecordingsRelationships(Array.from(recToRecRelationships.values()) + .map(x => x.srcRecordingId) + .map(gid => currentRecordings.get(gid)), button); + { + const srcRecordingIds = new Set(Array.from(recToRecRelationships.values()).map(x => x.srcRecordingId)); + const srcRecordingsWithoutRels = Array.from(srcRecordingIds) + .map(gid => currentRecordings.get(gid)) + .filter(rec => rec.relationships == null); + await resolveRecordingsRelationships(srcRecordingsWithoutRels, button); + } + for (const recToRecRel of recToRecRelationships.values()) { + const srcRecording = currentRecordings.get(recToRecRel.srcRecordingId); + const dstRecording = currentRecordings.get(recToRecRel.dstRecordingId); + for (const rel of srcRecording.relationships) { + if (rel.source_type === "recording" && rel.target_type === "recording") + continue; // probably don't want to copy + if (rel.source_type === "url" || rel.target_type === "url") + continue; // probably don't want to copy + let attrs = [...rel.attributes]; + let oldRelationshipState = null; + if (rel.linkTypeID === WORK_REL_LINK_TYPE_ID) { + if (doReverse) { + const removeAttrIds = new Set(); + if (recToRecRel.karaoke) + removeAttrIds.add(WORK_REL_KARAOKE_LINK_ATTR_TYPE_ID); + if (recToRecRel.partial) + removeAttrIds.add(WORK_REL_PARTIAL_LINK_ATTR_TYPE_ID); + attrs = attrs.filter(x => !removeAttrIds.has(x.typeID)); + } + else { + const neededAttrIds = new Set(); + if (recToRecRel.karaoke) + neededAttrIds.add(WORK_REL_KARAOKE_LINK_ATTR_TYPE_ID); + if (recToRecRel.partial) + neededAttrIds.add(WORK_REL_PARTIAL_LINK_ATTR_TYPE_ID); + for (const neededAttrId of neededAttrIds) { + if (attrs.find(x => x.typeID === neededAttrId) != null) + continue; + const neededAttr = MB.linkedEntities.link_attribute_type[neededAttrId]; + attrs.push({ + type: neededAttr, + typeID: neededAttr.id, + typeName: neededAttr.name, + }); + } + } + const dstRecAllRels = dstRecordingStates.get(dstRecording.gid)?.targetTypeGroups; + const dstRecWorkRelsObj = dstRecAllRels && MB.tree + .iterate(dstRecAllRels) + .find(r => r[0] === "work")?.[1]; + const dstRecWorkRels = dstRecWorkRelsObj && MB.tree + .iterate(dstRecWorkRelsObj) + .flatMap(r => MB.tree.iterate(r.phraseGroups)) + .flatMap(r => MB.tree.iterate(r.relationships)) + .toArray(); + console.log("drwrels", dstRecWorkRels, rel.backward); + const dstRecWorkRel = dstRecWorkRels?.find(r => rel.backward + ? r.entity0.id === rel.target.id && r.entity1.id === dstRecording.id + : r.entity0.id === dstRecording.id && r.entity1.id === rel.target.id); + if (dstRecWorkRel != null) { + const oldRelAttrIds = new Set(MB.tree.iterate(dstRecWorkRel.attributes ?? MB.tree.empty).map(x => x.typeID)); + if (attrs.every(x => oldRelAttrIds.has(x.typeID))) { + continue; + } + const oldRelAttrNames = MB.tree.iterate(dstRecWorkRel.attributes ?? MB.tree.empty).map(x => x.typeName).toArray().join(", ") || "(none)"; + const newRelAttrNames = attrs.map(x => x.typeName).join(", ") || "(none)"; + if (confirm([ + `While copying`, + "", + `Relationship to work "${rel.target.name}" (${rel.target.id})`, + `to Destination recording "${dstRecording.name}" (${dstRecording.id})`, + `from Source recording "${srcRecording.name}" (${srcRecording.id})`, + "", + "an existing relationship with the same work but different attributes was found.", + "", + "Do you want to overwrite the existing relationship's attributes with the new ones?", + "", + `${oldRelAttrNames} → ${newRelAttrNames}`, + "", + "OK = Overwrite", + "Cancel = Add New Recording-Work Relationship", + ].join("\n"))) { + oldRelationshipState = dstRecWorkRel; + } + } + } + editor.dispatch({ + type: "update-relationship-state", + sourceEntity: rel.backward ? rel.target : dstRecording, + oldRelationshipState, + newRelationshipState: { + _lineage: [], + _original: null, + editsPending: false, + entity0: rel.backward ? rel.target : dstRecording, + entity1: rel.backward ? dstRecording : rel.target, + entity0_credit: rel.entity0_credit, + entity1_credit: rel.entity1_credit, + begin_date: rel.begin_date, + end_date: rel.end_date, + ended: rel.ended, + id: editor.getRelationshipStateId(null), + linkTypeID: rel.linkTypeID, + linkOrder: rel.linkOrder, + // ↑ここまでは新規relationshipを作成する時限定 + ...(oldRelationshipState ? oldRelationshipState : {}), + _status: oldRelationshipState ? (oldRelationshipState._status || 2) : 1, + attributes: MB.tree.fromDistinctAscArray(attrs), + }, + batchSelectionCount: undefined, + creditsToChangeForSource: "", + creditsToChangeForTarget: "", + }); + } + } + let editNote = editor.state.editNoteField.value; + if (editNote.length) + editNote += "\n"; + editNote += "Script: \"" + GM.info.script.name + "\" (" + GM.info.script.version + ")"; + editor.dispatch({ type: "update-edit-note", editNote }); + } + const elm = DOMChef.h("div", null, + DOMChef.h("button", { onClick: (e) => { + const button = e.currentTarget; + button.disabled = true; + doIt(button, false).finally(() => { + button.querySelector(".only-for-loading")?.remove(); + button.disabled = false; + }); + } }, "Copy Recording Relationships to Karaoke/Edited Recordings"), + DOMChef.h("button", { onClick: e => { + const button = e.currentTarget; + button.disabled = true; + doIt(button, true).finally(() => { + button.querySelector(".only-for-loading")?.remove(); + button.disabled = false; + }); + }, style: { marginLeft: "1em" } }, "... or FROM Karaoke/Edited Recordings")); + document.querySelector("#content > div.tabs")?.insertAdjacentElement("afterend", elm); + +})(); diff --git a/mb-create-release-from-other-release.user.js b/mb-create-release-from-other-release.user.js new file mode 100644 index 0000000..ee0fcbd --- /dev/null +++ b/mb-create-release-from-other-release.user.js @@ -0,0 +1,72 @@ +// ==UserScript== +// @name MB: Create Release from Other Release +// @match https://*.musicbrainz.org/release-group/* +// @grant none +// @namespace https://rinsuki.net +// @author rinsuki +// @description in the release group page +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function main() { + const table = document.querySelector("table.mergeable-table"); + if (table == null) + return; + for (const row of table.rows) { + const link = row.querySelector("td > a[href^='/release/'][href*='-']:has(bdi)"); + if (link == null) + continue; + const button = document.createElement("button"); + button.textContent = "Copy"; + button.addEventListener("click", e => { + e.preventDefault(); + button.textContent = "……"; + button.disabled = true; + fetch(`/ws/js/release/${new URL(link.href).pathname.split("/")[2]}`).then(r => r.json()).then((r) => { + let f = { + name: r.name, + release_group: r.releaseGroup?.gid, + comment: r.comment, + barcode: r.barcode, + language: r.language?.iso_code_3, + script: r.script?.iso_code, + }; + for (let i = 0; i < (r.labels?.length ?? 0); i++) { + f[`labels.${i}.mbid`] = r.labels?.[i].label?.gid; + } + for (let i = 0; i < r.artistCredit.names.length; i++) { + f[`artist_credit.names.${i}.name`] = r.artistCredit.names[i].name; + f[`artist_credit.names.${i}.mbid`] = r.artistCredit.names[i].artist?.gid; + f[`artist_credit.names.${i}.join_phrase`] = r.artistCredit.names[i].joinPhrase; + } + const form = document.createElement("form"); + form.method = "POST"; + form.action = "/release/add"; + for (const [k, v] of Object.entries(f)) { + if (v == null) + continue; + const input = document.createElement("input"); + input.type = "hidden"; + input.name = k; + input.value = v; + form.appendChild(input); + } + document.body.appendChild(form); + form.submit(); + }).catch(e => { + console.error(e); + button.textContent = "Error"; + button.disabled = false; + }); + }); + link.parentElement?.prepend(button); + } + } + main(); + +})(); diff --git a/mb-match-tracklist-credits-with-other-credits.user.js b/mb-match-tracklist-credits-with-other-credits.user.js new file mode 100644 index 0000000..9f76ebf --- /dev/null +++ b/mb-match-tracklist-credits-with-other-credits.user.js @@ -0,0 +1,138 @@ +// ==UserScript== +// @name MB: Match Tracklist Credits with Other Credits +// @namespace https://rinsuki.net +// @version 0.3.1 +// @grant none +// @match https://*.musicbrainz.org/release/*/edit +// @match https://*.musicbrainz.org/release/add +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function isArtistExist(artist) { + return artist != null && artist.id != 0 && artist.id != null; + } + function doItForSpecificArtistCredit(creditMap, artistCredit) { + const names = [...artistCredit().names]; + console.log(JSON.stringify(names)); + // ループ中にnamesを書き換えるのであえて for-of を使わない + for (let i = 0; i < names.length; i++) { + const name = names[i]; + name.joinPhrase ??= ""; + // アーティストIDが不明の場合、先頭一致できる名前を探す + if (!isArtistExist(name.artist)) { + for (const [knownName, knownArtist] of creditMap) { + if (!isArtistExist(knownArtist)) + continue; + if (!name.name.startsWith(knownName)) + continue; + const remainName = name.name.slice(knownName.length); + if (remainName === "") { + // 完全一致したら設定だけでよい + name.artist = knownArtist; + break; + } + // おまけがある場合はそれをjoinPhraseに回す + name.name = knownName; + name.artist = knownArtist; + name.joinPhrase = remainName + (name.joinPhrase ?? ""); + break; + } + } + // joinPhraseに既知の名前があれば分割する + let firstArtist = null; + for (const [knownName, knownArtist] of creditMap) { + if (!isArtistExist(knownArtist)) + continue; + if (name.joinPhrase == null) + continue; + const index = name.joinPhrase.indexOf(knownName); + if (index === -1) + continue; + if (firstArtist == null || firstArtist[0] > index) { + firstArtist = [index, knownName, knownArtist]; + } + } + // 既知の名前があった! + if (firstArtist != null) { + const [index, knownName, knownArtist] = firstArtist; + const remainName = name.joinPhrase.slice(index + knownName.length); + name.joinPhrase = name.joinPhrase.slice(0, index); + const newCreditName = { + name: knownName, + artist: knownArtist, + joinPhrase: remainName, + }; + names.splice(i + 1, 0, newCreditName); + } + } + artistCredit({ + names, + }); + } + async function doItEntirely(withShiftKey) { + const MB = window.MB; + if (MB == null) + return; + const editor = MB.releaseEditor; + const currentCredits = [ + ...editor.rootField.release().releaseGroup().artistCredit?.names ?? [], + ...editor.rootField.release().artistCredit().names, + ...Array.from(editor.rootField.release().allTracks()).flatMap(t => [...t.artistCredit().names, ...t.recording().artistCredit.names]), + ]; + const releaseUrl = prompt("Enter the release URL to copy credits from (If you keep it empty, it will only match within the current release)"); + if (releaseUrl == null) + return; + if (releaseUrl.length) { + const mbid = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.exec(releaseUrl)?.[0]; + if (mbid == null) { + alert("Invalid release URL"); + return; + } + const otherJS = await fetch(`/ws/js/entity/${mbid}`).then(r => r.json()); + if (!("artistCredit" in otherJS)) { + alert("Could not fetch artist credit data"); + return; + } + currentCredits.push(...otherJS.artistCredit.names); + for (const media of otherJS.mediums) { + const otherJS = await fetch(`/ws/js/medium/${media.id}`).then(r => r.json()); + for (const track of otherJS.tracks) { + currentCredits.push(...track.artistCredit.names); + } + } + } + const creditMap = new Map(); + for (const credit of currentCredits) { + if (!isArtistExist(credit.artist)) + continue; + const current = creditMap.get(credit.name); + if (current == null || !isArtistExist(current)) { + creditMap.set(credit.name, credit.artist); + } + else if (current.id !== credit.artist.id) { + // conflict + creditMap.set(credit.name, null); + } + } + console.log(creditMap); + doItForSpecificArtistCredit(creditMap, editor.rootField.release().artistCredit); + for (const track of editor.rootField.release().allTracks()) { + doItForSpecificArtistCredit(creditMap, track.artistCredit); + } + alert("Done"); + } + const button = document.createElement("button"); + button.id = "mb-match-tracklist-credits-with-other-credits-button"; + document.getElementById(button.id)?.remove(); + button.textContent = "Match Tracklist Credits with Other Credits"; + button.addEventListener("click", (e) => { + doItEntirely(e.shiftKey); + }); + document.getElementById("release-editor")?.prepend(button); + +})(); diff --git a/mb-search-release-on-websites.user.js b/mb-search-release-on-websites.user.js new file mode 100644 index 0000000..a39454a --- /dev/null +++ b/mb-search-release-on-websites.user.js @@ -0,0 +1,218 @@ +// ==UserScript== +// @name MusicBrainz: Search Release on Websites +// @namespace https://rinsuki.net +// @match https://*.musicbrainz.org/release/*/edit +// @match https://*.musicbrainz.org/release/add +// @grant none +// @version 1.1 +// @author rinsuki +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + //#region node_modules/.pnpm/weight-balanced-tree@0.16.0/node_modules/weight-balanced-tree/src/utility/getSliceArgs.js + // @flow strict + + /*:: + import type {ImmutableTree} from '../types.js'; + */ + + function getSliceArgs( + tree/*: ImmutableTree */, + start/*: number | void */, + end/*: number | void */, + )/*: {+actualStart: number, +actualEnd: number} */ { + const size = tree.size; + let actualStart = Math.trunc(+start); + let actualEnd = end === undefined ? size : Math.trunc(+end); + if (Number.isNaN(actualStart)) { + actualStart = 0; + } + if (Number.isNaN(actualEnd)) { + actualEnd = 0; + } + actualStart = actualStart < 0 + ? Math.max(size + actualStart, 0) + : Math.min(actualStart, size); + actualEnd = actualEnd < 0 + ? Math.max(size + actualEnd, 0) + : Math.min(actualEnd, size); + return {actualStart, actualEnd}; + } + + //#endregion + + //#region node_modules/.pnpm/weight-balanced-tree@0.16.0/node_modules/weight-balanced-tree/src/iterate.js + // @flow strict + + + function* iterate/*::*/( + tree/*: ImmutableTree*/, + start/*:: ?: number */ = 0, + end/*:: ?: number */ = tree.size, + )/*: Generator*/ { + const {actualStart, actualEnd} = getSliceArgs(tree, start, end); + if (actualStart >= actualEnd) { + return; + } + + let stack/*: Stack | null */ = null; + let cursor/*: ImmutableTree*/ = tree; + let index = actualStart; + + while (cursor.size !== 0) { + const leftSize = cursor.left.size; + if (index < leftSize) { + stack = {node: cursor, next: stack}; + cursor = cursor.left; + } else if (index === leftSize) { + break; + } else { + index -= (leftSize + 1); + cursor = cursor.right; + } + } + + for (index = actualStart; index < actualEnd; index++) { + /*:: invariant(cursor.size !== 0); */ + yield cursor.value; + /*:: invariant(cursor.size !== 0); */ + cursor = cursor.right; + while (cursor.size !== 0) { + stack = {node: cursor, next: stack}; + cursor = cursor.left; + } + if (stack === null) { + break; + } + cursor = stack.node; + stack = stack.next; + } + } + + //#endregion + + function isMBWithReleaseEditor(mb) { + return mb.releaseEditor !== undefined; + } + + (async () => { + const sites = Object.entries({ + "YouTube Music": { + domains: ["music.youtube.com"], + search: "https://music.youtube.com/search?q={query}", + barcodeSearch: "https://music.youtube.com/search?q=\"{upc}\"", + }, + "Spotify": { + domains: ["open.spotify.com"], + search: "https://open.spotify.com/search/{query}/albums", + barcodeSearch: "https://open.spotify.com/search/upc:{upc}/albums", + }, + "Apple Music": { + domains: ["music.apple.com", "itunes.apple.com"], + search: "https://music.apple.com/jp/search?term={query}", + }, + "Apple Music (ISRCeam)": { + domains: ["music.apple.com", "itunes.apple.com"], + search: "https://isrceam.rinsuki.net/apple/jp/search?q={query}", + barcodeSearch: "https://isrceam.rinsuki.net/apple/jp/upc?upc={upc}", + }, + "OTOTOY": { + domains: ["ototoy.jp"], + search: "https://ototoy.jp/find/?q={query}", + }, + "OTOTOY (Google)": { + domains: ["ototoy.jp"], + search: "https://www.google.com/search?client=firefox-b-d&q=site:ototoy.jp+{query}", + }, + "mora": { + domains: ["mora.jp"], + search: "https://mora.jp/search/top?keyWord={query}", + }, + "Qobuz": { + domains: ["www.qobuz.com"], + search: "https://www.qobuz.com/jp-ja/search/albums/{query}", + }, + "TIDAL": { + domains: ["tidal.com"], + search: `https://tidal.com/search/albums?q={query}`, + }, + "Google": { + domains: [], + search: "https://www.google.com/search?client=firefox-b-d&q={query}", + } + }).map(e => ({ ...e[1], name: e[0] })); + let externalLinkEditor; + while (null == (externalLinkEditor = document.getElementById("external-links-editor"))) { + await new Promise(r => setTimeout(r, 100)); + } + const MB = (() => { + const MB = window.MB; + if (!isMBWithReleaseEditor(MB)) + throw new Error("window.MB is not ready"); + return MB; + })(); + const linkList = document.createElement("ul"); + function refresh() { + linkList.innerHTML = ""; + const links = Array.from(iterate(MB.releaseEditor.externalLinksData())); + for (const site of sites) { + let exists = false; + for (const link of links) { + if (link.url === "") + continue; + const domain = new URL(link.url).hostname; + if (site.domains.includes(domain)) { + exists = true; + break; + } + } + const link = document.createElement("a"); + if (exists) { + link.style.opacity = "0.5"; + } + const barcodeInput = document.querySelector("input#barcode"); + if (barcodeInput && barcodeInput.value.length > 4 && site.barcodeSearch != null) { + // seems barcode + const link2 = document.createElement("a"); + if (exists) { + link2.style.opacity = "0.5"; + } + link2.href = site.barcodeSearch.replace("{upc}", encodeURIComponent(barcodeInput.value)); + link2.textContent = `Search on ${site.name} (by barcode)`; + link2.target = "_blank"; + link2.style.marginRight = "1em"; + const li2 = document.createElement("li"); + li2.appendChild(link2); + linkList.appendChild(li2); + } + const nameInput = document.querySelector(`input#name`); + if (!nameInput) + continue; + link.href = site.search.replace("{query}", encodeURIComponent(nameInput.value)); + link.textContent = `Search on ${site.name}`; + link.target = "_blank"; + link.style.marginRight = "1em"; + const li = document.createElement("li"); + li.appendChild(link); + linkList.appendChild(li); + } + const button = document.createElement("button"); + button.textContent = "Refresh"; + button.onclick = refresh; + const li = document.createElement("li"); + li.appendChild(button); + linkList.appendChild(li); + } + externalLinkEditor.parentElement?.parentElement?.appendChild(linkList); + refresh(); + MB.releaseEditor.rootField.release().name.subscribe(() => refresh()); + MB.releaseEditor.rootField.release().barcode.value.subscribe(() => refresh()); + MB.releaseEditor.externalLinksData.subscribe(() => refresh()); + })(); + +})(); diff --git a/mb-seed-from-barcode-search.user.js b/mb-seed-from-barcode-search.user.js new file mode 100644 index 0000000..3d01492 --- /dev/null +++ b/mb-seed-from-barcode-search.user.js @@ -0,0 +1,102 @@ +// ==UserScript== +// @name MB: Barcode Search ++ +// @namespace https://rinsuki.net +// @match https://*.musicbrainz.org/search* +// @description Add seed barcode to new release button, highlights barcode-matched releases +// @grant none +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function removeFirstZeroes(str) { + return str.replace(/^0+/, ""); + } + + function main() { + const addReleaseLink = document.querySelector('#content a[href="/release/add"]'); + if (addReleaseLink == null) + return; + const url = new URL(location.href); + const searchQuery = url.searchParams.get('query'); + if (searchQuery == null) + return; + const barcodeMatch = searchQuery.split(" ").find(part => part.startsWith("barcode:")); + if (barcodeMatch == null) + return; + const barcode = barcodeMatch.replace("barcode:", ""); + // add seed button + const form = document.createElement("form"); + form.method = "POST"; + form.action = "/release/add"; + form.style.display = "inline"; + const barcodeInput = document.createElement("input"); + barcodeInput.type = "hidden"; + barcodeInput.name = "barcode"; + barcodeInput.value = barcode; + form.appendChild(barcodeInput); + const seedName = url.searchParams.get("seed.name"); + if (seedName) { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "name"; + input.value = seedName; + form.appendChild(input); + } + const seedUrl = url.searchParams.get("seed.url"); + if (seedUrl) { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "urls.0.url"; + input.value = seedUrl; + form.appendChild(input); + } + const addButton = document.createElement("button"); + addButton.type = "submit"; + addButton.textContent = "with barcode " + barcode + " [UserScript]"; + form.appendChild(addButton); + addReleaseLink.insertAdjacentElement("afterend", form); + form.insertAdjacentText("beforebegin", ", or "); + // try to highlight + let focused = false; + for (const td of document.querySelectorAll("td.barcode-cell")) { + if (removeFirstZeroes(barcode) === removeFirstZeroes(td.textContent.trim())) { + td.parentElement.style.border = "2px solid blue"; + const link = td.parentElement.querySelector('a[href*="release/"]:not([href*=cover-art])'); + if (link == null) + continue; + let focusTarget = link; + if (seedUrl) { + const form = document.createElement("form"); + form.method = "POST"; + form.action = link.href + "/edit"; + form.style.display = "inline"; + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "urls.0.url"; + input.value = seedUrl; + form.appendChild(input); + const editButton = document.createElement("button"); + editButton.type = "submit"; + editButton.textContent = "[Add URL UserScript]"; + form.appendChild(editButton); + link.insertAdjacentElement("afterend", form); + form.insertAdjacentText("beforebegin", " "); + focusTarget = editButton; + } + if (!focused) { + focusTarget.focus(); + focused = true; + } + } + } + if (!focused) { + addButton.focus(); + } + } + main(); + +})(); diff --git a/mb-seed-urls-to-release-recordings.user.js b/mb-seed-urls-to-release-recordings.user.js new file mode 100644 index 0000000..0ed1360 --- /dev/null +++ b/mb-seed-urls-to-release-recordings.user.js @@ -0,0 +1,272 @@ +// ==UserScript== +// @name MusicBrainz: Seed URLs to Release Recordings +// @namespace https://rinsuki.net +// @version 0.2.4 +// @description Import recording-url relationship to release's recordings. +// @author rinsuki +// @match https://*.musicbrainz.org/release/*/edit-relationships +// @grant none +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// @require https://cdn.jsdelivr.net/npm/zod@3.24.4/lib/index.umd.js#sha256-JWI6HDMt5FcbdaKm+4vh+uQBgMj9/XtEIPCb6nJ87hw=,sha512-w/o1QQewvZigMuaxE5iaUXhSUomguLoRvf4n95QwcYXWxEs7xdMzM0Wmhqwt+PCcUkmE8prQjzcqDOIMhVD6ew== +// ==/UserScript== + +(function () { + 'use strict'; + + //#region node_modules/.pnpm/typedbrainz@0.3.0/node_modules/typedbrainz/lib/index.js + // SPDX-License-Identifier: MIT + function isReleaseRelationshipEditor(relationshipEditor) { + return relationshipEditor.state?.entity.entityType === "release"; + } + + //#endregion + + const zSeedJSON = Zod.object({ + version: Zod.literal(1), + recordings: Zod.record(Zod.string(), // recording id + Zod.object({ + url: Zod.string().url(), + types: Zod.array(Zod.string()), // link_type UUID + ended: Zod.boolean().optional(), + })), + note: Zod.string(), + }).or(Zod.object({ + version: Zod.literal(2), + recordings: Zod.record(Zod.string(), // recording id + Zod.array(Zod.object({ + url: Zod.string().url(), + types: Zod.array(Zod.string()), // link_type UUID + ended: Zod.boolean().optional(), + }))), + note: Zod.string(), + })); + const zSeedJSONFallback = Zod.object({ + version: Zod.number(), + }); + + function applyRelationships(relationships, editNote, relationshipEditor) { + const tree = window.MB?.tree; + if (tree == null) + return alert("MB.tree is missing."); + relationshipEditor.dispatch({ + type: "update-edit-note", + editNote: (relationshipEditor.state.editNoteField.value + "\n" + editNote + "\n''Powered by \"" + GM_info.script.name + "\" script (" + GM_info.script.version + ")''").trim(), + }); + let alreadyAlerted = false; + for (const relationship of relationships) { + // it will be marked as "incomplete" in the UI, but actually working? + // @see https://github.com/metabrainz/musicbrainz-server/blob/e214b4d3c13f7ee6b2eb2f9c186ecab310354a5b/root/static/scripts/relationship-editor/components/RelationshipItem.js#L153-L163 + // if we already have a same relationship, just change ended flag + const existingRels = relationship.recording.relationships + .filter(r => !r.editsPending) + .filter(r => r.target_type === "url" && r.target.href_url === relationship.url && (relationship.linkTypeID === "all" || r.linkTypeID === relationship.linkTypeID)) + .toSorted((a, b) => { + if (a.ended !== b.ended) { + return (a.ended === relationship.ended) ? -1 : 1; + } + return a.id - b.id; + }); + const dispatchesEdit = []; + const dispatchesRemove = []; + if (existingRels.length > 0) { + let weAlreadyHaveRel = false; + for (const existingRel of existingRels) { + const oldRelationshipState = { + ...existingRel, + _lineage: ["loaded from database"], + _original: null, + _status: 0, + attributes: tree.fromDistinctAscArray(existingRel.attributes), + entity0: relationship.recording, + entity1: existingRel.target, + }; + oldRelationshipState._original = oldRelationshipState; + const isSimpleRel = existingRel.begin_date == null && existingRel.end_date == null; + if (isSimpleRel) { + if (weAlreadyHaveRel) { + dispatchesRemove.push({ + type: "update-relationship-state", + sourceEntity: relationship.recording, + batchSelectionCount: undefined, + creditsToChangeForSource: "", + creditsToChangeForTarget: "", + oldRelationshipState: null, + newRelationshipState: { + ...oldRelationshipState, + _lineage: [...oldRelationshipState._lineage, "removed"], + _status: 3, + }, + }); + continue; + } + weAlreadyHaveRel = true; + } + if (existingRel.ended !== relationship.ended) { + dispatchesEdit.push({ + type: "update-relationship-state", + sourceEntity: relationship.recording, + batchSelectionCount: undefined, + creditsToChangeForSource: "", + creditsToChangeForTarget: "", + oldRelationshipState: null, + newRelationshipState: { + ...oldRelationshipState, + _lineage: [...oldRelationshipState._lineage, "edited"], + _status: 2, + ended: relationship.ended, + } + }); + } + } + if (dispatchesRemove.length) { + for (const d of dispatchesRemove) { + relationshipEditor.dispatch(d); + } + if (!alreadyAlerted) { + alert("Please re-seed after submit these edits."); + alreadyAlerted = true; + } + } + else if (dispatchesEdit.length) { + for (const d of dispatchesEdit) { + relationshipEditor.dispatch(d); + } + } + continue; + } + if (relationship.linkTypeID === "all") + continue; + relationshipEditor.dispatch({ + type: "update-relationship-state", + sourceEntity: relationship.recording, + oldRelationshipState: null, + newRelationshipState: { + id: relationshipEditor.getRelationshipStateId(null), + linkOrder: 0, + linkTypeID: relationship.linkTypeID, + _lineage: ["added"], + _original: null, + _status: 1, + attributes: null, + begin_date: null, // TODO: support? + end_date: null, // TODO: support? + editsPending: false, + ended: !!relationship.ended, // defaults to false + entity0: relationship.recording, + entity0_credit: "", + entity1: { + decoded: "", + editsPending: false, + entityType: "url", + gid: "", + name: relationship.url, + id: relationshipEditor.getRelationshipStateId(null), + last_updated: null, + href_url: "", + pretty_name: "", + }, + entity1_credit: "", + }, + batchSelectionCount: undefined, + creditsToChangeForSource: "", + creditsToChangeForTarget: "", + }); + } + } + + async function main() { + // check hash + const urlParams = new URLSearchParams(location.hash.slice(1)); + const rawJson = urlParams.get("seed-urls-v1"); + if (rawJson == null) + return; + while (window.MB?.relationshipEditor?.state == null) { + console.log("Waiting for window.MB.relationshipEditor?.state to be defined..."); + await new Promise(resolve => setTimeout(resolve, 100)); + } + console.log("!", window.MB.relationshipEditor); + if (!isReleaseRelationshipEditor(window.MB.relationshipEditor)) { + return; + } + const { linkedEntities, relationshipEditor, tree } = window.MB; + if (tree == null) + throw new Error("MB.tree is missing..."); + const button = document.createElement("button"); + button.textContent = "Seed URLs to Recordings"; + button.style.zoom = "2"; + button.addEventListener("click", () => { + const anyJSON = JSON.parse(rawJson); + const baseJSON = zSeedJSONFallback.parse(anyJSON); + if (![1, 2].includes(baseJSON.version)) { + alert(`Unsupported version: ${baseJSON.version}, please update the script (or contact the seeder's developer)`); + return; + } + const json = zSeedJSON.parse(anyJSON); + const errors = []; + const preparedRelationships = []; + for (const track of tree.iterate(relationshipEditor.state.mediums).flatMap(m => tree.iterate(m[1]))) { + if (!(track.recording.gid in json.recordings)) + continue; + const rels = json.recordings[track.recording.gid]; + delete json.recordings[track.recording.gid]; + const alreadyAddedDomains = new Set(); + for (const rel of Array.isArray(rels) ? rels : [rels]) { + const relUrl = new URL(rel.url); + if (alreadyAddedDomains.has(relUrl.hostname)) { + errors.push(`You can't add multiple same domain URLs for a recording at once! URL: ${rel.url} Recording: ${track.recording.gid}`); + continue; + } + alreadyAddedDomains.add(relUrl.hostname); + for (const relType of rel.types) { + let linkTypeID; + if (relType in linkedEntities.link_type && linkedEntities.link_type[relType].type0 === "recording" && linkedEntities.link_type[relType].type1 === "url") { + linkTypeID = linkedEntities.link_type[relType].id; + } + if (linkTypeID == null) { + for (const lt of Object.values(linkedEntities.link_type)) { + if (lt.type0 !== "recording") + continue; + if (lt.type1 !== "url") + continue; + console.log(lt); + if (lt.name === relType) { + linkTypeID = lt.id; + break; + } + } + } + if (linkTypeID == null) { + errors.push(`Failed to find link type ${JSON.stringify(relType)} for recording ${track.recording.gid}`); + continue; + } + preparedRelationships.push({ + recording: track.recording, + url: rel.url, + linkTypeID, + ended: rel.ended ?? false, + }); + } + } + } + for (const remainingRecordingId of Object.keys(json.recordings)) { + errors.push(`Failed to find recording: ${remainingRecordingId}, you might be need to expand mediums before press seed button.`); + } + if (errors.length === 0) { + applyRelationships(preparedRelationships, json.note, relationshipEditor); + button.textContent = "URLs seeded successfully!"; + button.disabled = true; + } + else { + alert("Failed to seed urls:\n" + errors.map(x => "* " + x).join("\n")); + } + }); + const before = document.querySelector("#content > p"); + before.parentElement.insertBefore(button, before); + button.focus(); + console.log("done"); + } + main(); + +})(); diff --git a/mb-split-or-merge-medium.user.js b/mb-split-or-merge-medium.user.js new file mode 100644 index 0000000..397ae16 --- /dev/null +++ b/mb-split-or-merge-medium.user.js @@ -0,0 +1,90 @@ +// ==UserScript== +// @name MB: Split or Merge Medium (WIP!!) +// @namespace https://rinsuki.net +// @grant none +// @match https://*.musicbrainz.org/release/*/edit +// @match https://*.musicbrainz.org/release/add +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function isMBWithReleaseEditor(mb) { + return mb.releaseEditor !== undefined; + } + + function main() { + const MB = window.MB; + if (MB == null) + return; + if (!isMBWithReleaseEditor(MB)) + return; + const tracklist = document.getElementById("tracklist"); + if (tracklist == null) + return; + const splitButton = document.createElement("button"); + splitButton.textContent = "Split Medium"; + splitButton.addEventListener("click", () => { + if (MB.releaseEditor.rootField.release().mediums().length < 2) { + alert("There must be at least 2 mediums to split (src and dst)."); + return; + } + const srcMediumIndex = prompt([ + "Choose source medium:", + "", + ...Array.from(MB.releaseEditor.rootField.release().mediums().map((m, i) => [ + `${i + 1}: `, + m.name(), + ` (${m.tracks().length} tracks)` + ].join(""))) + ].join("\n")); + if (srcMediumIndex == null) + return; + const index = parseInt(srcMediumIndex, 10) - 1; + if (isNaN(index) || index < 0 || index >= MB.releaseEditor.rootField.release().mediums().length) { + alert("Invalid medium index."); + return; + } + const srcMedium = MB.releaseEditor.rootField.release().mediums()[index]; + const dstMediumIndex = prompt([ + "Choose destination medium:", + "", + ...Array.from(MB.releaseEditor.rootField.release().mediums().map((m, i) => [ + `${i + 1}: `, + m.name(), + ` (${m.tracks().length} tracks)` + ].join(""))) + ].join("\n")); + if (dstMediumIndex == null) + return; + const dstIndex = parseInt(dstMediumIndex, 10) - 1; + if (isNaN(dstIndex) || dstIndex < 0 || dstIndex >= MB.releaseEditor.rootField.release().mediums().length) { + alert("Invalid medium index."); + return; + } + const dstMedium = MB.releaseEditor.rootField.release().mediums()[dstIndex]; + const trackNumber = prompt([ + `Please enter the track number to move from "${srcMedium.position()}. ${srcMedium.name()}" to "${dstMedium.position()}. ${dstMedium.name()}".`, + "", + ...Array.from(srcMedium.tracks().map((t, i) => [ + `${i + 1}: `, + t.name() + ].join(""))) + ].join("\n")); + if (trackNumber == null) + return; + const trackIndex = parseInt(trackNumber, 10) - 1; + if (isNaN(trackIndex) || trackIndex < 0 || trackIndex >= srcMedium.tracks().length) { + alert("Invalid track number."); + return; + } + dstMedium.tracks.unshift(...srcMedium.tracks.splice(trackIndex)); + alert("Moved!"); + }); + tracklist.insertAdjacentElement("afterbegin", splitButton); + } + main(); + +})(); diff --git a/niconico-genten-kaiki.user.js b/niconico-genten-kaiki.user.js new file mode 100644 index 0000000..cb5426d --- /dev/null +++ b/niconico-genten-kaiki.user.js @@ -0,0 +1,167 @@ +// ==UserScript== +// @name 原点回帰(Re) あるいは ZenTube in 公式プレーヤー +// @namespace rinsuki.net +// @version 1.0 +// @description 動画説明文内のYouTubeへのリンクに「原点回帰」ボタンが追加され、そのボタンを押すとYouTubeの埋め込みプレーヤーで動画が再生されるようになります。 +// @author - +// @match https://www.nicovideo.jp/watch/* +// @grant none +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + const style = document.createElement("style"); + // CSS Class Prefix + const ccPrefix = "userjs-gentenkaiki"; + style.innerText = ` +body.${ccPrefix}-now .${ccPrefix}-start { display: none; } +body.${ccPrefix}-enableytclick .VideoSymbolContainer, +body.${ccPrefix}-enableytclick .CommentRenderer { pointer-events: none; } +body.${ccPrefix}-now #MainVideoPlayer > video { display: none !important; } + +.${ccPrefix}-wrapper { z-index: 0; } +.${ccPrefix}-wrapper, .${ccPrefix}-wrapper > iframe { position: absolute; width: 100%; height: 100%; top: 0; left: 0; border: none; } +.${ccPrefix}-wrapper > iframe { z-index: 1; } +body:not(.${ccPrefix}-enableytclick) .${ccPrefix}-player > iframe { pointer-events: none; } +.${ccPrefix}-ytb-b { fill: #fff } +.${ccPrefix}-ytb-p { fill: #000 } +body.${ccPrefix}-enableytclick .${ccPrefix}-ytb-b { fill: #f00 } +body.${ccPrefix}-enableytclick #UadPlayer { pointer-events: none; } +`; + document.head.appendChild(style); + + const YOUTUBE_ORIGIN = "https://www.youtube.com"; + + function sleep(msec) { return new Promise(resolve => setTimeout(resolve, msec)); } + + class YTPlayer { + iframe = document.createElement("iframe"); + widgetid; + connected = false; + constructor(videoId) { + this.widgetid = Date.now(); + this.iframe.src = `${YOUTUBE_ORIGIN}/embed/${videoId}?autoplay=1&fs=0&disablekb=1&modestbranding=1&playsinline=1&rel=0&showinfo=0&iv_load_policy=3&enablejsapi=1&origin=${location.origin}&vq=highres&widgetid=${this.widgetid}`; + window.addEventListener("message", e => { + if (e.origin !== YOUTUBE_ORIGIN) + return console.log("invalid origin"); + const data = JSON.parse(e.data); + if (data.id !== this.widgetid) + return; + this.connected = true; + switch (data.event) { + case "onReady": + if (this.onReadyCallback != null) + this.onReadyCallback(); + break; + case "infoDelivery": + const info = data.info; + const diff = Math.abs(info.currentTime - __videoplayer.originalCurrentTime()); + if (diff > 0.5) { + this.call("seekTo", __videoplayer.originalCurrentTime(), true); + } + break; + case "onStateChange": + const isPaused = data.info !== 1; + if (isPaused !== __videoplayer.paused()) { + if (isPaused) { + __videoplayer.pause(); + } + else { + __videoplayer.play(); + } + } + default: + console.log("iframe event", data); + } + }); + this.iframe.addEventListener("load", async () => { + for (let i = 0; i < 100 && !this.connected; i++) { + this.sendListening(); + await sleep(100); + } + }); + } + postMessage(obj) { + this.iframe.contentWindow.postMessage(JSON.stringify({ + ...obj, + id: this.widgetid, + }), YOUTUBE_ORIGIN); + } + onReadyCallback; + onLoad(callback) { + this.iframe.addEventListener("load", callback); + } + sendListening() { + this.postMessage({ event: "listening", channel: "widget" }); + } + call(func, ...args) { + this.postMessage({ event: "command", func, args, channel: "widget" }); + } + } + + function startYouTube(videoId) { + __videoplayer.pause(); + __videoplayer.replace(`https://api.rinsuki.net/s/dummymovie/mp4/seconds/${Math.floor(__videoplayer.duration())}`); + document.body.classList.add(`${ccPrefix}-now`); + const wrapper = document.createElement("div"); + wrapper.className = `${ccPrefix}-wrapper`; + const player = new YTPlayer(videoId); + wrapper.appendChild(player.iframe); + document.getElementById("MainVideoPlayer").appendChild(wrapper); + const button = document.createElement("button"); + button.className = `ActionButton ControllerButton ${ccPrefix}-switchbutton`; + button.innerHTML = `
`; + function updateButtonTitle() { + button.dataset.title = `YouTubeのコントロールをでき${document.body.classList.contains(`${ccPrefix}-enableytclick`) ? "ない" : "る"}ようにする`; + } + updateButtonTitle(); + button.addEventListener("click", () => { + document.body.classList.toggle(`${ccPrefix}-enableytclick`); + updateButtonTitle(); + }); + const cca = document.querySelector(".ControllerContainer-area:last-child"); + cca.insertBefore(button, cca.firstChild); + player.onReadyCallback = () => { + __videoplayer.play(); + player.call("addEventListener", "onStateChange"); + }; + } + + (async () => { + // 本当は Observe とかしたほうがいいんだろうが… + async function waitForSelectorExists(selector) { + for (let i = 0; i < 5; i++) { + const res = document.querySelector(selector); + if (res != null) + return res; + await sleep(1000); + } + } + function createActivateButton(videoId, dom) { + const button = document.createElement("button"); + button.innerText = "原点回帰"; + button.className = `${ccPrefix}-start`; + button.addEventListener("click", () => startYouTube(videoId)); + dom.parentElement.insertBefore(button, dom.nextSibling); + } + const domDescription = await waitForSelectorExists(".VideoDescription-html"); + if (domDescription == null) + return; + for (const link of domDescription.querySelectorAll("a")) { + console.log(link); + if (link.hostname === "youtu.be") + createActivateButton(link.pathname.slice(1), link); + if ((link.hostname === "youtube.com" || link.hostname.endsWith(".youtube.com")) && link.pathname === "/watch") { + const url = new URL(link.href); + const vid = url.searchParams.get("v"); + if (vid != null) + createActivateButton(vid, link); + } + } + })(); + +})(); diff --git a/niconico-search-sort-table.user.js b/niconico-search-sort-table.user.js new file mode 100644 index 0000000..0a92070 --- /dev/null +++ b/niconico-search-sort-table.user.js @@ -0,0 +1,64 @@ +// ==UserScript== +// @name ニコニコ動画の検索のソート選択を表にする +// @namespace rinsuki.net +// @version 1.0 +// @description ニコニコ動画の検索のソート順を選ぶところを表レイアウトにします。 +// @author rinsuki +// @match https://www.nicovideo.jp/tag/* +// @match https://www.nicovideo.jp/search/* +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// @require https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js#sha256-2Unxw2h67a3O2shSYYZfKbF80nOZfn9rK/xTsvnUxN0=,sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg== +// @require https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js#sha256-NfT5dPSyvNRNpzljNH+JUuNB+DkJ5EmCJ9Tia5j2bw0=,sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ== +// ==/UserScript== + +(function () { + 'use strict'; + + const sort = document.querySelector(".searchOption .sort.optionList"); + sort.style.width = "220px"; + const originalSortList = sort.querySelector(".sortList"); + const links = {}; + for (const anchor of originalSortList.querySelectorAll("a")) { + const sp = new URLSearchParams(anchor.search); + const key = `${sp.get("sort")}:${sp.get("order")}`; + links[key] = [anchor.href, anchor.parentElement.classList.contains("active"), anchor]; + } + const Link = props => { + let l = links[props.k]; + if (l == null) { + originalSortList.querySelector("li.active")?.remove(); + return React.createElement("a", { style: { padding: "4px", color: "#fff", background: "#999" }, className: "active" }, props.children); + } + else { + l[2].parentElement?.remove(); + return React.createElement("a", { href: l[0], style: { padding: "4px" } }, props.children); + } + }; + const Row = props => { + return React.createElement("tr", null, + React.createElement("td", { style: { textAlign: "right", padding: "4px" } }, + props.title, + "が"), + React.createElement("td", null, + React.createElement(Link, { k: props.k + ":d" }, + props.desc, + "順")), + React.createElement("td", null, + React.createElement(Link, { k: props.k + ":a" }, + props.asc, + "順"))); + }; + const li = document.createElement("li"); + ReactDOM.render(React.createElement("table", null, + React.createElement(Row, { title: "投稿日時", k: "f", desc: "新しい", asc: "古い" }), + React.createElement(Row, { title: "再生数", k: "v", desc: "多い", asc: "少ない" }), + React.createElement(Row, { title: "コメント数", k: "r", desc: "多い", asc: "少ない" }), + React.createElement(Row, { title: "いいね数", k: "likeCount", desc: "多い", asc: "少ない" }), + React.createElement(Row, { title: "マイリスト数", k: "m", desc: "多い", asc: "少ない" }), + React.createElement(Row, { title: "コメント", k: "n", desc: "新しい", asc: "古い" }), + React.createElement(Row, { title: "再生時間", k: "l", desc: "長い", asc: "短い" })), li); + originalSortList.appendChild(li); + +})(); diff --git a/package.json b/package.json deleted file mode 100644 index 6e6f5cb..0000000 --- a/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "userscripts", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "scripts": { - "build": "rollup -c", - "dev": "run-p watch devserver", - "watch": "rollup -wc", - "devserver": "node ./devserver" - }, - "devDependencies": { - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-typescript": "^12.1.4", - "@types/jest": "^29.4.0", - "@types/node": "^25.6.0", - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1", - "@types/tampermonkey": "^5.0.5", - "estree-walker": "^3.0.3", - "jest": "^29.4.3", - "knockout": "^3.5.1", - "npm-run-all": "^4.1.5", - "rollup": "^4.46.2", - "rollup-plugin-external-globals": "^0.13.0", - "rollup-plugin-string-import": "^1.2.6", - "ts-jest": "^29.0.5", - "tslib": "^2.8.1", - "typedbrainz": "^0.3.0", - "typescript": "^6.0.2" - }, - "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee", - "dependencies": { - "@rinsuki/dom-chef": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.6", - "magic-string": "^0.30.17", - "react": "18.3.1", - "react-dom": "^18.3.1", - "weight-balanced-tree": "^0.16.0", - "zod": "3.24.4" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 1f69019..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,3633 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@rinsuki/dom-chef': - specifier: ^5.1.1 - version: 5.1.1 - '@rollup/plugin-commonjs': - specifier: ^28.0.6 - version: 28.0.6(rollup@4.46.2) - magic-string: - specifier: ^0.30.17 - version: 0.30.17 - react: - specifier: 18.3.1 - version: 18.3.1 - react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) - weight-balanced-tree: - specifier: ^0.16.0 - version: 0.16.0 - zod: - specifier: 3.24.4 - version: 3.24.4 - devDependencies: - '@rollup/plugin-node-resolve': - specifier: ^16.0.1 - version: 16.0.1(rollup@4.46.2) - '@rollup/plugin-typescript': - specifier: ^12.1.4 - version: 12.1.4(rollup@4.46.2)(tslib@2.8.1)(typescript@6.0.2) - '@types/jest': - specifier: ^29.4.0 - version: 29.4.0 - '@types/node': - specifier: ^25.6.0 - version: 25.6.0 - '@types/react': - specifier: 18.3.12 - version: 18.3.12 - '@types/react-dom': - specifier: 18.3.1 - version: 18.3.1 - '@types/tampermonkey': - specifier: ^5.0.5 - version: 5.0.5 - estree-walker: - specifier: ^3.0.3 - version: 3.0.3 - jest: - specifier: ^29.4.3 - version: 29.4.3(@types/node@25.6.0) - knockout: - specifier: ^3.5.1 - version: 3.5.1 - npm-run-all: - specifier: ^4.1.5 - version: 4.1.5 - rollup: - specifier: ^4.46.2 - version: 4.46.2 - rollup-plugin-external-globals: - specifier: ^0.13.0 - version: 0.13.0(rollup@4.46.2) - rollup-plugin-string-import: - specifier: ^1.2.6 - version: 1.2.6(rollup@4.46.2) - ts-jest: - specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.21.0)(@jest/types@29.4.3)(babel-jest@29.4.3(@babel/core@7.21.0))(jest@29.4.3(@types/node@25.6.0))(typescript@6.0.2) - tslib: - specifier: ^2.8.1 - version: 2.8.1 - typedbrainz: - specifier: ^0.3.0 - version: 0.3.0 - typescript: - specifier: ^6.0.2 - version: 6.0.2 - -packages: - - '@ampproject/remapping@2.2.0': - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} - engines: {node: '>=6.0.0'} - - '@babel/code-frame@7.18.6': - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.21.0': - resolution: {integrity: sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.21.0': - resolution: {integrity: sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.21.1': - resolution: {integrity: sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.20.7': - resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-environment-visitor@7.18.9': - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.21.0': - resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.18.6': - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.18.6': - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.21.2': - resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.20.2': - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-simple-access@7.20.2': - resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.18.6': - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.19.4': - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.19.1': - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.21.0': - resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.21.0': - resolution: {integrity: sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.18.6': - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.21.2': - resolution: {integrity: sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.18.6': - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.20.0': - resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.20.7': - resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.21.2': - resolution: {integrity: sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.21.2': - resolution: {integrity: sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jest/console@29.4.3': - resolution: {integrity: sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.4.3': - resolution: {integrity: sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/environment@29.4.3': - resolution: {integrity: sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@29.4.3': - resolution: {integrity: sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect@29.4.3': - resolution: {integrity: sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.4.3': - resolution: {integrity: sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/globals@29.4.3': - resolution: {integrity: sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/reporters@29.4.3': - resolution: {integrity: sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/schemas@29.4.3': - resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/source-map@29.4.3': - resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-result@29.4.3': - resolution: {integrity: sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-sequencer@29.4.3': - resolution: {integrity: sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/transform@29.4.3': - resolution: {integrity: sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.4.3': - resolution: {integrity: sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.1.1': - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/gen-mapping@0.3.2': - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.0': - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.1.2': - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - - '@jridgewell/trace-mapping@0.3.17': - resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} - - '@rinsuki/dom-chef@5.1.1': - resolution: {integrity: sha512-ds24Wu+yfz8ebyMZRDirmb7y+co+BNOZj4Zba5yxHTb42nRzc4XIlXuyZx7WE9NyyGSbgzqw1CrXBY3yUQ2TUA==} - - '@rollup/plugin-commonjs@28.0.6': - resolution: {integrity: sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@16.0.1': - resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-typescript@12.1.4': - resolution: {integrity: sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.14.0||^3.0.0||^4.0.0 - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true - - '@rollup/pluginutils@5.2.0': - resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.46.2': - resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.46.2': - resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.46.2': - resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.46.2': - resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.46.2': - resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.46.2': - resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.46.2': - resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.46.2': - resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.46.2': - resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.46.2': - resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.46.2': - resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.46.2': - resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.46.2': - resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.46.2': - resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.46.2': - resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} - cpu: [x64] - os: [win32] - - '@sinclair/typebox@0.25.24': - resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} - - '@sinonjs/commons@2.0.0': - resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} - - '@sinonjs/fake-timers@10.0.2': - resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} - - '@types/babel__core@7.20.0': - resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==} - - '@types/babel__generator@7.6.4': - resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} - - '@types/babel__template@7.4.1': - resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} - - '@types/babel__traverse@7.18.3': - resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/graceful-fs@4.1.6': - resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} - - '@types/istanbul-lib-coverage@2.0.4': - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - - '@types/istanbul-lib-report@3.0.0': - resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} - - '@types/istanbul-reports@3.0.1': - resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} - - '@types/jest@29.4.0': - resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==} - - '@types/node@25.6.0': - resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} - - '@types/prettier@2.7.2': - resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} - - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - - '@types/react-dom@18.3.1': - resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} - - '@types/react@17.0.89': - resolution: {integrity: sha512-I98SaDCar5lvEYl80ClRIUztH/hyWHR+I2f+5yTVp/MQ205HgYkA2b5mVdry/+nsEIrf8I65KA5V/PASx68MsQ==} - - '@types/react@18.3.12': - resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - '@types/scheduler@0.16.8': - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - - '@types/stack-utils@2.0.1': - resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} - - '@types/tampermonkey@5.0.5': - resolution: {integrity: sha512-ZWitwJH4T/jcQiynhc3I5DNiwlsDzVTKPXg6A8ltTw3SDuf0b00bNRSV0Z/yldnhhBi9e8g8vY/X3LVSQcaCKQ==} - - '@types/yargs-parser@21.0.0': - resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} - - '@types/yargs@17.0.22': - resolution: {integrity: sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==} - - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - babel-jest@29.4.3: - resolution: {integrity: sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.4.3: - resolution: {integrity: sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-preset-current-node-syntax@1.0.1: - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} - peerDependencies: - '@babel/core': ^7.0.0 - - babel-preset-jest@29.4.3: - resolution: {integrity: sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - - browserslist@4.21.5: - resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001458: - resolution: {integrity: sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - - ci-info@3.8.0: - resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} - engines: {node: '>=8'} - - cjs-module-lexer@1.2.2: - resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collect-v8-coverage@1.0.1: - resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cross-spawn@6.0.5: - resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} - engines: {node: '>=4.8'} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - dedent@0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} - - deepmerge@4.2.2: - resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} - engines: {node: '>=0.10.0'} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - define-properties@1.1.3: - resolution: {integrity: sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==} - engines: {node: '>= 0.4'} - - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - - diff-sequences@29.4.3: - resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - electron-to-chromium@1.4.312: - resolution: {integrity: sha512-e7g+PzxzkbiCD1aNhdj+Tx3TLlfrQF/Lf+LAaUdoLvB1kCxf9wJimqXdWEqnoiYjFtxIR1hGBmoHsBIcCBNOMA==} - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - - es-abstract@1.19.1: - resolution: {integrity: sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - - escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - - expect@29.4.3: - resolution: {integrity: sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.1.1: - resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==} - - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - - has-bigints@1.0.1: - resolution: {integrity: sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-symbols@1.0.2: - resolution: {integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} - - has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - import-local@3.1.0: - resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - internal-slot@1.0.3: - resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - - is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} - - is-callable@1.2.4: - resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==} - engines: {node: '>= 0.4'} - - is-core-module@2.11.0: - resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-negative-zero@2.0.1: - resolution: {integrity: sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==} - engines: {node: '>= 0.4'} - - is-number-object@1.0.6: - resolution: {integrity: sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - - is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.1: - resolution: {integrity: sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} - - is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - - is-weakref@1.0.1: - resolution: {integrity: sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} - engines: {node: '>=8'} - - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.0: - resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} - engines: {node: '>=8'} - - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} - - istanbul-reports@3.1.5: - resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} - engines: {node: '>=8'} - - jest-changed-files@29.4.3: - resolution: {integrity: sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.4.3: - resolution: {integrity: sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.4.3: - resolution: {integrity: sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.4.3: - resolution: {integrity: sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.4.3: - resolution: {integrity: sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.4.3: - resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.4.3: - resolution: {integrity: sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.4.3: - resolution: {integrity: sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.4.3: - resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.4.3: - resolution: {integrity: sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.4.3: - resolution: {integrity: sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.4.3: - resolution: {integrity: sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.4.3: - resolution: {integrity: sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.4.3: - resolution: {integrity: sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.4.3: - resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.4.3: - resolution: {integrity: sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.4.3: - resolution: {integrity: sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.4.3: - resolution: {integrity: sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.4.3: - resolution: {integrity: sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.4.3: - resolution: {integrity: sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.4.3: - resolution: {integrity: sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.4.3: - resolution: {integrity: sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.4.3: - resolution: {integrity: sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.4.3: - resolution: {integrity: sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.4.3: - resolution: {integrity: sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - - json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - knockout@3.5.1: - resolution: {integrity: sha512-wRJ9I4az0QcsH7A4v4l0enUpkS++MBx0BnL/68KaLzJg7x1qmbjSlwEoCNol7KTYZ+pmtI7Eh2J0Nu6/2Z5J/Q==} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-json-file@4.0.0: - resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} - engines: {node: '>=4'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - - memorystream@0.3.1: - resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} - engines: {node: '>= 0.10.0'} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-releases@2.0.10: - resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} - - normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-all@4.1.5: - resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} - engines: {node: '>= 4'} - hasBin: true - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - object-inspect@1.11.0: - resolution: {integrity: sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.2: - resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==} - engines: {node: '>= 0.4'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-type@3.0.0: - resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} - engines: {node: '>=4'} - - picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - - pidtree@0.3.1: - resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} - engines: {node: '>=0.10'} - hasBin: true - - pify@3.0.0: - resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} - engines: {node: '>=4'} - - pirates@4.0.5: - resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} - engines: {node: '>= 6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - pretty-format@29.4.3: - resolution: {integrity: sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} - peerDependencies: - react: ^18.3.1 - - react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} - engines: {node: '>=0.10.0'} - - read-pkg@3.0.0: - resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} - engines: {node: '>=4'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - resolve.exports@2.0.0: - resolution: {integrity: sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==} - engines: {node: '>=10'} - - resolve@1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} - hasBin: true - - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - hasBin: true - - rollup-plugin-external-globals@0.13.0: - resolution: {integrity: sha512-wBS3hmoF0OtEnA0lWsmTC6Nhnkk2zjZbfhaX2gLo8VnfNGFdGhiYKwMpIPQPrYbAw+mAYUYmoHYktAl1eZHgVw==} - peerDependencies: - rollup: ^2.25.0 || ^3.3.0 || ^4.1.4 - - rollup-plugin-string-import@1.2.6: - resolution: {integrity: sha512-vChs/dPkgErR7RBXOuOoZ6gBnwQqoUILuQxdcb8uynKSSn9fzjympIIVa3b2iTW3J78Ufk6q9nOKM7jxtJJUxw==} - peerDependencies: - rollup: '>=1.20.0' - - rollup@4.46.2: - resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - - semver@5.7.1: - resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} - hasBin: true - - semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true - - semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shell-quote@1.7.3: - resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} - - side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - spdx-correct@3.1.1: - resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} - - spdx-exceptions@2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.10: - resolution: {integrity: sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string.prototype.padend@3.1.3: - resolution: {integrity: sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.4: - resolution: {integrity: sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==} - - string.prototype.trimstart@1.0.4: - resolution: {integrity: sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - svg-tag-names@3.0.1: - resolution: {integrity: sha512-R5EDS44RQSOwwKIOOUZLKZggMeFL8fDSSZr1hVK5Z0KkyuGVOZfyepBZDkPu/aoL+hSOysxxvAk8uN4yN2LOMQ==} - - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - ts-jest@29.0.5: - resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - typedbrainz@0.3.0: - resolution: {integrity: sha512-o/uRMDmeqAffVFnokofYhgk5ib5RomuSsKxwjV1mxwI7OZRVWYLabJzadMxDJ2my5a+4MVO9rzBsOw3JWkWteA==} - - typescript@6.0.2: - resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} - engines: {node: '>=14.17'} - hasBin: true - - unbox-primitive@1.0.1: - resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==} - - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - - update-browserslist-db@1.0.10: - resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - v8-to-istanbul@9.1.0: - resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} - engines: {node: '>=10.12.0'} - - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - - weight-balanced-tree@0.16.0: - resolution: {integrity: sha512-PAN9CX845j3gceU3VMLnBmi68FSQjIR7si7DSITh1b/AjtL72FCnR+SriDalTtYGWJc/MhRqUjRmwsP743nvVQ==} - - which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.1: - resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} - engines: {node: '>=12'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zod@3.24.4: - resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} - -snapshots: - - '@ampproject/remapping@2.2.0': - dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.17 - - '@babel/code-frame@7.18.6': - dependencies: - '@babel/highlight': 7.18.6 - - '@babel/compat-data@7.21.0': {} - - '@babel/core@7.21.0': - dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.21.1 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.0) - '@babel/helper-module-transforms': 7.21.2 - '@babel/helpers': 7.21.0 - '@babel/parser': 7.21.2 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.2 - '@babel/types': 7.21.2 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.21.1': - dependencies: - '@babel/types': 7.21.2 - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.17 - jsesc: 2.5.2 - - '@babel/helper-compilation-targets@7.20.7(@babel/core@7.21.0)': - dependencies: - '@babel/compat-data': 7.21.0 - '@babel/core': 7.21.0 - '@babel/helper-validator-option': 7.21.0 - browserslist: 4.21.5 - lru-cache: 5.1.1 - semver: 6.3.0 - - '@babel/helper-environment-visitor@7.18.9': {} - - '@babel/helper-function-name@7.21.0': - dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.2 - - '@babel/helper-hoist-variables@7.18.6': - dependencies: - '@babel/types': 7.21.2 - - '@babel/helper-module-imports@7.18.6': - dependencies: - '@babel/types': 7.21.2 - - '@babel/helper-module-transforms@7.21.2': - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.20.2 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.2 - '@babel/types': 7.21.2 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.20.2': {} - - '@babel/helper-simple-access@7.20.2': - dependencies: - '@babel/types': 7.21.2 - - '@babel/helper-split-export-declaration@7.18.6': - dependencies: - '@babel/types': 7.21.2 - - '@babel/helper-string-parser@7.19.4': {} - - '@babel/helper-validator-identifier@7.19.1': {} - - '@babel/helper-validator-option@7.21.0': {} - - '@babel/helpers@7.21.0': - dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.2 - '@babel/types': 7.21.2 - transitivePeerDependencies: - - supports-color - - '@babel/highlight@7.18.6': - dependencies: - '@babel/helper-validator-identifier': 7.19.1 - chalk: 2.4.2 - js-tokens: 4.0.0 - - '@babel/parser@7.21.2': - dependencies: - '@babel/types': 7.21.2 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.21.0)': - dependencies: - '@babel/core': 7.21.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/template@7.20.7': - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.21.2 - '@babel/types': 7.21.2 - - '@babel/traverse@7.21.2': - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.21.1 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.2 - '@babel/types': 7.21.2 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.21.2': - dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 - - '@bcoe/v8-coverage@0.2.3': {} - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jest/console@29.4.3': - dependencies: - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - chalk: 4.1.2 - jest-message-util: 29.4.3 - jest-util: 29.4.3 - slash: 3.0.0 - - '@jest/core@29.4.3': - dependencies: - '@jest/console': 29.4.3 - '@jest/reporters': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.8.0 - exit: 0.1.2 - graceful-fs: 4.2.10 - jest-changed-files: 29.4.3 - jest-config: 29.4.3(@types/node@25.6.0) - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-resolve-dependencies: 29.4.3 - jest-runner: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 - jest-watcher: 29.4.3 - micromatch: 4.0.5 - pretty-format: 29.4.3 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - - ts-node - - '@jest/environment@29.4.3': - dependencies: - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - jest-mock: 29.4.3 - - '@jest/expect-utils@29.4.3': - dependencies: - jest-get-type: 29.4.3 - - '@jest/expect@29.4.3': - dependencies: - expect: 29.4.3 - jest-snapshot: 29.4.3 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.4.3': - dependencies: - '@jest/types': 29.4.3 - '@sinonjs/fake-timers': 10.0.2 - '@types/node': 25.6.0 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 - jest-util: 29.4.3 - - '@jest/globals@29.4.3': - dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/types': 29.4.3 - jest-mock: 29.4.3 - transitivePeerDependencies: - - supports-color - - '@jest/reporters@29.4.3': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@jridgewell/trace-mapping': 0.3.17 - '@types/node': 25.6.0 - chalk: 4.1.2 - collect-v8-coverage: 1.0.1 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-instrument: 5.2.1 - istanbul-lib-report: 3.0.0 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 - jest-message-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.1.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.4.3': - dependencies: - '@sinclair/typebox': 0.25.24 - - '@jest/source-map@29.4.3': - dependencies: - '@jridgewell/trace-mapping': 0.3.17 - callsites: 3.1.0 - graceful-fs: 4.2.10 - - '@jest/test-result@29.4.3': - dependencies: - '@jest/console': 29.4.3 - '@jest/types': 29.4.3 - '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.1 - - '@jest/test-sequencer@29.4.3': - dependencies: - '@jest/test-result': 29.4.3 - graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - slash: 3.0.0 - - '@jest/transform@29.4.3': - dependencies: - '@babel/core': 7.21.0 - '@jest/types': 29.4.3 - '@jridgewell/trace-mapping': 0.3.17 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-regex-util: 29.4.3 - jest-util: 29.4.3 - micromatch: 4.0.5 - pirates: 4.0.5 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - - '@jest/types@29.4.3': - dependencies: - '@jest/schemas': 29.4.3 - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.1 - '@types/node': 25.6.0 - '@types/yargs': 17.0.22 - chalk: 4.1.2 - - '@jridgewell/gen-mapping@0.1.1': - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 - - '@jridgewell/gen-mapping@0.3.2': - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping': 0.3.17 - - '@jridgewell/resolve-uri@3.1.0': {} - - '@jridgewell/set-array@1.1.2': {} - - '@jridgewell/sourcemap-codec@1.4.14': {} - - '@jridgewell/sourcemap-codec@1.5.4': {} - - '@jridgewell/trace-mapping@0.3.17': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - - '@rinsuki/dom-chef@5.1.1': - dependencies: - '@types/react': 17.0.89 - svg-tag-names: 3.0.1 - - '@rollup/plugin-commonjs@28.0.6(rollup@4.46.2)': - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.2) - is-reference: 1.2.1 - magic-string: 0.30.17 - picomatch: 4.0.2 - optionalDependencies: - rollup: 4.46.2 - - '@rollup/plugin-node-resolve@16.0.1(rollup@4.46.2)': - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.10 - optionalDependencies: - rollup: 4.46.2 - - '@rollup/plugin-typescript@12.1.4(rollup@4.46.2)(tslib@2.8.1)(typescript@6.0.2)': - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) - resolve: 1.22.10 - typescript: 6.0.2 - optionalDependencies: - rollup: 4.46.2 - tslib: 2.8.1 - - '@rollup/pluginutils@5.2.0(rollup@4.46.2)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.2 - optionalDependencies: - rollup: 4.46.2 - - '@rollup/rollup-android-arm-eabi@4.46.2': - optional: true - - '@rollup/rollup-android-arm64@4.46.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.46.2': - optional: true - - '@rollup/rollup-darwin-x64@4.46.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.46.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.46.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.46.2': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.46.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.46.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.46.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.46.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.46.2': - optional: true - - '@sinclair/typebox@0.25.24': {} - - '@sinonjs/commons@2.0.0': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.0.2': - dependencies: - '@sinonjs/commons': 2.0.0 - - '@types/babel__core@7.20.0': - dependencies: - '@babel/parser': 7.21.2 - '@babel/types': 7.21.2 - '@types/babel__generator': 7.6.4 - '@types/babel__template': 7.4.1 - '@types/babel__traverse': 7.18.3 - - '@types/babel__generator@7.6.4': - dependencies: - '@babel/types': 7.21.2 - - '@types/babel__template@7.4.1': - dependencies: - '@babel/parser': 7.21.2 - '@babel/types': 7.21.2 - - '@types/babel__traverse@7.18.3': - dependencies: - '@babel/types': 7.21.2 - - '@types/estree@1.0.8': {} - - '@types/graceful-fs@4.1.6': - dependencies: - '@types/node': 25.6.0 - - '@types/istanbul-lib-coverage@2.0.4': {} - - '@types/istanbul-lib-report@3.0.0': - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - - '@types/istanbul-reports@3.0.1': - dependencies: - '@types/istanbul-lib-report': 3.0.0 - - '@types/jest@29.4.0': - dependencies: - expect: 29.4.3 - pretty-format: 29.4.3 - - '@types/node@25.6.0': - dependencies: - undici-types: 7.19.2 - - '@types/prettier@2.7.2': {} - - '@types/prop-types@15.7.15': {} - - '@types/react-dom@18.3.1': - dependencies: - '@types/react': 18.3.12 - - '@types/react@17.0.89': - dependencies: - '@types/prop-types': 15.7.15 - '@types/scheduler': 0.16.8 - csstype: 3.1.3 - - '@types/react@18.3.12': - dependencies: - '@types/prop-types': 15.7.15 - csstype: 3.1.3 - - '@types/resolve@1.20.2': {} - - '@types/scheduler@0.16.8': {} - - '@types/stack-utils@2.0.1': {} - - '@types/tampermonkey@5.0.5': {} - - '@types/yargs-parser@21.0.0': {} - - '@types/yargs@17.0.22': - dependencies: - '@types/yargs-parser': 21.0.0 - - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - - ansi-regex@5.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - babel-jest@29.4.3(@babel/core@7.21.0): - dependencies: - '@babel/core': 7.21.0 - '@jest/transform': 29.4.3 - '@types/babel__core': 7.20.0 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.4.3(@babel/core@7.21.0) - chalk: 4.1.2 - graceful-fs: 4.2.10 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.20.2 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.4.3: - dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.2 - '@types/babel__core': 7.20.0 - '@types/babel__traverse': 7.18.3 - - babel-preset-current-node-syntax@1.0.1(@babel/core@7.21.0): - dependencies: - '@babel/core': 7.21.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.21.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.0) - - babel-preset-jest@29.4.3(@babel/core@7.21.0): - dependencies: - '@babel/core': 7.21.0 - babel-plugin-jest-hoist: 29.4.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.0) - - balanced-match@1.0.2: {} - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - braces@3.0.2: - dependencies: - fill-range: 7.0.1 - - browserslist@4.21.5: - dependencies: - caniuse-lite: 1.0.30001458 - electron-to-chromium: 1.4.312 - node-releases: 2.0.10 - update-browserslist-db: 1.0.10(browserslist@4.21.5) - - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 - - buffer-from@1.1.2: {} - - call-bind@1.0.2: - dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.1.1 - - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001458: {} - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} - - ci-info@3.8.0: {} - - cjs-module-lexer@1.2.2: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - co@4.6.0: {} - - collect-v8-coverage@1.0.1: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - commondir@1.0.1: {} - - concat-map@0.0.1: {} - - convert-source-map@1.9.0: {} - - convert-source-map@2.0.0: {} - - cross-spawn@6.0.5: - dependencies: - nice-try: 1.0.5 - path-key: 2.0.1 - semver: 5.7.1 - shebang-command: 1.2.0 - which: 1.3.1 - - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.1.3: {} - - debug@4.3.4: - dependencies: - ms: 2.1.2 - - dedent@0.7.0: {} - - deepmerge@4.2.2: {} - - deepmerge@4.3.1: {} - - define-properties@1.1.3: - dependencies: - object-keys: 1.1.1 - - detect-newline@3.1.0: {} - - diff-sequences@29.4.3: {} - - electron-to-chromium@1.4.312: {} - - emittery@0.13.1: {} - - emoji-regex@8.0.0: {} - - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - - es-abstract@1.19.1: - dependencies: - call-bind: 1.0.2 - es-to-primitive: 1.2.1 - function-bind: 1.1.1 - get-intrinsic: 1.1.1 - get-symbol-description: 1.0.0 - has: 1.0.3 - has-symbols: 1.0.2 - internal-slot: 1.0.3 - is-callable: 1.2.4 - is-negative-zero: 2.0.1 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.1 - is-string: 1.0.7 - is-weakref: 1.0.1 - object-inspect: 1.11.0 - object-keys: 1.1.1 - object.assign: 4.1.2 - string.prototype.trimend: 1.0.4 - string.prototype.trimstart: 1.0.4 - unbox-primitive: 1.0.1 - - es-to-primitive@1.2.1: - dependencies: - is-callable: 1.2.4 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - - escalade@3.1.1: {} - - escape-string-regexp@1.0.5: {} - - escape-string-regexp@2.0.0: {} - - esprima@4.0.1: {} - - estree-walker@2.0.2: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit@0.1.2: {} - - expect@29.4.3: - dependencies: - '@jest/expect-utils': 29.4.3 - jest-get-type: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 - - fast-json-stable-stringify@2.1.0: {} - - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - - fdir@6.5.0(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - - fill-range@7.0.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - fs.realpath@1.0.0: {} - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - function-bind@1.1.1: {} - - function-bind@1.1.2: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.1.1: - dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-symbols: 1.0.2 - - get-package-type@0.1.0: {} - - get-stream@6.0.1: {} - - get-symbol-description@1.0.0: - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.1.1 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@11.12.0: {} - - graceful-fs@4.2.10: {} - - has-bigints@1.0.1: {} - - has-flag@3.0.0: {} - - has-flag@4.0.0: {} - - has-symbols@1.0.2: {} - - has-tostringtag@1.0.0: - dependencies: - has-symbols: 1.0.2 - - has@1.0.3: - dependencies: - function-bind: 1.1.1 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hosted-git-info@2.8.9: {} - - html-escaper@2.0.2: {} - - human-signals@2.1.0: {} - - import-local@3.1.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - internal-slot@1.0.3: - dependencies: - get-intrinsic: 1.1.1 - has: 1.0.3 - side-channel: 1.0.4 - - is-arrayish@0.2.1: {} - - is-bigint@1.0.4: - dependencies: - has-bigints: 1.0.1 - - is-boolean-object@1.1.2: - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - - is-callable@1.2.4: {} - - is-core-module@2.11.0: - dependencies: - has: 1.0.3 - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-date-object@1.0.5: - dependencies: - has-tostringtag: 1.0.0 - - is-fullwidth-code-point@3.0.0: {} - - is-generator-fn@2.1.0: {} - - is-module@1.0.0: {} - - is-negative-zero@2.0.1: {} - - is-number-object@1.0.6: - dependencies: - has-tostringtag: 1.0.0 - - is-number@7.0.0: {} - - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.8 - - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - is-regex@1.1.4: - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - - is-shared-array-buffer@1.0.1: {} - - is-stream@2.0.1: {} - - is-string@1.0.7: - dependencies: - has-tostringtag: 1.0.0 - - is-symbol@1.0.4: - dependencies: - has-symbols: 1.0.2 - - is-weakref@1.0.1: - dependencies: - call-bind: 1.0.2 - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.0: {} - - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.21.0 - '@babel/parser': 7.21.2 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - - istanbul-lib-report@3.0.0: - dependencies: - istanbul-lib-coverage: 3.2.0 - make-dir: 3.1.0 - supports-color: 7.2.0 - - istanbul-lib-source-maps@4.0.1: - dependencies: - debug: 4.3.4 - istanbul-lib-coverage: 3.2.0 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.1.5: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.0 - - jest-changed-files@29.4.3: - dependencies: - execa: 5.1.1 - p-limit: 3.1.0 - - jest-circus@29.4.3: - dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - chalk: 4.1.2 - co: 4.6.0 - dedent: 0.7.0 - is-generator-fn: 2.1.0 - jest-each: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 - p-limit: 3.1.0 - pretty-format: 29.4.3 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - supports-color - - jest-cli@29.4.3(@types/node@25.6.0): - dependencies: - '@jest/core': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.10 - import-local: 3.1.0 - jest-config: 29.4.3(@types/node@25.6.0) - jest-util: 29.4.3 - jest-validate: 29.4.3 - prompts: 2.4.2 - yargs: 17.7.1 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - - jest-config@29.4.3(@types/node@25.6.0): - dependencies: - '@babel/core': 7.21.0 - '@jest/test-sequencer': 29.4.3 - '@jest/types': 29.4.3 - babel-jest: 29.4.3(@babel/core@7.21.0) - chalk: 4.1.2 - ci-info: 3.8.0 - deepmerge: 4.2.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.4.3 - jest-environment-node: 29.4.3 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runner: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.4.3 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 25.6.0 - transitivePeerDependencies: - - supports-color - - jest-diff@29.4.3: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.4.3 - jest-get-type: 29.4.3 - pretty-format: 29.4.3 - - jest-docblock@29.4.3: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.4.3: - dependencies: - '@jest/types': 29.4.3 - chalk: 4.1.2 - jest-get-type: 29.4.3 - jest-util: 29.4.3 - pretty-format: 29.4.3 - - jest-environment-node@29.4.3: - dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - jest-mock: 29.4.3 - jest-util: 29.4.3 - - jest-get-type@29.4.3: {} - - jest-haste-map@29.4.3: - dependencies: - '@jest/types': 29.4.3 - '@types/graceful-fs': 4.1.6 - '@types/node': 25.6.0 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.10 - jest-regex-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 - micromatch: 4.0.5 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.2 - - jest-leak-detector@29.4.3: - dependencies: - jest-get-type: 29.4.3 - pretty-format: 29.4.3 - - jest-matcher-utils@29.4.3: - dependencies: - chalk: 4.1.2 - jest-diff: 29.4.3 - jest-get-type: 29.4.3 - pretty-format: 29.4.3 - - jest-message-util@29.4.3: - dependencies: - '@babel/code-frame': 7.18.6 - '@jest/types': 29.4.3 - '@types/stack-utils': 2.0.1 - chalk: 4.1.2 - graceful-fs: 4.2.10 - micromatch: 4.0.5 - pretty-format: 29.4.3 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.4.3: - dependencies: - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - jest-util: 29.4.3 - - jest-pnp-resolver@1.2.3(jest-resolve@29.4.3): - optionalDependencies: - jest-resolve: 29.4.3 - - jest-regex-util@29.4.3: {} - - jest-resolve-dependencies@29.4.3: - dependencies: - jest-regex-util: 29.4.3 - jest-snapshot: 29.4.3 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.4.3: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-pnp-resolver: 1.2.3(jest-resolve@29.4.3) - jest-util: 29.4.3 - jest-validate: 29.4.3 - resolve: 1.22.1 - resolve.exports: 2.0.0 - slash: 3.0.0 - - jest-runner@29.4.3: - dependencies: - '@jest/console': 29.4.3 - '@jest/environment': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.10 - jest-docblock: 29.4.3 - jest-environment-node: 29.4.3 - jest-haste-map: 29.4.3 - jest-leak-detector: 29.4.3 - jest-message-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runtime: 29.4.3 - jest-util: 29.4.3 - jest-watcher: 29.4.3 - jest-worker: 29.4.3 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - - jest-runtime@29.4.3: - dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/globals': 29.4.3 - '@jest/source-map': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - chalk: 4.1.2 - cjs-module-lexer: 1.2.2 - collect-v8-coverage: 1.0.1 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - - jest-snapshot@29.4.3: - dependencies: - '@babel/core': 7.21.0 - '@babel/generator': 7.21.1 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.0) - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.0) - '@babel/traverse': 7.21.2 - '@babel/types': 7.21.2 - '@jest/expect-utils': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@types/babel__traverse': 7.18.3 - '@types/prettier': 2.7.2 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.0) - chalk: 4.1.2 - expect: 29.4.3 - graceful-fs: 4.2.10 - jest-diff: 29.4.3 - jest-get-type: 29.4.3 - jest-haste-map: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 - natural-compare: 1.4.0 - pretty-format: 29.4.3 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - jest-util@29.4.3: - dependencies: - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - chalk: 4.1.2 - ci-info: 3.8.0 - graceful-fs: 4.2.10 - picomatch: 2.3.1 - - jest-validate@29.4.3: - dependencies: - '@jest/types': 29.4.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.4.3 - leven: 3.1.0 - pretty-format: 29.4.3 - - jest-watcher@29.4.3: - dependencies: - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 25.6.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.4.3 - string-length: 4.0.2 - - jest-worker@29.4.3: - dependencies: - '@types/node': 25.6.0 - jest-util: 29.4.3 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.4.3(@types/node@25.6.0): - dependencies: - '@jest/core': 29.4.3 - '@jest/types': 29.4.3 - import-local: 3.1.0 - jest-cli: 29.4.3(@types/node@25.6.0) - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - jsesc@2.5.2: {} - - json-parse-better-errors@1.0.2: {} - - json-parse-even-better-errors@2.3.1: {} - - json5@2.2.3: {} - - kleur@3.0.3: {} - - knockout@3.5.1: {} - - leven@3.1.0: {} - - lines-and-columns@1.2.4: {} - - load-json-file@4.0.0: - dependencies: - graceful-fs: 4.2.10 - parse-json: 4.0.0 - pify: 3.0.0 - strip-bom: 3.0.0 - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - lodash.memoize@4.1.2: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - - make-dir@3.1.0: - dependencies: - semver: 6.3.0 - - make-error@1.3.6: {} - - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - - memorystream@0.3.1: {} - - merge-stream@2.0.0: {} - - micromatch@4.0.5: - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - - mimic-fn@2.1.0: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - ms@2.1.2: {} - - natural-compare@1.4.0: {} - - nice-try@1.0.5: {} - - node-int64@0.4.0: {} - - node-releases@2.0.10: {} - - normalize-package-data@2.5.0: - dependencies: - hosted-git-info: 2.8.9 - resolve: 1.22.1 - semver: 5.7.1 - validate-npm-package-license: 3.0.4 - - normalize-path@3.0.0: {} - - npm-run-all@4.1.5: - dependencies: - ansi-styles: 3.2.1 - chalk: 2.4.2 - cross-spawn: 6.0.5 - memorystream: 0.3.1 - minimatch: 3.1.2 - pidtree: 0.3.1 - read-pkg: 3.0.0 - shell-quote: 1.7.3 - string.prototype.padend: 3.1.3 - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - object-inspect@1.11.0: {} - - object-keys@1.1.1: {} - - object.assign@4.1.2: - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.3 - has-symbols: 1.0.2 - object-keys: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-try@2.2.0: {} - - parse-json@4.0.0: - dependencies: - error-ex: 1.3.2 - json-parse-better-errors: 1.0.2 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.18.6 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@2.0.1: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-type@3.0.0: - dependencies: - pify: 3.0.0 - - picocolors@1.0.0: {} - - picomatch@2.3.1: {} - - picomatch@4.0.2: {} - - pidtree@0.3.1: {} - - pify@3.0.0: {} - - pirates@4.0.5: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - - pretty-format@29.4.3: - dependencies: - '@jest/schemas': 29.4.3 - ansi-styles: 5.2.0 - react-is: 18.2.0 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - react-dom@18.3.1(react@18.3.1): - dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - - react-is@18.2.0: {} - - react@18.3.1: - dependencies: - loose-envify: 1.4.0 - - read-pkg@3.0.0: - dependencies: - load-json-file: 4.0.0 - normalize-package-data: 2.5.0 - path-type: 3.0.0 - - require-directory@2.1.1: {} - - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - - resolve-from@5.0.0: {} - - resolve.exports@2.0.0: {} - - resolve@1.22.1: - dependencies: - is-core-module: 2.11.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@1.22.10: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - rollup-plugin-external-globals@0.13.0(rollup@4.46.2): - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) - estree-walker: 3.0.3 - is-reference: 3.0.3 - magic-string: 0.30.17 - rollup: 4.46.2 - - rollup-plugin-string-import@1.2.6(rollup@4.46.2): - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) - rollup: 4.46.2 - - rollup@4.46.2: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.2 - '@rollup/rollup-android-arm64': 4.46.2 - '@rollup/rollup-darwin-arm64': 4.46.2 - '@rollup/rollup-darwin-x64': 4.46.2 - '@rollup/rollup-freebsd-arm64': 4.46.2 - '@rollup/rollup-freebsd-x64': 4.46.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 - '@rollup/rollup-linux-arm-musleabihf': 4.46.2 - '@rollup/rollup-linux-arm64-gnu': 4.46.2 - '@rollup/rollup-linux-arm64-musl': 4.46.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 - '@rollup/rollup-linux-ppc64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-musl': 4.46.2 - '@rollup/rollup-linux-s390x-gnu': 4.46.2 - '@rollup/rollup-linux-x64-gnu': 4.46.2 - '@rollup/rollup-linux-x64-musl': 4.46.2 - '@rollup/rollup-win32-arm64-msvc': 4.46.2 - '@rollup/rollup-win32-ia32-msvc': 4.46.2 - '@rollup/rollup-win32-x64-msvc': 4.46.2 - fsevents: 2.3.3 - - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 - - semver@5.7.1: {} - - semver@6.3.0: {} - - semver@7.3.8: - dependencies: - lru-cache: 6.0.0 - - shebang-command@1.2.0: - dependencies: - shebang-regex: 1.0.0 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@1.0.0: {} - - shebang-regex@3.0.0: {} - - shell-quote@1.7.3: {} - - side-channel@1.0.4: - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.1.1 - object-inspect: 1.11.0 - - signal-exit@3.0.7: {} - - sisteransi@1.0.5: {} - - slash@3.0.0: {} - - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - spdx-correct@3.1.1: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.10 - - spdx-exceptions@2.3.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.10 - - spdx-license-ids@3.0.10: {} - - sprintf-js@1.0.3: {} - - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string.prototype.padend@3.1.3: - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.3 - es-abstract: 1.19.1 - - string.prototype.trimend@1.0.4: - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.3 - - string.prototype.trimstart@1.0.4: - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.3 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-bom@3.0.0: {} - - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-json-comments@3.1.1: {} - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - svg-tag-names@3.0.1: {} - - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - - tmpl@1.0.5: {} - - to-fast-properties@2.0.0: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - ts-jest@29.0.5(@babel/core@7.21.0)(@jest/types@29.4.3)(babel-jest@29.4.3(@babel/core@7.21.0))(jest@29.4.3(@types/node@25.6.0))(typescript@6.0.2): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.4.3(@types/node@25.6.0) - jest-util: 29.4.3 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.8 - typescript: 6.0.2 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.21.0 - '@jest/types': 29.4.3 - babel-jest: 29.4.3(@babel/core@7.21.0) - - tslib@2.8.1: {} - - type-detect@4.0.8: {} - - type-fest@0.21.3: {} - - typedbrainz@0.3.0: {} - - typescript@6.0.2: {} - - unbox-primitive@1.0.1: - dependencies: - function-bind: 1.1.1 - has-bigints: 1.0.1 - has-symbols: 1.0.2 - which-boxed-primitive: 1.0.2 - - undici-types@7.19.2: {} - - update-browserslist-db@1.0.10(browserslist@4.21.5): - dependencies: - browserslist: 4.21.5 - escalade: 3.1.1 - picocolors: 1.0.0 - - v8-to-istanbul@9.1.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.17 - '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.9.0 - - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.1.1 - spdx-expression-parse: 3.0.1 - - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - - weight-balanced-tree@0.16.0: {} - - which-boxed-primitive@1.0.2: - dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.6 - is-string: 1.0.7 - is-symbol: 1.0.4 - - which@1.3.1: - dependencies: - isexe: 2.0.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} - - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yallist@4.0.0: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.1: - dependencies: - cliui: 8.0.1 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} - - zod@3.24.4: {} diff --git a/rollup.config.ts b/rollup.config.ts deleted file mode 100644 index ddd5559..0000000 --- a/rollup.config.ts +++ /dev/null @@ -1,309 +0,0 @@ -/// -import fs from "node:fs" -import pluginTypescript from "@rollup/plugin-typescript" -import pluginNodeResolve from "@rollup/plugin-node-resolve" -import string from "rollup-plugin-string-import" -import type { RollupOptions } from "rollup" -import type { BannerType } from "./scripts/_common/banner-type" -import externalGlobals from "rollup-plugin-external-globals" -/// @ts-expect-error -import { table as externalGlobalsTableRaw } from "./external-globals.js" -import { readFile } from "node:fs/promises" -import { createHash } from "node:crypto" -import { join } from "node:path" -import { walk } from "estree-walker" -import { runInNewContext } from "node:vm" -import MagicString from "magic-string" -import pluginCommonjs from "@rollup/plugin-commonjs" - -const files = fs.readdirSync("./scripts").filter(x => !x.startsWith("_") && !x.startsWith(".")) -if (fs.existsSync("./scripts/_groups")) { - files.push(...fs.globSync("./scripts/_groups/*/*").map(s => s.slice("scripts/".length))) -} -console.log(files) - -const externalGlobalsTable: Record = externalGlobalsTableRaw -const umdTables = Object.entries(externalGlobalsTable) - .map(kv => { - return [kv[0], kv[1].var + ` /* ${[ - "__UMD_IMPORT", - kv[0] - ].join(":")} */`] as const - }) - -function evalDefineUserScript(code: string): BannerType { - return JSON.parse(runInNewContext(` - RegExp.prototype.toJSON = function() { - return this.toString(); - } - Function.prototype.toJSON = function() { - return this.toString(); - } - function defineUserScript(banner) { - return banner; - } - JSON.stringify(${code}) - `)) -} - -function convertBannerObjectToString(opts: BannerType): string { - if (process.env.NO_URLS !== "true") { - opts.homepageURL ??= "https://github.com/rinsuki/userscripts" - opts.supportURL ??= "https://github.com/rinsuki/userscripts/issues" - } - - const optsArray: (readonly [string, string])[] = Object.entries(opts).flatMap(([key, value]) => { - if (key === "includeContributionURL" && value) { - return [ - // Greasy Fork は最初の contributionURL しか表示しない - // ので、GitHub Sponsors だけを置く - // (どちらにせよ GitHub Sponsors の説明に FANBOX のリンクもあるし、GitHub Sponsors は手数料がタダなのでそちらを優先したい……) - // ["contributionURL", "https://rinsuki.fanbox.cc/"], - ["contributionURL", "https://github.com/sponsors/rinsuki"], - ] as const - } - if (key === "topInject") return [] as const - const normalizedKey = key - .replace(/[A-Z]/g, (m) => { - return "-" + m.toLowerCase() - }) - .replaceAll("-u-r-l", "URL") - if (Array.isArray(value)) { - return value.map(v => [normalizedKey, String(v)] as const) - } - if (value === false) return [] as [string, string][] - let stringValue = String(value) - if (value === true) stringValue = "" - if (typeof value === "object" && value != null) { - return Object.entries(value).map(kv => kv.join(" ")).map(x => [normalizedKey, x]) - } - return [[normalizedKey, stringValue]] as const - }) - const keysMax = Math.max(...optsArray.map(a => a[0].length)) - return [ - "// ==UserScript==", - ...optsArray.map(kv => { - const [key, value] = kv - return `// @${key.padEnd(keysMax)} ${value}`.replaceAll(/ +$/g, "") - }), - "// ==/UserScript==", - "", - ...(opts.topInject ? ["(" + opts.topInject.toString() + ")();", ""] : []), - "", - ].join("\n") -} - -const ourTsPlugin = (() => { - const basePlugin = pluginTypescript() - let isBuildStarted = false - - let timer: ReturnType | null = null - - return { - ...basePlugin, - buildStart(ro) { - if (timer != null) { - clearTimeout(timer) - timer = null - } - if (!isBuildStarted) { - if (!(basePlugin.buildStart instanceof Function)) throw new Error(`Expected buildStart to be a function`); - basePlugin.buildStart.call(this, ro) - isBuildStarted = true - } - }, - buildEnd(error) { - if (timer != null) clearTimeout(timer) - timer = setTimeout(() => { - if (!(basePlugin.buildEnd instanceof Function)) throw new Error(`Expected buildEnd to be a function`); - basePlugin.buildEnd.call(this, error) - isBuildStarted = false - }, 100) - } - } satisfies import("rollup").Plugin -})() - -const sharedPlugins = [ - ourTsPlugin, - pluginNodeResolve({ - browser: true, - }), - pluginCommonjs(), - externalGlobals(Object.fromEntries(umdTables)), - string({ - include: ["**/*.html", "**/*.css"], - }), -] - -console.log(sharedPlugins[0]) - -export default files.filter(a => (a.includes(".user.") || !a.includes("."))).map(file => { - const baseId = process.cwd() + "/node_modules/"; - - const fileAbs = join(process.cwd(), "scripts", file); - const stat = fs.statSync(fileAbs); - const entry = stat.isDirectory() ? join(fileAbs, "src", "index.tsx") : fileAbs; - - return { - input: entry, - output: [{ - name: file+".user", - file: "./dist/" + file.split("/").at(-1)!.split(".user.")[0] + ".user.js", - format: "iife", - }], - plugins: [ - ...sharedPlugins, - { - name: "raw-unicode", - // https://github.com/microsoft/TypeScript/issues/36174 - transform(code, id) { - const ms = new MagicString(code); - const ast = this.parse(code); - walk(ast, { - enter(node) { - // console.log(node) - if ( - node.type === "Literal" && - "start" in node && typeof node.start === "number" && - "end" in node && typeof node.end === "number" && - typeof node.value === "string" && - node.value != null && - node.raw != null && - node.raw.includes("\\u") - ) { - ms.overwrite(node.start, node.end, JSON.stringify(node.value)); - } - } - }) - return { - code: ms.toString(), - map: ms.generateMap({ hires: true }), - }; - } - }, - { - name: "region", - transform(code, id) { - if (!id.startsWith(baseId)) { - return code; - } - return "//#region node_modules/" + id.slice(baseId.length) + "\n" + code + "\n//#endregion" - } - }, - { - name: "banner-extractor", - generateBundle(options, bundle) { - const firstBundle = Object.values(bundle)[0]; - if (firstBundle.type !== "chunk") { - throw new Error("Expected first bundle to be a chunk"); - } - let code = firstBundle.code - if (!code.includes("defineUserScript")) { - return - } - const ast = this.parse(code); - walk(ast, { - enter(node) { - if ( - node.type === "CallExpression" && - node.callee.type === "Identifier" && - node.callee.name === "defineUserScript" && - "start" in node && typeof node.start === "number" && - "end" in node && typeof node.end === "number" - ) { - const defineCode = code.slice(node.start, node.end); - code = convertBannerObjectToString(evalDefineUserScript(defineCode)) + code.slice(0, node.start) + code.slice(code[node.end] === ";" ? node.end + 1 : node.end).trimStart(); - } - } - }) - firstBundle.code = code; - } - }, - { - name: "banner-fallback", - async generateBundle(options, bundle) { - const firstBundle = Object.values(bundle)[0]; - if (firstBundle.type !== "chunk") { - throw new Error("Expected first bundle to be a chunk"); - } - - const bannerJs = `./scripts/${file}/banner.js` - if (!fs.existsSync(bannerJs)) { - return - } - let ourBanner - const content = fs.readFileSync(bannerJs, { encoding: "utf-8" }) - if (content.startsWith("// ==UserScript==")) { - ourBanner = content - } else { - const mod = await import(`${process.cwd()}/scripts/${file}/banner.js?_=${Date.now()}`) - if (mod.default?.name == null) { - throw new Error(`Invalid banner in ${file}: Expected a default export with a 'name' property`) - } - const opts: BannerType = { - ...mod.default, - } - - ourBanner = convertBannerObjectToString(opts) - } - - const inlineBannerIndex = firstBundle.code.indexOf("// ==/UserScript==") - if (inlineBannerIndex === -1) { - firstBundle.code = ourBanner + firstBundle.code - return - } - const inlineBannerUntilEnd = firstBundle.code.slice(0, inlineBannerIndex) - const ourBannerIndex = ourBanner.indexOf("// ==/UserScript==") - const ourBannerUntilEnd = ourBanner.slice(0, ourBannerIndex) - if (inlineBannerUntilEnd === ourBannerUntilEnd) { - return - } - - // console.log(inlineBannerUntilEnd, ourBannerUntilEnd) - - throw new Error(`Inline banner in ${file} does not match the banner in banner.js.`) - } - }, - { - name: "add-require", - async generateBundle(options, bundle) { - const firstBundle = Object.values(bundle)[0]; - if (firstBundle.type !== "chunk") { - throw new Error("Expected first bundle to be a chunk"); - } - const requires = new Set() - let code = firstBundle.code - code = code.replaceAll(/ \/\* __UMD_IMPORT:(.+?) \*\//g, (match, name) => { - requires.add(name) - return "" - }) - // console.log(requires) - const headerEnd = code.indexOf("// ==/UserScript==") - let header = code.slice(0, headerEnd) - const pad = /\/\/ @([a-zA-Z]+ +)/.exec(header)![1].length - header += [ - ...await Promise.all(Array.from(requires).map(async name => { - const v = externalGlobalsTable[name] - let url: string - if ("path" in v) { - const path = v.path - const pj = JSON.parse(await readFile(`./node_modules/${name}/package.json`, "utf-8")) - const file = await readFile(`./node_modules/${name}/${path}`) - const hash = ["sha256", "sha512"].map(f => f + "-" + createHash(f).update(file).digest("base64")).join(",") - url = `https://cdn.jsdelivr.net/npm/${name}@${pj.version}/${path}#${hash}` - } else { - url = v.url - } - return `// ${"@require".padEnd(pad)} ${url}` - })), - ].join("\n") - code = header.trimEnd() + "\n" + code.slice(headerEnd) - firstBundle.code = code - } - } - ], - watch: { - clearScreen: false, - }, - } satisfies RollupOptions -}) diff --git a/scripts/_common/banner-type.ts b/scripts/_common/banner-type.ts deleted file mode 100644 index 34de74e..0000000 --- a/scripts/_common/banner-type.ts +++ /dev/null @@ -1,47 +0,0 @@ -export const licenses = ["MIT"] as const - -export type BannerType = { - name: string, - namespace: string, - description?: string, - version?: string, - icon?: string, - runAt?: "document-start" | "document-body" | "document-end" | "document-idle", - noframes?: true, - unwrap?: true, - - match?: string | string[], - include?: RegExp[], - exclude?: (string|RegExp)[], - excludeMatch?: string[], - - grant: ( - | "GM_getValue" - | "GM.getValue" - | "GM_setValue" - | "GM.registerMenuCommand" - | "GM.xmlHttpRequest" - | "GM_addStyle" - | "GM.getResourceUrl" - | "GM_getResourceURL" - | "GM.openInTab" - | "unsafeWindow" - | "window.close" - )[] | "none", - connect?: string[], - - require?: string[], - resource?: Record, - - license?: typeof licenses[number], - includeContributionURL: boolean, - author?: string, - homepageURL?: string, - supportURL?: string, - - topInject?: (() => void), -} - -declare global { - function defineUserScript(banner: BannerType): void; -} diff --git a/scripts/_common/fetch-ok-or-throw.ts b/scripts/_common/fetch-ok-or-throw.ts deleted file mode 100644 index a7be7d4..0000000 --- a/scripts/_common/fetch-ok-or-throw.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class HTTPError extends Error { - constructor(public response: Response, public text: string) { - super(`HTTP-${response.status}: ${text}`) - } -} - -export async function fetchOkOrThrow(...args: Parameters): Promise { - const res = await fetch(...args) - if (!res.ok) { - throw new HTTPError(res, await res.text()) - } - return res -} \ No newline at end of file diff --git a/scripts/_common/get-react-internals.ts b/scripts/_common/get-react-internals.ts deleted file mode 100644 index 75f5a9b..0000000 --- a/scripts/_common/get-react-internals.ts +++ /dev/null @@ -1,30 +0,0 @@ -type ReactInternalContainer = { - memoizedState: { - element: { - props: any - } - } -} - -export function getReactContainer(elem: Element): ReactInternalContainer | undefined { - const properties = Object.getOwnPropertyNames(elem) - const name = properties.find(x => x.startsWith("__reactContainer$")) - if (name != null) return (elem as any)[name] as ReactInternalContainer -} - -type ReactInternalFiber = { - memoizedProps: {} - return?: ReactInternalFiber -} - -export function getReactFiber(elem: Element): ReactInternalFiber | undefined { - const properties = Object.getOwnPropertyNames(elem) - const name = properties.find(x => x.startsWith("__reactFiber$")) - if (name != null) return (elem as any)[name] as ReactInternalFiber -} - -export function getReactProps(elem: Element): any { - const properties = Object.getOwnPropertyNames(elem) - const name = properties.find(x => x.startsWith("__reactProps$")) - if (name != null) return (elem as any)[name] as any -} diff --git a/scripts/_common/hook-function-by-proxy.ts b/scripts/_common/hook-function-by-proxy.ts deleted file mode 100644 index 7382999..0000000 --- a/scripts/_common/hook-function-by-proxy.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function hookFunctionReturnValueByProxy any>>( - obj: O, - funcName: K, - hook: (value: ReturnType, thisObj: O, args: Parameters) => ReturnType -) { - obj[funcName] = new Proxy(obj[funcName], { - apply(target, thisArg, argArray) { - const res = Reflect.apply(target, thisArg, argArray) - return hook.call(thisArg, res, thisArg as O, argArray as Parameters) - } - }) as O[K] -} diff --git a/scripts/_common/input-to-react-managed-element.ts b/scripts/_common/input-to-react-managed-element.ts deleted file mode 100644 index 73729a2..0000000 --- a/scripts/_common/input-to-react-managed-element.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function inputToReactManagedElement(input: T, value: string) { - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(input), "value")?.set?.call(input, value) - input.dispatchEvent(new InputEvent("input", { bubbles: true } )) -} diff --git a/scripts/_common/input-to-textfield.ts b/scripts/_common/input-to-textfield.ts deleted file mode 100644 index 675751c..0000000 --- a/scripts/_common/input-to-textfield.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function inputToTextfield(input: HTMLInputElement, text: string, replace: true) { - input.value = "" - input.dispatchEvent(new InputEvent("input", { - data: "null", - inputType: "deleteContentBackward", - })) - if (text.length) { - input.value = text - input.dispatchEvent(new InputEvent("input", { - data: text, - inputType: "insertFromPaste", - })) - } -} \ No newline at end of file diff --git a/scripts/_common/mb/artist-credit-clipboard.ts b/scripts/_common/mb/artist-credit-clipboard.ts deleted file mode 100644 index 3f68f62..0000000 --- a/scripts/_common/mb/artist-credit-clipboard.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { IncompleteArtistCreditT } from "typedbrainz/types" -const LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT = "copiedArtistCredit"; - -export function getArtistCreditClipboard(): IncompleteArtistCreditT | undefined { - const str = localStorage.getItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT) - if (str == null) return undefined - try { - return JSON.parse(str) - } catch(e) { - console.warn("Failed to parse artist credit clipboard data", e) - return undefined - } -} - -export function setArtistCreditClipboard(artistCredit: IncompleteArtistCreditT): void { - localStorage.setItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT, JSON.stringify(artistCredit)) -} \ No newline at end of file diff --git a/scripts/_common/mb/external-links-editor.ts b/scripts/_common/mb/external-links-editor.ts deleted file mode 100644 index 2c89b06..0000000 --- a/scripts/_common/mb/external-links-editor.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { LinksEditorActionT } from "typedbrainz/types" -import { getReactFiber } from "../get-react-internals" -import { MBWithReleaseEditor } from "./release-editor" - -export function getStandaloneExternalLinksEditorDispatch(): undefined | ((action: LinksEditorActionT) => void) { - const editorDom = document.getElementById("external-links-editor") - if (editorDom == null) return - const reactFiber = getReactFiber(editorDom) - if (reactFiber == null) return - const props = reactFiber.return?.memoizedProps - if (props == null) return - if ("dispatch" in props && props.dispatch instanceof Function) return (props.dispatch satisfies Function) as any -} - -export async function addReleaseUrlRelationship(MB: MBWithReleaseEditor, url: string, linkTypeID: number) { - const externalLinksEditorDispatch = getStandaloneExternalLinksEditorDispatch() - if (externalLinksEditorDispatch == null) throw new TypeError("Failed to get external links editor dispatch") - for (let tried = 0; tried < 10; tried++) { - await new Promise(r => requestIdleCallback(r)); - link_loop: for (const link of MB.tree.iterate(MB.releaseEditor.externalLinksData() ?? MB.tree.empty)) { - if (link.url === "") { // no url - externalLinksEditorDispatch({ - type: "handle-url-change", - rawUrl: url, - link, - }) - break - } - if (link.rawUrl !== url) continue - if (!link.isSubmitted && link.duplicateOf == null) { - externalLinksEditorDispatch({ - type: "submit-link", - link, - }) - break - } - for (const rel of link.relationships) { - if (rel.linkTypeID === linkTypeID) { - // DONE - if (rel.error?.blockMerge && link.duplicateOf != null) { - // 完全に duplicated なものを作ってしまった (がURL正規化の都合で検知できなかった) ものの後始末 - externalLinksEditorDispatch({ - type: "toggle-remove-link", - link, - }) - return - } - return link - } - if (rel.linkTypeID == null) { - externalLinksEditorDispatch({ - type: "set-type", - link, - relationship: rel, - linkTypeID, - }) - break link_loop - } - } - if (link.relationships.length) { - // 一致するやつがなかった - externalLinksEditorDispatch({ - type: "add-relationship", - link, - }) - break - } else { - console.log("awaiting", link) - // ないならちょい待ち - } - } - } -} diff --git a/scripts/_common/mb/find-entities-from-url.ts b/scripts/_common/mb/find-entities-from-url.ts deleted file mode 100644 index 1167e77..0000000 --- a/scripts/_common/mb/find-entities-from-url.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { LinkedEntitiesT } from "typedbrainz/types" -import { z } from "zod" - -const entityTypes = { - label: { - inc: "label-rels", - scheme: z.object({ - label: z.object({ - id: z.string(), - }) - }).transform(d => ({target: d.label.id})), - } -} as const - -export async function findEntitiesFromUrl(type: K, urls: string[]): Promise> { - if (urls.length === 0) return new Map() - const entityType = entityTypes[type] - - const apiUrl = new URL("/ws/2/url?fmt=json&inc=" + entityType.inc, location.href) - for (const url of urls) { - apiUrl.searchParams.append("resource", url) - } - const res = await fetch(apiUrl.href) - if (res.status === 404) return new Map() - const data = await res.json() - const parsed = z.array(z.object({ - resource: z.string(), - relations: z.array(z.object({}).and(entityType.scheme)), - })).parse("urls" in data ? data.urls : [data]) - - const ids = Array.from(new Set( - parsed - .flatMap(p => p.relations) - .map(r => r.target) - )) - - const jsApi: { - results: Record - } = await fetch(`/ws/js/entities/${type}/${ids.join("+")}`).then(r => r.json()) - - return new Map( - urls.map(url => { - const p = parsed.filter(p => p.resource === url) - const entities = p - .flatMap(p => p.relations) - .map(r => jsApi.results[r.target]) - return [url, entities] - }) - ) -} diff --git a/scripts/_common/mb/get-release-relationship-editor-or-throw.ts b/scripts/_common/mb/get-release-relationship-editor-or-throw.ts deleted file mode 100644 index d753e99..0000000 --- a/scripts/_common/mb/get-release-relationship-editor-or-throw.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { isReleaseRelationshipEditor } from "typedbrainz"; - -export function getReleaseRelationshipEditorOrThrow() { - const relEditor = window.MB?.relationshipEditor; - if (relEditor == null || !isReleaseRelationshipEditor(relEditor)) { - throw new Error("window.MB.relationshipEditor is not a ReleaseRelationshipEditor"); - } - return relEditor; -} - -export type CompleteReleaseRelationshipEditor = ReturnType; \ No newline at end of file diff --git a/scripts/_common/mb/gid.ts b/scripts/_common/mb/gid.ts deleted file mode 100644 index a3a6373..0000000 --- a/scripts/_common/mb/gid.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const LINK_TYPE_GID_RECORDING_FREE_STREAMING = "7e41ef12-a124-4324-afdb-fdbae687a89c" -export const LINK_TYPE_GID_RECORDING_PAID_STREAMING = "b5f3058a-666c-406f-aafb-f9249fc7b122" -export const LINK_TYPE_GIDS_RECORDING_STREAMING = [ - LINK_TYPE_GID_RECORDING_FREE_STREAMING, - LINK_TYPE_GID_RECORDING_PAID_STREAMING, -] -export const LINK_TYPE_GID_RECORDING_PAID_DOWNLOAD = "92777657-504c-4acb-bd33-51a201bd57e1" - -export const LINK_TYPE_GID_RELEASE_FREE_STREAMING = "08445ccf-7b99-4438-9f9a-fb9ac18099ee" -export const LINK_TYPE_GID_RELEASE_PAID_STREAMING = "320adf26-96fa-4183-9045-1f5f32f833cb" -export const LINK_TYPE_GID_RELEASE_PAID_DOWNLOAD = "98e08c20-8402-4163-8970-53504bb6a1e4" diff --git a/scripts/_common/mb/medium-format-id.ts b/scripts/_common/mb/medium-format-id.ts deleted file mode 100644 index 0d58efe..0000000 --- a/scripts/_common/mb/medium-format-id.ts +++ /dev/null @@ -1 +0,0 @@ -export const MEDIUM_FORMAT_DIGITAL_RELEASE = "12" \ No newline at end of file diff --git a/scripts/_common/mb/observe-wait-until-true.ts b/scripts/_common/mb/observe-wait-until-true.ts deleted file mode 100644 index 0cf8e07..0000000 --- a/scripts/_common/mb/observe-wait-until-true.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Subscribable } from "knockout"; - -export async function observeWaitUntilNotNull( - value: Subscribable, - transform: (value: I) => O | null | undefined, -): Promise { - { - const firstTry = transform(value()) - if (firstTry != null) return firstTry - } - return new Promise(resolve => { - const observer = value.subscribe(value => { - const transformed = transform(value) - if (transformed != null) { - observer.dispose() - resolve(transformed) - } - }) - }) -} diff --git a/scripts/_common/mb/release-editor.ts b/scripts/_common/mb/release-editor.ts deleted file mode 100644 index 8102c9d..0000000 --- a/scripts/_common/mb/release-editor.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Computed, Observable, ObservableArray } from "knockout" -import { ArtistCreditNameT, LabelT, LinkStateT, MediumT, TrackT } from "typedbrainz/types" -import { ImmutableTree } from "weight-balanced-tree" - -export declare class EditorMedium { - release: EditorRelease - formatID: Observable - - position: Observable - name: Observable - tracks: ObservableArray - - pushTrack(data: Partial): void - - constructor(medium: Partial, release: EditorRelease) -} - -export declare class EditorTrack { - name: Observable - medium: EditorMedium - /// milliseconds - length: Observable - artistCredit: Observable<{ - names: ArtistCreditNameT[] - }> - recording: Observable<{ - artistCredit: { - names: ArtistCreditNameT[] - } - }> -} - -// https://github.com/metabrainz/musicbrainz-server/blob/8ff871e17729fec9d032cd54b9a563660b728e63/root/static/scripts/release-editor/fields.js#L1054 -declare class EditorBarcode { - value: Computed -} - -declare class EditorReleaseLabel { - label: Observable<{ name: string } | LabelT> - catalogNumber: Observable -} - -// https://github.com/metabrainz/musicbrainz-server/blob/8737b40d9619917a0635f1dd70215c8593d18a30/root/static/scripts/release-editor/fields.js#L969 -declare class EditorReleaseEvent { - date: { - year: Observable, - month: Observable, - day: Observable, - } - countryID: Observable - hasInvalidDate: Computed -} - -// https://github.com/metabrainz/musicbrainz-server/blob/8ff871e17729fec9d032cd54b9a563660b728e63/root/static/scripts/release-editor/fields.js#L1110 -export declare class EditorRelease { - name: Observable - statusID: Observable - languageID: Observable - scriptID: Observable - packagingID: Observable - labels: ObservableArray - events: ObservableArray - barcode: EditorBarcode - artistCredit: Observable<{ - names: ArtistCreditNameT[] - }> - releaseGroup: Observable<{ - artistCredit?: { - names: ArtistCreditNameT[] - } - }> - mediums: ObservableArray - - allTracks: () => Iterable -} - -export interface MBReleaseEditor { - rootField: { - release: Observable - editNote: Observable - } - externalLinksData: Observable> - activeTabID: Observable<(string & {}) | "#information" | "#tracklist" | "#recordings" | "#edit-note"> -} - -export type MBWithReleaseEditor = typeof window.MB & { - releaseEditor: MBReleaseEditor - tree: NonNullable["tree"]> -} - -export function isMBWithReleaseEditor(mb: typeof window.MB): mb is MBWithReleaseEditor { - return (mb as MBWithReleaseEditor).releaseEditor !== undefined -} diff --git a/scripts/_common/mb/resolve-recordings-relationships.ts b/scripts/_common/mb/resolve-recordings-relationships.ts deleted file mode 100644 index 43f4968..0000000 --- a/scripts/_common/mb/resolve-recordings-relationships.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { RecordingT } from "typedbrainz/types"; - -export async function resolveRecordingsRelationships(recordings: RecordingT[], parent: HTMLElement) { - // とりあえず愚直な実装 (TODO: appearsOnを使ってmediumで引くとかもできるはず) - const progressBar = document.createElement("progress") - progressBar.max = recordings.length - progressBar.value = 0 - parent.appendChild(progressBar) - recordings.sort((a, b) => { - if (a.relationships != null && b.relationships == null) return -1 - if (a.relationships == null && b.relationships != null) return 1 - return 0 - }) - for (const recording of recordings) { - if (recording.relationships != null) { - progressBar.value += 1 - continue - } - const res = await fetch("/ws/js/entity/" + recording.gid + "?inc=rels") - if (!res.ok) { - alert(`Failed to fetch recording ${recording.gid}: ${res.status} ${await res.text()}`) - } - const freshRecording = await res.json() as RecordingT - Object.assign(recording, { - relationships: freshRecording.relationships, - }) - progressBar.value += 1 - } - progressBar.remove() -} \ No newline at end of file diff --git a/scripts/_common/remove-first-zeroes.ts b/scripts/_common/remove-first-zeroes.ts deleted file mode 100644 index 774096b..0000000 --- a/scripts/_common/remove-first-zeroes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function removeFirstZeroes(str: string): string { - return str.replace(/^0+/, "") -} diff --git a/scripts/_common/wait-dom.ts b/scripts/_common/wait-dom.ts deleted file mode 100644 index 3e9f086..0000000 --- a/scripts/_common/wait-dom.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function waitDOMByObserve(root: Element, check: () => T | undefined | null, options: { subtree: boolean }): Promise { - const firstRes = check() - if (firstRes != null) return Promise.resolve(firstRes) - return new Promise(resolve => { - const observer = new MutationObserver(() => { - const res = check() - if (res != null) { - observer.disconnect() - resolve(res) - } - }) - observer.observe(root, { - childList: true, - subtree: options.subtree - }) - }) -} \ No newline at end of file diff --git a/scripts/_common/wait-local-storage.ts b/scripts/_common/wait-local-storage.ts deleted file mode 100644 index eaeb9cb..0000000 --- a/scripts/_common/wait-local-storage.ts +++ /dev/null @@ -1,26 +0,0 @@ -export function waitLocalStorage(key: string, check: (value: string | null) => boolean = (v) => v != null): Promise { - if (check(localStorage.getItem(key))) { - return Promise.resolve() - } - return new Promise((resolve) => { - const callback = (e: StorageEvent) => { - if (e.key !== key) return - if (!check(e.newValue)) return - window.removeEventListener("storage", callback) - resolve() - } - window.addEventListener("storage", callback) - }) -} - -export function waitNextLocalStorage(key: string, cb: () => void): Promise { - return new Promise((resolve) => { - const callback = (e: StorageEvent) => { - if (e.key !== key) return - window.removeEventListener("storage", callback) - resolve(e.newValue) - } - window.addEventListener("storage", callback) - cb() - }) -} \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-artist-credit-splitter-by-ai/src/index.tsx b/scripts/_groups/musicbrainz/mb-artist-credit-splitter-by-ai/src/index.tsx deleted file mode 100644 index b9d118f..0000000 --- a/scripts/_groups/musicbrainz/mb-artist-credit-splitter-by-ai/src/index.tsx +++ /dev/null @@ -1,185 +0,0 @@ -defineUserScript({ - name: "MB: Artist Credit Splitter, but Powered by AI", - namespace: "https://rinsuki.net/", - version: "1.1.0", - description: "OpenRouter でいい感じに MusicBrainz のアーティストクレジットを分割します (失敗することもあります)", - author: "rinsuki", - match: "https://*.musicbrainz.org/*", - grant: [ - "GM_getValue", - "GM_setValue", - "GM.registerMenuCommand", - "GM.xmlHttpRequest", - ], - includeContributionURL: true, -}) - -const SYSTEM_PROMPT = [ - "あなたはアーティストクレジットを分割するプロフェッショナルです。", - "あなたは与えられた文字列を分割し、適切なオブジェクトの配列に変換する必要があります。", - "それぞれのオブジェクトは、`name`と`joinPhrase`の2つのフィールドを持つ必要があります。", - "`name`フィールドにはアーティストの名前を、`joinPhrase`フィールドには次の名前と結合するために使用されるフレーズを含める必要があります。", - "ルール:", - "* 出力は、入力のすべての文字を、そのままの文字で、削除せずに含める必要があります。句読点や括弧も全て含めます (だいたいの場合はそれらは`joinPhrase`に含めることになるでしょう)。文字種を変換してもいけません (例えば `、` を `,` に変換することはできません)。出力が入力と一致しない場合、ユーザーにエラーが表示されます。", - "* 出力は、1つのオブジェクトに付き1人のアーティスト(個人またはグループ)を含める必要があります。つまり、CVクレジットを1つのアーティストオブジェクトに入力することはできません。たとえば、`キャラ (CV. 人物名)`を`name`に設定することは許可されていません。", - "* `joinPharse`に人物名を含めることはできません。CVクレジットなどが含まれる場合は、人物名の部分でオブジェクトを分割し、次のオブジェクトの`name`に代入してください。", - "* 出力はJSON形式をそのまま出力する必要があります (Markdownのコードブロックなどは不要です)。", -].join("\n") - -const exampleInputs = [ - { role: "user", content: "せるふとぷりん(稲垣好 / 市ノ瀬加那)" }, - { role: "assistant", content: JSON.stringify([{"name": "せるふ", "joinPhrase": "と"}, {"name": "ぷりん", "joinPhrase": "("}, {"name": "稲垣好", "joinPhrase": " / "}, {"name": "市ノ瀬加那", "joinPhrase": ")"}])}, - { role: "user", content: "Triad Primus [渋谷凛(CV:福原綾香)×神谷奈緒(CV:松井恵理子)×北条加蓮(CV:渕上舞)]" }, - { role: "assistant", content: JSON.stringify([ - {"name":"Triad Primus","joinPhrase":" ["}, - {"name":"渋谷凛","joinPhrase":"(CV:"}, - {"name":"福原綾香","joinPhrase":")×"}, - {"name":"神谷奈緒","joinPhrase":"(CV:"}, - {"name":"松井恵理子","joinPhrase":")×"}, - {"name":"北条加蓮","joinPhrase":"(CV:"}, - {"name":"渕上舞","joinPhrase":")]"}, - ]) }, -] - -type MBCopiedArtistCredit = { - names: { - name: string, - joinPhrase: string - }[] -} - -function copiedArtistCreditToPlainString(artistCredit: MBCopiedArtistCredit["names"]): string { - return artistCredit.map(credit => credit.name + credit.joinPhrase).join("") -} - -GM.registerMenuCommand("Split clipboard content (OpenRouter)", () => { - const inputString = localStorage.getItem("copiedArtistCredit") - if (inputString == null) { - alert("No copied artist credit found.") - return - } - const inputJSON = JSON.parse(inputString) as MBCopiedArtistCredit - const input = copiedArtistCreditToPlainString(inputJSON.names) - - let apiKey = GM_getValue("openrouter_api_key") - if (apiKey == null) { - apiKey = prompt("Please input your OpenRouter API key") ?? undefined - if (apiKey == null) { - return - } - GM_setValue("openrouter_api_key", apiKey) - } - - const div = document.createElement("div") - div.style.position = "fixed" - div.style.top = "0" - div.style.left = "0" - div.style.width = "100%" - div.style.height = "100%" - div.style.backgroundColor = "rgba(0, 0, 0, 0.5)" - div.style.zIndex = "9999" - div.style.display = "flex" - div.style.alignItems = "center" - div.style.justifyContent = "center" - div.style.color = "white" - div.style.fontSize = "20px" - div.textContent = "Asking to the LLM..." - document.body.appendChild(div) - - fetch("https://openrouter.ai/api/v1/chat/completions", { - method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: prompt("model", "google/gemini-2.0-flash-exp:free"), - temperature: 0, - messages: [ - { role: "system", content: SYSTEM_PROMPT }, - ...exampleInputs, - { role: "user", content: input }, - ], - }) - }).then(r => r.json()).then(r => { - div.remove() - console.log(r) - const generated = JSON.parse(r.choices[0].message.content) as MBCopiedArtistCredit["names"] - const output = copiedArtistCreditToPlainString(generated) - if (output !== input) { - alert("LLM's output does not match with input. The LLM moment...\n\n" + input + "\n" + output) - return - } - if (confirm("Done! ... according to the LLM (" + r.model + ").\n\n" + generated.map(a => JSON.stringify(a)).join("\n"))) { - localStorage.setItem("copiedArtistCredit", JSON.stringify({ - names: generated, - })) - } - }) -}) - -GM.registerMenuCommand("Split clipboard content (OpenAI)", () => { - const inputString = localStorage.getItem("copiedArtistCredit") - if (inputString == null) { - alert("No copied artist credit found.") - return - } - const inputJSON = JSON.parse(inputString) as MBCopiedArtistCredit - const input = copiedArtistCreditToPlainString(inputJSON.names) - - let apiKey = GM_getValue("openai_api_key") - if (apiKey == null) { - apiKey = prompt("Please input your OpenAI API key") ?? undefined - if (apiKey == null) { - return - } - GM_setValue("openai_api_key", apiKey) - } - - const div = document.createElement("div") - div.style.position = "fixed" - div.style.top = "0" - div.style.left = "0" - div.style.width = "100%" - div.style.height = "100%" - div.style.backgroundColor = "rgba(0, 0, 0, 0.5)" - div.style.zIndex = "9999" - div.style.display = "flex" - div.style.alignItems = "center" - div.style.justifyContent = "center" - div.style.color = "white" - div.style.fontSize = "20px" - div.textContent = "Asking to the LLM..." - document.body.appendChild(div) - - fetch("https://api.openai.com/v1/chat/completions", { - method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: prompt("model", "gpt-4.1-mini"), - temperature: 0, - messages: [ - { role: "system", content: SYSTEM_PROMPT }, - ...exampleInputs, - { role: "user", content: input }, - ], - }) - }).then(r => r.json()).then(r => { - div.remove() - console.log(r) - const generated = JSON.parse(r.choices[0].message.content) as MBCopiedArtistCredit["names"] - const output = copiedArtistCreditToPlainString(generated) - if (output !== input) { - alert("LLM's output does not match with input. The LLM moment...\n\n" + input + "\n" + output) - return - } - if (confirm("Done! ... according to the LLM (" + r.model + ").\n\n" + generated.map(a => JSON.stringify(a)).join("\n"))) { - localStorage.setItem("copiedArtistCredit", JSON.stringify({ - names: generated, - })) - } - }) -}) diff --git a/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/index.tsx b/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/index.tsx deleted file mode 100644 index ef74013..0000000 --- a/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { getReactProps } from "#common/get-react-internals"; -import { getArtistCreditClipboard, setArtistCreditClipboard } from "#common/mb/artist-credit-clipboard"; -import { waitDOMByObserve } from "#common/wait-dom"; -import { splitCredit } from "./splitter"; - -defineUserScript({ - name: "MB: Artist Credit Splitter", - namespace: "https://rinsuki.net/", - version: "1.0.2", - description: "いい感じに MusicBrainz のアーティストクレジットを分割します (失敗することもあります)", - author: "rinsuki", - match: "https://*.musicbrainz.org/*", - grant: "none", - includeContributionURL: true, -}); - -async function whenBubbleHappens(bubble: HTMLElement) { - const buttons = await waitDOMByObserve(bubble, () => bubble.querySelector(".buttons"), { subtree: false }); - const button = document.createElement("button") - button.type = "button" - button.style.float = "left" - button.textContent = "USERJS: Split Automatically" - button.addEventListener("click", async () => { - const props = getReactProps(bubble) as { - children: [{ - props: { - children: { - props: { - dispatch: (action: { type: "copy" | "paste"}) => void; - } - } - } - }] - } - console.log(props) - if (props == null) return alert("Failed to get React container") - const tbody = bubble.querySelector("tbody") - if (tbody == null) return alert("Failed to get tbody") - const dispatch = props.children[0].props.children.props.dispatch - dispatch({ type: "copy" }) - await new Promise(resolve => requestAnimationFrame(resolve)) - const currentCredit = getArtistCreditClipboard()?.names.map(name => name.name + (name.joinPhrase ?? "")).join("") ?? "" - const splittedCredits = splitCredit(currentCredit) - if (!confirm("次のように指定します。よろしいですか?\n\n" + JSON.stringify(splittedCredits, null, 4))) return - let i = 0 - // localStorage.removeItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT) - // let p = waitLocalStorage(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT) - // props.copyArtistCredit() - // await p - // const stubArtistCredit = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT)!) - setArtistCreditClipboard({names: splittedCredits.map(([name, joinPhrase], i) => { - return { - joinPhrase, - name, - artist: null, - // artist: { - // entityType: "artist", - // uniqueID: stubArtistCredit[i].artist.uniqueID, - // name, - // } - } - })}) - dispatch({type: "paste" }) - alert("finish!") - }) - buttons.appendChild(button) -} - -const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (!(node instanceof HTMLElement)) continue - if (node.dataset.floatingUiPortal == null) continue - const bubble = node.querySelector("#artist-credit-bubble") - if (bubble != null && bubble instanceof HTMLElement) whenBubbleHappens(bubble) - } - } -}) - -observer.observe(document.body, { childList: true, subtree: false }) diff --git a/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/splitter.test.ts b/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/splitter.test.ts deleted file mode 100644 index ae8eb8b..0000000 --- a/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/splitter.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/// -import { splitCredit } from "./splitter"; - -describe("splitter", () => { - it("should split artist credit", () => { - expect(splitCredit("渋谷凛(CV:福原綾香)")).toMatchInlineSnapshot(` -[ - [ - "渋谷凛", - "(CV:", - ], - [ - "福原綾香", - ")", - ], -] -`) - expect(splitCredit("砂塚あきら(CV:富田美憂)、早坂美玲(CV:朝井彩加)、堀裕子(CV:鈴木絵理)、多田李衣菜(CV:青木瑠璃子)、二宮飛鳥(CV:青木志貴)")).toMatchInlineSnapshot(` -[ - [ - "砂塚あきら", - "(CV:", - ], - [ - "富田美憂", - ")、", - ], - [ - "早坂美玲", - "(CV:", - ], - [ - "朝井彩加", - ")、", - ], - [ - "堀裕子", - "(CV:", - ], - [ - "鈴木絵理", - ")、", - ], - [ - "多田李衣菜", - "(CV:", - ], - [ - "青木瑠璃子", - ")、", - ], - [ - "二宮飛鳥", - "(CV:", - ], - [ - "青木志貴", - ")", - ], -] -`) - expect(splitCredit("みらい(cv.林鼓子)")).toMatchInlineSnapshot(` -[ - [ - "みらい", - "(cv.", - ], - [ - "林鼓子", - ")", - ], -] -`); - }); - expect(splitCredit("双海亜美(CV.下田麻美), 秋月律子(CV.若林直美), 星井美希(CV.長谷川明子), 高槻やよい(CV.仁後真耶子), 双海真美(CV.下田麻美) ")).toMatchInlineSnapshot(` -[ - [ - "双海亜美", - "(CV.", - ], - [ - "下田麻美", - "), ", - ], - [ - "秋月律子", - "(CV.", - ], - [ - "若林直美", - "), ", - ], - [ - "星井美希", - "(CV.", - ], - [ - "長谷川明子", - "), ", - ], - [ - "高槻やよい", - "(CV.", - ], - [ - "仁後真耶子", - "), ", - ], - [ - "双海真美", - "(CV.", - ], - [ - "下田麻美", - ") ", - ], -] -`) - expect(splitCredit("RISE[葉山陽和(CV.野口瑠璃子)/小鷹咲希(CV.岡咲美保)/前原純華(CV.優木かな)/橘 雪乃(CV.福原綾香)/小日向理瀬(CV.小澤亜李)]")).toMatchInlineSnapshot(` -[ - [ - "RISE", - "[", - ], - [ - "葉山陽和", - "(CV.", - ], - [ - "野口瑠璃子", - ")/", - ], - [ - "小鷹咲希", - "(CV.", - ], - [ - "岡咲美保", - ")/", - ], - [ - "前原純華", - "(CV.", - ], - [ - "優木かな", - ")/", - ], - [ - "橘 雪乃", - "(CV.", - ], - [ - "福原綾香", - ")/", - ], - [ - "小日向理瀬", - "(CV.", - ], - [ - "小澤亜李", - ")]", - ], -] -`); - expect(splitCredit("May-Bee[桜井羽月(CV. 阿部里果)、御社 智(CV.大西沙織)、末宗祐里子(CV. 大地 葉)、本田千尋(CV.湯浅かえで)、ティーナ・メルキース(CV.森山由梨佳)]")).toMatchInlineSnapshot(` -[ - [ - "May-Bee", - "[", - ], - [ - "桜井羽月", - "(CV. ", - ], - [ - "阿部里果", - ")、", - ], - [ - "御社 智", - "(CV.", - ], - [ - "大西沙織", - ")、", - ], - [ - "末宗祐里子", - "(CV. ", - ], - [ - "大地 葉", - ")、", - ], - [ - "本田千尋", - "(CV.", - ], - [ - "湯浅かえで", - ")、", - ], - [ - "ティーナ・メルキース", - "(CV.", - ], - [ - "森山由梨佳", - ")]", - ], -] -`); -}) \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/splitter.ts b/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/splitter.ts deleted file mode 100644 index 5216f8d..0000000 --- a/scripts/_groups/musicbrainz/mb-artist-credit-splitter/src/splitter.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function splitCredit(input: string) { - const RE = /([  ]*((?:CV|cv)[.:.:] *|[\((]((?:CV|cv)[.:.:] *)?(?=[^)]{3,})|(?<=[^(]{3})[\))]\/?|[、,\//[]\[\]]|(?: & |&)| feat[.: .: ] *)[  ]*)+/g - const splittedCredits = [] as [string, string][] - let lastIndex = 0 - for (const match of input.matchAll(RE)) { - splittedCredits.push([input.slice(lastIndex, match.index), match[0]]) - lastIndex = match.index! + match[0].length - } - if (input.slice(lastIndex).length > 0) splittedCredits.push([input.slice(lastIndex), ""]) - return splittedCredits -} \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-auto-set-media-type-by-url-rels.user.tsx b/scripts/_groups/musicbrainz/mb-auto-set-media-type-by-url-rels.user.tsx deleted file mode 100644 index 27fa0d1..0000000 --- a/scripts/_groups/musicbrainz/mb-auto-set-media-type-by-url-rels.user.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { iterate } from "weight-balanced-tree" -import { LINK_TYPE_GID_RELEASE_FREE_STREAMING, LINK_TYPE_GID_RELEASE_PAID_DOWNLOAD, LINK_TYPE_GID_RELEASE_PAID_STREAMING } from "#common/mb/gid" -import { MEDIUM_FORMAT_DIGITAL_RELEASE } from "#common/mb/medium-format-id" -import { type EditorMedium, type EditorRelease, isMBWithReleaseEditor, MBWithReleaseEditor } from "#common/mb/release-editor" - -defineUserScript({ - name: "MB: Automatically Set Media Type by URL Relationships", - description: "Automatically set the media type based on the relationships of release's URL.", - namespace: "https://rinsuki.net", - author: "rinsuki", - grant: "none", - match: [ - "https://*.musicbrainz.org/release/add*", - "https://*.musicbrainz.org/release/*/edit*" - ], - excludeMatch: [ - "https://*.musicbrainz.org/release/*/edit-relationships*" - ], - runAt: "document-idle", - includeContributionURL: true, -}) - -const DIGITAL_MEDIA_TYPES = [ - LINK_TYPE_GID_RELEASE_FREE_STREAMING, - LINK_TYPE_GID_RELEASE_PAID_DOWNLOAD, - LINK_TYPE_GID_RELEASE_PAID_STREAMING, -] - -function estimateMediumType(MB: MBWithReleaseEditor) { - for (const url of iterate(MB.releaseEditor.externalLinksData())) { - for (const rel of url.relationships) { - if (rel.linkTypeID == null) continue - const gid = MB.linkedEntities.link_type[rel.linkTypeID]?.gid - if (DIGITAL_MEDIA_TYPES.includes(gid)) { - return MEDIUM_FORMAT_DIGITAL_RELEASE - } - } - } -} - -function doIt(medium: EditorMedium, type: string) { - if (medium.formatID()?.length < 1) { - medium.formatID(type) - } -} - -function main() { - const MB = window.MB - if (!isMBWithReleaseEditor(MB)) return console.log("You are not on the release editor page.") - MB.releaseEditor.rootField.release().mediums.subscribe(m => { - const type = estimateMediumType(MB) - if (type == null) return - for (const medium of m) { - doIt(medium, type) - } - }) - MB.releaseEditor.activeTabID.subscribe(tabID => { - if (tabID !== "#tracklist") return - const type = estimateMediumType(MB) - if (type == null) return - for (const medium of MB.releaseEditor.rootField.release().mediums()) { - doIt(medium, type) - } - }) -} - -main() \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-clear-tracklist-artist.user.tsx b/scripts/_groups/musicbrainz/mb-clear-tracklist-artist.user.tsx deleted file mode 100644 index 263ee1c..0000000 --- a/scripts/_groups/musicbrainz/mb-clear-tracklist-artist.user.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { isMBWithReleaseEditor } from "#common/mb/release-editor" - -defineUserScript({ - name: "MB: Clear Tracklist Artist", - namespace: "https://rinsuki.net", - match: [ - "https://*.musicbrainz.org/release/add", - "https://*.musicbrainz.org/release/*/edit" - ], - grant: "none", - author: "rinsuki", - includeContributionURL: true, -}) - -function main() { - const tracklist = document.getElementById("tracklist") - if (tracklist == null) return - const button = document.createElement("button") - button.textContent = "Clear Tracklist Artist" - button.addEventListener("click", () => { - const MB = window.MB - if (!isMBWithReleaseEditor(MB)) return alert("You are not on the release editor page.") - if (!confirm("Are you sure you want to clear the tracklist artist?")) return - for (const track of MB.releaseEditor.rootField.release().allTracks()) { - track.artistCredit({ names: [] }) - } - }) - tracklist.prepend(button) -} - -main() \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-copy-recording-rels-from-other-release.user.tsx b/scripts/_groups/musicbrainz/mb-copy-recording-rels-from-other-release.user.tsx deleted file mode 100644 index c201d25..0000000 --- a/scripts/_groups/musicbrainz/mb-copy-recording-rels-from-other-release.user.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/** @jsx h */ -import { h } from "@rinsuki/dom-chef" -import { fetchOkOrThrow } from "#common/fetch-ok-or-throw" -import { MediumT, RelationshipEditStatusT, ReleaseT } from "typedbrainz/types" -import { isReleaseRelationshipEditor } from "typedbrainz" - -defineUserScript({ - name: "MB: [WIP] Copy Recording Relationships from Other Release", - version: "0.1.0", - grant: "none", - namespace: "https://rinsuki.net", - author: "rinsuki", - match: [ - "https://*.musicbrainz.org/release/*/edit-relationships*" - ], - includeContributionURL: false, // since its wip -}) - -let abortController: AbortController | null = null - -const elm =
-
- -

Copy Recording Relationships from Other Release

-
-
{ - e.preventDefault() - const input = e.currentTarget.querySelector("input[type=text]") as HTMLInputElement - if (input == null) return - const mbid = (input.value.match(/(?:^|\/release\/)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i) ?? [])[1] - if (!mbid) { - alert("Invalid MBID or URL") - return - } - if (abortController) { - abortController.abort() - } - - abortController = new AbortController() - fetchReleaseAndShowCopyUI(mbid, abortController, e.currentTarget).catch(err => { - console.error(err) - alert(err.message) - }) - }}> - - -
-
-
- -async function fetchReleaseAndShowCopyUI(mbid: string, ac: AbortController, form: HTMLFormElement) { - const resRaw = await fetchOkOrThrow(`/ws/js/entity/${mbid}`).catch(e => { throw new Error(`failed to fetch release: ${e.message}`, { cause: e })}) - const resJSON: ReleaseT = await resRaw.json().catch(e => { throw new Error(`failed to fetch release (parsing json): ${e.message}`, { cause: e })}) - console.log(resJSON) - - const elm =
-

Copy from {resJSON.name}

-
{ - e.preventDefault() - const input = e.currentTarget.querySelector("select") - if (input == null) return - const mediumid = parseInt(input.value, 10) - if (!Number.isSafeInteger(mediumid)) { - alert("Invalid medium ID") - return - } - fetchMediumAndShowCopyUI(resJSON.gid, mediumid, ac, e.currentTarget).catch(err => { - console.error(err) - alert(err.message) - }) - }}> - { - resJSON.mediums - ?
- -
- : "(no mediums)" - } - -
-
- - if (ac.signal.aborted) return - form.nextElementSibling?.remove() - form.insertAdjacentElement("afterend", elm) - elm.querySelector("select")?.focus() -} - -async function fetchMediumAndShowCopyUI(releasegid: string, mediumid: number, ac: AbortController, form: HTMLFormElement) { - const resRaw = await fetchOkOrThrow(`/ws/js/medium/${mediumid}?inc=recordings+rels`).catch(e => { throw new Error(`failed to fetch medium: ${e.message}`, { cause: e })}) - const srcMedia: MediumT = await resRaw.json().catch(e => { throw new Error(`failed to fetch medium (parsing json): ${e.message}`, { cause: e })}) - - const elm =
- -
- if (ac.signal.aborted) return - form.nextElementSibling?.remove() - form.insertAdjacentElement("afterend", elm) -} - -document.querySelector("#content > div.tabs")?.insertAdjacentElement("afterend", elm) \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-copy-recording-rels-to-karaoke.user.tsx b/scripts/_groups/musicbrainz/mb-copy-recording-rels-to-karaoke.user.tsx deleted file mode 100644 index 81ca94e..0000000 --- a/scripts/_groups/musicbrainz/mb-copy-recording-rels-to-karaoke.user.tsx +++ /dev/null @@ -1,216 +0,0 @@ -/** @jsx h */ -import { h } from "@rinsuki/dom-chef" -import { isReleaseRelationshipEditor } from "typedbrainz" -import { MediumRecordingStateT, RecordingT, REL_STATUS_ADD_T, REL_STATUS_EDIT_T, RelationshipEditStatusT } from "typedbrainz/types" -import { resolveRecordingsRelationships } from "#common/mb/resolve-recordings-relationships" - -defineUserScript({ - name: "MB: Copy Recording Relationships to Karaoke/Edited Recordings", - description: "Copy recording-{artist, work, etc...} relationships to karaoke/edited recordings with one button!", - version: "0.4.0", - grant: "none", - namespace: "https://rinsuki.net", - author: "rinsuki", - match: [ - "https://*.musicbrainz.org/release/*/edit-relationships*" - ], - includeContributionURL: true, -}) - -const KARAOKE_REL_LINK_TYPE_ID = 226 // gid: 39a08d0e-26e4-44fb-ae19-906f5fe9435d -const EDITS_REL_LINK_TYPE_ID = 309 // gid: ce01b3ac-dd47-4702-9302-085344f96e84 -const WORK_REL_LINK_TYPE_ID = 278 // gid: a3005666-a872-32c3-ad06-98af558e99b0 -const WORK_REL_KARAOKE_LINK_ATTR_TYPE_ID = 1261 // gid: 3d984f6e-bbe2-4620-9425-5f32e945b60d -const WORK_REL_PARTIAL_LINK_ATTR_TYPE_ID = 579 // gid: d2b63be6-91ec-426a-987a-30b47f8aae2d - -async function doIt(button: HTMLButtonElement, doReverse: boolean) { - const MB = window.MB - if (MB == null) return alert("MB global not found") - if (MB.tree == null) return alert("MB.tree not found") - const editor = MB.relationshipEditor - if (editor == null || !isReleaseRelationshipEditor(editor)) return alert("Relationship editor not found") - const currentRecordings = new Map() - const dstRecordingStates = new Map() - for (const medium of MB.tree.iterate(editor.state.mediums)) { - for (const track of MB.tree.iterate(medium[1])) { - currentRecordings.set(track.recording.gid, track.recording) - dstRecordingStates.set(track.recording.gid, track) - } - } - - const recToRecRelationships = new Map<`${string}:${string}`, { - srcRecordingId: string, - dstRecordingId: string, - karaoke: boolean, - partial: boolean, - }>() - for (const dstRecording of Array.from(/* currentRecordings はループ内で変化するので Array.from で確定させる */currentRecordings.values())) { - console.log("dstrec", dstRecording) - const srcRecordingRels = [ - ...dstRecording.relationships!.filter(rel => KARAOKE_REL_LINK_TYPE_ID === rel.linkTypeID && (doReverse ? rel.entity0_id : rel.entity1_id) === dstRecording.id), - ...dstRecording.relationships!.filter(rel => EDITS_REL_LINK_TYPE_ID === rel.linkTypeID && (doReverse ? rel.entity1_id : rel.entity0_id) === dstRecording.id), - ] - for (const srcRecordingRel of srcRecordingRels) { - const srcRecording = srcRecordingRel.target - if (srcRecording.entityType !== "recording") continue // ? - let obj = recToRecRelationships.get(`${dstRecording.gid}:${srcRecording.gid}`) - if (obj == null) { - obj = { - srcRecordingId: srcRecording.gid, - dstRecordingId: dstRecording.gid, - karaoke: false, - partial: false, - } - if (!currentRecordings.has(srcRecording.gid)) currentRecordings.set(srcRecording.gid, srcRecording) - recToRecRelationships.set(`${dstRecording.gid}:${srcRecording.gid}`, obj) - } - if (srcRecordingRel.linkTypeID === KARAOKE_REL_LINK_TYPE_ID) { - obj.karaoke = true - } else if (srcRecordingRel.linkTypeID === EDITS_REL_LINK_TYPE_ID) { - obj.partial = true - } - } - } - await resolveRecordingsRelationships( - Array.from(recToRecRelationships.values()) - .map(x => x.srcRecordingId) - .map(gid => currentRecordings.get(gid)!), - button, - ) - { - const srcRecordingIds = new Set(Array.from(recToRecRelationships.values()).map(x => x.srcRecordingId)) - const srcRecordingsWithoutRels = Array.from(srcRecordingIds) - .map(gid => currentRecordings.get(gid)!) - .filter(rec => rec.relationships == null) - await resolveRecordingsRelationships(srcRecordingsWithoutRels, button) - } - for (const recToRecRel of recToRecRelationships.values()) { - const srcRecording = currentRecordings.get(recToRecRel.srcRecordingId)! - const dstRecording = currentRecordings.get(recToRecRel.dstRecordingId)! - for (const rel of srcRecording.relationships!) { - if (rel.source_type === "recording" && rel.target_type === "recording") continue // probably don't want to copy - if (rel.source_type === "url" || rel.target_type === "url") continue // probably don't want to copy - let attrs = [...rel.attributes] - let oldRelationshipState = null - if (rel.linkTypeID === WORK_REL_LINK_TYPE_ID) { - if (doReverse) { - const removeAttrIds = new Set() - if (recToRecRel.karaoke) removeAttrIds.add(WORK_REL_KARAOKE_LINK_ATTR_TYPE_ID) - if (recToRecRel.partial) removeAttrIds.add(WORK_REL_PARTIAL_LINK_ATTR_TYPE_ID) - attrs = attrs.filter(x => !removeAttrIds.has(x.typeID)) - } else { - const neededAttrIds = new Set() - if (recToRecRel.karaoke) neededAttrIds.add(WORK_REL_KARAOKE_LINK_ATTR_TYPE_ID) - if (recToRecRel.partial) neededAttrIds.add(WORK_REL_PARTIAL_LINK_ATTR_TYPE_ID) - - for (const neededAttrId of neededAttrIds) { - if (attrs.find(x => x.typeID === neededAttrId) != null) continue - const neededAttr = MB.linkedEntities.link_attribute_type[neededAttrId] - attrs.push({ - type: neededAttr, - typeID: neededAttr.id, - typeName: neededAttr.name, - }) - } - } - const dstRecAllRels = dstRecordingStates.get(dstRecording.gid)?.targetTypeGroups - const dstRecWorkRelsObj = dstRecAllRels && MB.tree! - .iterate(dstRecAllRels) - .find(r => r[0] === "work") - ?.[1] - const dstRecWorkRels = dstRecWorkRelsObj && MB.tree! - .iterate(dstRecWorkRelsObj) - .flatMap(r => MB.tree!.iterate(r.phraseGroups)) - .flatMap(r => MB.tree!.iterate(r.relationships)) - .toArray() - console.log("drwrels", dstRecWorkRels, rel.backward) - - const dstRecWorkRel = dstRecWorkRels?.find(r => - rel.backward - ? r.entity0.id === rel.target.id && r.entity1.id === dstRecording.id - : r.entity0.id === dstRecording.id && r.entity1.id === rel.target.id - ) - if (dstRecWorkRel != null) { - const oldRelAttrIds = new Set(MB.tree!.iterate(dstRecWorkRel.attributes ?? MB.tree!.empty).map(x => x.typeID)) - if (attrs.every(x => oldRelAttrIds.has(x.typeID))) { - continue - } - - const oldRelAttrNames = MB.tree!.iterate(dstRecWorkRel.attributes ?? MB.tree!.empty).map(x => x.typeName).toArray().join(", ") || "(none)" - const newRelAttrNames = attrs.map(x => x.typeName).join(", ") || "(none)" - - if (confirm([ - `While copying`, - "", - `Relationship to work "${rel.target.name}" (${rel.target.id})`, - `to Destination recording "${dstRecording.name}" (${dstRecording.id})`, - `from Source recording "${srcRecording.name}" (${srcRecording.id})`, - "", - "an existing relationship with the same work but different attributes was found.", - "", - "Do you want to overwrite the existing relationship's attributes with the new ones?", - "", - `${oldRelAttrNames} → ${newRelAttrNames}`, - "", - "OK = Overwrite", - "Cancel = Add New Recording-Work Relationship", - ].join("\n"))) { - oldRelationshipState = dstRecWorkRel - } - } - } - editor.dispatch({ - type: "update-relationship-state", - sourceEntity: rel.backward ? rel.target : dstRecording, - oldRelationshipState, - newRelationshipState: { - _lineage: [], - _original: null, - editsPending: false, - entity0: rel.backward ? rel.target : dstRecording, - entity1: rel.backward ? dstRecording : rel.target, - entity0_credit: rel.entity0_credit, - entity1_credit: rel.entity1_credit, - begin_date: rel.begin_date, - end_date: rel.end_date, - ended: rel.ended, - id: editor.getRelationshipStateId(null), - linkTypeID: rel.linkTypeID, - linkOrder: rel.linkOrder, - // ↑ここまでは新規relationshipを作成する時限定 - ...(oldRelationshipState ? oldRelationshipState : {}), - _status: oldRelationshipState ? (oldRelationshipState._status || 2 satisfies REL_STATUS_EDIT_T) : (1 satisfies REL_STATUS_ADD_T), - attributes: MB.tree.fromDistinctAscArray(attrs), - }, - batchSelectionCount: undefined, - creditsToChangeForSource: "", - creditsToChangeForTarget: "", - }) - } - } - let editNote = editor.state.editNoteField.value - if (editNote.length) editNote += "\n" - editNote += "Script: \"" + GM.info.script.name + "\" (" + GM.info.script.version + ")" - editor.dispatch({ type: "update-edit-note", editNote }) -} - -const elm =
- - -
- -document.querySelector("#content > div.tabs")?.insertAdjacentElement("afterend", elm) \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-create-release-from-other-release.user.tsx b/scripts/_groups/musicbrainz/mb-create-release-from-other-release.user.tsx deleted file mode 100644 index 3e87506..0000000 --- a/scripts/_groups/musicbrainz/mb-create-release-from-other-release.user.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { ReleaseT } from "typedbrainz/types" - -defineUserScript({ - name: "MB: Create Release from Other Release", - match: ["https://*.musicbrainz.org/release-group/*"], - grant: "none", - namespace: "https://rinsuki.net", - author: "rinsuki", - description: "in the release group page", - includeContributionURL: true, -}) - -function main() { - const table = document.querySelector("table.mergeable-table") - if (table == null) return - for (const row of table.rows) { - const link = row.querySelector("td > a[href^='/release/'][href*='-']:has(bdi)") - if (link == null) continue - const button = document.createElement("button") - button.textContent = "Copy" - button.addEventListener("click", e => { - e.preventDefault() - button.textContent = "……" - button.disabled = true - fetch(`/ws/js/release/${new URL(link.href).pathname.split("/")[2]}`).then(r => r.json()).then((r: ReleaseT) => { - let f: Record = { - name: r.name, - release_group: r.releaseGroup?.gid, - comment: r.comment, - barcode: r.barcode, - language: r.language?.iso_code_3, - script: r.script?.iso_code, - } - for (let i=0; i<(r.labels?.length ?? 0); i++) { - f[`labels.${i}.mbid`] = r.labels?.[i].label?.gid - } - for (let i=0; i { - console.error(e) - button.textContent = "Error" - button.disabled = false - }) - }) - link.parentElement?.prepend(button) - } -} - -main() - diff --git a/scripts/_groups/musicbrainz/mb-match-tracklist-credits-with-other-credits.user.tsx b/scripts/_groups/musicbrainz/mb-match-tracklist-credits-with-other-credits.user.tsx deleted file mode 100644 index 9a4692c..0000000 --- a/scripts/_groups/musicbrainz/mb-match-tracklist-credits-with-other-credits.user.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { ArtistCreditNameT, ArtistT } from "typedbrainz/types" -import type { Observable } from "knockout" -import { MBReleaseEditor } from "#common/mb/release-editor" - -defineUserScript({ - name: "MB: Match Tracklist Credits with Other Credits", - namespace: "https://rinsuki.net", - version: "0.3.1", - grant: "none", - match: [ - "https://*.musicbrainz.org/release/*/edit", - "https://*.musicbrainz.org/release/add", - ], - includeContributionURL: true, -}) - -function isArtistExist(artist: ArtistT | null): artist is ArtistT { - return artist != null && artist.id != 0 && artist.id != null -} - -function doItForSpecificArtistCredit(creditMap: Map, artistCredit: Observable<{ names: ArtistCreditNameT[] }>) { - const names = [...artistCredit().names] - console.log(JSON.stringify(names)) - // ループ中にnamesを書き換えるのであえて for-of を使わない - for (let i = 0; i < names.length; i++) { - const name = names[i] - name.joinPhrase ??= "" - // アーティストIDが不明の場合、先頭一致できる名前を探す - if (!isArtistExist(name.artist)) { - for (const [knownName, knownArtist] of creditMap) { - if (!isArtistExist(knownArtist)) continue - if (!name.name.startsWith(knownName)) continue - const remainName = name.name.slice(knownName.length) - if (remainName === "") { - // 完全一致したら設定だけでよい - name.artist = knownArtist - break - } - // おまけがある場合はそれをjoinPhraseに回す - name.name = knownName - name.artist = knownArtist - name.joinPhrase = remainName + (name.joinPhrase ?? "") - break - } - } - // joinPhraseに既知の名前があれば分割する - let firstArtist: null | [number, string, ArtistT] = null - for (const [knownName, knownArtist] of creditMap) { - if (!isArtistExist(knownArtist)) continue - if (name.joinPhrase == null) continue - const index = name.joinPhrase.indexOf(knownName) - if (index === -1) continue - if (firstArtist == null || firstArtist[0] > index) { - firstArtist = [index, knownName, knownArtist] - } - } - // 既知の名前があった! - if (firstArtist != null) { - const [index, knownName, knownArtist] = firstArtist - const remainName = name.joinPhrase.slice(index + knownName.length) - name.joinPhrase = name.joinPhrase.slice(0, index) - const newCreditName: ArtistCreditNameT = { - name: knownName, - artist: knownArtist, - joinPhrase: remainName, - } - names.splice(i + 1, 0, newCreditName) - } - } - artistCredit({ - names, - }) -} - -async function doItEntirely(withShiftKey: boolean) { - const MB = window.MB - if (MB == null) return - const editor: MBReleaseEditor = (MB as any).releaseEditor - - const currentCredits: ArtistCreditNameT[] = [ - ...editor.rootField.release().releaseGroup().artistCredit?.names ?? [], - ...editor.rootField.release().artistCredit().names, - ...Array.from(editor.rootField.release().allTracks()).flatMap(t => [...t.artistCredit().names, ...t.recording().artistCredit.names]), - ] - - const releaseUrl = prompt("Enter the release URL to copy credits from (If you keep it empty, it will only match within the current release)") - if (releaseUrl == null) return - if (releaseUrl.length) { - const mbid = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.exec(releaseUrl)?.[0] - if (mbid == null) { - alert("Invalid release URL") - return - } - const otherJS = await fetch(`/ws/js/entity/${mbid}`).then(r => r.json()) - if (!("artistCredit" in otherJS)) { - alert("Could not fetch artist credit data") - return - } - currentCredits.push(...otherJS.artistCredit.names) - for (const media of otherJS.mediums) { - const otherJS = await fetch(`/ws/js/medium/${media.id}`).then(r => r.json()) - for (const track of otherJS.tracks) { - currentCredits.push(...track.artistCredit.names) - } - } - } - - const creditMap = new Map() - for (const credit of currentCredits) { - if (!isArtistExist(credit.artist)) continue - const current = creditMap.get(credit.name) - if (current == null || !isArtistExist(current)) { - creditMap.set(credit.name, credit.artist) - } else if (current.id !== credit.artist.id) { - // conflict - creditMap.set(credit.name, null) - } - } - - console.log(creditMap) - - doItForSpecificArtistCredit(creditMap, editor.rootField.release().artistCredit) - for (const track of editor.rootField.release().allTracks()) { - doItForSpecificArtistCredit(creditMap, track.artistCredit) - } - - alert("Done") -} - -const button = document.createElement("button") -button.id = "mb-match-tracklist-credits-with-other-credits-button" -document.getElementById(button.id)?.remove() -button.textContent = "Match Tracklist Credits with Other Credits" -button.addEventListener("click", (e) => { - doItEntirely(e.shiftKey) -}) - -document.getElementById("release-editor")?.prepend(button) \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-search-release-on-websites.user.tsx b/scripts/_groups/musicbrainz/mb-search-release-on-websites.user.tsx deleted file mode 100644 index 02860a4..0000000 --- a/scripts/_groups/musicbrainz/mb-search-release-on-websites.user.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { iterate } from "weight-balanced-tree"; -import { isMBWithReleaseEditor } from "#common/mb/release-editor"; - -defineUserScript({ - name: "MusicBrainz: Search Release on Websites", - namespace: "https://rinsuki.net", - match: [ - "https://*.musicbrainz.org/release/*/edit", - "https://*.musicbrainz.org/release/add" - ], - grant: "none", - version: "1.1", - author: "rinsuki", - includeContributionURL: true, -}); - -(async () => { - const sites = Object.entries({ - "YouTube Music": { - domains: ["music.youtube.com"], - search: "https://music.youtube.com/search?q={query}", - barcodeSearch: "https://music.youtube.com/search?q=\"{upc}\"", - }, - "Spotify": { - domains: ["open.spotify.com"], - search: "https://open.spotify.com/search/{query}/albums", - barcodeSearch: "https://open.spotify.com/search/upc:{upc}/albums", - }, - "Apple Music": { - domains: ["music.apple.com", "itunes.apple.com"], - search: "https://music.apple.com/jp/search?term={query}", - }, - "Apple Music (ISRCeam)": { - domains: ["music.apple.com", "itunes.apple.com"], - search: "https://isrceam.rinsuki.net/apple/jp/search?q={query}", - barcodeSearch: "https://isrceam.rinsuki.net/apple/jp/upc?upc={upc}", - }, - "OTOTOY": { - domains: ["ototoy.jp"], - search: "https://ototoy.jp/find/?q={query}", - }, - "OTOTOY (Google)": { - domains: ["ototoy.jp"], - search: "https://www.google.com/search?client=firefox-b-d&q=site:ototoy.jp+{query}", - }, - "mora": { - domains: ["mora.jp"], - search: "https://mora.jp/search/top?keyWord={query}", - }, - "Qobuz": { - domains: ["www.qobuz.com"], - search: "https://www.qobuz.com/jp-ja/search/albums/{query}", - }, - "TIDAL": { - domains: ["tidal.com"], - search: `https://tidal.com/search/albums?q={query}`, - }, - "Google": { - domains: [] as string[], - search: "https://www.google.com/search?client=firefox-b-d&q={query}", - } - } satisfies Record).map(e => ({...e[1], name: e[0]})) - - let externalLinkEditor - while (null == (externalLinkEditor = document.getElementById("external-links-editor"))) { - await new Promise(r => setTimeout(r, 100)) - } - const MB = (() => { - const MB = window.MB - if (!isMBWithReleaseEditor(MB)) throw new Error("window.MB is not ready") - return MB - })() - - const linkList = document.createElement("ul") - function refresh() { - linkList.innerHTML = "" - const links = Array.from(iterate(MB.releaseEditor.externalLinksData())) - for (const site of sites) { - let exists = false - for (const link of links) { - if (link.url === "") continue; - const domain = new URL(link.url).hostname - if (site.domains.includes(domain)) { - exists = true - break - } - } - const link = document.createElement("a") - if (exists) { - link.style.opacity = "0.5" - } - const barcodeInput = document.querySelector("input#barcode") - if (barcodeInput && barcodeInput.value.length > 4 && site.barcodeSearch != null) { - // seems barcode - const link2 = document.createElement("a") - if (exists) { - link2.style.opacity = "0.5" - } - link2.href = site.barcodeSearch.replace("{upc}", encodeURIComponent(barcodeInput.value)) - link2.textContent = `Search on ${site.name} (by barcode)` - link2.target = "_blank" - link2.style.marginRight = "1em" - const li2 = document.createElement("li") - li2.appendChild(link2) - linkList.appendChild(li2) - } - const nameInput = document.querySelector(`input#name`) - if (!nameInput) continue; - link.href = site.search.replace("{query}", encodeURIComponent(nameInput.value)) - link.textContent = `Search on ${site.name}` - link.target = "_blank" - link.style.marginRight = "1em" - const li = document.createElement("li") - li.appendChild(link) - linkList.appendChild(li) - } - const button = document.createElement("button") - button.textContent = "Refresh" - button.onclick = refresh - const li = document.createElement("li") - li.appendChild(button) - linkList.appendChild(li) - } - externalLinkEditor.parentElement?.parentElement?.appendChild(linkList) - refresh() - - MB.releaseEditor.rootField.release().name.subscribe(() => refresh()) - MB.releaseEditor.rootField.release().barcode.value.subscribe(() => refresh()) - MB.releaseEditor.externalLinksData.subscribe(() => refresh()) -})() diff --git a/scripts/_groups/musicbrainz/mb-seed-from-barcode-search.user.tsx b/scripts/_groups/musicbrainz/mb-seed-from-barcode-search.user.tsx deleted file mode 100644 index e910115..0000000 --- a/scripts/_groups/musicbrainz/mb-seed-from-barcode-search.user.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { removeFirstZeroes } from "#common/remove-first-zeroes" - -defineUserScript({ - name: "MB: Barcode Search ++", - namespace: "https://rinsuki.net", - match: ["https://*.musicbrainz.org/search*"], - description: "Add seed barcode to new release button, highlights barcode-matched releases", - grant: "none", - includeContributionURL: true, -}) - -function main() { - const addReleaseLink = document.querySelector('#content a[href="/release/add"]') - if (addReleaseLink == null) return - const url = new URL(location.href) - const searchQuery = url.searchParams.get('query') - if (searchQuery == null) return - const barcodeMatch = searchQuery.split(" ").find(part => part.startsWith("barcode:")) - if (barcodeMatch == null) return - const barcode = barcodeMatch.replace("barcode:", "") - - // add seed button - - const form = document.createElement("form") - form.method = "POST" - form.action = "/release/add" - form.style.display = "inline" - - const barcodeInput = document.createElement("input") - barcodeInput.type = "hidden" - barcodeInput.name = "barcode" - barcodeInput.value = barcode - form.appendChild(barcodeInput) - - const seedName = url.searchParams.get("seed.name") - if (seedName) { - const input = document.createElement("input") - input.type = "hidden" - input.name = "name" - input.value = seedName - form.appendChild(input) - } - const seedUrl = url.searchParams.get("seed.url") - if (seedUrl) { - const input = document.createElement("input") - input.type = "hidden" - input.name = "urls.0.url" - input.value = seedUrl - form.appendChild(input) - } - - const addButton = document.createElement("button") - addButton.type = "submit" - addButton.textContent = "with barcode " + barcode + " [UserScript]" - form.appendChild(addButton) - addReleaseLink.insertAdjacentElement("afterend", form) - form.insertAdjacentText("beforebegin", ", or ") - - // try to highlight - - let focused = false - for (const td of document.querySelectorAll("td.barcode-cell")) { - if (removeFirstZeroes(barcode) === removeFirstZeroes(td.textContent.trim())) { - td.parentElement!.style.border = "2px solid blue" - const link = td.parentElement!.querySelector('a[href*="release/"]:not([href*=cover-art])') - if (link == null) continue - let focusTarget: HTMLElement = link - if (seedUrl) { - const form = document.createElement("form") - form.method = "POST" - form.action = link.href + "/edit" - form.style.display = "inline" - - const input = document.createElement("input") - input.type = "hidden" - input.name = "urls.0.url" - input.value = seedUrl - form.appendChild(input) - - const editButton = document.createElement("button") - editButton.type = "submit" - editButton.textContent = "[Add URL UserScript]" - form.appendChild(editButton) - link.insertAdjacentElement("afterend", form) - form.insertAdjacentText("beforebegin", " ") - focusTarget = editButton - } - if (!focused) { - focusTarget.focus() - focused = true - } - } - } - if (!focused) { - addButton.focus() - } -} - -main() \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/apply-relationships.ts b/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/apply-relationships.ts deleted file mode 100644 index e1d378b..0000000 --- a/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/apply-relationships.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { RecordingT, REL_STATUS_EDIT_T, REL_STATUS_REMOVE_T, RelationshipEditStatusT, RelationshipStateT, ReleaseRelationshipEditorActionT, UrlT } from "typedbrainz/types" -import { CompleteReleaseRelationshipEditor } from "#common/mb/get-release-relationship-editor-or-throw" - -export type PreparedRelationship = { - recording: RecordingT, - url: string, - linkTypeID: number | "all", - ended: boolean, -} - -export function applyRelationships( - relationships: PreparedRelationship[], - editNote: string, - relationshipEditor: CompleteReleaseRelationshipEditor, -): void { - const tree = window.MB?.tree - if (tree == null) return alert("MB.tree is missing.") - relationshipEditor.dispatch({ - type: "update-edit-note", - editNote: (relationshipEditor.state.editNoteField.value + "\n" + editNote + "\n''Powered by \"" + GM_info.script.name + "\" script (" + GM_info.script.version + ")''").trim(), - }) - let alreadyAlerted = false - - for (const relationship of relationships) { - // it will be marked as "incomplete" in the UI, but actually working? - // @see https://github.com/metabrainz/musicbrainz-server/blob/e214b4d3c13f7ee6b2eb2f9c186ecab310354a5b/root/static/scripts/relationship-editor/components/RelationshipItem.js#L153-L163 - - // if we already have a same relationship, just change ended flag - const existingRels = relationship.recording.relationships! - .filter(r => !r.editsPending) - .filter(r => r.target_type === "url" && (r.target as UrlT).href_url === relationship.url && (relationship.linkTypeID === "all" || r.linkTypeID === relationship.linkTypeID)) - .toSorted((a, b) => { - if (a.ended !== b.ended) { - return (a.ended === relationship.ended) ? -1 : 1 - } - return a.id - b.id - }) - const dispatchesEdit: ReleaseRelationshipEditorActionT[] = [] - const dispatchesRemove: ReleaseRelationshipEditorActionT[] = [] - if (existingRels.length > 0) { - let weAlreadyHaveRel = false - for (const existingRel of existingRels) { - const oldRelationshipState: RelationshipStateT = { - ...existingRel, - _lineage: ["loaded from database"], - _original: null, - _status: 0, - attributes: tree.fromDistinctAscArray(existingRel.attributes), - entity0: relationship.recording, - entity1: existingRel.target as UrlT, - } - oldRelationshipState._original = oldRelationshipState - const isSimpleRel = existingRel.begin_date == null && existingRel.end_date == null - if (isSimpleRel) { - if (weAlreadyHaveRel) { - dispatchesRemove.push({ - type: "update-relationship-state", - sourceEntity: relationship.recording, - batchSelectionCount: undefined, - creditsToChangeForSource: "", - creditsToChangeForTarget: "", - oldRelationshipState: null, - newRelationshipState: { - ...oldRelationshipState, - _lineage: [...oldRelationshipState._lineage, "removed"], - _status: 3 satisfies REL_STATUS_REMOVE_T, - }, - }) - continue - } - weAlreadyHaveRel = true - } - if (existingRel.ended !== relationship.ended) { - dispatchesEdit.push({ - type: "update-relationship-state", - sourceEntity: relationship.recording, - batchSelectionCount: undefined, - creditsToChangeForSource: "", - creditsToChangeForTarget: "", - oldRelationshipState: null, - newRelationshipState: { - ...oldRelationshipState, - _lineage: [...oldRelationshipState._lineage, "edited"], - _status: 2 satisfies REL_STATUS_EDIT_T, - ended: relationship.ended, - } - }) - } - } - if (dispatchesRemove.length) { - for (const d of dispatchesRemove) { - relationshipEditor.dispatch(d) - } - if (!alreadyAlerted) { - alert("Please re-seed after submit these edits.") - alreadyAlerted = true - } - } else if (dispatchesEdit.length) { - for (const d of dispatchesEdit) { - relationshipEditor.dispatch(d) - } - } - continue - } - - if (relationship.linkTypeID === "all") continue - - relationshipEditor.dispatch({ - type: "update-relationship-state", - sourceEntity: relationship.recording, - oldRelationshipState: null, - newRelationshipState: { - id: relationshipEditor.getRelationshipStateId(null), - linkOrder: 0, - linkTypeID: relationship.linkTypeID, - _lineage: ["added"], - _original: null, - _status: 1 as RelationshipEditStatusT, - attributes: null, - begin_date: null, // TODO: support? - end_date: null, // TODO: support? - editsPending: false, - ended: !!relationship.ended, // defaults to false - entity0: relationship.recording, - entity0_credit: "", - entity1: { - decoded: "", - editsPending: false, - entityType: "url", - gid: "", - name: relationship.url, - id: relationshipEditor.getRelationshipStateId(null), - last_updated: null, - href_url: "", - pretty_name: "", - }, - entity1_credit: "", - }, - batchSelectionCount: undefined, - creditsToChangeForSource: "", - creditsToChangeForTarget: "", - }) - - } -} \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/index.tsx b/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/index.tsx deleted file mode 100644 index ed47621..0000000 --- a/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { isReleaseRelationshipEditor } from "typedbrainz" -import { zSeedJSON, zSeedJSONFallback } from "./schema" -import { applyRelationships, PreparedRelationship } from "./apply-relationships" - -defineUserScript({ - name: "MusicBrainz: Seed URLs to Release Recordings", - namespace: "https://rinsuki.net", - version: "0.2.4", - description: "Import recording-url relationship to release's recordings.", - author: "rinsuki", - match: [ - "https://*.musicbrainz.org/release/*/edit-relationships", - ], - grant: "none", - includeContributionURL: true, -}) - -async function main() { - // check hash - const urlParams = new URLSearchParams(location.hash.slice(1)) - const rawJson = urlParams.get("seed-urls-v1") - if (rawJson == null) return - while (window.MB?.relationshipEditor?.state == null) { - console.log("Waiting for window.MB.relationshipEditor?.state to be defined...") - await new Promise(resolve => setTimeout(resolve, 100)) - } - console.log("!", window.MB.relationshipEditor) - if (!isReleaseRelationshipEditor(window.MB.relationshipEditor)) { - return - } - - const { linkedEntities, relationshipEditor, tree } = window.MB - if (tree == null) throw new Error("MB.tree is missing...") - const button = document.createElement("button") - button.textContent = "Seed URLs to Recordings" - button.style.zoom = "2" - button.addEventListener("click", () => { - const anyJSON = JSON.parse(rawJson) - const baseJSON = zSeedJSONFallback.parse(anyJSON) - if (![1, 2].includes(baseJSON.version)) { - alert(`Unsupported version: ${baseJSON.version}, please update the script (or contact the seeder's developer)`) - return - } - const json = zSeedJSON.parse(anyJSON) - const errors: string[] = [] - const preparedRelationships: PreparedRelationship[] = [] - for (const track of tree.iterate(relationshipEditor.state.mediums).flatMap(m => tree.iterate(m[1]))) { - if (!(track.recording.gid in json.recordings)) continue - const rels = json.recordings[track.recording.gid] - delete json.recordings[track.recording.gid] - - const alreadyAddedDomains = new Set() - for (const rel of Array.isArray(rels) ? rels : [rels]) { - const relUrl = new URL(rel.url) - if (alreadyAddedDomains.has(relUrl.hostname)) { - errors.push(`You can't add multiple same domain URLs for a recording at once! URL: ${rel.url} Recording: ${track.recording.gid}`) - continue - } - alreadyAddedDomains.add(relUrl.hostname) - for (const relType of rel.types) { - let linkTypeID: number | undefined - if (relType in linkedEntities.link_type && linkedEntities.link_type[relType].type0 === "recording" && linkedEntities.link_type[relType].type1 === "url") { - linkTypeID = linkedEntities.link_type[relType].id - } - if (linkTypeID == null) { - for (const lt of Object.values(linkedEntities.link_type)) { - if (lt.type0 !== "recording") continue - if (lt.type1 !== "url") continue - console.log(lt) - if (lt.name === relType) { - linkTypeID = lt.id - break - } - } - } - if (linkTypeID == null) { - errors.push(`Failed to find link type ${JSON.stringify(relType)} for recording ${track.recording.gid}`) - continue - } - preparedRelationships.push({ - recording: track.recording, - url: rel.url, - linkTypeID, - ended: rel.ended ?? false, - }) - } - } - } - - for (const remainingRecordingId of Object.keys(json.recordings)) { - errors.push(`Failed to find recording: ${remainingRecordingId}, you might be need to expand mediums before press seed button.`) - } - - if (errors.length === 0) { - applyRelationships(preparedRelationships, json.note, relationshipEditor) - button.textContent = "URLs seeded successfully!" - button.disabled = true - } else { - alert("Failed to seed urls:\n" + errors.map(x => "* " + x).join("\n")) - } - }) - - const before = document.querySelector("#content > p")! - before.parentElement!.insertBefore(button, before) - button.focus() - console.log("done") -} - -main() \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/schema.ts b/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/schema.ts deleted file mode 100644 index c64b995..0000000 --- a/scripts/_groups/musicbrainz/mb-seed-urls-to-release-recordings/src/schema.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Zod from "zod" - -export const zSeedJSON = Zod.object({ - version: Zod.literal(1), - recordings: Zod.record( - Zod.string(), // recording id - Zod.object({ - url: Zod.string().url(), - types: Zod.array(Zod.string()), // link_type UUID - ended: Zod.boolean().optional(), - }), - ), - note: Zod.string(), -}).or(Zod.object({ - version: Zod.literal(2), - recordings: Zod.record( - Zod.string(), // recording id - Zod.array(Zod.object({ // NOTE: you cannot have multiple *same domain* URLs for a recording at once! - url: Zod.string().url(), - types: Zod.array(Zod.string()), // link_type UUID - ended: Zod.boolean().optional(), - })), - ), - note: Zod.string(), -})) - -export const zSeedJSONFallback = Zod.object({ - version: Zod.number(), -}) \ No newline at end of file diff --git a/scripts/_groups/musicbrainz/mb-split-or-merge-medium.user.tsx b/scripts/_groups/musicbrainz/mb-split-or-merge-medium.user.tsx deleted file mode 100644 index c9b2109..0000000 --- a/scripts/_groups/musicbrainz/mb-split-or-merge-medium.user.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { isMBWithReleaseEditor } from "#common/mb/release-editor" - -defineUserScript({ - name: "MB: Split or Merge Medium (WIP!!)", - namespace: "https://rinsuki.net", - grant: "none", - match: [ - "https://*.musicbrainz.org/release/*/edit", - "https://*.musicbrainz.org/release/add", - ], - includeContributionURL: false, // still wip -}) - -function main() { - const MB = window.MB - if (MB == null) return - if (!isMBWithReleaseEditor(MB)) return - - const tracklist = document.getElementById("tracklist") - if (tracklist == null) return - - const splitButton = document.createElement("button") - splitButton.textContent = "Split Medium" - splitButton.addEventListener("click", () => { - if (MB.releaseEditor.rootField.release().mediums().length < 2) { - alert("There must be at least 2 mediums to split (src and dst).") - return - } - const srcMediumIndex = prompt( - [ - "Choose source medium:", - "", - ...Array.from(MB.releaseEditor.rootField.release().mediums().map((m, i) => [ - `${i + 1}: `, - m.name(), - ` (${m.tracks().length} tracks)` - ].join(""))) - ].join("\n") - ) - if (srcMediumIndex == null) return - const index = parseInt(srcMediumIndex, 10) - 1 - if (isNaN(index) || index < 0 || index >= MB.releaseEditor.rootField.release().mediums().length) { - alert("Invalid medium index.") - return - } - const srcMedium = MB.releaseEditor.rootField.release().mediums()[index] - - const dstMediumIndex = prompt([ - "Choose destination medium:", - "", - ...Array.from(MB.releaseEditor.rootField.release().mediums().map((m, i) => [ - `${i + 1}: `, - m.name(), - ` (${m.tracks().length} tracks)` - ].join(""))) - ].join("\n")) - if (dstMediumIndex == null) return - const dstIndex = parseInt(dstMediumIndex, 10) - 1 - if (isNaN(dstIndex) || dstIndex < 0 || dstIndex >= MB.releaseEditor.rootField.release().mediums().length) { - alert("Invalid medium index.") - return - } - const dstMedium = MB.releaseEditor.rootField.release().mediums()[dstIndex] - - const trackNumber = prompt([ - `Please enter the track number to move from "${srcMedium.position()}. ${srcMedium.name()}" to "${dstMedium.position()}. ${dstMedium.name()}".`, - "", - ...Array.from(srcMedium.tracks().map((t, i) => [ - `${i + 1}: `, - t.name() - ].join(""))) - ].join("\n")) - - if (trackNumber == null) return - const trackIndex = parseInt(trackNumber, 10) - 1 - if (isNaN(trackIndex) || trackIndex < 0 || trackIndex >= srcMedium.tracks().length) { - alert("Invalid track number.") - return - } - - dstMedium.tracks.unshift( - ...srcMedium.tracks.splice(trackIndex) - ) - alert("Moved!") - }) - tracklist.insertAdjacentElement("afterbegin", splitButton) -} - -main() \ No newline at end of file diff --git a/scripts/erogamescape-campaign-histories.user.tsx b/scripts/erogamescape-campaign-histories.user.tsx deleted file mode 100644 index a28923f..0000000 --- a/scripts/erogamescape-campaign-histories.user.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from "@rinsuki/dom-chef" - -defineUserScript({ - name: "ErogameScape: ゲームのセール履歴を追加", - description: "エロゲー批評空間のゲームページに、ゲームのセール(キャンペーン)履歴を表示します。", - version: "0.1.0", - grant: "none", - match: ["https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/game.php?*"], - includeContributionURL: false, - namespace: "https://rinsuki.net", -}) - -const gameIdPlaceholder = "__GAME_ID__" -const sql = `SELECT - row_to_json(campaignlist) AS campaign, - row_to_json(campaign_game) AS campaign_game -FROM campaign_game -INNER JOIN campaignlist ON campaign_game.campaign = campaignlist.id -WHERE campaign_game.game = ${gameIdPlaceholder} ORDER BY campaignlist.end_timestamp DESC --- UserScript: ${GM_info.script.name} ${GM_info.script.version}` - -type Row = { - campaign: { - name: string, - url: string, - start_timestamp: string, - end_timestamp: string, - }, - campaign_game: { - content: string, - url: string | null, - } -} - -const domainsToServiceName = { - ".dmm.co.jp": "FANZA", - ".dmm.com": "DMM", - ".dlsite.com": "DLsite", - ".dlaf.jp": "DLsite", -} - -function domainToServiceName(url: string): string | null { - const parsed = URL.parse(url) - if (parsed == null) return null - for (const [domain, serviceName] of Object.entries(domainsToServiceName)) { - if (parsed.hostname.endsWith(domain)) return serviceName - if (domain.startsWith(".") && parsed.hostname === domain.slice(1)) return serviceName - } - return null -} - -const sectionId = "userscript-erogamescape-campaign-histories" - -const style = `#${sectionId} { - & .current-campaign { - color: red; - font-weight: bolder; - } -}` - -async function main() { - const url = new URL(location.href) - const gameId = url.searchParams.get("game") - if (gameId == null) return - - const section =
-

セール履歴 (UserScript: {GM_info.script.name} {GM_info.script.version})

- -
- const beforeElements = [ - document.querySelector(`#toContents + div[style="margin-bottom: 20px;"]`), - document.getElementById("toContents"), - ] - for (const el of beforeElements) { - if (el == null) continue - el.insertAdjacentElement("afterend", section) - break - } - - fetch("/~ap2/ero/toukei_kaiseki/sql_for_erogamer_form.php", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `sql=${encodeURIComponent(sql.replace(gameIdPlaceholder, gameId))}`, - }).then(async r => { - if (!r.ok) throw new Error(`HTTP error ${r.status}\n\n${await r.text()}`) - const dom = new DOMParser().parseFromString(await r.text(), "text/html") - const table = dom.querySelector("#query_result_main > table") - if (table == null) throw new Error(`テーブルが見つからない`) - const rows = [] - const keys: string[] = [] - for (const rowEl of table.rows) { - console.log(rowEl) - if (rowEl.querySelector("th") != null) { - keys.push(...Array.from(rowEl.cells, c => c.textContent.trim() ?? "")) - continue - } - const values = Array.from(rowEl.cells, c => c.textContent.trim() ?? "") - - const row = Object.fromEntries(values.map((v, i) => [keys[i], JSON.parse(v)])) as Row - rows.push(row) - } - - console.log(rows) - - if (rows.length) { - section.append( - - - - - - {rows.map(row => { - const startDate = new Date(row.campaign.start_timestamp) - const endDate = new Date(row.campaign.end_timestamp) - const isCurrentCampaign = endDate.getTime() > Date.now() - const serviceName = domainToServiceName(row.campaign_game.url ?? row.campaign.url ?? "") - const duration = Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) - return - - - - - })} -
終了日時 (たぶんJST)内容キャンペーン名
- {row.campaign.end_timestamp} - {" "} - (約{duration}日間) - {row.campaign_game.content}{serviceName && `[${serviceName}] `}{row.campaign.name}
) - } else { - section.append(

セール履歴が見つからなかったか、取得に失敗しました。

) - } - }) -} - -main().catch(e => { - console.error(e) - alert(`セール情報の取得に失敗しました:\n${e}\n\nfrom UserScript ${GM_info.script.name} ${GM_info.script.version}`) -}) \ No newline at end of file diff --git a/scripts/fix-1password-extension-passkey.user.tsx b/scripts/fix-1password-extension-passkey.user.tsx deleted file mode 100644 index 3ce1e78..0000000 --- a/scripts/fix-1password-extension-passkey.user.tsx +++ /dev/null @@ -1,29 +0,0 @@ -defineUserScript({ - name: "1Password Extension Passkey Fix", - namespace: "rinsuki.net", - description: "Treat 1Password extension's response as a instance of window.PublicKeyCredential", - version: "1.0", - match: ["https://accounts.nintendo.com/*"], - grant: "none", - includeContributionURL: false, -}); - -(() => { - const origPublicKeyCredential = window.PublicKeyCredential - window.PublicKeyCredential = new Proxy(origPublicKeyCredential, { - get(...args) { - if (args[1] === Symbol.hasInstance) { - return (instance: unknown) => { - if (typeof instance !== "object") return false - if (instance == null) return false - if (!("type" in instance)) return false - if (instance.type !== "public-key") return false - if (!("id" in instance)) return false - if (typeof instance.id !== "string") return false - return true - } - } - return Reflect.get(...args) - } - }) -})() \ No newline at end of file diff --git a/scripts/mastodon-show-act-users/src/components/list.tsx b/scripts/mastodon-show-act-users/src/components/list.tsx deleted file mode 100644 index d38b80c..0000000 --- a/scripts/mastodon-show-act-users/src/components/list.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useEffect, useState } from "react" -import { MastodonUser } from "../types" -import { User } from "./user" -import React from "react" - -export const List: React.FC<{type: "favourite" | "reblog", statusId: string}> = ({type, statusId}) => { - const [loading, setLoading] = useState(false) - const [users, setUsers] = useState([]) - const [error, setError] = useState(undefined) - - useEffect(() => ((async () => { - setLoading(true) - try { - const res = await fetch(`${location.origin}/api/v1/statuses/${statusId}/${type === "favourite" ? "favourited_by" : "reblogged_by"}`) - const text = await res.text() - try { - const json = JSON.parse(text) - const error = json.error - if (error) throw new Error(`API: ${error}`) - setError(undefined) - setUsers(json) - } catch(e) { - throw new Error(`HTTP ${res.status} ${res.statusText} - ${text}`) - } - } catch(e) { - console.log(e) - setError(e) - } finally { - setLoading(false) - } - })(), undefined), []) - - const centeringStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - height: "100%", - padding: "30px 15px" - } - if (error) { - return
- - {error != null && typeof error === "object" && "stack" in error && typeof error.stack === "string" && error.stack} - -
- } else if (loading) { - return
- Loading... -
- } else { - return
- {users.map(user => )} -
- } -} \ No newline at end of file diff --git a/scripts/mastodon-show-act-users/src/components/section-header.tsx b/scripts/mastodon-show-act-users/src/components/section-header.tsx deleted file mode 100644 index a38ec03..0000000 --- a/scripts/mastodon-show-act-users/src/components/section-header.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react" - -export const SectionHeader: React.FC<{icon: string, name: string}> = ({icon, name}) => { - return
- - {name} -
-} diff --git a/scripts/mastodon-show-act-users/src/components/user.tsx b/scripts/mastodon-show-act-users/src/components/user.tsx deleted file mode 100644 index 4241b44..0000000 --- a/scripts/mastodon-show-act-users/src/components/user.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react" -import { MastodonUser } from "../types" - -export const User: React.FC<{user: MastodonUser}> = ({user}) => { - return -} \ No newline at end of file diff --git a/scripts/mastodon-show-act-users/src/index.tsx b/scripts/mastodon-show-act-users/src/index.tsx deleted file mode 100644 index e19b113..0000000 --- a/scripts/mastodon-show-act-users/src/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect, useState } from "react"; -import { List } from "./components/list" -import { SectionHeader } from "./components/section-header" -import React from "react"; -import { render } from "react-dom"; - -defineUserScript({ - name: "Mastodon Show act users in /@xxx/12345", - namespace: "https://rinsuki.net", - version: "0.3.2", - description: "Mastodonの投稿詳細画面でBT/favしたユーザー一覧を見れるようにする", - author: "rinsuki", - include: [ - /^https:\/\/[^/]*\/@[A-Za-z0-9_]+\/[0-9]+([?#].*)?$/, - ], - excludeMatch: [ - "https://*/@*/*/embed", - "https://*.tiktok.com/*", - ], - grant: [], - - includeContributionURL: true, -}); - -(function() { - 'use strict'; - - // Mastodonかどうかを確かめる - const regex = /^\/@[A-Za-z0-9_]+\/(\d+)/ - const matchedUrl = regex.exec(location.pathname) - if (matchedUrl == null) return // URLがそれっぽくない - // これログインしてるとひっかかることに気づいたので無効化 - // if (document.querySelector('a[href="https://joinmastodon.org/#getting-started"]') == null) return // Mastodonっぽくなさそう - if (document.querySelector('a[href^="/interact/"]') == null) return // 新しいMastodonを使え - - const statusId = matchedUrl[1] - - const statusMetaDiv = document.querySelector(".detailed-status__meta") - if (statusMetaDiv == null) return // UIを出すところがないので帰る - const boostButton = parent(document.querySelector('a[href^="/interact/"][href*="type=reblog"] > .detailed-status__reblogs')) - const favButton = parent(document.querySelector('a[href^="/interact/"] > .detailed-status__favorites')) - const isNicoru = favButton && favButton.querySelector(".fa-nicoru--status") - - // ReactでUIを作る - - const Root: React.FC = () => { - const [active, setActive] = useState<"favourite" | "reblog" | undefined>(undefined) - useEffect(() => { - if (boostButton) { - boostButton.addEventListener("click", e => { - e.preventDefault() - e.stopPropagation() - setActive("reblog") - }) - } - if (favButton) { - favButton.addEventListener("click", e => { - e.preventDefault() - e.stopPropagation() - setActive("favourite") - }) - } - }, []) - if (active == null) return
- か {isNicoru ? : } をクリックするとブーストした/ふぁぼったユーザーが表示されます -
- const [name, icon] = ({ - "favourite": isNicoru ? ["ニコる", "nicoru"] : ["ふぁぼ", "star"], - "reblog": ["ブースト", "retweet"] - } as const)[active] - return
- -
- -
- -
- } - - function parent(dom: HTMLElement | null): HTMLElement | null { - if (dom == null) return dom - return dom.parentElement - } - // Reactをマウント - const myDiv = document.createElement("div") - const component = render(, myDiv) - statusMetaDiv.appendChild(myDiv) -})(); \ No newline at end of file diff --git a/scripts/mastodon-show-act-users/src/types.ts b/scripts/mastodon-show-act-users/src/types.ts deleted file mode 100644 index b0309ba..0000000 --- a/scripts/mastodon-show-act-users/src/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface MastodonUser { - id: string - display_name: string - acct: string - url: string - avatar_static: string -} diff --git a/scripts/niconico-genten-kaiki/src/const.ts b/scripts/niconico-genten-kaiki/src/const.ts deleted file mode 100644 index d7f5ef1..0000000 --- a/scripts/niconico-genten-kaiki/src/const.ts +++ /dev/null @@ -1 +0,0 @@ -export const YOUTUBE_ORIGIN = "https://www.youtube.com" \ No newline at end of file diff --git a/scripts/niconico-genten-kaiki/src/css.ts b/scripts/niconico-genten-kaiki/src/css.ts deleted file mode 100644 index 2f32fff..0000000 --- a/scripts/niconico-genten-kaiki/src/css.ts +++ /dev/null @@ -1,20 +0,0 @@ -const style = document.createElement("style") -// CSS Class Prefix -export const ccPrefix = "userjs-gentenkaiki" -style.innerText = ` -body.${ccPrefix}-now .${ccPrefix}-start { display: none; } -body.${ccPrefix}-enableytclick .VideoSymbolContainer, -body.${ccPrefix}-enableytclick .CommentRenderer { pointer-events: none; } -body.${ccPrefix}-now #MainVideoPlayer > video { display: none !important; } - -.${ccPrefix}-wrapper { z-index: 0; } -.${ccPrefix}-wrapper, .${ccPrefix}-wrapper > iframe { position: absolute; width: 100%; height: 100%; top: 0; left: 0; border: none; } -.${ccPrefix}-wrapper > iframe { z-index: 1; } -body:not(.${ccPrefix}-enableytclick) .${ccPrefix}-player > iframe { pointer-events: none; } -.${ccPrefix}-ytb-b { fill: #fff } -.${ccPrefix}-ytb-p { fill: #000 } -body.${ccPrefix}-enableytclick .${ccPrefix}-ytb-b { fill: #f00 } -body.${ccPrefix}-enableytclick #UadPlayer { pointer-events: none; } -` - -document.head.appendChild(style) \ No newline at end of file diff --git a/scripts/niconico-genten-kaiki/src/global.d.ts b/scripts/niconico-genten-kaiki/src/global.d.ts deleted file mode 100644 index e4aa70c..0000000 --- a/scripts/niconico-genten-kaiki/src/global.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare var __videoplayer: { - play(): void - duration(): number - replace(url: string): void - originalCurrentTime(): number - paused(): boolean - pause(): void -} \ No newline at end of file diff --git a/scripts/niconico-genten-kaiki/src/index.tsx b/scripts/niconico-genten-kaiki/src/index.tsx deleted file mode 100644 index be810c2..0000000 --- a/scripts/niconico-genten-kaiki/src/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ccPrefix } from "./css" -import { startYouTube } from "./start-yt" -import { sleep } from "./utils" - -defineUserScript({ - name: "原点回帰(Re) あるいは ZenTube in 公式プレーヤー", - namespace: "rinsuki.net", - version: "1.0", - description: "動画説明文内のYouTubeへのリンクに「原点回帰」ボタンが追加され、そのボタンを押すとYouTubeの埋め込みプレーヤーで動画が再生されるようになります。", - author: "-", - match: "https://www.nicovideo.jp/watch/*", - grant: "none", - includeContributionURL: true, -}); - -(async () => { - // 本当は Observe とかしたほうがいいんだろうが… - async function waitForSelectorExists(selector: string) { - for (let i=0; i<5; i++) { - const res = document.querySelector(selector) - if (res != null) return res - await sleep(1000) - } - } - function createActivateButton(videoId: string, dom: HTMLElement) { - const button = document.createElement("button") - button.innerText = "原点回帰" - button.className = `${ccPrefix}-start` - button.addEventListener("click", () => startYouTube(videoId)) - dom.parentElement!.insertBefore(button, dom.nextSibling) - } - const domDescription = await waitForSelectorExists(".VideoDescription-html") - if (domDescription == null) return - for (const link of domDescription.querySelectorAll("a")) { - console.log(link) - if (link.hostname === "youtu.be") createActivateButton(link.pathname.slice(1), link) - if ((link.hostname === "youtube.com" || link.hostname.endsWith(".youtube.com")) && link.pathname === "/watch") { - const url = new URL(link.href) - const vid = url.searchParams.get("v") - if (vid != null) createActivateButton(vid, link) - } - } -})() \ No newline at end of file diff --git a/scripts/niconico-genten-kaiki/src/start-yt.ts b/scripts/niconico-genten-kaiki/src/start-yt.ts deleted file mode 100644 index d8dc5f1..0000000 --- a/scripts/niconico-genten-kaiki/src/start-yt.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ccPrefix } from "./css" -import { YTPlayer } from "./yt-player" - -export function startYouTube(videoId: string) { - __videoplayer.pause() - __videoplayer.replace(`https://api.rinsuki.net/s/dummymovie/mp4/seconds/${Math.floor(__videoplayer.duration())}`) - document.body.classList.add(`${ccPrefix}-now`) - const wrapper = document.createElement("div") - wrapper.className = `${ccPrefix}-wrapper` - const player = new YTPlayer(videoId) - wrapper.appendChild(player.iframe) - document.getElementById("MainVideoPlayer")!.appendChild(wrapper) - const button = document.createElement("button") - button.className = `ActionButton ControllerButton ${ccPrefix}-switchbutton` - button.innerHTML = `
` - function updateButtonTitle() { - button.dataset.title = `YouTubeのコントロールをでき${document.body.classList.contains(`${ccPrefix}-enableytclick`) ? "ない": "る"}ようにする` - } - updateButtonTitle() - button.addEventListener("click", () => { - document.body.classList.toggle(`${ccPrefix}-enableytclick`) - updateButtonTitle() - }) - const cca = document.querySelector(".ControllerContainer-area:last-child")! - cca.insertBefore(button, cca.firstChild) - - player.onReadyCallback = () => { - __videoplayer.play() - player.call("addEventListener", "onStateChange") - } -} \ No newline at end of file diff --git a/scripts/niconico-genten-kaiki/src/utils.ts b/scripts/niconico-genten-kaiki/src/utils.ts deleted file mode 100644 index 9dd46bf..0000000 --- a/scripts/niconico-genten-kaiki/src/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export function sleep(msec: number) { return new Promise(resolve => setTimeout(resolve, msec)) } \ No newline at end of file diff --git a/scripts/niconico-genten-kaiki/src/yt-player.ts b/scripts/niconico-genten-kaiki/src/yt-player.ts deleted file mode 100644 index 6676b41..0000000 --- a/scripts/niconico-genten-kaiki/src/yt-player.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { YOUTUBE_ORIGIN } from "./const" -import { sleep } from "./utils" - -export class YTPlayer { - iframe = document.createElement("iframe") - widgetid: number - connected = false - - constructor(videoId: string) { - this.widgetid = Date.now() - this.iframe.src = `${YOUTUBE_ORIGIN}/embed/${videoId}?autoplay=1&fs=0&disablekb=1&modestbranding=1&playsinline=1&rel=0&showinfo=0&iv_load_policy=3&enablejsapi=1&origin=${location.origin}&vq=highres&widgetid=${this.widgetid}` - window.addEventListener("message", e => { - if (e.origin !== YOUTUBE_ORIGIN) return console.log("invalid origin") - const data = JSON.parse(e.data) - if (data.id !== this.widgetid) return - this.connected = true - switch (data.event) { - case "onReady": - if (this.onReadyCallback != null) this.onReadyCallback() - break - case "infoDelivery": - const info = data.info as { - currentTime: number, - } - const diff = Math.abs(info.currentTime - __videoplayer.originalCurrentTime()) - if (diff > 0.5) { - this.call("seekTo", __videoplayer.originalCurrentTime(), true) - } - break - case "onStateChange": - const isPaused = data.info !== 1 - if (isPaused !== __videoplayer.paused()) { - if (isPaused) { - __videoplayer.pause() - } else { - __videoplayer.play() - } - } - default: - console.log("iframe event", data) - } - }) - this.iframe.addEventListener("load", async () => { - for (let i=0; i<100 && !this.connected; i++) { - this.sendListening() - await sleep(100) - } - }) - } - - postMessage(obj: any) { - this.iframe.contentWindow!.postMessage(JSON.stringify({ - ...obj, - id: this.widgetid, - }), YOUTUBE_ORIGIN) - } - - onReadyCallback?: () => any - - onLoad(callback: () => any) { - this.iframe.addEventListener("load", callback) - } - - sendListening() { - this.postMessage({event: "listening", channel: "widget"}) - } - - call(func: string, ...args: any[]) { - this.postMessage({event: "command", func, args, channel: "widget"}) - } -} \ No newline at end of file diff --git a/scripts/niconico-search-sort-table.user.tsx b/scripts/niconico-search-sort-table.user.tsx deleted file mode 100644 index 30cf88a..0000000 --- a/scripts/niconico-search-sort-table.user.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom" - -defineUserScript({ - name: "ニコニコ動画の検索のソート選択を表にする", - namespace: "rinsuki.net", - version: "1.0", - description: "ニコニコ動画の検索のソート順を選ぶところを表レイアウトにします。", - author: "rinsuki", - match: [ - "https://www.nicovideo.jp/tag/*", - "https://www.nicovideo.jp/search/*", - ], - grant: [], - includeContributionURL: true, -}) - -const sort = document.querySelector(".searchOption .sort.optionList")! as HTMLElement -sort.style.width="220px" -const originalSortList = sort.querySelector(".sortList")! - -const links: {[key: string]: undefined | [string, boolean, HTMLElement]} = {} - -for (const anchor of originalSortList.querySelectorAll("a")) { - const sp = new URLSearchParams(anchor.search) - const key = `${sp.get("sort")}:${sp.get("order")}` - links[key] = [anchor.href, anchor.parentElement!.classList.contains("active"), anchor] -} - -const Link: React.FC<{ - k: string, - children: React.ReactNode, -}> = props => { - let l = links[props.k] - if (l == null) { - originalSortList.querySelector("li.active")?.remove() - return {props.children} - } else { - l[2].parentElement?.remove() - return {props.children} - } -} - -const Row: React.FC<{ - title: string, - k: string, - desc: string, - asc: string, -}> = props => { - return - {props.title}が - {props.desc}順 - {props.asc}順 - -} - -const li = document.createElement("li") -ReactDOM.render( - - - - - - - -
, li) -originalSortList.appendChild(li) \ No newline at end of file diff --git a/scripts/yt-select-codec.user.tsx b/scripts/yt-select-codec.user.tsx deleted file mode 100644 index 2134e9c..0000000 --- a/scripts/yt-select-codec.user.tsx +++ /dev/null @@ -1,92 +0,0 @@ -defineUserScript({ - name: "YouTube: Select Codec", - description: "Select the codec for YouTube videos.", - grant: "none", - namespace: "https://rinsuki.net", - includeContributionURL: false, - match: ["https://www.youtube.com/*"], -}) - -type AdaptiveFormatVideo = { - qualityLabel: string, - fps: number, - width: number, - height: number, -} - -type AdaptiveFormatAudio = { - todo: number, -} - -type InternalPlayer = { - getPlayerResponse(): { - streamingData: { - adaptiveFormats: ({ - itag: number, - quality: string, - bitrate: number, - mimeType: string, - } & AdaptiveFormatVideo | AdaptiveFormatAudio)[] - } - }, - setPlaybackQuality(quality: string, itag: string): void, -} - -function getInternalPlayerObject(): InternalPlayer | undefined { - const playerElm = document.getElementById("movie_player") - if (!playerElm) return - if (!("getPlayerResponse" in playerElm)) return - if (typeof playerElm.getPlayerResponse !== "function") return - let internalPlayer: InternalPlayer | undefined - const oldApply = Function.prototype.apply - const newApply = new Proxy(Function.prototype.apply, { - apply(target, thisArg, args) { - if (internalPlayer == null) internalPlayer = args[0] - return Reflect.apply(target, thisArg, args) - }, - }) - Function.prototype.apply = newApply - playerElm.getPlayerResponse() - if (Function.prototype.apply === newApply) Function.prototype.apply = oldApply - return internalPlayer -} - -const button = document.createElement("button") -button.style.zIndex = "2147483647" -button.style.position = "fixed" -button.style.top = "0px" -button.style.right = "0px" -button.innerText = "SC" -document.body.appendChild(button) - -button.addEventListener("click", () => { - const player = getInternalPlayerObject() - if (player == null) { - alert("Failed to get the internal player object.") - return - } - - const adaptiveFormats = player.getPlayerResponse().streamingData.adaptiveFormats - const videoFormats = adaptiveFormats.filter(format => "qualityLabel" in format) - - const itag = prompt([ - "Select itag:", - "", - ...videoFormats - .toSorted((a, b) => { - if (a.qualityLabel.length !== b.qualityLabel.length) return b.qualityLabel.length - a.qualityLabel.length - if (a.width !== b.width) return b.width - a.width - return b.bitrate - a.bitrate - }) - .map(format => `${format.itag}: ${format.qualityLabel} (${format.mimeType}, ${format.width}x${format.height}, ${format.fps}fps, ${format.bitrate}bps)`), - ].join("\n")) - - if (itag == null) return - const selectedFormat = videoFormats.find(format => format.itag === Number(itag)) - if (!selectedFormat) { - alert("Invalid itag.") - return - } - - player.setPlaybackQuality(selectedFormat.quality, selectedFormat.itag.toString()) -}) \ No newline at end of file diff --git a/scripts/ytm-dont-suggest-video.user.tsx b/scripts/ytm-dont-suggest-video.user.tsx deleted file mode 100644 index cd3cc6e..0000000 --- a/scripts/ytm-dont-suggest-video.user.tsx +++ /dev/null @@ -1,70 +0,0 @@ -defineUserScript({ - name: "YTM: Don't Suggest Video", - namespace: "https://rinsuki.net", - match: "https://music.youtube.com/*", - grant: "none", - author: "rinsuki", - runAt: "document-start", - version: "0.1.0", - description: "Hide video-only tracks from \"Up Next\" tab on YTM.", - includeContributionURL: true, -}) - -function urlFromFetch(input: Parameters[0]): string { - if (typeof input === "string") return input - if ("url" in input) return input.url - return input.href -} - -function shouldFilterThisEntry(json: any): boolean { - if (json.playlistPanelVideoWrapperRenderer) return true // たぶん - if (json.automixPreviewVideoRenderer) return true - const thumbnails = json.playlistPanelVideoRenderer?.thumbnail?.thumbnails - if (!Array.isArray(thumbnails)) { - debugger; - return true // ? - } - for (const thumbnail of thumbnails) { - if (typeof thumbnail.url === "string" && thumbnail.url.includes("i.ytimg.com")) { - // debugger; - return false - } - } - return true -} - -function handleNextResponse(json: any) { - if (Array.isArray(json.continuationContents?.playlistPanelContinuation?.contents)) { - json.continuationContents.playlistPanelContinuation.contents = json.continuationContents.playlistPanelContinuation.contents.filter(shouldFilterThisEntry); - } - if (Array.isArray(json.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs)) { - for (const tab of json.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs) { - if (Array.isArray(tab.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents)) { - tab.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer.contents = tab.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer.contents.filter(shouldFilterThisEntry); - } - } - } -} - -window.fetch = new Proxy(window.fetch, { - apply(target, thisArg, args: Parameters) { - const promise = Reflect.apply(target, thisArg, args); - const url = urlFromFetch(args[0]); - if (!url.includes("/next")) return promise - return promise.then(async resOriginal => { - const res = resOriginal.clone(); - try { - const json = await res.json(); - handleNextResponse(json); - return Response.json(json, { - status: resOriginal.status, - statusText: resOriginal.statusText, - headers: resOriginal.headers, - }) - } catch(e) { - debugger; - return resOriginal; - } - }) - } -}) \ No newline at end of file diff --git a/simple-scripts/annict-slot-episode-setter.user.js b/simple-scripts/annict-slot-episode-setter.user.js deleted file mode 100644 index a895d18..0000000 --- a/simple-scripts/annict-slot-episode-setter.user.js +++ /dev/null @@ -1,72 +0,0 @@ -// ==UserScript== -// @name Annict Slot Generator -// @namespace https://rinsuki.net -// @description Annictのスロットを自動生成します -// @grant none -// @version 1.0 -// @author rinsuki -// @license MIT -// @match https://annict.com/* -// ==/UserScript== - -// @ts-check -async function main() { - const path = location.pathname - const match = /\/db\/works\/(\d+)\/slots\/new/.exec(path) - if (match == null) return - const programIds = new URL(location.href).searchParams.get("program_ids") - if (programIds == null) return - const workId = match[1] - /** @type {HTMLTextAreaElement} */ - // @ts-ignore - const textarea = document.getElementById("deprecated_db_slot_rows_form_rows") - if (textarea == null) return console.warn("textarea not found") - - const episodesPage = await fetch(`/db/works/${workId}/episodes`) - .then(r => r.text()) - .then(r => new DOMParser().parseFromString(r, "text/html")) - console.log(episodesPage) - const episodes = Array.from(episodesPage.querySelectorAll(".f-application__main__content > .container > .card > .card-body > table tr")) - .slice(1) - .map(tr => { - return { - // @ts-ignore - id: tr.querySelector(`td:first-child a[href*="/episodes/"]`).textContent, - sort: parseFloat(tr.querySelector(`td:nth-child(2)`)?.lastChild?.nodeValue ?? "-1"), - countText: tr.querySelector(`td:nth-child(2)`)?.firstChild?.nodeValue?.trim() ?? "", - } - }) - .sort((a, b) => a.sort - b.sort) - console.log(episodes) - - const csv = textarea.value.split("\n").filter(a => a.trim().length) - let alertText = "更新内容: \n" - for (let i = 0; i < csv.length; i++) { - let values = csv[i].split(",") - const episode = episodes[i] - if (values.length < 3) { - if (episode != null) { - // @ts-ignore - values.push(episodes[i].id) - alertText += `${i+1}回目: ${episode.id} (sort: ${episode.sort}): ${episode.countText}\n` - } else { - alertText += `${i+1}回目: エピソードなし\n` - } - } else { - alertText += `${i+1}回目: 指定済み、` - if (episode.id === values[2]) { - alertText += "一致\n" - } else { - alertText += `不一致… (想定: ${episode.id}, 実際: ${values[2]})\n` - } - } - csv[i] = values.join(",") - } - alertText += "\n\n以下の内容でCSVを更新しますか?" - if (!confirm(alertText)) { - return - } - textarea.value = csv.join("\n") - -} -addEventListener("turbo:load", main) \ No newline at end of file diff --git a/simple-scripts/annict-track-broker.user.js b/simple-scripts/annict-track-broker.user.js deleted file mode 100644 index a59aa4a..0000000 --- a/simple-scripts/annict-track-broker.user.js +++ /dev/null @@ -1,113 +0,0 @@ -// ==UserScript== -// @name Annict: Track Broker -// @namespace https://rinsuki.net -// @match https://annict.com/* -// @grant GM_getValue -// @grant GM_setValue -// @grant GM_xmlhttpRequest -// @version 1.1 -// @author rinsuki -// @description Annict の「記録する」ページの内容を指定されたURLに送信します。 -// ==/UserScript== - -// @ts-check -(() => { - /** @type {MutationObserver | null} */ - let observer - const configUI = document.createElement("details") - configUI.innerHTML = `Track Broker Config
URL:
Authorization Header:
method:
` - configUI.style.position = "fixed" - configUI.style.bottom = "1em" - configUI.style.right = "1em" - configUI.style.zIndex = "9999" - configUI.style.backgroundColor = "white" - configUI.style.border = "1px solid black" - configUI.style.padding = "0.5em" - const form = configUI.querySelector("form") - if (form == null) return - /** @type {HTMLInputElement | null} */ - const urlField = form.querySelector("input[name=url]") - /** @type {HTMLInputElement | null} */ - const authField = form.querySelector("input[name=auth]") - /** @type {HTMLSelectElement | null} */ - const methodField = form.querySelector("select[name=method]") - if (urlField == null || authField == null || methodField == null) return - form.addEventListener("submit", e => { - e.preventDefault() - GM_setValue("url", urlField.value) - GM_setValue("auth", authField.value) - GM_setValue("method", methodField.value) - alert("Saved") - }) - urlField.value = GM_getValue("url", "") - authField.value = GM_getValue("auth", "") - methodField.value = GM_getValue("method", "POST") - async function main() { - if (location.pathname !== "/track") { - configUI.remove() - return - } else { - document.body.appendChild(configUI) - } - const watchTarget = document.querySelector(`[data-reloadable-event-name-value="trackable-episode-list"]`) - if (watchTarget == null) return - function changed() { - const works = document.querySelectorAll("#trackable-episode-list .card > .card-body > :first-child") - let episodes = [] - for (const work of works) { - const workName = work.querySelector(".col > .small.u-cursor-pointer")?.textContent - const episodeName = work.querySelector(".col > .fw-bold")?.textContent - const episodeSource = work.querySelector(".col > .mt-1.small > .text-muted") - const episodeTimestamp = work.querySelector(".col > .small:not(.mt-1) > .text-muted")?.textContent - const episodeJSON = { - workName, - episodeName, - episodeSource: episodeSource != null ? { - name: episodeSource.textContent, - url: episodeSource.getAttribute("href"), - } : null, - episodeTimestamp, - } - episodes.push(episodeJSON) - } - const body = { - episodes, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - } - console.log("body", body) - const url = GM_getValue("url", "") - const auth = GM_getValue("auth", "") - if (url.length && url.startsWith("http")) { - const urlObj = new URL(url) - GM_xmlhttpRequest({ - url, - method: GM_getValue("method", "POST"), - headers: { - "Content-Type": "application/json", - "Authorization": auth, - }, - data: JSON.stringify(body), - onload: res => { - console.log(res) - if (res.status >= 300) { - alert(`failed to send data (${res.status})`) - } - } - }) - } else { - console.warn("skip sending because url is not set or invalid") - } - } - if (observer != null) { - observer.disconnect() - } - observer = new MutationObserver((mutations) => { - changed() - }) - observer.observe(watchTarget, { - childList: true, - }) - changed() - } - addEventListener("turbo:load", main) -})() \ No newline at end of file diff --git a/simple-scripts/danime-niconico-gacha.user.js b/simple-scripts/danime-niconico-gacha.user.js deleted file mode 100644 index e145ec2..0000000 --- a/simple-scripts/danime-niconico-gacha.user.js +++ /dev/null @@ -1,59 +0,0 @@ -// ==UserScript== -// @name dアニメニコニコ支店 アニメガチャ -// @namespace https://rinsuki.net -// @description 何を見たらいいかわからない時に適当にアニメを選んで1話を再生するためのボタンを追加します -// @grant none -// @version 1.0 -// @author rinsuki -// @license MIT -// @match https://site.nicovideo.jp/danime/all_contents_1201* -// ==/UserScript== - -// @ts-check -(() => { - document.body.appendChild((() => { - const details = document.createElement("details") - details.style.position = "fixed" - details.style.bottom = "1em" - details.style.right = "1em" - details.style.background = "white" - details.style.color = "black" - details.style.padding = "0.5em" - details.style.border = "1px solid black" - details.open = true - details.appendChild((() => { - const div = document.createElement("div") - div.appendChild((() => { - const gachaButton = document.createElement("button") - gachaButton.textContent = "ガチャ" - gachaButton.style.backgroundColor = "#eb5528" - gachaButton.style.color = "white" - gachaButton.style.border = "none" - gachaButton.style.padding = "0.5em" - gachaButton.style.display = "block" - gachaButton.style.width = "100%" - gachaButton.addEventListener("click", () => { - /** @type {HTMLAnchorElement[]} */ - const allLinks = Array.from(document.querySelectorAll("#listbox a")) - const links = allLinks.filter(link => !link.href.includes("/watch/")) - const link = allLinks[Math.floor(Math.random() * allLinks.length)] - const anchor = document.createElement("a") - anchor.target = "_blank" - anchor.href = link.href - anchor.click() - }) - return gachaButton - })()) - return div - })()) - details.appendChild((() => { - const summary = document.createElement("summary") - const span = document.createElement("span") - span.style.display = "inline-block" - span.innerText = "アニメガチャ" - summary.appendChild(span) - return summary - })()) - return details - })()) -})() \ No newline at end of file diff --git a/simple-scripts/mb-copy-artist-credit.user.js b/simple-scripts/mb-copy-artist-credit.user.js deleted file mode 100644 index d6114c0..0000000 --- a/simple-scripts/mb-copy-artist-credit.user.js +++ /dev/null @@ -1,54 +0,0 @@ -// ==UserScript== -// @name MB: copy artist credit -// @namespace https://rinsuki.net -// @match https://*.musicbrainz.org/artist/*/aliases -// @grant none -// @version 1.0.0 -// @author rinsuki -// @license MIT -// @description copy artist credit to clipboard (localStorage) -// ==/UserScript== - -(function() { - const editLinks = document.querySelectorAll('a[href*="/credit/"][href$="/edit"]') - for (const editLink of editLinks) { - const button = document.createElement("button") - button.textContent="Copy" - button.addEventListener("click", () => { - button.textContent = "Copying..." - const xhr = new XMLHttpRequest() - xhr.responseType = "document" - xhr.open("GET", editLink.href, true) - xhr.addEventListener("load", () => { - if (xhr.status >= 300) { - console.error(xhr) - button.textContent = "Error" - return - } - /** @type {HTMLDocument} */ - const doc = xhr.response - console.log(xhr) - for (const script of doc.querySelectorAll("script")) { - const text = script.text.trim() - if (text.length === 0) continue - console.log(text) - const matched = /MB\.initializeArtistCredit\((.+)\);/s.exec(text) - if (matched != null) { - const parsed = JSON.parse("[" + matched[1] + "]") - const credits = parsed[1].names - if (!Array.isArray(credits)) { - alert("invalid...") - continue - } - localStorage.setItem("copiedArtistCredit", JSON.stringify({names: credits})) - button.textContent = "Copied!" - return - } - } - button.textContent = "Failed..." - }) - xhr.send() - }) - editLink.parentElement.parentElement.children.item(0).prepend(button) - } -})() \ No newline at end of file diff --git a/simple-scripts/mb-copy-tracklist-from-mora.user.js b/simple-scripts/mb-copy-tracklist-from-mora.user.js deleted file mode 100644 index 77269cc..0000000 --- a/simple-scripts/mb-copy-tracklist-from-mora.user.js +++ /dev/null @@ -1,36 +0,0 @@ -// ==UserScript== -// @name MB: copy tracklist from mora -// @namespace https://rinsuki.net -// @match https://mora.jp/package/* -// @grant none -// @version 1.0.1 -// @author rinsuki -// @license MIT -// @description generates Track List for MusicBrainz, from mora.jp page. -// ==/UserScript== - -const origFunc = Package.renderMain - -Package.renderMain = function() { - origFunc.call(this) - const json = Package.sfPackageData - console.log(json) - var tracks = "" - for (const track of json.trackList) { - tracks += `${track.trackNo}. ${track.title} - ${track.artistName} (${track.durationStr})\n` - } - if (tracks === "") return - const textarea = document.createElement("textarea") - textarea.value = tracks - textarea.style.width = "100%" - const desc = document.querySelector("#package_description") - desc.parentElement.insertBefore(textarea, desc) - tracks = "" - const rawLink = SFPath.getPackage(mountPoint, labelId, materialNo) + '/packageMeta.json' - const rawLinkAnchor = document.createElement("a") - rawLinkAnchor.href = rawLink - rawLinkAnchor.textContent = "packageMeta.json" - rawLinkAnchor.style.fontSize = "1rem" - rawLinkAnchor.style.textDecoration = "underline" - desc.parentElement.insertBefore(rawLinkAnchor, desc) -} \ No newline at end of file diff --git a/simple-scripts/mb-copy-tracklist-from-ototoy.user.js b/simple-scripts/mb-copy-tracklist-from-ototoy.user.js deleted file mode 100644 index 8a0db1f..0000000 --- a/simple-scripts/mb-copy-tracklist-from-ototoy.user.js +++ /dev/null @@ -1,38 +0,0 @@ -// ==UserScript== -// @name MB: copy tracklist from OTOTOY -// @namespace https://rinsuki.net -// @match https://ototoy.jp/_/default/p/* -// @grant none -// @version 1.0 -// @author rinsuki -// @license MIT -// @description generates Track List for MusicBrainz, from ototoy.jp album page. -// ==/UserScript== - -(() => { - const tracklist = document.getElementById("tracklist") - if (tracklist == null) return - var tracks = "" - function createTextarea() { - if (tracks === "") return - const textarea = document.createElement("textarea") - textarea.value = tracks - document.querySelector(".album-addendum").appendChild(textarea) - tracks = "" - } - for (const tr of tracklist.querySelectorAll("tr")) { - if (tr.classList.contains("disc-row")) { - createTextarea() - continue - } - const number = tr.querySelector("td:nth-child(1) canvas") - const title = tr.querySelector("td > span[id^=title-]") - if (title == null) continue - const artist = title.parentElement.querySelector("span > a.artist") - const time = tr.querySelector("td:nth-child(3)") - tracks += `${number.textContent.trim()}. ${title.textContent.trim()}` - if (artist != null) tracks += ` - ${artist.textContent.trim()}` - tracks += ` (${time.textContent.trim()})\n` - } - createTextarea() -})() \ No newline at end of file diff --git a/simple-scripts/mb-isrc-add-spotify-link.user.js b/simple-scripts/mb-isrc-add-spotify-link.user.js deleted file mode 100644 index 07687ca..0000000 --- a/simple-scripts/mb-isrc-add-spotify-link.user.js +++ /dev/null @@ -1,21 +0,0 @@ -// ==UserScript== -// @name MB: Add Spotify Search Link on ISRC page -// @namespace https://rinsuki.net -// @match https://*.musicbrainz.org/isrc/* -// @grant none -// @version 1.0 -// @author rinsuki -// @license MIT -// @description Adds Spotify search link on ISRC page. -// ==/UserScript== - -(() => { - const isrc = document.querySelector("#page > h1 > a[href^=\"/isrc\"] > bdi > code").textContent - const url = `https://open.spotify.com/search/` + encodeURIComponent(`isrc:${isrc}`) - const a = document.createElement("a") - a.href = url - a.textContent = "Search on Spotify" - a.target = "_blank" - const header = document.querySelector("#page > h1 > a[href^=\"/isrc/\"]").parentElement - header.parentElement.insertBefore(a, header.nextSibling) -})() \ No newline at end of file diff --git a/simple-scripts/mb-new-work-add-jasrac-and-nextone-field.user.js b/simple-scripts/mb-new-work-add-jasrac-and-nextone-field.user.js deleted file mode 100644 index d02e737..0000000 --- a/simple-scripts/mb-new-work-add-jasrac-and-nextone-field.user.js +++ /dev/null @@ -1,30 +0,0 @@ -// ==UserScript== -// @name MB (New Work): Add JASRAC and NexTone ID Fields -// @namespace https://rinsuki.net -// @match https://*.musicbrainz.org/dialog?path=%2Fwork%2Fcreate -// @grant none -// @version 1.0 -// @author rinsuki -// @license MIT -// @description Add JASRAC and NexTone ID Fields to New Work. -// ==/UserScript== - -addEventListener("load", async () => { - let workAttributesField - while (null == (workAttributesField = document.getElementById("work-attributes"))) { - await new Promise(r => setTimeout(r, 100)) - } - const addButton = workAttributesField.querySelector("#add-work-attribute") - addButton.click() - addButton.click() - while (Array.from(workAttributesField.querySelectorAll(`select[name="edit-work.attributes.1.type_id"] option`)).length < 10) { - await new Promise(r => setTimeout(r, 100)) - } - function selectSpecifiedOption(select, optionChecker) { - const option = Array.from(select.querySelectorAll("option")).find(optionChecker) - if (option == null) alert("cant find option!!") - select.value = option.value - } - selectSpecifiedOption(workAttributesField.querySelector(`select[name="edit-work.attributes.0.type_id"]`), option => option.textContent.trim() === "JASRAC ID") - selectSpecifiedOption(workAttributesField.querySelector(`select[name="edit-work.attributes.1.type_id"]`), option => option.textContent.trim() === "NexTone ID") -}) \ No newline at end of file diff --git a/simple-scripts/mobamas-puchikore-schedule-ics.user.js b/simple-scripts/mobamas-puchikore-schedule-ics.user.js deleted file mode 100644 index 0696f67..0000000 --- a/simple-scripts/mobamas-puchikore-schedule-ics.user.js +++ /dev/null @@ -1,156 +0,0 @@ -// ==UserScript== -// @name ぷちコレスケジュールをicsにするやつ -// @namespace https://rinsuki.net -// @match http://sp.pf.mbga.jp/12008305/ -// @grant none -// @version 1.0 -// @author rinsuki -// @description 2021/10/28 9:17:47 -// ==/UserScript== -// @ts-check - -(() => { - const tpl = document.getElementById("tpl_schedule") - if (tpl == null) return console.warn("schedule tpl is not found") - const scheduleButton = document.querySelector("#event_main_graphic a.js_schedule") - if (scheduleButton == null) return console.warn("schedule button is not found") - - const current = new Date() - /** - * @param {string | number} input - * @param {number} keta - * @returns {string} - */ - function padStart(input, keta = 2) { - return input.toString().padStart(keta, "0") - } - /** - * いい感じに年とかを推定して Date を作ってくれるようにしようと思ったけど後々のこと考えてobjectを返すようにした - * @param {number} month - * @param {number} day - * @param {number} hour - * @param {number} minute - */ - function createDate(month, day, hour, minute) { - // 12月〜1月みたいな跨ぎに対応したいという気持ちを込めたコード - const year = (current.getMonth()) > month ? current.getFullYear() + 1 : current.getFullYear() - const sec = minute === 0 ? 0 : 59 - return { - year, - month, - day, - hour, - minute, - sec, - } - } - - /** - * RFC 5545 の DATE-TIME (WITH LOCAL TIME) 形式文字列を作る - * @param {ReturnType} date - * @returns {string} - */ - function rfc5545String(date) { - return [ - padStart(date.year, 4), - padStart(date.month, 2), - padStart(date.day, 2), - "T", - padStart(date.hour, 2), - padStart(date.minute, 2), - padStart(date.sec, 2), - ].join("") - } - - const body = new DOMParser().parseFromString(tpl.innerHTML, "text/html").querySelector("body > div") - let month = -1, day = -1, rival = "千川ちひろ", round = -1, stages = [] - let rounds = [] - for (const _b of body.children) { - /** @type {HTMLElement} */ - // @ts-expect-error - const b = _b - const text = b.innerText - const matchDate = text.match(/【(\d+)\/(\d+)】第(\d+)ラウンド/) - if (matchDate != null) { - month = parseInt(matchDate[1], 10) - day = parseInt(matchDate[2], 10) - round = parseInt(matchDate[3], 10) - } - const matchStages = Array.from(text.matchAll(/\[第(\d)ステージ\]\s*(\d+):(\d+)\uFF5E(\d+):(\d+)/g)) - if (matchStages.length > 0) { - for (const matchStage of matchStages) { - stages.push({ - i: matchStage[1], - start: createDate(month, day, parseInt(matchStage[2], 10), parseInt(matchStage[3], 10)), - end: createDate(month, day, parseInt(matchStage[4], 10), parseInt(matchStage[5], 10)), - }) - } - } - const matchRival = text.match(/ライバルぷちデレラ: (.+?)\n.+、(Vo|Da|Vi)ア/s) - if (matchRival != null) { - rival = matchRival[1] - rounds.push({month,day,rival,round,stages, attr: matchRival[2]}) - // init - month = -1, day = -1, rival = "千川ちひろ", round = -1, stages = [] - } - } - if (rounds.length < 1) return console.warn("zero schedule") - let ics = [] - const firstStage = rounds[0].stages[0] - ics.push( - "BEGIN:VCALENDAR", - "VERSION:2.0", - "PRODID:-//rinsuki//mobamas-puchikore-schedule-parser//JP", - `X-WR-CALNAME:ぷちデレラコレクション (${firstStage.start.year}/${firstStage.start.month}/${firstStage.start.day}〜)` - ) - let lastAttr = "" - for (const round of rounds) { - for (const stage of round.stages) { - ics.push( - "BEGIN:VEVENT", - "UID:" + rfc5545String(stage.start) + "@mobamas-puchikore-schedule-ics.rinsuki.invalid", - "DTSTART;TZID=Asia/Tokyo:" + rfc5545String(stage.start), - "DTEND;TZID=Asia/Tokyo:" + rfc5545String(stage.end), - `SUMMARY:ぷちコレ第${round.round}R: ${round.rival}(${round.attr}) 第${stage.i}ステージ`, - "URL:http://sp.pf.mbga.jp/12008305/?guid=ON&url=http%3A%2F%2Fmobamas.net%2Fidolmaster%2Fevent_fashion%2Findex", - ) - // 通知: 属性違う時の1時間前 - if (lastAttr != round.attr) { - lastAttr = round.attr - ics.push( - "BEGIN:VALARM", - "TRIGGER:-PT1H", - "ATTACH;VALUE=URI:Chord", - "ACTION:AUDIO", - "END:VALARM", - ) - } - // 通知: 終了10分前 - // 本当は TRIGGER;RELATED=END:-PT10M で行けるはずなのだが Apple の Calendar.app が対応してないので泥臭い対応を… - const startmin = (stage.start.hour * 60) + stage.start.minute - let endmin = (stage.end.hour * 60) + stage.end.minute + 1 /* だいたい x9分なので */ - const before10min = endmin - startmin - 10 - ics.push( - "BEGIN:VALARM", - `TRIGGER:PT${before10min}M`, - "ATTACH;VALUE=URI:Chord", - "ACTION:AUDIO", - "END:VALARM", - ) - ics.push( - "END:VEVENT", "" - ) - } - } - ics.push("END:VCALENDAR") - const icsString = ics.join("\n") - const icsFilename = `ぷちデレラコレクション_${padStart(firstStage.start.year, 4)}${padStart(firstStage.start.month)}${padStart(firstStage.start.day)}.ics` - const icsFile = new File([icsString], icsFilename, { type: "text/calendar" }) - const link = document.createElement("a") - link.href = URL.createObjectURL(icsFile) - link.download = icsFilename - link.innerText = "🐒.icsファイル" - link.className = "info_link unlink" - link.style.textAlign = "center" - scheduleButton.parentElement.insertBefore(link, scheduleButton.nextElementSibling) -})() \ No newline at end of file diff --git a/simple-scripts/nicolive-comment-smoother.user.js b/simple-scripts/nicolive-comment-smoother.user.js deleted file mode 100644 index cbe753b..0000000 --- a/simple-scripts/nicolive-comment-smoother.user.js +++ /dev/null @@ -1,57 +0,0 @@ -// ==UserScript== -// @name Comment Smoother for niconico live -// @description ニコニコ生放送のコメントをぬるぬるにします -// @namespace https://rinsuki.net/ -// @match https://live.nicovideo.jp/watch/* -// @grant none -// @version 1.0 -// @author rinsuki -// @license MIT -// ==/UserScript== - -(() => { - if (!navigator.userAgent.includes("Firefox/")) return - const origGetter = HTMLVideoElement.prototype.__lookupGetter__("currentTime") - const origSetter = HTMLVideoElement.prototype.__lookupSetter__("currentTime") - class Smoother { - lastCurrentTime = 0 - lastNow = 0 - /** - * @param {HTMLVideoElement} elm - */ - currentTime(elm) { - const currentTime = origGetter.call(elm) - if (elm.paused) { - return currentTime - } - if (this.lastCurrentTime !== currentTime) { - // 更新された - this.lastCurrentTime = currentTime - this.lastNow = performance.now() - return currentTime - } - const now = performance.now() - const diff = now - this.lastNow - if (diff > 1000) { - // 一秒ずれてるというのはおかしい…ので帰る - return currentTime - } - const currentNow = this.lastCurrentTime + (diff / 1000) - return currentNow - } - } - if (origGetter != null && origSetter != null) { - HTMLVideoElement.prototype.__defineGetter__("currentTime", function() { - if (!("__userjs_smoother" in this)) { - console.log("new smoother", this) - this.__userjs_smoother = new Smoother() - } - return this.__userjs_smoother.currentTime(this) - }) - HTMLVideoElement.prototype.__defineSetter__("currentTime", function() { - origSetter.apply(this, arguments) - }) - } else { - console.log("[nicolive-comment-smoother]", "origGetter/origSetter is not available", origGetter, origSetter) - } -})() diff --git a/simple-scripts/niconico-embed-hq.user.js b/simple-scripts/niconico-embed-hq.user.js deleted file mode 100644 index 785e821..0000000 --- a/simple-scripts/niconico-embed-hq.user.js +++ /dev/null @@ -1,89 +0,0 @@ -// ==UserScript== -// @name embed.nicovideo.jp 1080p support -// @namespace https://rinsuki.net -// @match https://embed.nicovideo.jp/watch/* -// @grant GM_xmlhttpRequest -// @connect www.nicovideo.jp -// @version 1.0.1 -// @author rinsuki -// @description ニコニコ動画の埋め込みプレーヤーでの画質を上げます。ログインしていないと埋め込みプレーヤーでの視聴に失敗するようになるので注意してください。 -// ==/UserScript== -// @ts-check - -(() => { - if ((location.hash + location.search).includes("_userscript_skip_hq_hook_")) return - /** @type {{error: string} | {url: string}} */ - // @ts-expect-error - const IResponse = "you shouldn't use this as value" - function userland() { - const egui = document.getElementById("EMBED_HQ_USERSCRIPT") - const origFetch = window.fetch - window.fetch = (...args) => { - if (typeof args[0] !== "string") return origFetch(...args) - if (!args[0].startsWith("https://www.nicovideo.jp/api/watch/v3_guest/")) return origFetch(...args) - console.log("hooking request", ...args) - const reqdiv = document.createElement("div") - reqdiv.dataset.url = args[0].replace("/v3_guest/", "/v3/").replace("?_frontendId=70&", "?_frontendId=6&") + "&withoutHistory=true" - return new Promise((resolve, reject) => { - const observer = new MutationObserver(events => { - for (const event of events) { - if (event.attributeName !== "data-response") continue - /** @type {typeof IResponse} */ - const res = JSON.parse(reqdiv.dataset.response) - console.log("hooking request finish received", res) - if ("error" in res) { - reject(res.error) - } else { - resolve(fetch(res.url + "#" + args[0]).then(r => { - URL.revokeObjectURL(res.url) - return r - })) - } - observer.disconnect() - reqdiv.remove() - } - }) - observer.observe(reqdiv, { - attributes: true, - attributeFilter: ["data-response"] - }) - egui.appendChild(reqdiv) - }) - } - } - const egui = document.createElement("div") - egui.id = "EMBED_HQ_USERSCRIPT" - egui.style.display = "none !important" - const script = document.createElement("script") - script.innerHTML = `(${userland.toString()})()` - egui.appendChild(script) - const observer = new MutationObserver(events => { - for (const event of events) { - for (const node of event.addedNodes) { - if (!(node instanceof HTMLDivElement)) continue - const url = new URL(node.dataset.url).href - if (!url.startsWith("https://www.nicovideo.jp/api/watch/v3/")) { - return - } - console.log("hooking request received", url) - GM_xmlhttpRequest({ - method: "GET", - url, - responseType: "blob", - onload(r) { - console.log("hooking request finish", r) - const url = URL.createObjectURL(r.response) - node.dataset.response = JSON.stringify({url}) - }, - onerror(r) { - node.dataset.response = JSON.stringify({error: r.error}) - }, - }) - } - } - }) - observer.observe(egui, { - childList: true, - }) - document.body.appendChild(egui) -})() \ No newline at end of file diff --git a/simple-scripts/niconico-pip.user.js b/simple-scripts/niconico-pip.user.js deleted file mode 100644 index 393bc44..0000000 --- a/simple-scripts/niconico-pip.user.js +++ /dev/null @@ -1,47 +0,0 @@ -// ==UserScript== -// @name niconico pip (ボツ) -// @namespace https://rinsuki.net -// @match https://www.nicovideo.jp/watch/* -// @grant none -// @version 1.0 -// @author rinsuki -// @description ボツ案 -// ==/UserScript== -// @ts-check - -(() => { - function start() { - const buttonContainer = document.querySelector(".ControllerContainer-area:last-child") - if (buttonContainer == null) return - const inputVideo = document.querySelector("video") - const commentCanvas = document.querySelector(".CommentRenderer").querySelector("canvas") - const pipVideo = document.createElement("video") - pipVideo.muted = true - pipVideo.srcObject = commentCanvas.captureStream() - pipVideo.style.position = "fixed" - pipVideo.style.bottom = "0" - pipVideo.style.left = "0" - pipVideo.style.width = "160px" - pipVideo.style.height = "90px" - pipVideo.play() - pipVideo.controls = true - document.body.appendChild(pipVideo) - const origClearRect = CanvasRenderingContext2D.prototype.clearRect - CanvasRenderingContext2D.prototype.clearRect = function(x, y, w, h) { - if (this.canvas != commentCanvas || pipVideo.paused) return origClearRect.call(this, x, y, w, h) - this.drawImage(inputVideo, 0, 0, inputVideo.videoWidth, inputVideo.videoHeight, x, y, w, h) - } - } - const button = document.createElement("button") - button.onclick = () => { - button.remove() - start() - } - button.className = "ActionButton ControllerButton" - button.textContent = "PiP" - button.dataset.title = "PiPを有効にする" - button.style.color = "white" - button.style.lineHeight = "18px" - const container = document.querySelector(".ControllerContainer-area:last-child") - container.insertBefore(button, container.firstElementChild) -})() \ No newline at end of file diff --git a/simple-scripts/niconico-preview-player.user.js b/simple-scripts/niconico-preview-player.user.js deleted file mode 100644 index d8b07cf..0000000 --- a/simple-scripts/niconico-preview-player.user.js +++ /dev/null @@ -1,139 +0,0 @@ -// ==UserScript== -// @name niconico instant play -// @description なんか判定が微妙なので使わないように -// @namespace https://rinsuki.net/ -// @match https://www.nicovideo.jp/tag/* -// @grant none -// @version 1.0 -// @author rinsuki -// ==/UserScript== - -(() => { - function generateActionTrackID() { - let randomPart = "" - const parts = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - for (let i=0; i<10; i++) { - randomPart += parts[Math.floor(Math.random() * parts.length)] - } - return `${randomPart}_${Date.now()}` - } - - /** @type {NodeListOf} */ - const thumbWraps = document.querySelectorAll(".column.main .item:not(.nicoadVideoItem) a.itemThumbWrap") - for (const thumbWrap of thumbWraps) { - const videoIdArr = /\/watch\/([a-z0-9]+)/.exec(thumbWrap.pathname) - if (videoIdArr == null) continue - const videoId = videoIdArr[1] - let timer = null - /** @type {HTMLVideoElement} */ - let video = null - let session = null - let hoverElem = thumbWrap - while (!hoverElem.classList.contains("item")) { - hoverElem = hoverElem.parentElement - } - hoverElem.addEventListener("mouseenter", () => { - timer = setTimeout(() => { - // plays - const apiURL = new URL("https://www.nicovideo.jp/api/watch/v3_guest/" + videoId) - apiURL.searchParams.append("_frontendId", "6") - apiURL.searchParams.append("_frontendVersion", "0") - apiURL.searchParams.append("actionTrackId", generateActionTrackID()) - apiURL.searchParams.append("noSideEffect", "true") - apiURL.searchParams.append("skips", "harmful") - fetch(apiURL).then(res => res.json()).then(json => { - console.log(json) - const { delivery } = json.data.media - if (delivery.encryption != null) return - fetch("https://api.dmc.nico/api/sessions?_format=json", { - method: "POST", - body: JSON.stringify({ - session: { - client_info: { - player_id: delivery.movie.session.playerId, - }, - content_auth: { - auth_type: "ht2", - content_key_timeout: delivery.movie.session.contentKeyTimeout, - service_id: "nicovideo", - service_user_id: delivery.movie.session.serviceUserId, - }, - content_id: delivery.movie.session.contentId, - content_src_id_sets: [ - { - content_src_ids: [ - { - src_id_to_mux: { - audio_src_ids: [delivery.movie.audios.sort((a, b) => a.metadata.bitrate - b.metadata.bitrate)[0].id], - video_src_ids: [delivery.movie.videos.sort((a, b) => a.metadata.bitrate - b.metadata.bitrate)[0].id], - } - } - ] - } - ], - content_type: "movie", - contnet_uri: "", - keep_method: { - heartbeat: { - lifetime: delivery.movie.session.heartbeatLifetime, - } - }, - priority: delivery.movie.session.priority, - protocol: { - name: "http", - parameters: { - http_parameters: { - parameters: { - http_output_download_parameters: { - transfer_preset: "", - } - } - } - } - }, - recipe_id: delivery.movie.session.recipeId, - session_operation_auth: { - session_operation_auth_by_signature: { - signature: delivery.movie.session.signature, - token: delivery.movie.session.token, - } - }, - timing_constraint: "unlimited" - }, - }) - }).then(r => r.json()).then(json => { - session = json.data.session - video = document.createElement("video") - video.src = session.content_uri - video.style.position = "absolute" - video.style.top = 0 - video.style.left = 0 - video.style.width = "100%" - video.style.aspectRatio = "16 / 9" - video.style.zIndex = 2147483646 - video.style.opacity = 0 - video.style.transition = "0.5s opacity" - video.volume = 0.25 - video.autoplay = true - thumbWrap.appendChild(video) - video.addEventListener("playing", () => { - video.style.opacity = 1 - }) - }) - }) - }, 1000); - }) - hoverElem.addEventListener("mouseleave", () => { - clearTimeout(timer) - video?.remove() - if (session != null) { - fetch("https://api.dmc.nico/api/sessions/" + session.id + "?_format=json&_method=DELETE", { - method: "POST", - body: JSON.stringify({ - session, - }) - }) - } - }) - } -})() \ No newline at end of file diff --git a/simple-scripts/swarm-export-history.user.js b/simple-scripts/swarm-export-history.user.js deleted file mode 100644 index dcb68d6..0000000 --- a/simple-scripts/swarm-export-history.user.js +++ /dev/null @@ -1,61 +0,0 @@ -// ==UserScript== -// @name Swarm Export History -// @namespace https://rinsuki.net -// @match https://*.swarmapp.com/history -// @version 1.0 -// @author - -// @description export history -// @grant GM_registerMenuCommand -// @grant unsafeWindow -// @grant GM_xmlhttpRequest -// ==/UserScript== - -// @ts-check - -(function () { - // @ts-ignore - const callSwarmAPI = (path, options) => new Promise((resolve, reject) => unsafeWindow.fourSq.api.services.service_(path, options, { - success: resolve, - error: reject, - })) - /** - * - * @param {Window} window - */ - async function main(window) { - await new Promise(resolve => window.onload = resolve) - function appendText(text) { - const pre = window.document.createElement("div") - pre.textContent = text - window.document.body.appendChild(pre) - } - appendText("Exporting...") - // @ts-ignore - const { LOCALE: locale, USER_PROFILE: { id: userID } } = unsafeWindow.fourSq.config.user - let items = [] - while (true) { - const [res] = await Promise.all([callSwarmAPI(`v2/users/${userID}/historysearch`, { - offset: items.length, - limit: 50, - m: "swarm", - clusters: "false", - sort: "newestfirst", - }), new Promise(resolve => setTimeout(resolve, 8000))]) - items.push(...res.response.checkins.items) - appendText(`Got ${res.response.checkins.items.length} items, total ${items.length} items.`) - if (res.response.checkins.items.length === 0) break - } - const file = new File([JSON.stringify(items, null, 2)], `swarm_export.${userID}.${new Date().toISOString().replace(/[^0-9T]/g, "")}.json`, { type: "application/json" }) - const url = URL.createObjectURL(file) - const a = window.document.createElement("a") - a.href = url - a.download = file.name - window.document.body.appendChild(a) - a.click() - } - GM_registerMenuCommand("Export History", () => { - const progress = window.open("about:blank", "progress", "width=800,height=600") - if (progress == null) return alert("please allow popup") - main(progress) - }) -})() \ No newline at end of file diff --git a/simple-scripts/tweetdeck-revive-fav.user.js b/simple-scripts/tweetdeck-revive-fav.user.js deleted file mode 100644 index 6705988..0000000 --- a/simple-scripts/tweetdeck-revive-fav.user.js +++ /dev/null @@ -1,48 +0,0 @@ -// ==UserScript== -// @name TweetDeck ふぁぼ復活 -// @namespace https://rinsuki.net -// @match https://tweetdeck.twitter.com/* -// @version 1.0 -// @author - -// @description for old tweetdeck -// @run-at document-start -// ==/UserScript== - -function hook() { - const xp = XMLHttpRequest.prototype - const origOpen = xp.open - xp.open = function (method, url, ...args) { - if (url !== "https://api.twitter.com/1.1/favorites/create.json") { - return origOpen.apply(this, [method, url, ...args]) - } - console.info("hooked") - origOpen.apply(this, [method, "https://api.twitter.com/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet", ...args]) - const origSend = this.send - this.send = function (data) { - const d = new URLSearchParams(data) - console.info("hook like") - origSend.call(this, JSON.stringify({ - variables: { - tweet_id: d.get("id"), - } - })) - } - // override setRequestHeader to application/json - const origSetRequestHeader = this.setRequestHeader - this.setRequestHeader = function (key, value) { - // key to lowercase - if (key.toLowerCase() === "content-type") { - return origSetRequestHeader.call(this, key, "application/json") - } - return origSetRequestHeader.call(this, key, value) - } - } -} - -if ("wrappedJSObject" in window) { - console.info("hook for firefox") - window.wrappedJSObject.eval(`(${hook.toString().replace(" hook", "")})()`) -} else { - console.info("hook for chromium") - hook() -} \ No newline at end of file diff --git a/simple-scripts/tweetdeck-revive.user.js b/simple-scripts/tweetdeck-revive.user.js deleted file mode 100644 index fe20e3e..0000000 --- a/simple-scripts/tweetdeck-revive.user.js +++ /dev/null @@ -1,47 +0,0 @@ -// ==UserScript== -// @name TweetDeck 延命 -// @namespace https://rinsuki.net -// @match https://tweetdeck.twitter.com/* -// @version 1.0.1 -// @author - -// @description for old tweetdeck -// @run-at document-start -// ==/UserScript== - -function hook() { - const xp = XMLHttpRequest.prototype - const config = Object.getOwnPropertyDescriptor(xp, "responseText") - let selfDestructTimer - const myConfig = { - ...config, - get() { - const orig = config.get.bind(this)() - if (this.responseURL.startsWith("https://api.twitter.com/1.1/help/settings.json")) { - const obj = JSON.parse(orig) - console.info("original settings", obj) - obj.config.tweetdeck_graphql_login = { value: true } - const newResponseText = JSON.stringify(obj) - console.info("feature flag hooked", newResponseText) - if (selfDestructTimer != null) clearTimeout(selfDestructTimer) - selfDestructTimer = setTimeout(() => { - if (Object.getOwnPropertyDescriptor(xp, "responseText").get === myConfig.get) { - console.info("it seems getter is not overrided, so hook will self-destruct...") - Object.defineProperty(xp, "responseText", config) - } - }, 1000) - return newResponseText - } - console.info("unrelated request...") - return orig - } - } - Object.defineProperty(xp, "responseText", myConfig) -} - -if ("wrappedJSObject" in window) { - console.info("hook for firefox") - window.wrappedJSObject.eval(`(${hook.toString().replace(" hook", "")})()`) -} else { - console.info("hook for chromium") - hook() -} \ No newline at end of file diff --git a/simple-scripts/youtube-embed-storage-access-api.user.js b/simple-scripts/youtube-embed-storage-access-api.user.js deleted file mode 100644 index 27d7030..0000000 --- a/simple-scripts/youtube-embed-storage-access-api.user.js +++ /dev/null @@ -1,45 +0,0 @@ -// ==UserScript== -// @name YouTube Embed: Use Storage Access API -// @namespace https://rinsuki.net -// @match https://www.youtube.com/embed/* -// @grant none -// @version 1.0 -// @author rinsuki -// @license MIT -// @description Use Storage Access API to Play YouTube (Music)? Premium Required Videos on Embed Player -// ==/UserScript== - -if ("requestStorageAccess" in document) { - document.requestStorageAccess().then(()=> { - console.warn("[UserScript] --- requestStorageAccess success ---") - }, () => { - console.warn("[UserScript] --- requestStorageAccess failed ---") - }) - - const origOpen = XMLHttpRequest.prototype.open - const origSend = XMLHttpRequest.prototype.send - const weakMap = new WeakMap() - - // addEventListener("mouseenter", () => document.requestStorageAccess()) - - XMLHttpRequest.prototype.open = function(method, url, ...args) { - weakMap.set(this, url) - return origOpen.apply(this, [method, url, ...args]) - } - - XMLHttpRequest.prototype.send = function(...args) { - const url = weakMap.get(this) - if (!url.includes("youtubei/v1/player")) { - return origSend.apply(this, args) - } - document.requestStorageAccess().then(()=> { - console.warn("[UserScript] --- requestStorageAccess success ---") - }, () => { - console.warn("[UserScript] --- requestStorageAccess failed ---") - }).then(() => { - origSend.apply(this, args) - }) - } -} else { - console.warn("[UserScript] -- requestStorageAccess not found --") -} \ No newline at end of file diff --git a/simple-scripts/ytm-mb-checker.user.js b/simple-scripts/ytm-mb-checker.user.js deleted file mode 100644 index c632c76..0000000 --- a/simple-scripts/ytm-mb-checker.user.js +++ /dev/null @@ -1,107 +0,0 @@ -// ==UserScript== -// @name YTM: MusicBrainz Checker -// @namespace Violentmonkey Scripts -// @match https://music.youtube.com/* -// @grant GM_notification -// @grant GM_openInTab -// @grant GM_registerMenuCommand -// @grant GM_openInTab -// @version 1.0 -// @author - -// @description Check Currently Playing Album is Registered in YouTube Music -// ==/UserScript== - -// @ts-check - -(() => { - const contentInfoWrapper = document.querySelector("ytmusic-player-bar > .middle-controls > .content-info-wrapper") - if (contentInfoWrapper == null) return alert("Failed to find .content-info-wrapper") - console.log("Starting YTM: MusicBrainz Checker", contentInfoWrapper) - - const browseIdToPlaylistIdMap = new Map() - /** - * @param {string} browseId - */ - async function browseIdToPlaylistId(browseId) { - if (browseIdToPlaylistIdMap.has(browseId)) { - return browseIdToPlaylistIdMap.get(browseId) - } - const r = await fetch("https://music.youtube.com/youtubei/v1/browse?key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30&prettyPrint=false", { - method: "POST", - body: JSON.stringify({ - browseId, - context: { - client: { - clientName: "WEB_REMIX", - clientVersion: "1.20211221.01.00" - } - } - }), - }).then(res => res.json()) - const url = new URL(r.microformat.microformatDataRenderer.urlCanonical) - const playlistId = url.searchParams.get("list") - if (url.pathname !== "/playlist" || playlistId == null) throw new Error("??? Unknown URL " + url) - browseIdToPlaylistIdMap.set(browseId, playlistId) - return playlistId - } - const mbExistsCache = new Set() - let currentPlaylistId = null - GM_registerMenuCommand("Open Album in MusicBrainz", () => { - const urlFind = new URL("https://musicbrainz.org/otherlookup/url") - urlFind.searchParams.set("other-lookup.url", "https://music.youtube.com/playlist?list=" + currentPlaylistId) - GM_openInTab(urlFind.href) - }) - const observer = new MutationObserver(events => { - /** @type { HTMLAnchorElement | null } */ - const browseLink = contentInfoWrapper.querySelector(`a[href*="browse/"]`) - if (browseLink == null) { - console.log("can't find browseLink") - currentPlaylistId = null - return - } - const albumTitle = browseLink.textContent - if (albumTitle == null) { - alert("albumTitle is null") - currentPlaylistId = null - return - } - const browseId = /^\/browse\/([^?/]+)$/.exec(browseLink.pathname)?.[1] - if (browseId == null) return - if (!browseId.startsWith("MPREb_")) { - console.log(`Unknown browseId ${browseId}`) - currentPlaylistId = null - return - } - (async () => { - try { - const playlistId = await browseIdToPlaylistId(browseId) - const playlistURL = `https://music.youtube.com/playlist?list=${playlistId}` - const endpoint = new URL("https://musicbrainz.org/ws/2/url") - endpoint.searchParams.set("resource", playlistURL) - const r = await fetch(endpoint.href, { - method: "HEAD" - }) - if (r.status === 404) { - GM_notification(`アルバム「${albumTitle}」は MusicBrainz に登録されていません`, "YTM: MusicBrainz Checker", undefined, () => { - const url = new URL("https://musicbrainz.org/search?type=release&limit=25&method=indexed") - url.searchParams.set("query", albumTitle) - GM_openInTab(url.href) - }) - currentPlaylistId = null - } else if (r.status < 300) { - console.log("Found in MusicBrainz", albumTitle, playlistId) - currentPlaylistId = playlistId - mbExistsCache.add(playlistId) - } - } catch(e) { - console.error("FAIL!!!", e) - } - })() - }) - observer.observe(contentInfoWrapper, { - subtree: true, - childList: true, - attributes: true, - attributeFilter: ["href"], - }) -})() \ No newline at end of file diff --git a/simple-styles/mb-make-some-headers-sticky-on-edit-release-rels.user.css b/simple-styles/mb-make-some-headers-sticky-on-edit-release-rels.user.css deleted file mode 100644 index 89e98c8..0000000 --- a/simple-styles/mb-make-some-headers-sticky-on-edit-release-rels.user.css +++ /dev/null @@ -1,42 +0,0 @@ -/* ==UserStyle== -@name MB: make some headers sticky on edit release relationships -@namespace rinsuki.net -@version 1.0.0 -@author rinsuki -==/UserStyle== */ - -@-moz-document regexp("https?://(?:[^/]+\\.)?musicbrainz.org/release/(?:[0-9a-f-]+)/edit-relationships") { - /* ここにコードを挿入... */ - #batch-tools { - position: sticky; - top: 0; - background: white; - border-bottom: 1px solid #888; - } - #tracklist > thead { - position: sticky; - top: calc(1.9em + 1px); - } - #tracklist > tbody:has(tr.subh)::before { - /* fake border */ - position: absolute; - background: #888; - height: 1px; - width: 100%; - top: 0px; - content: " "; - } - #tracklist > tbody:has(tr.subh)::after { - /* fake border */ - position: absolute; - background: #888; - height: 1px; - width: 100%; - bottom: 0px; - content: " "; - } - #tracklist > tbody:has(tr.subh) { - position: sticky; - top: calc(1.9em + 1px + 14px + 6px + 0.4em); - } -} diff --git a/simple-styles/niconico-nicobetter.user.css b/simple-styles/niconico-nicobetter.user.css deleted file mode 100644 index 727ba79..0000000 --- a/simple-styles/niconico-nicobetter.user.css +++ /dev/null @@ -1,195 +0,0 @@ -/* ==UserStyle== -@name nvbetter -@namespace rinsuki.net -@version 1.0.7 -@description ニコニコ動画の新HTML5プレーヤーを旧HTML5プレーヤーっぽい配置にしたりします -@author rinsuki -==/UserStyle== */ - -@-moz-document url-prefix("https://www.nicovideo.jp/watch/") { - /* ヘッダー追従を切る */ - #root > div > header.h_webHeader\.height { - position: relative !important; - - &:hover > div:has(a[href*="/feedback/video_watch"]) { - & > a { - /* こんな状態で本家にフィードバック送られても困る */ - display: none; - } - &:after { - content: "nvbetterにより視聴ページのスタイルが変更されているため、nvbetterによってフィードバックボタンは無効化されています。"; - } - } - } - - /* 動画再生部分の border-radius を無効化 */ - .grid-area_\[player\] > div > div.bdr_m { - border-radius: unset !important; - } - - .grid-template-areas_\[_\"player_sidebar\"_\"meta_sidebar\"_\"bottom_sidebar\"_\"\._sidebar\"_\] { - /* 動画情報を上に持ってくる */ - grid-template-areas: - "meta meta" - "player sidebar" - "bottom sidebar" - ". sidebar" !important; - - /* 旧プレーヤーのようにリサイズ時にガタガタさせる */ - --watch-player-max-width: 640px !important; - - /* 854pxの動画が入るなら 854px まで大きくする */ - @media screen and (min-width: calc(854px + 440px + calc(24px * 3) + 15px)) { - & { - --watch-player-max-width: 854px !important; - } - } - /* 960pxの動画が入るなら 960px まで大きくする */ - @media screen and (min-width: calc(960px + 440px + calc(24px * 3) + 15px)) { - & { - --watch-player-max-width: 960px !important; - } - } - /* 1280pxの動画が入るなら 1280px まで大きくする */ - @media screen and (min-width: calc(1280px + 440px + calc(24px * 3) + 15px)) { - & { - --watch-player-max-width: 1280px !important; - } - } - /* 1920pxの動画が入るなら 1920px まで大きくする */ - @media screen and (min-width: calc(1920px + 440px + calc(24px * 3) + 15px)) { - & { - --watch-player-max-width: 1920px !important; - } - } - } - - section > .grid-area_\[meta\] { - /* タグ編集行 */ - & > div:has(> button[data-element-name="tag_edit"]) { - /* を最後に持ってくる */ - order: 1; - - /* タグ編集ボタンを最初に持ってくる */ - & > button[data-element-name="tag_edit"] { - order: -1; - } - } - } - /* 動画詳細を開いた状態 */ - section > .grid-area_\[meta\]:has(> section > header + div) { - & > div:first-child { - /* 動画タイトルの下の再生数とかを隠す */ - & > div:first-child > h1 + div { - display: none; - } - /* 動画タイトルの右のユーザー情報を隠す */ - & > div:first-child:has(h1) + div { - display: none; - } - } - & > section { - /* 開いたら畳ませない */ - & > header { - display: none; - } - & > div { - /* 動画詳細をインライン展開っぽく見せる */ - background: none; - border-top: none; - padding: 0; - - /* 突然のgrid投入 */ - display: grid; - grid-template: - "desc desc" auto - "user info" auto - "user genre" auto - / 400px 1fr; - - /* 説明欄grid追従 */ - & > .cursor_default { - grid-area: desc; - } - - /* ユーザー欄grid追従*/ - & > .pb_x2 { - grid-area: user; - } - - /* grid で並び換えた影響でhrの意味がなくなったので消す */ - & > hr { - display: none; - } - - /* 背景消したせいでボーダーが見えなくなるので再導入 */ - & > dl > div { - border-color: var(--colors-border-high-em); - } - - /* ジャンル・シリーズ情報は横に並べる */ - & > dl + div { - flex-direction: row; - } - } - } - } - - - /* 下のほうにあるやつを白背景にしない */ - section > .grid-area_\[bottom\] > section { - background: none; - padding-left: 0; - padding-right: 0; - padding-top: var(--spacing-x2); - } - - /* コメントリストが開かれている時に発動する */ - section:has(> .grid-area_\[sidebar\] > div > div:first-child > section > header + div) { - & > .grid-area_\[sidebar\] > div > .h_var\(--watch-player-height\) { - /* コメントリストをプレーヤーの高さと合わせる */ - height: calc(var(--watch-player-height) + var(--watch-controller-height)); - /* 上でコメント入力欄を右まで伸ばした分の受け入れスペースを作る */ - margin-bottom: calc(40px + var(--watch-player-actionbar-gap-height) - var(--spacing-x0_5)); - } - - /* 動画プレーヤー下のコメント入力欄のセレクタ */ - & > .grid-area_\[player\] > div:not(.w_\[100dvw\]) > div:last-child { - /* 動画プレーヤー下のコメント入力欄とかを右まで伸ばす */ - margin-right: calc(-1 * calc(var(--spacing-x3) + var(--watch-sidebar-width))); - - /* コメント入力欄の横幅をプレーヤーと合わせる */ - & > div.w_100\% { - width: var(--watch-player-width) !important; - flex-grow: 0 !important; - } - - /* コメント入力欄の右にあるボタン郡を伸ばす */ - & > div:last-child { - flex: 1; - & > div { - /* コメント入力欄の右にあるボタン郡を伸ばす */ - max-width: unset !important; - - & > button { - /* コメント入力欄の右にあるボタン郡をテキトーに伸ばす (説明消すとそれはそれで幅が余るので) */ - flex-grow: 1; - - & > svg+span { - /* コメント入力欄の右にあるボタン郡で、アイコンがあるボタンの説明は消す */ - display: none; - } - } - } - } - } - } - - /* フルスクリーン時にコメント入力欄の透過をする */ - section > .grid-area_\[player\] > div.w_\[100dvw\] > div:only-child > div:only-child > div:last-child > div:last-child { - background: none; - & > div > div > button { - background: var(--colors-layer-background); - } - } -} diff --git a/simple-styles/niconico-show-full-nicoru-count.user.css b/simple-styles/niconico-show-full-nicoru-count.user.css deleted file mode 100644 index d016ebb..0000000 --- a/simple-styles/niconico-show-full-nicoru-count.user.css +++ /dev/null @@ -1,17 +0,0 @@ -/* ==UserStyle== -@name niconico show full nicoru count -@namespace github.com/rinsuki -@version 1.0.0 -@description ニコるのカウントを略さず表示します -@author rinsuki -==/UserStyle== */ - -@-moz-document url-prefix("https://www.nicovideo.jp/watch/") { - span.NicoruCell-count[data-nicoru-count]::before { - content: attr(data-nicoru-count); - padding-right: 10em; - } - .CommentPanelDataGrid-TableCell[data-name="nicoruCount"] { - padding: 0 4px; - } -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index e7be4b6..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "compilerOptions": { - /* Basic Options */ - "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ - "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ - "types": ["tampermonkey"], - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./dist/", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - "importHelpers": false, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - "moduleResolution": "bundler", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - "incremental": true, - "paths": { - "#common/*": ["./scripts/_common/*"] - } - } -} \ No newline at end of file diff --git a/yt-select-codec.user.js b/yt-select-codec.user.js new file mode 100644 index 0000000..4437cf9 --- /dev/null +++ b/yt-select-codec.user.js @@ -0,0 +1,75 @@ +// ==UserScript== +// @name YouTube: Select Codec +// @description Select the codec for YouTube videos. +// @grant none +// @namespace https://rinsuki.net +// @match https://www.youtube.com/* +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function getInternalPlayerObject() { + const playerElm = document.getElementById("movie_player"); + if (!playerElm) + return; + if (!("getPlayerResponse" in playerElm)) + return; + if (typeof playerElm.getPlayerResponse !== "function") + return; + let internalPlayer; + const oldApply = Function.prototype.apply; + const newApply = new Proxy(Function.prototype.apply, { + apply(target, thisArg, args) { + if (internalPlayer == null) + internalPlayer = args[0]; + return Reflect.apply(target, thisArg, args); + }, + }); + Function.prototype.apply = newApply; + playerElm.getPlayerResponse(); + if (Function.prototype.apply === newApply) + Function.prototype.apply = oldApply; + return internalPlayer; + } + const button = document.createElement("button"); + button.style.zIndex = "2147483647"; + button.style.position = "fixed"; + button.style.top = "0px"; + button.style.right = "0px"; + button.innerText = "SC"; + document.body.appendChild(button); + button.addEventListener("click", () => { + const player = getInternalPlayerObject(); + if (player == null) { + alert("Failed to get the internal player object."); + return; + } + const adaptiveFormats = player.getPlayerResponse().streamingData.adaptiveFormats; + const videoFormats = adaptiveFormats.filter(format => "qualityLabel" in format); + const itag = prompt([ + "Select itag:", + "", + ...videoFormats + .toSorted((a, b) => { + if (a.qualityLabel.length !== b.qualityLabel.length) + return b.qualityLabel.length - a.qualityLabel.length; + if (a.width !== b.width) + return b.width - a.width; + return b.bitrate - a.bitrate; + }) + .map(format => `${format.itag}: ${format.qualityLabel} (${format.mimeType}, ${format.width}x${format.height}, ${format.fps}fps, ${format.bitrate}bps)`), + ].join("\n")); + if (itag == null) + return; + const selectedFormat = videoFormats.find(format => format.itag === Number(itag)); + if (!selectedFormat) { + alert("Invalid itag."); + return; + } + player.setPlaybackQuality(selectedFormat.quality, selectedFormat.itag.toString()); + }); + +})(); diff --git a/ytm-dont-suggest-video.user.js b/ytm-dont-suggest-video.user.js new file mode 100644 index 0000000..f0a7a0f --- /dev/null +++ b/ytm-dont-suggest-video.user.js @@ -0,0 +1,80 @@ +// ==UserScript== +// @name YTM: Don't Suggest Video +// @namespace https://rinsuki.net +// @match https://music.youtube.com/* +// @grant none +// @author rinsuki +// @run-at document-start +// @version 0.1.0 +// @description Hide video-only tracks from "Up Next" tab on YTM. +// @contributionURL https://github.com/sponsors/rinsuki +// @homepageURL https://github.com/rinsuki/userscripts +// @supportURL https://github.com/rinsuki/userscripts/issues +// ==/UserScript== + +(function () { + 'use strict'; + + function urlFromFetch(input) { + if (typeof input === "string") + return input; + if ("url" in input) + return input.url; + return input.href; + } + function shouldFilterThisEntry(json) { + if (json.playlistPanelVideoWrapperRenderer) + return true; // たぶん + if (json.automixPreviewVideoRenderer) + return true; + const thumbnails = json.playlistPanelVideoRenderer?.thumbnail?.thumbnails; + if (!Array.isArray(thumbnails)) { + debugger; + return true; // ? + } + for (const thumbnail of thumbnails) { + if (typeof thumbnail.url === "string" && thumbnail.url.includes("i.ytimg.com")) { + // debugger; + return false; + } + } + return true; + } + function handleNextResponse(json) { + if (Array.isArray(json.continuationContents?.playlistPanelContinuation?.contents)) { + json.continuationContents.playlistPanelContinuation.contents = json.continuationContents.playlistPanelContinuation.contents.filter(shouldFilterThisEntry); + } + if (Array.isArray(json.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs)) { + for (const tab of json.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs) { + if (Array.isArray(tab.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents)) { + tab.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer.contents = tab.tabRenderer.content.musicQueueRenderer.content.playlistPanelRenderer.contents.filter(shouldFilterThisEntry); + } + } + } + } + window.fetch = new Proxy(window.fetch, { + apply(target, thisArg, args) { + const promise = Reflect.apply(target, thisArg, args); + const url = urlFromFetch(args[0]); + if (!url.includes("/next")) + return promise; + return promise.then(async (resOriginal) => { + const res = resOriginal.clone(); + try { + const json = await res.json(); + handleNextResponse(json); + return Response.json(json, { + status: resOriginal.status, + statusText: resOriginal.statusText, + headers: resOriginal.headers, + }); + } + catch (e) { + debugger; + return resOriginal; + } + }); + } + }); + +})();