MPMediaPropertyPredicateでは文字列型プロパティの不一致検索ができない

はじめに
Pythonista for iOSで音楽再生その1 - MPMusicPlayer+MPMediaQuery+MPMediaPropertyPredicate - - 雑魚グラマの妄言

MPMediaPropertyPredicateにはcomparisonTypeプロパティがあるので、完全一致だけじゃなく「〜〜という文字列を含む」という検索もできます*3。equalToは0で、containsは1です*4。一応createFilter関数にも対応させてあります(デフォルトはequalTo)。

ちなみに「〜〜という文字列を含まない」とか「〜〜という文字列と完全一致したものは除外」とかはできません。これに関しては後日恨みと共に解決策を書きます。

と書いたので少しまとめて書いてみようと思います。


事の発端


ミュージックライブラリにアルバムを複数入れてると思いますが、サントラとか一緒くたに再生キューに入れて聞かなくていいアルバムってあると思います。
そういったアルバムをフィルタで除外したかったんですが、結局そんな手法は用意されていませんでした。comparisonTypeもequalToとcontainsの2つしかないですし*1、フィルタを否定の意味で使うようなメソッドもありません。
というわけで、対象のリストからアルバム/曲名で完全一致/部分一致した楽曲のリストを弾くことで実現します。
このときはよく調べてなかったのですが、再生キューにはMPMediaQueryではなくMPMediaItemCollectionを使います。


本編
上で書いた通り、一度対象にするリストを取得した上で、除外する曲リストを新たに取得して、これを対象のリストから弾きます。

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

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

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

def extractMediaQueryItems(tgt):
	return tgt.items().mutableCopy()

if __name__ == '__main__':
	NSBundle.bundleWithPath_('/System/Library/Frameworks/MediaPlayer.framework').load()
	
	player = ObjCClass('MPMusicPlayerController').systemMusicPlayer()
	mq = ObjCClass('MPMediaQuery').albumsQuery()
	
	f0 = createFilter('isCloudItem', False)
	mq.addFilterPredicate(f0)
	all = extractMediaQueryItems(mq)

	f1 = createFilter('albumTitle', 'SOUNDTRACK', 1)
	mq.addFilterPredicate(f1)
	exc = extractMediaQueryItems(mq)

	tgt = all.mutableCopy()
	tgt.removeObjectsInArray(exc)
	
	pl = ObjCClass('MPMediaItemCollection').alloc()
	pl.initWithItems(tgt)

	player.setQueueWithItemCollection(pl)
	player.prepareToPlay()

紹介用なのでデバッグ用のコードは省いてます。では説明していきます。

def extractMediaQueryItems(tgt):
	return tgt.items().mutableCopy()
	f0 = createFilter('isCloudItem', False)
	mq.addFilterPredicate(f0)
	all = extractMediaQueryItems(mq)

この辺はあまり変わりませんが、allにメディアアイテムの可変長リストを抽出しています。
MPMediaQuery.items()が返すのはMPMediaEntityResultSetArrayというクラスで、この中にMPConcreteMediaItem(MPMediaItemの具象クラスっぽい)が詰まっています。
後で出てきますが私が使いたいのはNSMutableArrayクラスのremoveObjectsInArrayメソッドなので、mutableCopyメソッドでNSMutableArrayにコピー&変換してもらいます。というのをやってくれているのがextractMediaQueryItemsです。
NSMutableArrayはNSArrayの可変長版みたいなものらしいです。

	f1 = createFilter('albumTitle', 'SOUNDTRACK', 1)
	mq.addFilterPredicate(f1)
	exc = extractMediaQueryItems(mq)

今回は除外するアルバムとして、ごちうさのサントラを使います。(すまぬ……)
サントラはDisk1, 2とあるので共通する部分を使って部分一致検索でフィルタします。
そして除外リストのexcにさっきと同じようにNSMutableArrayを抽出します。

	tgt = all.mutableCopy()
	tgt.removeObjectsInArray(exc)

allそのまま使うのは気持ち悪いので「対象」リストの意味も込めてtgtにコピーしてます。
あとは対象リストからexcのリストの要素を消し飛ばすだけです。

	pl = ObjCClass('MPMediaItemCollection').alloc()
	pl.initWithItems(tgt)

	player.setQueueWithItemCollection(pl)
	player.prepareToPlay()

tgtはNSMutableArrayで、そのままだと再生キューとして使えないのでMPMediaItemCollectionというクラスに変換します。
SwiftやObjective-Cのネイティブならコンストラクタに渡すんでしょうが、Pythonistaでコンストラクタにどうやって渡すのかわからないので別のイニシャライザを使います。
dirしてみたらちょうど良さげなinitWithItemsというメソッドがあったので、これにメディアアイテムを詰め込んだNSMutableArrayのtgtを渡してあげると無事に再生キューとして使えるいい感じのMPMediaItemCollectionが出来上がります。
あとはもうsetQueueするだけです。前回はWithQueryでしたが、MPMediaQueryとMPMediaItemCollectionは全然別物らしいので、ちゃんとWithItemCollectionを使いましょう。
これで完成です。


おわりに
いかかでしょうか。フィルタを否定の意味で使おうと思うとかなり面倒なことになるのがわかっていただけたと思います。
MPMediaFilterPredicateに否定用のフラグ一つ足しておいてくれるだけで良かったと思いますが、これがAppleAPI実装らしいです(憤怒)。
このクソさは次回も多分愚痴と共に語ることになると思います。では。