LoginSignup
4

posted at

updated at

Python + Flask でアニメGIF ダウンロードサーバーを作る

アニメGIF ダウンロードサーバーとは

  • フリーソフト9VAeをベースに開発された、PEASmotch! は、キッズプラザ大阪に5台設置され、毎日150から200本のアニメが作られている。
  • 作成したベクトルアニメーションは、共有サーバーに保存され、専用プレーヤー 9view を使って、連続したアニメーションとして大型ディスプレイで上映されているが、かねてから、作ったアニメーションをスマホに入れて持ち帰りたいという要望があった。
  • それを実現する方法として、共有サーバーのアニメーションをアニメGIFに変換する python プログラムを公開した。
  • 今回、Flask を用いて、スマホからアクセスできるサーバープログラムを作成したので公開する。
    • スマホにアニメをダウンロードできる
    • サイトのURLアドレスをQRコードで表示する
    • スマホアプリ(9VAe, PEASmotch)からアニメをアップロードできる

以下の記事も関連している

項目 パソコン スマホ
WiFiアクセスポイントの設置
共有フォルダにアニメを保存、連続上映
作成したアニメをスマホにダウンロード(本記事)
スマホアプリからアニメをアップロード(本記事)

以下はプログラムの実行例。表示されているURLをスマホでアクセスすれば同じ画面が表示され、アニメーションをダウンロードできる。

サーバーを実行した図

Python のインストール

プログラムは python を使う。Windows と ラズベリーパイで作成してみた。

Windowsの場合

Windowsの場合(仮想環境)

  • こちらの記事の anaconda は、仮想環境で実行できる。アプリごとにライブラリを使い分けるときに便利。
  • 日本語を使いたいので、Python Ver3をダウンロード。(anacondaインストール後の「anaconda-navigator」の実行はWindowsロゴから行う必要があったが、ほかは記事どおりに実行できた)

ラズベリーパイの場合

  • python(Ver2), python3(Ver3) が最初からはいっている

Flask とQRコードライブラリのインストール

次のコマンドで、Flask とQRコード用ライブラリをインストールする。(ラズベリーパイの場合 pip3 、 python3 をつかう)pip が見つからない場合、pythonをインストールしたなかに、pip がはいっていると思われるので、そのフォルダに移動してから実行するとよい。

pip install Flask
pip install qrcode
pip install pillow

アニメGIF ダウンロードサーバー

環境が整ったのであとはプログラム

フォルダ構成

Flask サンプルを参考に、以下のフォルダ構成とした。app.py がプログラム本体。ここに Flask を起動するコードがかかれている。templatesの中の設定データを利用して Web サイトの応答が行われる。

py    
  ├──app.py (プログラム本体)   
  ├──static    
  │    ├──qrcode.png(プログラムで作成される)    
  │    └──setpath.ini(プログラムで作成される)    
  └──templates    
       ├──index.html    
       └──layout.html    

python Flask ソースコード

  • ソースコードは、すべて UTF-8 で作成すること

templates    

Webサイトの内容が index.html に書かれている。

index.html
{% extends "layout.html" %}
{% block content %}
<img src="{{ url_for('static', filename='qrcode.png') }}" width="200">
<br>
画像を長押しすればダウンロードできます。<br>
Long press on the image to download.<br>
<div  align="right">
{{ message }} <br>
<a href="{{ url_for('static', filename='setpath.ini') }}">setpath.ini</A>
</div>
{% if images %}
  {% for path in images %}
    {% if '.eva.gif##' in path %} 
    {% elif '.eva.gif' in path %} 
      <div>
        <img src="images/{{ path }}" style="margin-top: 10px; vertical-align: bottom; width: 200px;">
        {{ path }}
      </div>
    {% endif %}
  {% endfor %}
{% endif %}

