普段からAppSalesというアプリで、日々の売上をチェックしているのですが、
iTunes Connectの更新時間が19:00〜20:00くらいとまちまちで、
処理が遅れてる時はそれよりも遅れることもあります。
まだ初アプリをだしてから半月というところなので、
毎日この時間帯になるとソワソワして、他の事が手につきません。
何回も更新ボタンを押し続けるのは時間の無駄なので、
売上情報が更新されたら、通知してくれるようにできないかなと考えてました。
なんとなく出来そうだったので、↓のような感じで実装してみました。
iTunes Connectからの情報取得
まずは、iTunesConnectからの情報の取得ですが、
AppSalesがやってるので、他の言語でもできるだろうなと思って、
既に他言語で公開されてないか調べてみました。
iPhoneアプリで稼げるのかで、Ruby版が公開されていました。
GoogleCodeでPython版を見つけました。
ただ普段はぺちぱーだったりするので、
PHPの方がやりやすいかなと思ってたらありました。
enormego.comでHeartbeatというアプリの売上を提供するサービスを
作っている方が公開してくれているようです。
Heartbeatと連携するスクリプトのようですが、
* Feel free to modify this as necessary to satisfy your needs.
と書いてくれているので、これを修正させてもらってiPhoneに通知させてみようと思います。
ソースコード
みなさまありがとうございます。
iPhoneへの通知(PushNotification)
データ取得はできそうなので、次は通知部分。
メールとかでもさくっとできそうですが、
せっかくiPhoneアプリ開発をしていて、AppSalesも使わせてもらっているので、
PushNotificationで通知を受け取り、そのままAppSalesを立ち上げるとうい
流れが作れないか考えてみました。
そこで、ちょっと前に知ったim.kayac.comという
PushNotificationを送ってくれるAPIを提供しているサービスを思い出しました。
これは何かに使える!と思って、アプリとアカウントを用意して試用したくらいで
しばらく放置していたものだったのですが、これしかない!と思いました。
PHPからこのAPIを叩く方法は、im.kayac.comからリンクされていた、
/halt/Snapshotさんの記事を参考にさせていただきました。
この方法の場合、アカウント設定でAPIの認証方式を”秘密鍵認証を使用する”にしておきます。
im.kayac.comでアカウントを作成し、
iPhoneアプリをインストールして設定します。
僕は最初から有料版を入れましたが、無料版もあったようです。
im.kayac.com:FREE Edition(iTunes)
im.kayac.com(iTunes)
AppSalesでURL Schemeを設定する
im.kayac.comの通知には、他のiPhoneアプリを立ち上げるためのURLを付加する事ができます。
なので、AppSales側でURL Schemeを設定してあげれば、
通知を受けてから、AppSalesをそのまま立ち上げる事が可能になります。
URL Schemeについては、まだ細かい部分は理解していないのですが、
ひとまずはinfo.plistに下記の様にURL Typesを追加すると
URL Schemesに設定した名前で呼び出せるようになるようです。
この場合だと、appsales:// で呼び出せます。
もっと細かく起動を制御する時はinfo.plistとアプリ側での開発も必要になるようです。
今回は起動までできれば不都合はないので、ここまでとします。
※こちらはもとまかさんの記事を参考にさせていただきました。ありがとうございます。

まとめ
・im.kayac.comでアカウント登録、設定
・im.kayac.comのiPhoneアプリをインストールして設定
・下記スクリプトを実行する
と、すると
・iTunes Connectに前日分の売上データがあれば、iPhoneに通知が送られ、
そのままAppSalesを立ち上げる事が可能になります。
・売上データ取得時に、ローカルに日付名のファイルを作ります。
同日のファイルがあると、処理をスキップするので、
このスクリプトをCronなどに登録して、データが反映されそうな時間帯で
繰り返し実行させておけば、あまりタイムラグなく通知が受けられると思います。
(あまり実行させすぎも迷惑だと思いますが)

PushNotification

im.kayac.com

