Tweetの画像をGyazoにuploadするUserScript
機能
yesterday?cFQ2f7LRuLYP.icon
機能=昨日takker.icon
スクリプトを実行したprojectで貼り替えられますtakker.icon
自分のとこで実行すれば、自分のとこにあるtwitterの画像がすべてgyazoられる
使い方
requirements
admin以上の権限
2. gyazoに画像を移動したいprojectをブラウザで開く
3. 開発コンソールを開いて1.のコードを貼り付けて実行する
4. おわり
井戸端には900枚くらいtwitter由来の画像があった
いくつかはリンク切れしてる
とりあえずすべて自分のgyazoアカウントに取り込みます
todo
動画もアップロードする
できなくはないけど、容量を使い切ってしまいそう
2023-07-07 07:53:48 古いページから更新するようにした
2023-07-05 18:55:48 完成した。動かしてます
一時的にDate modifiedが死にますが堪忍ください
もう一回実行しよう
リンク切れしている画像があるときにプログラムが止まってしまっていた
直す
元画像を取り出す
これでは取り出せなかった
refererURLにtweetのURLを紐づけたいけど、方法が無いので断念する
代わりにスクボのページへのリンクをつけておく
code:script.ts
import { uploadAndReplace } from "./replace.ts";
import { upload } from "../../takker/scrapbox-file-uploader/mod.ts";
import { parse, Node } from "../../takker/scrapbox-parser/mod.ts";
import { Scrapbox } from "../../takker/scrapbox-jp%2Ftypes/userscript.ts";
import { getPage, exportPages, patch, Socket, makeSocket, disconnect } from "../../takker/scrapbox-userscript-std/mod.ts";
declare const scrapbox: Scrapbox;
const getTargetedPages = async (project: string): Promise<string[]> => {
const result = await exportPages(scrapbox.Project.name, { metadata: false });
if (!result.ok) {
alert(${result.value.name} ${result.value.message});
throw Error(result.value.name);
}
return result.value.pages.flatMap(
(page) => page.lines.some((line) => line.includes("pbs.twimg.com/media")) ? page.title : [] );
};
const main = async () => {
const project = scrapbox.Project.name;
// 古いページから更新する
const list = (await getTargetedPages(project)).reverse();
let socket: Socket | undefined;
try {
for (let i = 0; i < list.length; i++) {
const result = await getPage(project, title);
if (!result.ok) continue;
const stack: [string, URL[]][] = [];
const blocks = parse(result.value.lines.map((line) => line.text).join("\n"), { hasTitle: true });
let lineNo = 0;
let total = 0;
for (const block of blocks) {
switch (block.type) {
case "title":
lineNo++;
break;
case "codeBlock":
lineNo += block.content.split("\n").length + 1;
break;
case "table":
lineNo += block.cells.length + 1;
break;
case "line": {
const urls = block.nodes.flatMap((node) => getURLs(node));
if (urls.length > 0) {
total += urls.length;
stack.push([result.value.lineslineNo.id, urls]); }
lineNo++;
break;
}
}
}
if (total === 0) continue;
console.debug([${i}/${list.length}] replace ${total} links in "/${project}/${result.value.title}");
let counter = 0;
for (const url of urls) {
const permalink = await uploadAndReplace(project, result.value.title, hash, url);
if (!permalink) continue;
console.debug([${i}/${list.length}][${counter}/${total}] ${url} => ${permalink});
pairs.push([${url}, permalink]);
counter++;
}
}
本当は外部リンク記法内のURLのみを変換すべきだが、面倒なので文字列置換してしまう
code:script.ts
socket ??= await makeSocket();
await patch(project, result.value.title, (lines) => lines.map((line) => {
let text = line.text;
for (const b, a of pairs) { text = text.replaceAll(b, a);
}
return text;
}),
{ socket }
);
console.debug([${i}/${list.length}] replaced links in "/${project}/${result.value.title}");
}
} finally {
if (socket) disconnect(socket);
}
};
リンクがある行を抽出する
code:script.ts
const getURLs = (node: Node): URL[] => {
switch (node.type) {
case "decoration":
case "quote":
return node.nodes.flatMap((node) => getURLs(node));
case "image": {
const url = new URL(node.src);
if (url.hostname !== "pbs.twimg.com") return [];
if (!url.pathname.startsWith("/media")) return [];
}
default:
return [];
}
};
await main();
code:replace.ts
import { upload } from "../../takker/deno-gyazo/mod.ts";
import { getGyazoToken } from "../../takker/scrapbox-userscript-std/rest.ts";
import { encodeTitleURI } from "../../takker/scrapbox-userscript-std/text.ts";
declare const GM_fetch: typeof fetch;
let token = "";
let checked = false;
export const uploadAndReplace = async (project: string, title: string, lineId: string, url: URL): Promise<string | undefined> => {
if (url.hostname !== "pbs.twimg.com") return;
if (!url.pathname.startsWith("/media")) return;
if (!checked) {
const result = await getGyazoToken();
checked = true;
if (!result.ok) {
alert(
"You haven't logged in Gyazo yet, so you can only upload images to scrapbox.io.",
);
return;
}
token = result.value || "";
if (!token) {
alert(
"You haven't connect Gyazo to scrapbox.io yet.",
);
return;
}
} else if (!token) {
return;
}
const res = await GM_fetch(url);
if (!res.ok) return;
const result = await upload(await res.blob(), {
accessToken: token,
// 本当は元tweetのURLにしたい
refererURL: https://scrapbox.io/${project}/${encodeTitleURI(title)}#${lineId},
});
if (!result.ok) throw Error(result.value.name);
return result.value.permalink_url;
};
テスト:video URLを取得する
2023-07-17 09:36:08 時点で動画を含むページは57 pagesあった
code:list.ts
import { exportPages } from "../../takker/scrapbox-userscript-std/rest.ts";
import { Scrapbox } from "../../takker/scrapbox-jp%2Ftypes/userscript.ts";
declare const scrapbox: Scrapbox;
const result = await exportPages(scrapbox.Project.name, { metadata: false });
if (!result.ok) {
alert(${result.value.name} ${result.value.message});
} else {
const titles = result.value.pages.flatMap(
(page) => page.lines.some((line) => line.includes("video.twimg.com")) ? page.title : [] );
const url = URL.createObjectURL(blob);
open(url);
}