Pythonista for iOSで音楽再生その1 - MPMusicPlayer+MPMediaQuery+MPMediaPropertyPredicate -

はじめに
手始めに、ミュージックライブラリから取得した全楽曲を再生してみることにします。ただしクラウド上のアイテムだけは省くようにしてみます。
今のところPythonista向けの解説記事みたいなものはまだあまりないと思います。名前が紛らわしいので引っかからないだけかもしれません。
まぁObjective-CPythonでラップしてるだけなので複数言語使える人ならどうにかできると思いますが、自分向けのメモという意味でも残しておこうと思います。


参考ページ一覧
1. Pythonista for iOS で Nowplaying する
そもそもPythonistaでObjective-Cのオブジェクトを扱う方法がわからなかったので、こちらの記事を参考にしました。
サンプルではMPMusicPlayerController.iPodMusicPlayerを使っていますが、2017年2月現在、まだiPodMusicPlayerは使えますが一応deprecatedなのでsystemMusicPlayerを使うようにしましょう*1 *2
2. [iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用)
プレイヤーごとの違いも丁寧に書いてくださっていて非常にわかりやすいです。
再生や一時停止などの基本操作も載っているので言語はSwiftですが一読してみると良いと思います。
3. [iOS][Swift]MPMediaQueryを使って曲を絞り込む
2と同サイト様の記事です。2ではMPMediaPickerControllerで曲を選択していましたが、とりあえず全曲一括で欲しいのでMPMediaQueryというものを使います。
Queryと書いてありますしプロパティ設定してデータベースに問い合わせて楽曲リストを受け取るみたいなイメージでしょうか。まぁ詳しいことは参考ページを読んで下さい。


nackpan様のサイト http://nackpan.net/blog/ は今回の目的にはかなり合致しているので、度々紹介することになると思います。気になる方は色々読んでみると勉強になるかと思います。


本編
いきなりですが全体のコードです。

# -*- coding: utf-8 -*-

from __future__ import print_function, unicode_literals
from objc_util import NSBundle, ObjCClass
import inspect

def dp(o):
	print(o.__class__.__name__)
	if hasattr(o, '__call__'):
		print(inspect.getargspec(o))
	else:
		print(dir(o))

def printItems(ls, verbose=False):
	for item in ls:
		print(item.title())
		if verbose:
			print('- artist: %s' % item.artist())
			print('- albumTitle: %s' % item.albumTitle())
			print('- persistentID: %d' % item.persistentID())
			print('- isCloudItem: %r' % item.isCloudItem())
	print()

def createFilter(property, value, is_contains=0):
	mpp = ObjCClass('MPMediaPropertyPredicate').alloc()
	mpp.setProperty(property)
	mpp.setValue(value)
	mpp.setComparisonType(is_contains)
	return mpp
	
if __name__ == '__main__':
	NSBundle.bundleWithPath_('/System/Library/Frameworks/MediaPlayer.framework').load()
	MPMusicPlayerController = ObjCClass('MPMusicPlayerController')
	MPMediaQuery = ObjCClass('MPMediaQuery')
	
	player = MPMusicPlayerController.systemMusicPlayer()
	mq = MPMediaQuery.songsQuery()
	
	f0 = createFilter('isCloudItem', False)
	mq.addFilterPredicate(f0)
	
	player.setQueueWithQuery(mq)
	player.prepareToPlay()
	#player.play()
	
	printItems(mq.items())

(はてなダイアリープログラミング言語ハイライトとかそんな機能あったの知らなかった……)
それでは説明していきます。

from __future__ import print_function, unicode_literals

Pythonista3では2系と3系両方使えるのでできれば書いておきましょう。2系でも3系と同様にprintが関数形式で使えるのとプリフィックスなしの文字列がunicode扱いになるとかなんとかだったと思います。詳しくはググってください。

from objc_util import NSBundle, ObjCClass

これが肝です。Objective-Cのクラスを扱えるようにしてくれるobjc_utilを作った方は偉大です。↓
http://omz-software.com/pythonista/docs/ios/objc_util.html

import inspect

def dp(o):
	print(o.__class__.__name__)
	if hasattr(o, '__call__'):
		print(inspect.getargspec(o))
	else:
		print(dir(o))

def printItems(ls, verbose=False):
	for item in ls:
		print(item.title())
		if verbose:
			print('- artist: %s' % item.artist())
			print('- albumTitle: %s' % item.albumTitle())
			print('- persistentID: %d' % item.persistentID())
			print('- isCloudItem: %r' % item.isCloudItem())
	print()

