Silent Foreign Perspective

Abemaプレイヤーの挙動改善(ED再生し自動で次の話へ)

Tags:

Published at 2022-12-26 04:33:00

動機

ぼっち・ざ・ろっく!というアニメの最速配信がAbemaで行われているのでそちらで視聴しているが、EDの曲を最後まで聴きたいのに勝手に次話に遷移するのが鬱陶しいので改造する。他にも色々あるので直す。

やること

  • 「次のエピソードへ」というボタンが出てきた時、何もしないと10秒後に次話へ遷移してしまうので、出てきた瞬間にキャンセルする
  • 再生が最後まで終わったら次のエピソードへ遷移する
  • エピソード一覧から話を選んでも途中から再生されてしまうのを無効にする

調査

調べると以下のようになっていた。

// 「次のエピソードへ」ボタンの部分
<div class="com-vod-VODNextProgramInfo">
  // ボタンが表示されているときはcom-vod-VODNextProgramInfo--is-showというclassがこの要素に加わる
  <div class="com-vod-VODNextProgramInfo__inner">
    <button type="button" class="com-vod-VODNextProgramInfo__cancel-button" tabindex="-1" disabled="">
      キャンセル
    </button>
    <div class="com-vod-VODNextProgramInfo__next">
      <a href="/video/episode/[話URL]?next=true" tabindex="-1" class="com-a-Link com-a-Link--block">
        // 次の話へのリンク
        <div class="com-vod-VODNextProgramInfo__next-inner">
          <span class="com-vod-VODNextProgramInfo__next-symb width="100%" height="100%" role="img" focusable="false">
            <svg aria-label="" aria-hidden="true" width="100%" height="100%" role="img" focusable="false">
              <use xlink:href="/assets/images/icons/player/play.svg?v=e4aed34cff834c273461#svg-body"/>
            </svg>
          </span>
          <span>次のエピソード : 10</span> // 10の部分は残り秒数で変化
        </div>
      </a>
    </div>
  </div>
</div>

// 時刻を表示する部分(再生が終了したことを判定するために使用)
<div class="com-vod-VideoControlBar__time">
  <span class="com-vod-VODTime">
    <span class="com-a-Text--regular com-a-Text--dark com-a-Text--s">
      <time datetime="PT23M40S">23:40</time> // 現在の再生時刻
      <span class="com-vod-VODTime__separator">/</span>
      <time datetime="PT23M40S">23:40</time>
    </span>
  </span>
</div>

// エピソード一覧のアイテム(必要な部分のみ抜粋)
<div class="com-content-list-ContentListEpisodeItem">
  <div class="com-content-list-ContentListEpisodeItem__content-container">
    <div class="com-content-list-ContentListEpisodeItem__overview">
      <a href="/video/episode/[話URL]" class="com-content-list-ContentListEpisodeItem__link">
        <p class="com-content-list-ContentListEpisodeItem__title">
          <span class="com-a-CollapsedText__container" style="line-height:1.5;max-height:1.5em;-webkit-line-clamp:1">
            [話タイトル]
          </span>
        </p>
      </a>
    </div>
  </div>
</div>

特に何も考えずクラス名で要素を選択していじればよさそう。
話のURLについている?next=trueというパラメータについて、これを付けると最初から再生されてくれることがわかった。(自動で次の話に行って前回の続きから再生されることはないのでそれはそう)エピソード一覧のリンクにもこれを付けてしまえばよい。

実装

ユーザースクリプトで書き換える。筆者はTampermonkeyを使用。
最初はMutationObserverで賢くやろうと思ったが、全く変更を検知できないか数百msごとに1回走るかのどちらかになってしまったので脳死でsetInterval()を使うことにした。いつかリベンジしたくはある。

自動キャンセル

ボタンが表示されるときには button.com-vod-VODNextProgramInfo__cancel-buttontabindexが0になっている。

const closeNextProgramInfo = () => {
  const cancelButton = document.querySelector('.com-vod-VODNextProgramInfo__cancel-button');
  const v = cancelButton?.attributes?.tabindex.value
  if (!!v && v !== '-1') {
    cancelButton.click();
  }
};

自動遷移

現在再生時刻と終端再生時刻を比較して一致したら再生が終わったと判断して遷移する。最初PHPの感覚で文字列にそのまま60掛け算したらめちゃくちゃバグった。

