Filmarks の自分の投稿へ飛ぶ疑似ソーシャルボタン

映画のレビューを書いている別サイト「そんなには褒めないよ。映画評」の各記事から Filmarks の自分の投稿ページに飛ぶリンク、疑似ソーシャルボタンをつくります。

前記事:Node.js でスクレイピングしてクライアントに返す



クリックからページ表示までの流れ

  1. Filmarks ボタンをクリックすると関数 openFilmarks を呼ぶ
  2. openFilmarks
    1. ページの title タグから映画タイトルを取得する
    2. filmarks-window 名のサブウィンドウを開いてローディング画像を表示しておく
    3. Filmarks のユーザー名と映画タイトルを Nodejs サーバーに送りレスポンスを待つ
  3. Nodejs サーバー
    1. Filmarks のユーザーページをリクエストする
    2. ユーザーページから最大ページ数を取得する
    3. ページ数分ループさせて各ユーザーページをリクエストする
    4. 各ページ36タイトルを取得しクライアントから送られた映画タイトルと比較する
    5. 合致するタイトルであれば変数 url に投稿ページの URL を代入する
    6. 全ページの照合が終了し、url が空であれば no mark を代入する
    7. url をクライアントに返す
  4. openFilmarks
    1. 返ってきた値が Filmarks の投稿ページ形式の URL であればサブウィンドウを URL 遷移させる
    2. 返ってきた値が no marks であれば、サブウィンドウを閉じて投稿がない旨アラートする


クライアントサイド

HTML(Filmarksボタン)

<!-- filmarks -->
<button type="button" class="filmarks" onclick="javascript:openFilmarks(); return false;">
    <img alt="Filmarks" src="https://d2ueuvlup6lbue.cloudfront.net/assets/pc/project/logo_brand-c14549a90b4abe0a29f4be8b0c5f29f6700c595656d0e3d03dfc7a993d91a4ee.svg">
</button>

ロゴの使用には許可が必要です。


Javascript(openFilmarks)

function openFilmarks(){

  const srv = 'http://localhost:8080'; // テスト用のローカルサーバー
  const user = 'ausnichts'; // Filmarks のユーザー名
  const title = document.title.match(/^「(.+)」.+$/); // 映画タイトル名を取り出す
    
  const url = new URL(srv);
  const params = { user: user, movie: title[1] };
  url.search = new URLSearchParams(params);

  // サブウィンドウを開きローディング画像を表示
  const windowFilmarks = window.open('', 'filmarks-window');
  windowFilmarks.document.write('<html><head><title>Now loading...</title></head>');
  windowFilmarks.document.write('<body>');
  windowFilmarks.document.write('<div style="display: flex; justify-content: center; align-items: center; height: 100%;">');
  windowFilmarks.document.write('<img src="ローディングイメージ">');
  windowFilmarks.document.write('</div>');
  windowFilmarks.document.write('</body></html>');

  fetch(url.toString(), {
    mode: 'cors'
  })
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.text();
    })
  .then(result => {
    const regex = /^https:\/\/filmarks.com\/movies\/\d+\/reviews\/\d+/;
    const found = result.match(regex);
    if(found !== null){
      windowFilmarks.location.href = result;
    }else{
      windowFilmarks.close();
      let intervalId = setInterval(function(){
        if(windowFilmarks.closed){
          clearInterval(intervalId);
          alert('まだこの映画のレビューはありません');
        }
      },10);
    }
  })
  .catch(error => {
    console.error('There has been a problem with your fetch operation:', error);
  });
}


サーバーサイド

const http = require('http');
const server = http.createServer();
const client = require('cheerio-httpcli');
const urlParser = require('url');

const filmarksUrl = 'https://filmarks.com/users/';

server.on('request', function(req, res){
    
  if (req.url === '/favicon.ico'){
    res.end();
  }else{
    const user = urlParser.parse(req.url, true).query.user;
    const movie = urlParser.parse(req.url, true).query.movie;
    const myPageUrl = filmarksUrl + encodeURI(user);

    client.fetch(myPageUrl)
      .then((result) => {
        const href = result.$('a.c-pagination__last').attr('href');
        const page = urlParser.parse(href, true).query.page;
        return page;
      })
      .then((page) => {
        let j = 0;
        let url = '';
        for (let i=1; i<=page; i++){
          let targetUrl = myPageUrl + `?page=${i}`;
          client.fetch(targetUrl)
            .then((result) => {
              result.$('h3.c-content-card__title').each(function (idx){
                const title = result.$(this).text();
                if(title.indexOf(movie) !== -1){
                  url = result.$(this).find('a').url();
                  url = url.replace('?mark_id=', '/reviews/');
                }
              });
            })
            .catch((err) => {
              console.log(err);
            })
            .finally(() => {
              j++;
              if (j === Number(page)){
                if (url === '') url = 'no mark';
                res.setHeader('Access-Control-Allow-Origin', 'https://ausnichts.hatenadiary.jp'); // クライアントのオリジン
                res.writeHead(200, {'Content-Type' : 'application/plain'});
                res.write(url);
                res.end();
              }
            });
        }
      })
      .catch((err) => {
        console.log(err);
      })
      .finally(() => {
        console.log('done');
      });
  }
});

server.listen(process.env.PORT || 8080);
console.log('Server running at http://localhost:8080/');


プログラムのポイント

オリジン間リソース共有 (CORS)

ブラウザから Filmarks のページをリクエストしても CORS エラーが出て取得できません。前記事で説明しています。

そのため Nodejs サーバーを立て Access-Control-Allow-Origin: クライアントのオリジン を指定してブラウザからのリクエストを受け付けるようにしています。


非同期処理 Fetch API

Filmarks のページ取得に非同期処理の Fetch API を使っていますので全てのページ取得の完了を検知できません。目的のページが取得できた時はそれを返せばいいのですが、そうしますと取得できなかった場合を検知できなくなります。

そのため、正常終了でもエラーでも必ず最後に通る処理である finally で最終処理をしています。finally に到達した回数が総ページ数(ループ回数)と一致すれば全てのページを読み込んだことを検知できます。


Nodejs 無料ホスティング

Nodejs サーバーを立ち上げられるホスティングサービスを探さなくてはいけません。

Heroku は一度試したことがありますが、ググってみましたら Glitch というサービスがヒットしますので一度試してみようかと思います。


glitch.com


次回です。