Open AppSales
スクリプト
作成した(といっても足し合わせただけすが)スクリプトを公開しておきます。
わりとやっつけでくっつけてしまっているので、
エラー処理や構成など機会を見て、キレイにしようと思います。
※細かい処理まで見きれていないので、ご利用は自己責任でお願いします。
<?php
/**
* Date: Dec 19th, 2009
* Copyright MaltedMilk. 2009 all rights reserved
*
* このスクリプトは下記ソースコードを参考にしています。
* http://snippie.net/snip/f835bd0d
* http://project-p.jp/halt/anubis/blog_show/1269
*
*/
// Run Artery
$run = new Artery('iTUNES_CONNECT_USERNAME', 'iTUNES_CONNECT_PASSWORD', 'KAYAC_USERNAME', 'KAYAC_PASSWORD', 'DATA_DIR');
unset($run);
/**
* There should be no need to modify anything below this line.
*/
class Artery {
private $cookie;
private $user_agent = 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; en) AppleWebKit/528.5+ (KHTML, like Gecko) Version/4.0 Safari/528.1';
private $referrer = 'https://itts.apple.com/cgi-bin/WebObjects/Piano.woa/';
private $username;
private $password;
private $kayac_username;
private $kayac_password;
private $curl_common = array();
private $login_output; // Output from login method
private $available_daily_reports_output; // Output from available_reports method
private $wosid; // Session ID container
private $keys = array(); // Form keys container
public function __construct($username, $password, $kayac_username, $kayac_password, $data_dir = './') {
// Set Some Data
$this->username = $username;
$this->password = $password;
$this->kayac_username = $kayac_username;
$this->kayac_password = $kayac_password;
$this->data_file;
// Setup cURL
$this->setup_curl();
// File Path
$timestamp = strtotime('yesterday');
$date = date('m/d/Y', $timestamp);
$this->data_file = $data_dir . date('Y-m-d', $timestamp) . '.tsv';
if (file_exists($this->data_file)) {
exit(0);
}
// Login
if(!$this->login()) {
exit('Login failed. Check your credentials and try again.');
}
// What's available to us? (Only if we need reports)
$available = $this->available_daily_reports();
// Get Report
if(in_array($date, $available)) {
$report = $this->report($date);
}
// Check Data
if(stristr($report, 'Vendor Identifier')) {
$this->pushNotification($report);
$this->saveData($report);
}
}
private function login() {
// Get Login Form URL
$result = $this->get('https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa');
preg_match('#action="(.*?)"#ismu', $result, $matches);
// Check for login url
if(empty($matches[1])) {
return false;
}
// Attempt Login
$this->login_output = $this->post("https://itunesconnect.apple.com".$matches[1], array('theAccountName' => $this->username, 'theAccountPW' => $this->password));
// Validate that we are indeed logged in
if(!stristr($this->login_output, 'theAccountPW')) {
$this->login_output = $this->get("https://itts.apple.com/cgi-bin/WebObjects/Piano.woa/");
if(!stristr($this->login_output, 'theAccountPW')) {
$this->logged_in = true;
// Get Session ID
preg_match('#name=\"wosid\" value=\"(.*?)\"#ismu', $this->login_output, $matches);
$this->wosid = $matches[1];
return true;
} else {
return false;
}
} else {
return false;
}
}
public function available_daily_reports() {
// Check login
if(!$this->logged_in) {
throw new Eight_Exception('You must run the login method before proceeding.');
}
// Find correct form names
preg_match('#onchange\=\"handleSelReportType\(\)\" name\=\"(.*?)\"#ismu', $this->login_output, $matches);
$this->keys['summary'] = $matches[1];
preg_match('#onchange\=\"handleSelDateType\(\)\" name\=\"(.*?)\"#ismu', $this->login_output, $matches);
$this->keys['daily'] = $matches[1];
// Select Report
$values = array();
$values[$this->keys['summary']] = 'Summary';
$values[$this->keys['daily']] = 'Daily';
$values['hiddenDayOrWeekSelection'] = 'Daily';
$values['hiddenSubmitTypeName'] = 'ShowDropDown';
preg_match('#name="frmVendorPage" action="(.*?)"#ismu', $this->login_output, $matches);
$this->available_daily_reports_output = $this->post("https://itts.apple.com".$matches[1], $values);
preg_match('#\<select Id\=\"dayorweekdropdown\".*?\>(.*?)\<\/select\>#ismu', $this->available_daily_reports_output, $matches);
preg_match_all('#\<option value\=\"(.*?)\"\>#ismu', $matches[1], $matches);
return $matches[1];
}
public function report($date) {
// Check login
if(!$this->logged_in) {
throw new Eight_Exception('You must run the login method before proceeding.');
}
// Find correct form keys
preg_match('#onChange\=\"jsClearErrorMsg\(\);\" name\=\"(.*?)\"#ismu', $this->available_daily_reports_output, $matches);
$this->keys['date'] = $matches[1];
// Select Report
$values = array();
$values[$this->keys['daily']] = 'Daily';
$values['hiddenDayOrWeekSelection'] = $date;
$values['hiddenSubmitTypeName'] = 'Download';
$values[$this->keys['summary']] = 'Summary';
$values[$this->keys['date']] = $date;
$values['download'] = 'Download';
$values['wosid'] = $this->wosid;
preg_match('#name="frmVendorPage" action="(.*?)"#ismu', $this->login_output, $matches);
$output = $this->post("https://itts.apple.com".$matches[1], $values);
return $this->gzuncompress($output);
}
private function get($url) {
return $this->curl($url, $this->curl_common);
}
private function post($url, $data = array()) {
$options = array(
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_POST => true,
CURLOPT_FOLLOWLOCATION => true,
);
return $this->curl($url, $this->curl_common + $options);
}
private function curl($url, $options=array()) {
$options = $options + array(CURLOPT_RETURNTRANSFER => true) + $this->curl_common;
$c = curl_init($url);
curl_setopt_array($c, $options);
$output = curl_exec($c);
curl_close($c);
return $output;
}
private function setup_curl() {
// Create Cookie
$this->cookie = tempnam('/tmp', 'cookie');
// Set cURL Options
$this->curl_common = array(
CURLOPT_USERAGENT => $this->user_agent,
CURLOPT_REFERER => $this->referrer,
CURLOPT_COOKIEJAR => $this->cookie,
CURLOPT_COOKIEFILE => $this->cookie,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => array("Expect:", 'Accept: */*', 'Connection: Keep-Alive'),
);
}
// Custom gz uncompress because PHP's is broken
private function gzuncompress($data) {
$f = tempnam('/tmp', 'gz_fix');
file_put_contents($f, $data);
$uncompressed = file_get_contents('compress.zlib://'.$f);
unlink($f);
return $uncompressed;
}
public function __destruct() {
// Delete Cookie
unlink($this->cookie);
}
private function pushNotification($report) {
$pushData = '';
$records = explode("\n", $report);
array_shift($records);
foreach ($records as $record) {
if (! $record) {
continue;
}
$data = explode("\t", $record);
$pushData .= sprintf("%s %s * %s = %s (%s)[%s]" . PHP_EOL,
$data[6], // Title
$data[10], // Royality Price
$data[9], // Units
$data[10] * $data[9],
$data[15], // Royality Currency
$data[14] // Country Code
);
}
$data = array(
"message" => $pushData,
"password" => $this->kayac_password,
'handler' => 'appsales://',
);
$data['sig'] = sha1($data['message'] . $data['password']);
unset($data['password']);
$data = http_build_query($data, "", "&");
//header
$header = array(
"Content-Type: application/x-www-form-urlencoded",
"Content-Length: ".strlen($data)
);
$context = array(
"http" => array(
"method" => "POST",
"header" => implode("\r\n", $header),
"content" => $data
)
);
$url = "http://im.kayac.com/api/post/{$this->kayac_username}";
file_get_contents($url, false, stream_context_create($context));
}
private function saveData($report) {
$result = file_put_contents($this->data_file, $report);
if ($result === false) {
echo "Can not write {$this->data_file}" . PHP_EOL;
exit(1);
}
}
} // End Artery Class
最近のコメント