const clickNextProgramInfo = () => {
  const time = document.querySelectorAll("span.com-vod-VODTime time");
  if (!time) {
    return
  }
  const nowMS = time[0]?.attributes.datetime.value.match(/^PT(\d+)M(\d+)S/);
  const now = parseInt(nowMS?.[1]) * 60 + parseInt(nowMS?.[2]);
  const maxMS = time[1]?.attributes.datetime.value.match(/^PT(\d+)M(\d+)S/);
  const max = parseInt(maxMS?.[1]) * 60 + parseInt(maxMS?.[2]);
  if (now && max && now >= max) {
    location.href = document.querySelector('div.com-vod-VODNextProgramInfo__next a').attributes.href.value;
  }
};

エピソード一覧リンク書き換え

他のものは定期実行されても問題ない(1度きり何かが起きる)が、これは気を付けないと無限にURLが長くなっていってしまうので注意。stopImmediatePropagation()は、この部分がaタグのデフォルト動作ではなく何らかのクリックイベントで遷移しているっぽく、他のイベントを殺さないとhrefを書き換えても遷移先が変わらなかったため付けている。

const changeLink = () => {
  const links = document.querySelectorAll('.com-content-list-ContentListEpisodeItem__link');
  links.forEach(function(element) {
    const before = element.getAttribute('href')
    if (!before.includes('?')) {
      element.setAttribute('href', before + '?next=true');
      element.addEventListener('click', (event) => {
        event.stopImmediatePropagation();
      });
    }
  })
};

定期実行部分

setInterval()のドキュメントそのまま。

const intervalID = setInterval(() => {
  closeNextProgramInfo();
  clickNextProgramInfo();
  changeLink();
}, 1000);

課題

  • 終了時間を検知する部分のコードが1時間以上ある番組に対応していない(アニメ用途なので不要だった)
    • 再生時間が1:23:45のとき、datetime属性が PT1H23M45S みたいな感じに変わるっぽいので必要な方は頑張ってください
  • 公開中の最終話で次話に遷移すると404
    • 放映中は特に気にならなかった。現在は完結し特番等へ遷移してしまうようになってしまったので、12話のときだけ1話に戻るようにするなどするとよりQOLの向上が見込めるだろう。
  • 遷移時にフルスクリーンが解除されるのでずっとフルスクリーンで流したい

まとめ

君だけの最強のAbemaプレイヤーを手に入れよう!

おまけ:ユーザースクリプト全文

自己責任で。

// ==UserScript==
// @name         Abema Enhancer
// @version      1
// @description  なんやかんや
// @match        https://abema.tv/*
// @grant        none
// ==/UserScript==

(() => {
  'use strict';

  const closeNextProgramInfo = () => {
    const cancelButton = document.querySelector('.com-vod-VODNextProgramInfo__cancel-button');
    const v = cancelButton?.attributes?.tabindex.value
    if (!!v && v !== '-1') {
      cancelButton.click();
    }
  };

  const clickNextProgramInfo = () => {
    const time = document.querySelectorAll("span.com-vod-VODTime time");
    if (!time) {
      return
    }
    const nowMS = time[0]?.attributes.datetime.value.match(/^PT(\d+)M(\d+)S/);
    const now = parseInt(nowMS?.[1]) * 60 + parseInt(nowMS?.[2]);
    const maxMS = time[1]?.attributes.datetime.value.match(/^PT(\d+)M(\d+)S/);
    const max = parseInt(maxMS?.[1]) * 60 + parseInt(maxMS?.[2]);
    if (now && max && now >= max) {
      location.href = document.querySelector('div.com-vod-VODNextProgramInfo__next a').attributes.href.value;
    }
  };

  const changeLink = () => {
    const links = document.querySelectorAll('.com-content-list-ContentListEpisodeItem__link');
    links.forEach(function(element) {
      const before = element.getAttribute('href')
      if (!before.includes('?')) {
        element.setAttribute('href', before + '?next=true');
        element.addEventListener('click', (event) => {
          event.stopImmediatePropagation();
        });
      }
    })
  }

  const intervalID = setInterval(() => {
    closeNextProgramInfo();
    clickNextProgramInfo();
    changeLink();
  }, 1000);
})();