BoxとFastAPIを使用したメタデータサービスの構築

Yuko Taniguchi
Box Developer Japan Blog
15 min readFeb 17, 2023

この記事では、メディアファイルのメタデータを自動的に入力するためのサービスを構築します。Box Platformの機能を組み合わせ、FastAPIとPythonを使用して簡単にサードパーティのライブラリと統合する方法を説明します。

ユースケース

この例は、開発者によって投稿された以下の質問から発想を得たものです。

box.comのファイルのリストにビデオの再生時間を表示しようとしていますが、APIのどこにもその方法が見つかりません。Boxにアップロードされる多数のビデオに対してこの処理が必要なため、ファイル全体をダウンロードせずにビデオの再生時間を取得したい (ビデオのプレビューに表示できるようにしたい) と思っています。

問題1: ビデオファイルには、縦横比、解像度、ビットレート、再生時間、エンコーディング、フレームレートなど、実に多くの種類のプロパティがありますが、簡単には取得できません。ビデオファイルを取得してこのメタデータを出力できるライブラリを見つける必要があります。

問題2: 通常、ビデオファイルはサイズが大きい (例えば、標準的なエンコーディングを使用した1080pの平均的な動画は5 GBになります) ものの、最新/最先端のエンコーディングにより1 GBまで小さくなります。このようなファイルを多数ダウンロードすると、時間がかかるうえ、使用されるストレージ容量もかなり多くなります。

問題3: このデータすべてをどのように扱えばよいでしょうか。どこにデータを保存すれば、検索可能にできるなど、役立てることができるでしょうか。

ソリューションを探す

MediaInfoは、MediaAreaが作成したオープンソースライブラリです。MediaAreaはデジタルメディアの分析を専門としており、そのライブラリはメディアファイルの興味深いプロパティを多数出力できます。これにより、問題1が解決します。

このライブラリを使用しているうちに、URLからファイルを分析する方が、ファイルをダウンロードして解析するよりも時間がかからないことに気付きました。コードは調べていませんが、その処理を行うために必要なのは最初の数キロバイトだけのようです。これにより、問題2が解決します。

tic_download = time.perf_counter()
media_info = MediaInfo.parse(item_url)
print(f"MediaInfo w/ URL time: {time.perf_counter() - tic_download} seconds")

tic_download = time.perf_counter()
with open('./tmp/tmp_'+item.name, 'wb') as tmp_file:
item.download_to(tmp_file)
media_info = MediaInfo.parse('./tmp/tmp_'+item.name)
print(f"MediaInfo w/ download time: {time.perf_counter() - tic_download} seconds")
Folder: 191494027812:Video Samples
Item: 1121082178302:BigBuckBunny.mp4:file
MediaInfo w/ URL time: 3.798498541000299 seconds
MediaInfo w/ download time: 21.247453375020996 seconds
Done

Box Platformは、コンテンツに関するメタデータを格納および検索することができます。また、コンテンツのメタデータとそのテンプレートを管理するための一連のAPIも備わっています。基本的には、メタデータテンプレートを定義してから、それをコンテンツに適用することができます。これにより、問題3が解決します。

手動で50を超える属性を入力したい人はいないと思いますので、この処理をすべて実行するAPIを作成しましょう。

APIを作成するためのツール

Box統合の処理には、box-python-sdkを使用します。このSDKは、認証とメタデータの操作を処理します。

さらに、PythonとFastAPIも使用します。FastAPIを使用してこのAPIを作成することにより、この機能をその他のアプリで再利用でき、Box PlatformのWebhookを利用してメタデータの分類を自動化することもできます。

このデモアプリのソースコードは、こちらのGitHubリポジトリにあります。

BoxにWebhookを実装する方法は、以下に示す以前の記事でWebhookについて説明しています。

メタデータテンプレートの作成

Boxのメタデータテンプレートは、管理コンソールから作成することも、APIを使用してプログラムで作成することもできます。

テンプレートは非常にシンプルで、文字列、日付、浮動小数点、単一選択、複数選択の属性を作成できます。

ビデオの属性は50以上あり、ライブラリの出力と厳密に一致させたいため、ビデオファイルのサンプル出力からプログラムによってテンプレートを作成します。

@router.post("/metadata", status_code=201)
async def create_metadata_template(
force: bool | None = False, settings: Settings = Depends(get_settings)
):
"""Creates the metadata template for use in this service"""
client = get_box_client(settings)

template = get_metadata_template_by_name(
client, settings.MEDIA_METADATA_TEMPLATE_NAME
)
#### Code remove for simplicity ####

media_info = get_sample_dictionary()

template = create_metadata_template_from_dict(
client, settings.MEDIA_METADATA_TEMPLATE_NAME, media_info
)

result = template.response_object
return {"status": "success", "data": result}

当然ながら、このようなメソッドをAPIで公開することはありません。これは説明のためのもので、メソッドの呼び出しをテストできるFastAPIの自動生成ドキュメントを利用するための例です。

get_sample_dictionary()は、メディアファイルライブラリのサンプル出力を返します。