{% endblock %}
記述 意味 補足
{% extends "layout.html" %} ページレイアウトの設定ファイル
{% block content %}...{% endblock %} この部分が中身
<img src="{{ url_for('static', filename='qrcode.png') }}" staticフォルダの中のQR画像を表示
{{ message }} 引数messageの文字列表示
{% if images %} 引数images(ファイル名リスト)があれば
{% for path in images %} imagesリストの要素path(ファイル名)を順番に
{% if '.eva.gif' in path %} .eva.gifが含まれるファイルだけ処理
<img src="images/{{ path }}" images/フォルダのファイル
{{ path }} ファイル名も表示
{% endif %} ifの終わり
{% endfor %} forの終わり
layout.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>9VAe Anime Post</title>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>
記述 意味 補足
{% block content %}{% endblock %} この部分に中身がはいる

app.py

以下の値は環境に応じて設定

定数 意味 補足
EVA_FOLDER アニメを保存するフォルダ 例では「/home/pi/2018」
QVIEW_EXE 9view.exe のフルパス Windowsでもパスの区切りは'/'
HTTP_URL 公開するURLアドレス 最初は'127.0.0.1'でテストする
HTTP_PORT 公開するポート 最初は5000でテストする
app.py
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, make_response

import os         # ファイル操作
import subprocess # プログラム実行
import time       # 時間処理
import re         # 文字置換,正規表現
import webbrowser # ブラウザ
import threading  # multi thread
import qrcode     # QRコード生成

EVA_FOLDER = '/home/pi/xxxxxx'    #アニメを保存するフォルダ
QVIEW_EXE = '/home/pi/9va/9view'  #9view.exe のフルパス
HTTP_URL = '192.168.99.1'         #公開するURLアドレス
HTTP_PORT = 8080                  #公開するポート

app = Flask(__name__)

inpFolder = EVA_FOLDER

class MyThread(threading.Thread):
    def __init__(self):
        super(MyThread, self).__init__()

    def run(self):
        loop(self,False)

@app.route('/')
def index():
  global inpFolder
  return render_template('index.html', images=sorted(os.listdir(inpFolder)[::-1], reverse=True), message = inpFolder)

@app.route('/images/<path:path>')
def send_js(path):
  global inpFolder
  return send_from_directory(inpFolder, path)

@app.route('/',methods=["POST"]) #アニメのアップロード
def save_eva():
  global inpFolder
  mno = -1
  #print("Posted file: {}".format(request.files['file']))
  file = request.files['file']
  if(file.filename.endswith('.eva')):
      files = [f for f in os.listdir(inpFolder)]   #入力
      for fn in files:
        eva = os.path.join(inpFolder,fn)
        if(not eva.endswith('.eva')):
            continue
        if( '_編集中' in eva):     #自動保存ファイルは無視
            continue
        if( '_とちゅう' in eva):
            continue
        if( '_autosave' in eva):
            continue
        no = int(os.path.splitext(os.path.basename(fn))[0])
        if(no > mno):               #最終番号を探す
            mno = no

      savepath = os.path.join(inpFolder, '%04d.eva' % (mno+1))
      file.save(savepath)
  return str(mno+1)   #保存した番号を返す


# GIF変換ループ
def loop(self,askFolder):
  global inpFolder
  drmax = '0000-0000'
  while True:
    dirs = [f for f in os.listdir(EVA_FOLDER)] #一番新しい日付を取得
    drs = [s for s in dirs if re.match('[0-9]{4}-[0-9]{4}', s)]
    for dr in drs:
      if dr > drmax:
        drmax = dr
    inpFolder = EVA_FOLDER + '/' + drmax
    if(askFolder):
        return inpFolder

    files = [f for f in os.listdir(inpFolder)]   #入力
    for fn in files:
        if(os.path.splitext(fn)[1] != '.eva'):
            continue
        gif = fn + '.gif'  #GIFファイル名
        gif = os.path.join(inpFolder,gif)
        eva = os.path.join(inpFolder,fn)
        if( '_編集中' in eva):     #自動保存ファイルは無視
            continue
        if( '_とちゅう' in eva):
            continue
        if( '_autosave' in eva):
            continue
        if(os.path.exists(gif)):    #gifが存在,作成すみチェック
            if(os.path.getmtime(gif) > os.path.getmtime(eva)):
                continue
        cmd = (QVIEW_EXE , eva , '-gif') #-gif オプションでGIFに変換
        print(cmd)                  #変換したことを表示
        subprocess.run(cmd)         #gif作成
    time.sleep(1.0) #sleep(秒指定)
  return inpFolder


