single.php

Jellyfinのページ操作をマウスホイールで行うChrome拡張機能を作ってみた(その4)

Ubuntuなどにインストールして動画や画像を管理できる[Jellyfin]。先回自作した、Chrome拡張機能にページを一定間隔で自動で送る機能を追加ました。

マウスホイールでページ操作をしたい

ローカルに保存された動画や画像などのメディアを、ネトフリなどの配信サービスのような形で様々なデバイスに表示してくれる[Jellyfin]。

ライブラリを追加すると、指定したフォルダー内のメディアファイルを閲覧しやすい表示方法で配信してくれます。

使ってみて気になったのが、Google Chromeなどウェブブラウザーで利用する場合にマウスのホイールでページ送りが出来ない部分。

先回、マウスホイールでページ送りを可能にするChromeブラウザーの拡張機能を作ってみました。

詳しい内容は別記事をご覧ください。

ページの自動送り

マウスのホイールでページ送り(戻り)が可能になるので、かなり便利になりました。

実際に使っていて、マウスやキーボードから解放され、自動でページ送りがしたくなりコードを追加してみました。

ざっくりとした仕様としては、マウスの右クリックで表示されるメニューに[自動ページ送り]を追加して選択されたら一定のインターバルでページ送りの処理を続けます。

とりあえず、コンテキストメニューを表示させます。(太字の部分が先回から修正したコード)

[manifest.json]

{
  "manifest_version": 3,
  "name": "Jellyfin Wheel",
  "version": "1.0.1",
  "description": "Enable mouse wheel page navigation in Jellyfin Web slideshow.",
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ],
  "permissions": [
    "contextMenus"
  ]
}

新しく[background.js]ファイルを作成して表示されるメニューの作成と操作部分を追加します。

[background.js]

let autoPagingState = false;

chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "autoPaging",
    title: "自動ページ送り",
    contexts: ["image"],
  });
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId !== "autoPaging") return;

  try {
    const response = await chrome.tabs.sendMessage(tab.id, {
      action: "checkPageState"
    });

    if (response?.isLastPage) {
      console.log("Already at last page.");
      return;
    }

  } catch (err) {
    console.warn("State check failed:", err);
    return;
  }

  autoPagingState = !autoPagingState;

  await chrome.tabs.sendMessage(tab.id, {
    action: autoPagingState ? "startAutoPaging" : "stopAutoPaging"
  });

  chrome.contextMenus.update("autoPaging", {
    title: autoPagingState
      ? "自動ページ送り停止"
      : "自動ページ送り"
  });
});

chrome.runtime.onMessage.addListener((message, sender) => {

  if (message.action === "autoPagingStopped") {

    autoPagingState = false;

    chrome.contextMenus.update("autoPaging", {
      title: "自動ページ送り"
    });

    console.log("Auto paging stopped at last page");
  }
});

実際に一定間隔でページ送りを行う操作を修正します。

[content.js]

console.log("[Jellyfin Wheel] loaded");

let wheelAccum = 0;

const PAGE_THRESHOLD = 100; // 1ページ分のホイール量
const MAX_PAGES = 10;       // 進める最大ページ数
const CLICK_INTERVAL = 120; // ミリ秒

let autoPaging = false;
let autoPagingTimer = null;
const AUTO_INTERVAL = 7000; // 7秒(好みで変更)

// --------------------
// ページ送り
// --------------------
async function clickMultiple(button, count) {
  for (let i = 0; i < count; i++) {
    button.click();
    await new Promise(r => setTimeout(r, CLICK_INTERVAL));
  }
}

// --------------------
// Jellyfinページ判定
// --------------------
function isJellyfinPage() {
  const swiperContainer = document.querySelector(".slideshowSwiperContainer");
  const nextBtn = document.querySelector(".swiper-button-next");
  const prevBtn = document.querySelector(".swiper-button-prev");
  const zoomContainer = document.querySelector(".slider-zoom-container");

  // いずれか存在しなければ非対象
  if (!swiperContainer || !nextBtn || !prevBtn || !zoomContainer) {
    return false;
  }

  return true;
}

// --------------------
// ページ端チェック
// --------------------
function canGoNext() {
  const nextBtn = document.querySelector(".swiper-button-next");
  return nextBtn && nextBtn.getAttribute("aria-disabled") !== "true";
}

