Axis2でnasneから録画タイトル一覧を取得する
私がアニメ録画に愛用しているnasneですが、これはUPnPプロトコルで通信するためSOAPでAPIが呼び出せます。
そこで先人の皆様の知恵をお借りしつつ、JavaのSOAPライブラリであるApache Axis2で録画一覧の情報を取得してみました。
クライアントスタブの生成
UPnPではWSDLに相当するSCPD(Service Control Protocol Document)にSOAPアクションが記載されていますが、フォーマットは結構異なります。
ちなみにnasneの予約関連は以下に定義されています。
http://(nasneのIPアドレス):64230/XSRS.xml
今回はSCPDを見ながらEclipseのエディタでWSDLを作成しています。グラフィカルに編集できるので意外と楽でした。
XSRS.wsdl
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:schemas-xsrs-org:service:X_ScheduledRecording:2" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="XSRS" targetNamespace="urn:schemas-xsrs-org:service:X_ScheduledRecording:2"> <wsdl:types> <xsd:schema targetNamespace="urn:schemas-xsrs-org:service:X_ScheduledRecording:2"> <xsd:element name="X_CreateRecordSchedule"> <xsd:complexType> <xsd:sequence> <xsd:element name="Elements" type="xsd:string"/> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_CreateRecordScheduleResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="RecordScheduleID" type="xsd:string" /> <xsd:element name="Result" type="xsd:string"></xsd:element> <xsd:element name="UpdateID" type="xsd:int"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_GetConflictList"> <xsd:complexType> <xsd:sequence> <xsd:element name="Elements" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_GetConflictListResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="Result" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_DeleteRecordSchedule"> <xsd:complexType> <xsd:sequence> <xsd:element name="RecordScheduleID" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_DeleteRecordScheduleResponse"> <xsd:complexType> <xsd:sequence> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_GetRecordScheduleList"> <xsd:complexType> <xsd:sequence> <xsd:element name="SearchCriteria" type="xsd:string" nillable="true"> </xsd:element> <xsd:element name="Filter" type="xsd:string" nillable="true"></xsd:element> <xsd:element name="StartingIndex" type="xsd:int"></xsd:element> <xsd:element name="RequestedCount" type="xsd:int"></xsd:element> <xsd:element name="SortCriteria" type="xsd:string" nillable="true"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_GetRecordScheduleListResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="Result" type="xsd:string"></xsd:element> <xsd:element name="NumberReturned" type="xsd:int"></xsd:element> <xsd:element name="TotalMatches" type="xsd:int"></xsd:element> <xsd:element name="UpdateID" type="xsd:int"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_UpdateRecordSchedule"> <xsd:complexType> <xsd:sequence> <xsd:element name="Elements" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_UpdateRecordScheduleResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="Result" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_GetTitleList"> <xsd:complexType> <xsd:sequence> <xsd:element name="SearchCriteria" type="xsd:string" nillable="true"> </xsd:element> <xsd:element name="Filter" type="xsd:string" nillable="true"></xsd:element> <xsd:element name="StartingIndex" type="xsd:int"></xsd:element> <xsd:element name="RequestedCount" type="xsd:int"></xsd:element> <xsd:element name="SortCriteria" type="xsd:string" nillable="true"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_GetTitleListResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="Result" type="xsd:string"></xsd:element> <xsd:element name="NumberReturned" type="xsd:int"></xsd:element> <xsd:element name="TotalMatches" type="xsd:int"></xsd:element> <xsd:element name="UpdateID" type="xsd:int"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_DeleteTitle"> <xsd:complexType> <xsd:sequence> <xsd:element name="TitleID" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_DeleteTitleResponse"> <xsd:complexType> <xsd:sequence> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_UpdateTitle"> <xsd:complexType> <xsd:sequence> <xsd:element name="Elements" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="X_UpdateTitleResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="Result" type="xsd:string"></xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:schema> </wsdl:types> <wsdl:message name="X_CreateRecordScheduleRequest"> <wsdl:part element="tns:X_CreateRecordSchedule" name="parameters"/> </wsdl:message> <wsdl:message name="X_CreateRecordScheduleResponse"> <wsdl:part element="tns:X_CreateRecordScheduleResponse" name="parameters"/> </wsdl:message> <wsdl:message name="X_GetConflictListRequest"> <wsdl:part name="parameters" element="tns:X_GetConflictList"></wsdl:part> </wsdl:message> <wsdl:message name="X_GetConflictListResponse"> <wsdl:part name="parameters" element="tns:X_GetConflictListResponse"></wsdl:part> </wsdl:message> <wsdl:message name="X_DeleteRecordScheduleRequest"> <wsdl:part name="parameters" element="tns:X_DeleteRecordSchedule"></wsdl:part> </wsdl:message> <wsdl:message name="X_DeleteRecordScheduleResponse"> <wsdl:part name="parameters" element="tns:X_DeleteRecordScheduleResponse"></wsdl:part> </wsdl:message> <wsdl:message name="X_GetRecordScheduleListRequest"> <wsdl:part name="parameters" element="tns:X_GetRecordScheduleList"></wsdl:part> </wsdl:message> <wsdl:message name="X_GetRecordScheduleListResponse"> <wsdl:part name="parameters" element="tns:X_GetRecordScheduleListResponse"></wsdl:part> </wsdl:message> <wsdl:message name="X_UpdateRecordScheduleRequest"> <wsdl:part name="parameters" element="tns:X_UpdateRecordSchedule"></wsdl:part> </wsdl:message> <wsdl:message name="X_UpdateRecordScheduleResponse"> <wsdl:part name="parameters" element="tns:X_UpdateRecordScheduleResponse"></wsdl:part> </wsdl:message> <wsdl:message name="X_GetTitleListRequest"> <wsdl:part name="parameters" element="tns:X_GetTitleList"></wsdl:part> </wsdl:message> <wsdl:message name="X_GetTitleListResponse"> <wsdl:part name="parameters" element="tns:X_GetTitleListResponse"></wsdl:part> </wsdl:message> <wsdl:message name="X_DeleteTitleRequest"> <wsdl:part name="parameters" element="tns:X_DeleteTitle"></wsdl:part> </wsdl:message> <wsdl:message name="X_DeleteTitleResponse"> <wsdl:part name="parameters" element="tns:X_DeleteTitleResponse"></wsdl:part> </wsdl:message> <wsdl:message name="X_UpdateTitleRequest"> <wsdl:part name="parameters" element="tns:X_UpdateTitle"></wsdl:part> </wsdl:message> <wsdl:message name="X_UpdateTitleResponse"> <wsdl:part name="parameters" element="tns:X_UpdateTitleResponse"></wsdl:part> </wsdl:message> <wsdl:portType name="X_ScheduledRecordingType"> <wsdl:operation name="X_CreateRecordSchedule"> <wsdl:input message="tns:X_CreateRecordScheduleRequest"/> <wsdl:output message="tns:X_CreateRecordScheduleResponse"/> </wsdl:operation> <wsdl:operation name="X_GetConflictList"> <wsdl:input message="tns:X_GetConflictListRequest"></wsdl:input> <wsdl:output message="tns:X_GetConflictListResponse"></wsdl:output> </wsdl:operation> <wsdl:operation name="X_DeleteRecordSchedule"> <wsdl:input message="tns:X_DeleteRecordScheduleRequest"></wsdl:input> </wsdl:operation> <wsdl:operation name="X_GetRecordScheduleList"> <wsdl:input message="tns:X_GetRecordScheduleListRequest"></wsdl:input> <wsdl:output message="tns:X_GetRecordScheduleListResponse"></wsdl:output> </wsdl:operation> <wsdl:operation name="X_UpdateRecordSchedule"> <wsdl:input message="tns:X_UpdateRecordScheduleRequest"></wsdl:input> <wsdl:output message="tns:X_UpdateRecordScheduleResponse"></wsdl:output> </wsdl:operation> <wsdl:operation name="X_GetTitleList"> <wsdl:input message="tns:X_GetTitleListRequest"></wsdl:input> <wsdl:output message="tns:X_GetTitleListResponse"></wsdl:output> </wsdl:operation> <wsdl:operation name="X_DeleteTitle"> <wsdl:input message="tns:X_DeleteTitleRequest"></wsdl:input> </wsdl:operation> <wsdl:operation name="X_UpdateTitle"> <wsdl:input message="tns:X_UpdateTitleRequest"></wsdl:input> <wsdl:output message="tns:X_UpdateTitleResponse"></wsdl:output> </wsdl:operation> </wsdl:portType> <wsdl:binding name="X_ScheduledRecordingBinding" type="tns:X_ScheduledRecordingType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" /> <wsdl:operation name="X_CreateRecordSchedule"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_CreateRecordSchedule" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="X_GetConflictList"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_GetConflictList" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="X_DeleteRecordSchedule"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_DeleteRecordSchedule" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> </wsdl:operation> <wsdl:operation name="X_GetRecordScheduleList"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_GetRecordScheduleList" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="X_UpdateRecordSchedule"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_UpdateRecordSchedule" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="X_GetTitleList"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_GetTitleList" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="X_DeleteTitle"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_DeleteTitle" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> </wsdl:operation> <wsdl:operation name="X_UpdateTitle"> <soap:operation soapAction="urn:schemas-xsrs-org:service:X_ScheduledRecording:2/X_UpdateTitle" /> <wsdl:input> <soap:body use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="X_ScheduledRecordingService"> <wsdl:port binding="tns:X_ScheduledRecordingBinding" name="X_ScheduledRecording"> <soap:address location="http://192.168.0.2:64230/XSRS"/> </wsdl:port> </wsdl:service> </wsdl:definitions>
そしてAxis2同梱のWSDL2Javaツールを使って、クライアントスタブを生成します。私はWindowsなのでこんな感じです。
wsdl2java.bat -uri XSRS.wsdl -o (出力先ディレクトリ)
あとはこのスタブを呼び出すだけ…のはずでしたが、意外にもハマってしまいました。
</soap:Header>の除去
Axis2で生成したスタブが送信したリクエストを見ると、空のヘッダー要素である</soap:Header>が含まれていました。
しかしnasneはこれを含むリクエストをエラーとして処理してしまいます。
自動生成されたスタブのコードには手を加えず、リクエスト送信前に除去するモジュールを作成して対処します。
src/headerremover/RemovingModule.java
インターフェースを実装していますが中身はありません。
package headerremover; import org.apache.axis2.AxisFault; import org.apache.axis2.context.ConfigurationContext; import org.apache.axis2.description.AxisDescription; import org.apache.axis2.description.AxisModule; import org.apache.axis2.modules.Module; import org.apache.neethi.Assertion; import org.apache.neethi.Policy; public class RemovingModule implements Module { @Override public void applyPolicy(Policy arg0, AxisDescription arg1) throws AxisFault { } @Override public boolean canSupportAssertion(Assertion arg0) { return false; } @Override public void engageNotify(AxisDescription arg0) throws AxisFault { } @Override public void init(ConfigurationContext arg0, AxisModule arg1) throws AxisFault { } @Override public void shutdown(ConfigurationContext arg0) throws AxisFault { } }
src/headerremover/RemoveHandler.java
こちらに実際の処理である、ヘッダー要素を取り除く処理を書きます。
package headerremover; import org.apache.axiom.soap.SOAPEnvelope; import org.apache.axis2.AxisFault; import org.apache.axis2.context.MessageContext; import org.apache.axis2.engine.Handler; import org.apache.axis2.handlers.AbstractHandler; public class RemoveHandler extends AbstractHandler implements Handler { @Override public InvocationResponse invoke(MessageContext context) throws AxisFault { SOAPEnvelope envelope = context.getEnvelope(); if (envelope.getHeader() != null) { envelope.getHeader().detach(); } return InvocationResponse.CONTINUE; } }
META-INF/module.xml
モジュール名称、それに紐づくクラス、実行タイミングを定義します。
このモジュールでは"OperationOutPhase"を指定したので、処理はリクエスト送信前に実行されます。
<module name="header-remover" class="headerremover.RemovingModule"> <OutFlow> <handler name="OutFlowLogHandler" class="headerremover.RemoveHandler"> <order phase="OperationOutPhase"/> </handler> </OutFlow> </module>
以上を含むjarを作成して、拡張子を.marに変更します。
これをクラスパスに配置すると使用可能なモジュールとして読み込まれます。
レスポンスをJAXBでマッピング
レスポンスはXML形式なので、JAXBでオブジェクトにマッピングすると扱いやすくなります。
TrangでレスポンスからXMLスキーマを生成し、少し手を加えたものがこちらです。
XSRS.xsd
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:schemas-xsrs-org:metadata-1-0/x_srs/" xmlns:x_srs="urn:schemas-xsrs-org:metadata-1-0/x_srs/"> <xs:element name="xsrs"> <xs:complexType> <xs:sequence> <xs:element maxOccurs="unbounded" ref="x_srs:item"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="item"> <xs:complexType> <xs:sequence> <xs:element minOccurs="0" ref="x_srs:title"/> <xs:element ref="x_srs:scheduledStartDateTime"/> <xs:element ref="x_srs:scheduledDuration"/> <xs:element ref="x_srs:scheduledConditionID"/> <xs:element ref="x_srs:scheduledChannelID"/> <xs:element ref="x_srs:desiredMatchingID"/> <xs:element ref="x_srs:desiredQualityMode"/> <xs:element minOccurs="0" ref="x_srs:genreID"/> <xs:element ref="x_srs:conflictID"/> <xs:element ref="x_srs:mediaRemainAlertID"/> <xs:element ref="x_srs:reservationCreatorID"/> <xs:element ref="x_srs:titleProtectFlag"/> <xs:element ref="x_srs:titleNewFlag"/> <xs:element ref="x_srs:recordingFlag"/> <xs:element ref="x_srs:recordDestinationID"/> <xs:element ref="x_srs:recordSize"/> <xs:element ref="x_srs:lastPlaybackTime"/> <xs:element ref="x_srs:portableRecordFile"/> </xs:sequence> <xs:attribute name="id" use="required" type="xs:integer"/> </xs:complexType> </xs:element> <xs:element name="title" type="xs:string"/> <xs:element name="scheduledStartDateTime" type="xs:string" /> <xs:element name="scheduledDuration" type="xs:integer"/> <xs:element name="scheduledConditionID" type="xs:integer"/> <xs:element name="scheduledChannelID"> <xs:complexType> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="broadcastingType" use="required" type="xs:integer"/> <xs:attribute name="channelType" use="required" type="xs:integer"/> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> <xs:element name="desiredMatchingID"> <xs:complexType mixed="true"> <xs:attribute name="type" use="required" type="xs:string"/> </xs:complexType> </xs:element> <xs:element name="desiredQualityMode" type="xs:string" /> <xs:element name="genreID"> <xs:complexType> <xs:simpleContent> <xs:extension base="xs:integer"> <xs:attribute name="type" use="required" type="xs:integer"/> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> <xs:element name="conflictID" type="xs:integer"/> <xs:element name="mediaRemainAlertID" type="xs:integer"/> <xs:element name="reservationCreatorID" type="xs:integer"/> <xs:element name="titleProtectFlag" type="xs:integer"/> <xs:element name="titleNewFlag" type="xs:integer"/> <xs:element name="recordingFlag" type="xs:integer" /> <xs:element name="recordDestinationID" type="xs:string" /> <xs:element name="recordSize" type="xs:integer"/> <xs:element name="lastPlaybackTime"> <xs:complexType> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="resumePoint" use="required" type="xs:integer"/> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> <xs:element name="portableRecordFile"> <xs:complexType> <xs:simpleContent> <xs:extension base="xs:integer"> <xs:attribute name="target" use="required" type="xs:string"/> <xs:attribute name="transferPath" use="required" type="xs:string"/> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> </xs:schema>
そして作成したXMLスキーマから、JDK付属のxjcコマンドで対応するJavaクラスを生成します。
xjc XSRS.xsd -d (出力先ディレクトリ)
サンプル
上記で作成したクラスを使って、nasneから録画タイトルの一覧を取得してみます。
import java.io.StringReader; import javax.xml.bind.JAXB; import org.apache.axis2.client.ServiceClient; import org.apache.axis2.transport.http.HTTPConstants; import org.xsrs.schemas.metadata_1_0.x_srs.Xsrs; import _2.x_scheduledrecording.service.schemas_xsrs_org.X_ScheduledRecordingServiceStub; import _2.x_scheduledrecording.service.schemas_xsrs_org.X_ScheduledRecordingServiceStub.X_GetTitleList; import _2.x_scheduledrecording.service.schemas_xsrs_org.X_ScheduledRecordingServiceStub.X_GetTitleListResponse; public class SampleClient { private static final String END_POINT = "http://192.168.0.2:64230/XSRS"; public static void main(String[] args) throws Exception { // スタブを生成 X_ScheduledRecordingServiceStub stub = new X_ScheduledRecordingServiceStub(END_POINT); ServiceClient client = stub._getServiceClient(); // チャンク転送を無効に設定 client.getOptions().setProperty(HTTPConstants.CHUNKED, false); // モジュールを登録 client.engageModule("header-remover"); // リクエスト作成 X_GetTitleList req = new X_GetTitleList(); req.setStartingIndex(0); req.setRequestedCount(0); // レスポンス取得 X_GetTitleListResponse res = stub.x_GetTitleList(req); // JAXBでオブジェクトにマッピング Xsrs data = JAXB.unmarshal(new StringReader(res.getResult()), Xsrs.class); // 全件のタイトルを出力する data.getItem().stream().forEach(item -> System.out.println(item.getTitle())); } }
実行するとコンソールに一覧が出力されるはずです。
Windows TV ゴシックなどARIB外字を含むフォントがおすすめです。
参考
- PHPでnasneの録画情報を取得する - モロ屋
- Axis2 機能拡張モジュールの作成方法 ~SOAP レスポンスにヘッダーを追加する~ | Tk2Kpdn Wiki
- How does Axis2 load modules from the classpath? | WSO2 Inc
- XMLからスキーマ生成 - HK's Weblog
- UPnP Tools - Open Software Projects
- リクエストを試すのに便利でした。
- JEITA / JEITA規格・AV電子機器部門(その他)
- CPR-1206の59ページ以降にX_SRSの詳細が載っています。
これを参考に予約投入も試してみましたが上手くいきません…
- CPR-1206の59ページ以降にX_SRSの詳細が載っています。