t = MyThread()
inpFolder = loop(t,True)            #evaが修正されたらgif作成、新フォルダへの移動
t.start()

html = 'http://' + HTTP_URL + ':' + str(HTTP_PORT)
img = qrcode.make(html)          #QR code作成
img.save('static/qrcode.png')
with open('static/setpath.ini', mode='w') as f:
    f.write(html)


if __name__ == '__main__':
    webbrowser.open(html)            #ブラウザ起動
    app.run(debug=True, host=HTTP_URL, port=HTTP_PORT)

記述 意味 補足
from flask import Flask, ... Flask用拡張
import 関数を使えるようにする
app = Flask(name) 処理の名前
class MyThread(threading.Thread): マルチスレッド定義 Flask内で同時実行する
def run(self): MyThreadで実行する処理
@app.route('/') ルートアクセスで表示される内容
def: 関数定義
global inpFolder 変数inpFolderは全体で共通
render_template('index.html' Flaskのtemplates/index.html を表示
os.listdir(inpFolder)[::-1] inpFolderのファイル全部 -1は最後の意味
sorted(xxx, reverse=True) リストを逆順でソート
@app.route('/images/path:path') /images/のアクセスで表示される内容 index.htmlから呼ばれる
send_from_directory(inpFolder, path) inpFolderフォルダのpathファイル
@app.route('/',methods=["POST"]) POSTを受け取ったときの処理 アップロード用
request.files['file'] POSTされたファイル
if(file.filename.endswith('.eva')): 拡張子が.evaの場合
files = [f for f in os.listdir(inpFolder)] inpFolderのファイルをリスト化
os.path.splitext(os.path.basename(fn))[0] 拡張子のないファイル名
int(文字列) 文字列を数値に変換
'%04d.eva' % (mno+1) 数値(mno+1)を文字列に変換
savepath = os.path.join(inpFolder,ファイル名) フォルダとファイル名の結合
file.save(savepath) ファイルをsavepathに保存 POSTされたファイルを別名で保存
if re.match('[0-9]{4}-[0-9]{4}', s) s が「数字4桁-数字4桁」の場合 正規表現
if(os.path.exists(gif)): ファイルが存在した場合
os.path.getmtime(gif) ファイルの更新時間
cmd = (QVIEW_EXE , eva , '-gif') コマンド作成 EVAアニメをGIFに変換
subprocess.run(cmd) コマンド実行
time.sleep(1.0) 1秒停止
t = MyThread()
t.start()
マルチスレッドの実行
img = qrcode.make(html) QRコード画像imgを作成
img.save('static/qrcode.png') 画像imgをstaticフォルダに保存
with open('static/setpath.ini', mode='w') as f: 書き込みファイルをオープン
f.write(html) 文字列の書き込み
webbrowser.open(html) ブラウザ起動
app.run(debug=True, host=HTTP_URL, port=HTTP_PORT) URL、Portを指定して実行

使い方

ラズベリーパイで実行した例を示す

  • ラズベリーパイで WiFi アクセスポイントを作成し、URL「192.168.99.1」で公開した。やり方はこちら
  • ターミナルでpyフォルダに移動。python3 app.py でプログラムを実行
  • ブラウザが開き、以下のように表示されるezgif.com-video-to-gif.gif

スマホにアニメをダウンロードする方法

  1. スマホのWiFi設定で、ラズベリーパイアクセスポイントにWiFiをつなぐ
  2. スマホでQRコードをみてURLをひらく > 上と同じ画面が開く
  3. アニメーションを長押しし、メニューから「ダウンロード」を選べばGIFアニメをダウンロードできます。

スマホアプリ(PEASmotch!one)からアニメをアップロードする方法

  1. ラズベリーパイアクセスポイントにWiFiをつなぐ
  2. スマホでQRコードをみてURLをひらく > 上と同じ画面が開く

setpath.iniの設定

  1. setpath.ini を長押しし、「リンクをダウンロード」>setpath.ini ファイルをダウンロードする
  2. ファイルマネージャなどのアプリをつかって、setpath.ini ファイルを「PEASmotch」フォルダに入れる
  3. PEASmotchを起動し、PEASロゴをタッチ、メニューの一番下に「アップロード」が追加されていればOK。
  • メニューの最後の「アップロード」項目は、スマホが、9VAeアニメGIF POSTと接続しており、保存用フォルダにアクセスできるときだけ表示される。
  • アニメを作成し「アップロード」をタッチすればアニメが保存され、自動的に gif ファイルに変換される。(上の app.py プログラムの MyThread が新しいファイルを検出し、EVAからGIFに変換を行う)
  • スマホ版のアップロード機能は、Ver.0.6.12(200110) 以降の PEASmotch!one に搭載された。なお、パソコン版の場合は、9va_dataフォルダの中の setpath.ini に共有フォルダのフルパスを記載しておき、直接保存する機能が従来から搭載されている。

問題点と改良版

上のシステムを実際にキッズプラザ大阪に設置したところ、以下のような問題があった。

問題点 修正
Windows7の共有フォルダにfopenではアクセスできたが、statがアクセスできなかった ファイルの存在チェックを、statを使わずにfopenを使う関数に変更
ラズパイで共有フォルダ上でEVA-GIF変換を行うと、ほかの端末から共有フォルダへの接続ができなくなった。Windowsの記憶域不足エラーが発生 共有フォルダからラズパイに1日分のフォルダを転送し、ラズパイ上でEVA-GIF変換、Flaskサーバー応答を行うように変更
上のプログラムだと、EVA-GIF変換が2つ起動してしまう Flaskの応答とEVA-GIF変換を別プログラムにして起動する
EVA-GIF変換の変換途中のファイルをFlaskサーバーが表示しようとしてエラー表示が出る 変換途中の拡張子を、.gif##とし、変換終了後に.gifにリネームする
EVAファイルを転送するときに、0バイトのファイルができることがある。 0バイトのファイルは削除する
Macから共有フォルダをアクセスしたときに、隠しファイルがいくつも作成される。そのファイルを処理しようとしてエラー終了する Macが作成する隠しファイルは読み飛ばす処理が必要

改良版プログラム

上の問題を修正したプログラム app-2.py, gif-2.py が以下

ダウンロードサーバープログラム

サーバーは内部フォルダをみる。内部フォルダで、GIF変換を行う。内部フォルダを最新にするのは、GIF変換プログラム。

app-2.py
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, make_response

import os         # ファイル操作
import shutil     # ファイル操作
import subprocess # プログラム実行
import time       # 時間処理
import re         # 文字置換,正規表現
import webbrowser # ブラウザ
import threading  # multi thread
import qrcode     # QRコード生成

EVA_FOLDER = '/home/share/xxxxxx' #アニメを保存する内部フォルダ(1日分)
HTTP_URL = '192.168.99.1'         #公開するURLアドレス(例)
HTTP_PORT = 8080                  #公開するポート

app = Flask(__name__)

inpFolder = EVA_FOLDER

class MyThread(threading.Thread):
    def __init__(self):
        super(MyThread, self).__init__()

    def run(self):
        loop(self,False)

@app.route('/')
def index():
  global inpFolder
  return render_template('index.html', images=sorted(os.listdir(inpFolder)[::-1], reverse=True), message = inpFolder)

@app.route('/images/<path:path>')
def send_js(path):
  global inpFolder
  return send_from_directory(inpFolder, path)

@app.route('/',methods=["POST"]) #アニメのアップロード
def save_eva():
  global inpFolder
  mno = -1
  #print("Posted file: {}".format(request.files['file']))
  file = request.files['file']
  if(file.filename.endswith('.eva')):
      files = [f for f in os.listdir(inpFolder)]   #入力
      for fn in files:
        eva = os.path.join(inpFolder,fn)
        if(not eva.endswith('.eva')):
            continue
        if( '_編集中' in eva):     #自動保存ファイルは無視
            continue
        if( '_とちゅう' in eva):
            continue
        if( '_autosave' in eva):
            continue
        no = int(os.path.splitext(os.path.basename(fn))[0])
        if(no > mno):               #最終番号を探す
            mno = no

      savepath = os.path.join(inpFolder, '%04d.eva' % (mno+1))
      file.save(savepath)
  return str(mno+1)   #保存した番号を返す


# 新しいフォルダができていないか確認するループ
def loop(self,askFolder):
  global inpFolder
  drmax = '0000-0000'
  while True:
    dirs = [f for f in os.listdir(EVA_FOLDER)] #一番新しい日付を取得
    drs = [s for s in dirs if re.match('[0-9]{4}-[0-9]{4}', s)]
    for dr in drs:
      if dr > drmax:
        drmax = dr
    inpFolder = EVA_FOLDER + '/' + drmax

    if(not os.path.exists(inpFolder)):    #フォルダがなかったら作成
        os.mkdir(inpFolder)

    if(askFolder):
        return inpFolder

    time.sleep(5.0) #sleep(秒指定)
  return inpFolder


t = MyThread()
inpFolder = loop(t,True)            #新フォルダへの移動
t.start()

html = 'http://' + HTTP_URL + ':' + str(HTTP_PORT)
img = qrcode.make(html)          #QR code作成
img.save('static/qrcode.png')
with open('static/setpath.ini', mode='w') as f:
    f.write(html)


if __name__ == '__main__':
    webbrowser.open(html)            #ブラウザ起動
    app.run(debug=True, host=HTTP_URL, port=HTTP_PORT)

EVA→GIF変換プログラム

gif-2.py
import os         # ファイル操作
import shutil     # ファイル操作
import subprocess # プログラム実行
import time       # 時間処理
import re         # 文字置換,正規表現

#ラズベリーパイ 例
ORG_FOLDER = '/home/pi/xxxxxx'    #アニメを保存するフォルダ(オリジナルをmount)
EVA_FOLDER = '/home/share/xxxxxx' #アニメを保存する内部フォルダ(1日分)
QVIEW_EXE = '/home/pi/9va/9view'  #9view.exe のフルパス

# Windows 例
#ORG_FOLDER = 'Q:/share/xxxxxx'    #ネットワークドライブで指定
#EVA_FOLDER = 'C:/share/xxxxxx'    #内部フォルダ


orgFolder = ORG_FOLDER
inpFolder = EVA_FOLDER

# GIF変換ループ
drmax = '0000-0000'
while True:
    dirs = [f for f in os.listdir(ORG_FOLDER)] #一番新しい日付を取得
    drs = [s for s in dirs if re.match('[0-9]{4}-[0-9]{4}', s)]
    for dr in drs:
      if dr > drmax:
        drmax = dr
    orgFolder = ORG_FOLDER + '/' + drmax
    inpFolder = EVA_FOLDER + '/' + drmax

    #ORGとINPを比較してデータ取得
    if(not os.path.exists(inpFolder)):    #フォルダがなかったら作成
        os.mkdir(inpFolder)

    files = [f for f in os.listdir(orgFolder)]   #入力
    for fn in files:
        eva = os.path.join(inpFolder,fn)
        if(not os.path.exists(eva)):    #ファイルがなかったら転送
          org = os.path.join(orgFolder,fn)
          shutil.copy(org, inpFolder)

    #不要フォルダ削除
    files = [f for f in os.listdir(EVA_FOLDER)]   
    for fn in files:
        if( '.DS_Store' in fn): # Mac用システムフォルダは無視
            continue
        if(fn != drmax):
            dr = os.path.join(EVA_FOLDER,fn)
            shutil.rmtree(dr)

    #サイズ0のEVAファイルは削除
    files = [f for f in os.listdir(inpFolder)]   #入力
    for fn in files:
        if(os.path.splitext(fn)[1] == '.eva'):
            dr = os.path.join(inpFolder,fn)
            if(os.path.getsize(dr)==0): #サイズが0のファイルは削除
                os.remove(dr)

    #GIF作成
    files = [f for f in os.listdir(inpFolder)]   #入力
    for fn in files:
        if(os.path.splitext(fn)[1] != '.eva'):
            continue
        gif = fn + '.gif'  #GIFファイル名
        gif = os.path.join(inpFolder,gif)
        eva = os.path.join(inpFolder,fn)
        if( '_編集中' in eva):     #自動保存ファイルは無視
            continue
        if( '_とちゅう' in eva):
            continue
        if( '_autosave' in eva):
            continue
        if(os.path.exists(gif)):    #gifが存在,作成すみチェック
            if(os.path.getmtime(gif) > os.path.getmtime(eva)):
                continue
        cmd = (QVIEW_EXE , eva , '-gif') #-gif オプションでGIFに変換
        #cmd = (QVIEW_EXE , eva , '-gif-trans') #-trans 背景透明にする場合
        print(cmd)                  #変換したことを表示
        subprocess.run(cmd)         #gif作成
        break;
    time.sleep(1.0) #sleep(秒指定)

起動方法(ラズベリーパイ)

起動時に、以下のシェルスクリプトで、EVAアニメを保存する共有フォルダをマウントし、その後、GIF変換、ダウンロードサーバーを起動しています。

#!/bin/sh 
sudo mount -t cifs //xx.xx.xx.xx/xx/xxxxxx /home/pi/xxxxxx -o user=xxxx,password=xxxx,sec=ntlmssp,nounix,noperm,rw 
sleep 10
cd /home/share/py
python3 /home/share/py/gif-2.py &
python3 /home/share/py/app-2.py

起動方法(Windows)

  • Windowsの場合、EVAアニメの共有フォルダをネットワークドライブに割り当てれば、Pythonからアクセスできます。(以下の例では gif.bat の中のnet use 命令で、Qドライブに割り当て)
  • GIF変換とサーバーの2つを別々のバッチファイルで起動します。

GIF変換の起動

gif.bat
timeout /t 30
net use Q: \\xx.xx.xx.xx\xxxx パスワード /user:ユーザー名
cd C:\Users\xx\xx\xx\py
python gif.py
pause

ダウンロードサーバーの起動

app.bat
timeout /t 50
cd C:\Users\xx\xx\xx\py
python app.py
pause

timeout は指定した時間(秒)待つ命令で、この2つをスタートアップフォルダに入れれば、先にGIF変換が起動し、そのあと、サーバーが起動します。

固定IPアドレスの割り当て

  • ダウンロードサーバー用 WiFiアクセスポイントは、WiFiルーターを使用しました。
  • Windows 機は、Wifi と、EVAアニメを保存する共有サーバーと2つのネットワークに接続します。それぞれ、固定IPアドレスを割り当てました。(コントロールパネル>ネットワークとインターネット>アダプターオプション>WiFiやLANアダプタアイコンの右ボタンメニュー>プロパティ>インターネットプロトコル(TCP/IPV4)>プロパティ>「IPアドレスを自動的に取得する」から「次のIPアドレスを使う」に変更)
  • 設定した固定IPアドレスを、Pythonプログラムの記載と一致させます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4