function canGoPrev() {
  const prevBtn = document.querySelector(".swiper-button-prev");
  return prevBtn && prevBtn.getAttribute("aria-disabled") !== "true";
}


// --------------------
// ホイールイベント
// --------------------
window.addEventListener(
  "wheel",
  async (e) => {
    if (!isJellyfinPage()) return;

    // 横スワイプ(ブラウザ戻る/進む)は無視
    if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
      return;
    }


    const nextBtn = document.querySelector(".swiper-button-next");
    const prevBtn = document.querySelector(".swiper-button-prev");
    if (!nextBtn || !prevBtn) return;

    // ページ端チェック
    if (e.deltaY > 0 && !canGoNext()) return;
    if (e.deltaY < 0 && !canGoPrev()) return;

    e.preventDefault();
    e.stopPropagation();

    // ホイール量を蓄積
    wheelAccum += e.deltaY;

    const pages = Math.trunc(wheelAccum / PAGE_THRESHOLD);
    if (pages === 0) return;

    // 上限
    const pageCount = Math.min(Math.abs(pages), MAX_PAGES);

    wheelAccum %= PAGE_THRESHOLD;

    if (pages > 0) {
      console.log(`[Jellyfin Wheel] next x${pageCount}`);
      await clickMultiple(nextBtn, pageCount);
    } else {
      console.log(`[Jellyfin Wheel] prev x${pageCount}`);
      await clickMultiple(prevBtn, pageCount);
    }
  },
  { passive: false }
);

// --------------------
// 読み方向 自動補正
// --------------------
let langDirFixed = false;

function autoFixReadingDirection() {
  if (langDirFixed) return;

  const btn = document.querySelector(".btnToggleLangDir");
  if (!btn) return;

  if (btn.getAttribute("title") === "Right To Left") {
    console.log("[Jellyfin Wheel] Auto toggle reading direction");
    btn.click();
  }

  langDirFixed = true;
}

// DOM変化を監視(SPA対策)
const observer = new MutationObserver(() => {
  if (!isJellyfinPage()) return;
  autoFixReadingDirection();
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

// ページの自動送り開始
function startAutoPaging() {
  if (autoPaging) return;

  const nextBtn = document.querySelector(".swiper-button-next");
  if (!nextBtn) return;

  autoPaging = true;

  autoPagingTimer = setInterval(() => {
    if (!canGoNext()) {
      stopAutoPaging();

      chrome.runtime.sendMessage({ action: "autoPagingStopped" });
      return;
    }

    nextBtn.click();
    console.log("[Jellyfin Wheel] auto next");
  }, AUTO_INTERVAL);

  console.log("[Jellyfin Wheel] Auto paging started");
}

// ページの自動送り停止
function stopAutoPaging() {
  if (!autoPaging) return;

  clearInterval(autoPagingTimer);
  autoPagingTimer = null;
  autoPaging = false;

  console.log("[Jellyfin Wheel] Auto paging stopped");
}

chrome.runtime.onMessage.addListener((message) => {
  if (message.action === "startAutoPaging") {
    startAutoPaging();
  }

  if (message.action === "stopAutoPaging") {
    stopAutoPaging();
  }

  if (message.action === "checkPageState") {

    const nextBtn = document.querySelector(".swiper-button-next");

    const isLastPage =
      !nextBtn ||
      nextBtn.classList.contains("swiper-button-disabled");

    sendResponse({ isLastPage });

    return true; // async対応
  }
    
});

修正すると、閲覧画面で右クリックで表示された[自動ぺージ送り]メニューを選択すると7秒間隔でページ送りを行うイベントが走ります。

拡張機能をインストールする詳しい手順は別記事をご覧ください。

実際に試していませんが、Chromeの他にMicrosoft Edgeでも使えるはずです。

まとめ

動画や画像を管理できる[Jellyfin]をウェブブラウザーで閲覧する際に、Chrome拡張機能にページを一定間隔で自動で送る機能を追加ました。

マンガなど閲覧する際に、マウスやキーボードを操作することなく自動でページが送られるので手放しで閲覧ができます。

[Jellyfin]でブラウザーで画像を閲覧する際に、ページ送りを自動化したいなと思っている人の参考になれば幸いです。

スポンサーリンク

最後までご覧いただき、ありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です