この辺は私が作ったデバッグ用の関数です。
一つ目の参考ページだとvalueForProperty_を使ってプロパティを取得していますが、printItemsでやっているように直接メソッドを呼び出しても取れます。どっちでもいいと思います。
あとメソッド名の後ろにアンダーバーもつけなくても大丈夫っぽいです。
ちなみにdp内で、メソッドだけは引数の数とか見たくてargspec取ってますが意味ないです。メソッド本体もただのプロキシになってるのでselfしか引数取らないように見えますが、実際には渡せば送ってくれるっぽいです。
つまりAppleのリファレンスに載ってないメソッドだったら試行錯誤するしかないです。仮引数名くらいはどうにかすれば見れる気がします(白目)。

def createFilter(property, value, is_contains=0):
	mpp = ObjCClass('MPMediaPropertyPredicate').alloc()
	mpp.setProperty(property)
	mpp.setValue(value)
	mpp.setComparisonType(is_contains)
	return mpp

ここ、中身はフィルタ用のオブジェクトを生成して返すだけですが、PythonistaでのiOSアプリのコーディングに重要な要素が含まれてます。
MPMediaPropertyPredicateクラスがフィルター用のクラスなんですが、ObjCClassで取得できるのは「クラスオブジェクト」なのでallocでインスタンス化する必要があります
取得したクラスオブジェクトをdpで覗いてみればわかりますが、クラスメソッドかスタティックメソッドしか呼び出せないと思います。
私はPythonオブジェクト指向もド素人なので、この程度の説明しかできませんが、とりあえずインスタンス化する必要がある場合はallocを忘れないでください。

if __name__ == '__main__':
	NSBundle.bundleWithPath_('/System/Library/Frameworks/MediaPlayer.framework').load()

入れる場所がここで正しいのかわかりませんが、まぁ呼び出す側の最初に呼べばいいと思います。

	MPMusicPlayerController = ObjCClass('MPMusicPlayerController')
	MPMediaQuery = ObjCClass('MPMediaQuery')
	
	player = MPMusicPlayerController.systemMusicPlayer()
	mq = MPMediaQuery.songsQuery()

クラスオブジェクトをそのクラス名をつけた変数に入れといてあとで使い回す感じです。後でクラスオブジェクトを使う予定がなければ直接呼んでも構いません。今回の分に関しては直接呼んでも構いませんね。
ちなみにsystemMusicPlayerとsongsQueryの取得はインスタンス化する必要がないのでそのまま呼べます。
あとsongsQueryは全曲を曲名でソートして返してくれます。albumsQuery()とかだとアルバム名順にソートしてくれたりします。いくつか試してみると良いです。

	f0 = createFilter('isCloudItem', False)
	mq.addFilterPredicate(f0)
	
	player.setQueueWithQuery(mq)
	player.prepareToPlay()
	#player.play()
	
	printItems(mq.items())

あとは特に変わったところはないですね。
songsQueryが返してくれたクエリーに、生成したisCloudItem=Falseなフィルターをaddしてあげて、playerにセットします。
playじゃなくてprepareToPlayを使うと再生せずにキューのセットだけして待機してくれます。
フィルタに使うプロパティはhttps://developer.apple.com/reference/mediaplayer/mpmediaitemを参照してください。
MPMediaPropertyPredicateにはcomparisonTypeプロパティがあるので、完全一致だけじゃなく「〜〜という文字列を含む」という検索もできます*3。equalToは0で、containsは1です*4。一応createFilter関数にも対応させてあります(デフォルトはequalTo)。
ちなみに「〜〜という文字列を含まない」とか「〜〜という文字列と完全一致したものは除外」とかはできません。これに関しては後日恨みと共に解決策を書きます。


終わり
こんなところです。とりあえずこれでミュージックライブラリから取得した(クラウド上の曲を除く)全曲を曲名ソートした再生キューがセット+再生できます。
systemMusicPlayerを使っているのでBluetoothやイヤホン付属のリモコン、コントロールセンターからも操作できると思います。(リモコンは試してない)
systemMusicPlayerはプリインストールアプリの「ミュージック」を「終了する」という動作(再生キューセット後に開く→終了でも同様)に反応して再生が停止されるので注意です。
PythonistaはMac無しでいいところまでiOSアプリ開発ができそうな予感がしてるので、ユーザーが増えてくれるといいと思います。
実行後にコマンドインターフェイスも使えるのでぜひ色々試してみてください。大丈夫です、iOSならちょっとやそっとじゃ壊れないはずです。
長くなりましたが以上です。