【概要】
Youtubeの高評価ボタンを自動で押すchrome拡張機能を作りました。
コーディングはせずchatGPTとGeminiで作成しました(無料版使用)。
【制作時間】
基本のコーディング30分、テスト&バグ修正1週間
【苦労話】
Youtubeのチャンネルページからサムネをクリックして配信ページを開いた際に高評価がクリックされないという事象に遭遇し、解決に時間を要しました。細かくコンソールにログを書き出してどこで何が起こっているのか突き止めて、Geminiに相談したら解決しました。
【機能】
◆ 動画・配信・Short動画、全て高評価が自動で押されます。
◆ 上部に拡張機能を表示した際のアイコンをクリックすることで機能のON/OFFができます。
【拡張機能】
下記で公開しています。
コードは以下の通りです。誰かの参考になれば幸いです。
【manifest.json】
{
"manifest_version": 3,
"name": "YouTube Auto Like",
"version": "1.0.0",
"description": "YouTube動画を自動で高評価するChrome拡張",
"permissions": [
"storage"
],
"host_permissions": [
"https://www.youtube.com/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "YouTube Auto Like",
"default_icon": {
"16": "icons/off16.png",
"48": "icons/off48.png",
"128": "icons/off128.png"
}
},
"content_scripts": [
{
"matches": ["*://*.youtube.com/*"],
"js": ["content.js"],
"all_frames": true,
"match_about_blank": true,
"run_at": "document_start"
}
]
}
【background.js】
const DEFAULT_STATE = false;
// 初回インストール時
chrome.runtime.onInstalled.addListener(async () => {
const result = await chrome.storage.local.get("enabled");
if (typeof result.enabled === "undefined") {
await chrome.storage.local.set({
enabled: DEFAULT_STATE
});
}
updateIcon(result.enabled ?? DEFAULT_STATE);
});
// アイコンクリックでON/OFF
chrome.action.onClicked.addListener(async () => {
const result = await chrome.storage.local.get("enabled");
const nextState = !result.enabled;
await chrome.storage.local.set({
enabled: nextState
});
updateIcon(nextState);
});
function updateIcon(enabled) {
const path = enabled
? {
"16": "icons/on16.png",
"48": "icons/on48.png",
"128": "icons/on128.png"
}
: {
"16": "icons/off16.png",
"48": "icons/off48.png",
"128": "icons/off128.png"
};
chrome.action.setIcon({ path });
}
【content.js】
// -----------------------------
// グローバル変数
// -----------------------------
let currentContentId = null;
let isEnabled = false;
let likeObserverInterval = null;
// -----------------------------
// フレーム判定ロジック
// -----------------------------
if (window.top !== window) {
// 1. iframe(子画面)側の処理
// iframe側はストレージ等には一切触れず、ただ親にメッセージを送るだけに徹する(エラー予防)
window.top.postMessage({ type: "YOUTUBE_LIVE_TRIGGERED" }, "*");
} else {
// 2. メインウィンドウ(Top)側の処理
// iframeからの合図を待ち受けるリスナー
window.addEventListener("message", async (event) => {
if (event.data && event.data.type === "YOUTUBE_LIVE_TRIGGERED") {
const valid = await initEnabled();
if (!valid || !isEnabled) return;
observeLikeButton();
}
});
// 初回ロード
window.addEventListener("load", async () => {
const valid = await initEnabled();
if (!valid || !isEnabled) return;
const contentId = getContentId();
if (!contentId) return;
currentContentId = contentId;
observeLikeButton();
});
// SPA遷移
window.addEventListener("yt-navigate-finish", async () => {
const valid = await initEnabled();
if (!valid || !isEnabled) return;
const contentId = getContentId();
if (!contentId) return;
if (currentContentId !== contentId) {
currentContentId = contentId;
observeLikeButton();
}
});
}
// -----------------------------
// 共通関数群
// -----------------------------
// ストレージ初期化(コンテキスト無効化対策を強化)
async function initEnabled() {
// 拡張機能のコンテキストが切断されているかチェック
if (!chrome.runtime || !chrome.runtime.id) {
console.log("[AutoLike] Extension context invalidated. スクリプトの実行を安全に停止します。");
return false; // 無効化されているので処理中断
}
try {
const result = await chrome.storage.local.get("enabled");
isEnabled = result.enabled;
return true; // 正常に取得完了
} catch (e) {
// エラーメッセージに "invalidated" が含まれる場合を考慮
if (e.message && e.message.includes("invalidated")) {
return false;
}
isEnabled = false;
return true;
}
}
// background.js からのON/OFF通知
if (chrome.runtime && chrome.runtime.id) {
chrome.runtime.onMessage.addListener((msg) => {
if (!chrome.runtime || !chrome.runtime.id) return; // 安全弁
if (msg.type === "enabledUpdate") {
isEnabled = msg.value;
}
});
}
// 動画ID取得
function getContentId() {
if (location.pathname.startsWith("/watch")) {
return new URL(location.href).searchParams.get("v");
}
if (location.pathname.startsWith("/shorts")) {
return location.pathname.split("/")[2];
}
return null;
}
// 高評価ボタン監視
function observeLikeButton() {
if (!isEnabled) return;
if (likeObserverInterval) {
clearInterval(likeObserverInterval);
}
let retry = 0;
likeObserverInterval = setInterval(() => {
// 監視の途中でもコンテキスト確認(念のため)
if (!chrome.runtime || !chrome.runtime.id) {
clearInterval(likeObserverInterval);
return;
}
retry++;
const isShorts = location.pathname.startsWith("/shorts");
const likeButton = isShorts
? getShortsLikeButton()
: getNormalLikeButton();
if (likeButton) {
const buttonEl = likeButton.closest("button");
const pressed = buttonEl?.getAttribute("aria-pressed") === "true";
if (!pressed) {
["pointerdown", "mousedown", "pointerup", "mouseup", "click", "tap"].forEach(type => {
likeButton.dispatchEvent(
new CustomEvent(type, { bubbles: true, cancelable: true })
);
});
}
clearInterval(likeObserverInterval);
likeObserverInterval = null;
}
if (retry > 60) {
clearInterval(likeObserverInterval);
likeObserverInterval = null;
}
}, 1000);
}
// 通常動画ボタン取得
function getNormalLikeButton() {
const likeView = document.querySelector("like-button-view-model");
if (!likeView) return null;
return likeView.querySelector("yt-animated-icon, yt-touch-feedback-shape");
}
// Shortsボタン取得
function getShortsLikeButton() {
const likeModels = document.querySelectorAll("like-button-view-model");
for (const model of likeModels) {
const reelRenderer = model.closest("ytd-reel-video-renderer");
if (!reelRenderer) continue;
const rect = reelRenderer.getBoundingClientRect();
const visible =
rect.width > 0 &&
rect.height > 0 &&
rect.top < window.innerHeight &&
rect.bottom > 0;
if (!visible) continue;
const button = model.querySelector("yt-animated-icon, yt-touch-feedback-shape");
if (!button) continue;
const label = button.closest("button")?.getAttribute("aria-label") || "";
if (
label.includes("高く評価") ||
label.includes("高評価") ||
label.toLowerCase().includes("like")
) {
return button;
}
}
return null;
}