【乃木坂46】モバメ画像の保存を自動化してみた

スポンサーリンク
Pop Culture
スポンサーリンク

はじめに

最近4期生の配信が追加されたモバイルメール、通称モバメ。このタイミングでメンバーを追加された方も多いのではないでしょうか。私も二人だけですが追加してしまいました。閉塞感のある毎日の中で、救われるような思いになったことも一度や二度じゃありません。

ただメンバーが増えてくると送られてくるモバメを管理するのも一苦労ですよね。特に阪口珠美さんのようにまめなメンバーを購読していると(宣伝)、画像を保存するだけで嬉しい悲鳴をあげることになりかねません。

せっかく送ってくれた画像、保存しておきたいけど、一々手作業で写真アプリに保存してアルバムに追加しての繰り返しはめんどくさいなぁ。

そこで筆者は思ったのです。手作業が面倒なら、自動化すればいいじゃない。

実現できたこと

こちらがモバメの画像の保存を自動化した様子。

乃木坂モバメ画像自動取得

iPhoneのショートカットアプリを使うことで、たった数回のタップだけで指定した期間のモバメ画像をそれぞれのアルバムに保存できました。

これでいちいちメールアプリからモバメを探したり、保存した画像をアルバムに移動し直したり、なんて作業ともおさらばです。

よかったですね。

前回実行時の日時を保存して次に実行する時はその日時以降のメールを検索するようになっているので、画像の保存のダブり、漏れがなくなったというのも地味に嬉しいです。少しサボるとどこまで保存したか分からなくなりがちでしたが、日時の管理までプログラムに任せることで悩みを解決できました。

感想

実は自分でこういうものを作るのは初めてだったのですが、シンプルにめちゃ楽しかったです。やっぱり自分が作ったものが考えた通りに動くというのは嬉しいものですね。いざ自動化しようと思ったはいいものの右も左も分からないことばかりで、少し進んでは壁にぶち当たり、を繰り返してなんとか動くものを完成させることができたので、特にそう思えるのかもしれません。ただ、翌日の仕事のことなんて考えず深夜までコードいじっていた時間はかなり充実感があったなぁなんて思います。

また、分からないことだらけだった分、この機会にいろんなことを学ぶことが出来ました。Pythonを仕事で触る機会なんて一生来ないだろうし、JSONの操作やSSHの仕様など、手を動かしてみて初めて得られる発見も多かったです。自分の趣味に関することで勉強もできることほど幸せなこともなかなかないです。なんか自分イケてるやんという謎の自己効力感の上昇も感じました。

実はこんなことをしようと思ったきっかけには乃木坂ファン兼エンジニアとして推しのファンサイトを作ってらっしゃる方の存在がありまして。そういう方を見て、働くとかキャリアということにこだわりすぎず、自分の好きなものにリソースを注げるようになりたいと思っていました。なので今回は、そんな理想像に近づく一歩目にもなったような気がします。

これからもこんな感じで、自分の興味があることと自己研鑽を掛け合わせていきたいなと思います。

実現方法

せっかくなのでどうやって実現したかも最後に少しご紹介。

大まかな流れとしては、iPhoneのショートカットアプリで自宅PC上のプログラムを動かして画像URLを抽出してファイルに格納、iPhoneでそのファイルを受信して、URLから画像を保存する、という感じです。

具体的な手順を説明するとなるとかなり長くなってしまうので今回は割愛しますが、Apple公式のショートカットアプリから他人が作成したショートカットを保存して自分で実行することもできるので、一応ショートカットのフローだけならこちらからどうぞ。他にいろいろ設定したり必要ですが。

モバメ画像一括所得(仮)
Shortcuts

※ショートカットアプリが開きます

ぱっと中身見てもらえればわかると思うのですが、まぁまぁ複雑です。正直自分でもこれがベストプラクティスだとは全く思えません・・・。実際に動かすには↑のショートカットに加え、

  1. 実行日時を記録するメモを作成
  2. iPhoneからPCにSSH接続できるようにする
  3. Gmail APIを叩いて結果を整形するプログラムを組む

という作業が必要になります。このショートカットは特に3で組んだプログラム(ショートカットでいうと最初にsshで呼び出しているPythonファイル)に依存しているところがあるので、参考までにソースも最後に置いておきます。