from boxsdk.object.metadata_template import (
MetadataField,
MetadataFieldType,
MetadataTemplate,
)
def create_metadata_template_from_dict(
client: Client, name: str, media_info: dict
) -> MetadataTemplate:
"""create a metadata template from a dict"""

# check if template exists
template = get_metadata_template_by_name(client, name)
if template is not None:
raise ValueError(f"Metadata template {name} already exists")

fields = []
for key in media_info:
fields.append(MetadataField(MetadataFieldType.STRING, key))

template = client.create_metadata_template(name, fields, hidden=False)

return template

fields.app(MetadataField(MetadataFieldType.STRING,key))では、client.create_metadata_template()メソッドのテンプレートで使用するフィールドのリストを追加するだけです。

最終的な結果として、以下のとおり、55の属性を含むテンプレートが作成されます。

ファイルのメタデータの設定

この一連の手順を以下に示します。

@router.post("/file/{file_id}/{as_user_id}", status_code=201)
async def set_file_metadata_as_user(
file_id: str,
as_user_id: str | None = None,
settings: Settings = Depends(get_settings),
):
"""Process media file and fill in the metadata info using 'as-user'
security context"""
exec_start = time.perf_counter()

client = get_box_client(settings, as_user_id)

最初に、認証済みクライアントを取得します (get_box_client())。今回のケースでは、JWT認証を使用します。

def get_box_client(settings: Settings, as_user: str | None = None) -> Client:
"""Returns a box client, optionally impersonating a user"""
client = jwt_check_client(settings)
if as_user is not None:
user = client.user(user_id=as_user).get()
client = client.as_user(user)
return client

これは、JWTトークンに関連付けられたサービスアカウントのセキュリティコンテキストでは、コンテンツにアクセスできない可能性があることを意味します。その場合、サービスユーザーが代理を務めるas_user_idを指定できます。

次に、テンプレートを取得する必要があります。

@router.post("/file/{file_id}/{as_user_id}", status_code=201)
async def set_file_metadata_as_user(###):
### ...
template = get_metadata_template_by_name(
client, settings.MEDIA_METADATA_TEMPLATE_NAME
)
if template is None:
raise HTTPException(
status_code=404,
detail=f"Metadata template {settings.MEDIA_METADATA_TEMPLATE_NAME} does not exist",
)
### ...

その後、ファイルを取得します。

@router.post("/file/{file_id}/{as_user_id}", status_code=201)
async def set_file_metadata_as_user(###):
### ...
file = get_file_by_id(client, file_id)
if file is None:
raise HTTPException(
status_code=404,
detail=f"File {file_id} does not exist",
)
### ....

get_file_by_id()は、次に示すように、Box Python SDKのシンプルなBoxクライアントメソッドです。

def get_file_by_id(client: Client, file_id: str) -> File:
"""Returns the box file by id"""
file = client.file(file_id=file_id).get()
return file

次に、メディア情報を取得します。

@router.post("/file/{file_id}/{as_user_id}", status_code=201)
async def set_file_metadata_as_user(###):

### ...
media_info = get_media_info_by_url(file.get_download_url())
if media_info is None:
raise HTTPException(
status_code=404,
detail=f"Unable to get media info for file {file_id}",
)
### ...

ここで必要なのはGeneralトラックのみです。ほとんどの場合、ビデオファイルにはビデオトラック、オーディオトラック、テキストトラックが複数含まれています (監督による解説、複数の音声言語、字幕など) が、MediaInfoはこの全般的なトラックの情報の概略を返します。

def get_media_info_by_url(download_url: str) -> dict:
"""get the file by id"""
media_info_raw = MediaInfo.parse(download_url)
return media_info_raw.general_tracks[0].to_data()

最後に、ファイルのメタデータを設定し、呼び出し元に返すことができます。

@router.post("/file/{file_id}/{as_user_id}", status_code=201)
async def set_file_metadata_as_user(###):
### ...
try:
metadata = file_metadata_set(file, template, media_info)
except BoxAPIException as error:
raise HTTPException(
status_code=error.status,
detail=error.message,
) from error

exec_end = time.perf_counter()
exec_time = exec_end - exec_start

return {
"status": "success",
"executed_in_seconds": exec_time,
"data": metadata,
}

最初にローカルの一時フォルダにダウンロードしてからそのフォルダを分析するメソッドも作成しているので、比較してみましょう。

実際の動作の確認

URLからの場合、このファイルでは約8.8秒かかります。

最初にダウンロードする場合は、23.2秒かかります。

Boxアプリで、ユーザーは、メタデータが設定されていることを確認できます。

さらに、メタデータの検索も適用されています。

メタデータはコンテンツに関する多くの情報を提供し、さまざまな点でユーザーの役に立ちます。メタデータは、コンテンツに関するコンテキスト (コンテンツの作成者、作成日、ライセンス情報など) を提供します。

主なメリットの1つとして、Box内で検索可能である点が挙げられます。これにより、ユーザーは、探しているコンテンツを簡単に見つけることができます。

また、通常の検索と比較した場合、新しいファイルでは、コンテンツにインデックスを作成するため、インデックスの作成に数分かかることがありますが、メタデータのインデックスはほぼ即座に作成されます。

Boxのメタデータの詳細については、以下のドキュメントを参照してください。

--

--