ちなみに、というかほとんど言い訳なのですが、Pythonで10行以上書いたのこれが初めてでして。。いろいろ拙いところもあるかと思いますが大目に見てください、。。

さいごに

この自動化自体は諸事情あってかなり複雑になってしまい、ショートカット以外の設定も必要なものになってしまいしたが、Appleのショートカットアプリ単独でもかなりいろんなことができるようでちょっとハマりそうです。ご興味あれば是非お試しを。

付録:getImageURL.py

#!/usr/bin/env python3

import base64
import codecs
import collections
import datetime
import email
import json
import os.path
import pickle
import re
import sys

from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

# "保存先アルバム名":"メールアドレス"で登録しておく
members = { "たまみより": "nogizaka46-tamami_sakaguchi@m.nogizaka46.com",
            "hinakomail": "nogizaka46-hinako_kitano@m.nogizaka46.com",
            "もももばめ": "nogizaka46-momoko_oozono@m.nogizaka46.com",
            "reimail": "nogizaka46-rei_seimiya@m.nogizaka46.com",
            "seiramail": "nogizaka46-seira_hayakawa@m.nogizaka46.com"}

#UNIXタイムを取得する関数
def getUnixTime (str):
    
    #iPhoneで秒まで管理したくないのでここで秒を付加
    str += ":00"
    date_time = datetime.datetime.strptime(str, "%Y-%m-%d %H:%M:%S")
    unix_time = datetime.datetime.timestamp(date_time)
    return int(unix_time)

# メールソースから本文だけ抽出する関数
def extractTextFromEmail(raw):

     eml = email.message_from_bytes(raw)
     body = ""
     for part in eml.walk():
         # content typeがtext/htmlの部分だけ整型
         if part.get_content_type() != 'text/html':
             continue
         head = {}
         for k,v in part.items():
           head[k] = v
         s = part.get_payload(decode=True)
         if isinstance(s, bytes):
             charset = part.get_content_charset() or 'iso-2022-jp'
             s = s.decode(str(charset), errors="replace")
         body += s
     return body

# messageIDのリストを元に画像URLを抽出する関数
def getURLList(service, messageIDlist):

    URLList = []
    for message in messageIDlist['messages']:
        msg = service.users().messages().get(userId='me', id=message['id'], format='raw').execute()
        raw = base64.urlsafe_b64decode(msg['raw'])
        data = extractTextFromEmail(raw)
        # 抽出した本文から画像URLを探して配列に格納する
        iterator = re.finditer('http://mail-web.c-nogizaka46.com/mail/output.*?jpg', data)
        for m in iterator:
            URLList.append(m.group())
    return URLList

# 指定日時以降に指定メールアドレスから受信したメールのmessageIDを取得する関数
def getMessageID(service, dateFrom, mailadr):

    messageIDlist = []
    query = 'after:' + str(dateFrom) +' From:' + str(mailadr)

    messageIDlist = service.users().messages().list(userId='me',maxResults=100,q=query).execute()

    return messageIDlist

# GmailAPIに必要なインスタンスを取得する関数
# https://developers.google.com/gmail/api/quickstart/python より
def getGmailService():

    creds = None
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)
    service = build('gmail', 'v1', credentials=creds)
    return service

# 指定日時以降の画像URLをjsonファイルに格納するメイン関数。
def main(dateFrom):

    # iPhoneに送る結果を入れる辞書
    result = collections.OrderedDict()

    service = getGmailService()
    
    # メンバー一人ずつ処理していく
    for k, v in members.items():

        messageIDlist = getMessageID(service, dateFrom, v)
        # メールを一件も受信していなかったらスキップする
        if messageIDlist["resultSizeEstimate"] >= 1:
            result [k] = getURLList(service, messageIDlist)

    # ファイルに書き込み
    with codecs.open('result.json', 'w') as f:
        dump = json.dumps(result, ensure_ascii=False)
        f.write(dump)


if __name__ == "__main__":
    # 実行ディレクトリをこのファイルのあるディレクトリにする
    # credentials.json等各種設定ファイルはこのファイルと同階層に配置すること
    os.chdir(os.path.dirname(__file__))
    # 引数には取得開始日時を与える:"YYYY-MM-DD HH:mm"
    dateFrom = getUnixTime(str(sys.argv[1]))
    main(dateFrom)