ElectronでSoundCloud SDKを使う

[Wantedly Advent Calendar 2015](http://qiita.com/advent-calendar/2015/wantedly)の15日目です!

最近Electronをやりたくて、SoundCloudクライアントを作り始めました。

認証~SDKの初期化しか作っていない絶賛WIPですが、ElectronでSoundCloud SDKを使うまでのハマリポイントをまとめます!

[shimpeiws/sc-fav](https://github.com/shimpeiws/sc-fav)

# TL;DR

* ElectronでSoundCloud SDKを使うときハマったのは **認証** と **SDKの初期化**
	* 認証は通常のWebブラウザ同様にはいかないので、 **Electronのprotocol API**を使う
	* SDKの初期化は **レンダラープロセス側**でやる

# SouncCloud SDK

SoundCloudはAPIが公開されていて、JavaScript用のSDKも用意されています。APIをラップしたインタフェースになっていて、Promiseが返ってくるいい感じのSDKです。

[SoundCloud JavaScript SDK](http://connect.soundcloud.com/examples/basic.html)

ただこのSDK、普通のWebブラウザでの使用を前提にしているので、Electronで使うのには、認証・SDKの初期化でハマりました...

## SoudCloud SDK or 自前実装

ちなみに、Electron製のSoundCloudクライアントとして[gillesdemey/Cumulus](https://github.com/gillesdemey/Cumulus)があるのですが、こちらはSDKは使わず、自前でAPIラッパーを書いています。

どちらを選ぶかはあなた次第...

# 認証

## Webブラウザでの流れ

SoundCloud APIの認証は通常、↓の流れになります。

1. SDKを初期化、新規ウィンドウが開く
2. 開かれたウィンドウ内で、SDK初期化時に設定したコールバックURLにリダイレクト
3. 2のウィンドウからTokenを取得する

[HTTP API Guide](https://developers.soundcloud.com/docs/api/guide#client-side)によると、2のコールバック先に↓のようなhtmlをおいておく、ということになっています。

```callback.html
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Connect with SoundCloud</title>
  </head>
  <body onload="window.opener.setTimeout(window.opener.SC.connectCallback, 1)">
    <b style="width: 100%; text-align: center;">This popup should automatically close in a few seconds</b>
  

</body></html>
```

`window.opener.SC.connectCallback`がキモなんですが、ElectronのBrowserWindowから開くと、 **window.openerがnullになるのです...**

調べてみると、[Electronのissue](https://github.com/atom/electron/issues/2464)にコメントが...、バグなのか?

![スクリーンショット 2015-12-13 23.31.11.png](https://qiita-image-store.s3.amazonaws.com/0/29637/c92b02f0-19f7-20f1-15cc-cf6668e43b5c.png "スクリーンショット 2015-12-13 23.31.11.png")


## Electronのprotocol APIを使う

普通なブラウザ同様の流れは使えないので、APIからのコールバックを **Electronのprotocol APIでインターセプトする**ことで、APIからのコールバックをElectronでうけとります。

[electron/docs/api/protocol.md](https://github.com/atom/electron/blob/master/docs/api/protocol.md)

↓のように実装して、APIのコールバック先を`sc-fav://oauth/callback`で指定しておくと、コールバックを受け取れるようになります。

```auth.js
setupProtocol() {
    let protocol = require('protocol');
    protocol.registerHttpProtocol('sc-fav', (req) => {
        let uri = url.parse(req.url);

        switch(uri.host) {
        case 'oauth':
            if (uri.pathname !== '/callback'){
                return;
            }

            let hash = uri.hash.substr(1);
            // トークン取得
            let token = querystring.parse(hash).access_token;
            // TODO: ログイン処理
            console.log('token', token);

            break;
        default:

        }
    });
}
```

この部分は[Cumulusの実装](https://github.com/gillesdemey/Cumulus/blob/master/index.js#L71)を参考にしました。

# SDKの初期化

[API Documentation](https://developers.soundcloud.com/docs/api/sdks#documentation)を見ながら実装します。

SDKの初期化はこんなインタフェース

```
SoundCloudSDK.initialize(
  {
    client_id: 'CLIENT_ID',
    redirect_uri: 'http://example.com/', //オプション項目
    oauth_token: 'TOKEN' // オプション項目
  }
);
```

## 'getUserMedia' of undefined

認証をElectronのメインプロセス(Node.jsのほう)でやったので、SDKの初期化もメインプロセスでやろうとすると、SDKから"'getUserMedia' of undefined"のエラーが...

![スクリーンショット_2015-12-12_10_22_38_のコピー.png](https://qiita-image-store.s3.amazonaws.com/0/29637/9a668dc5-9db6-4f05-9852-b9c2599fdbce.png)


SDK内の該当箇所を見ると、音声録音ができる機能があるので、navigator.getUserMediaでデバイスへのアクセスを求めるところがあるよう。メインプロセスの方では、navigatorがとれないので、undefinedになる様子...

## 認証からの流れ

悩んだ挙句、以下の流れで実装することに

1. **認証はメインプロセス**で行う
2. **SDKの初期化はレンダラーブロセス(BrowserWindow側)**で行う
3. 認証で取得したOAuthのトークンは **IPC(プロセス間通信)**でメインプロセスからレンダラープロセスに渡す

メインプロセス

```window_manager.js
// ログイン完了でTokenが帰ってきた後のコールバック
loginCallback: () => {
    this.mainWindow = new BrowserWindow({ width: 350, height: 640 });
    this.mainWindow.on('closed', () => {
        app.quit();
    });
    // 画面の読み込み完了を待つ
    this.mainWindow.webContents.on('did-finish-load', () => {
        this.auth.getToken().then((token) => {
            // IPCでトークンを送信
            this.mainWindow.webContents.send('token', token);
        });
    });
    this.mainWindow.loadURL(`file://${__dirname}/..${options.indexPath}`);
}
```

レンダラープロセス

```bootstrap.js
import React from 'react';
import ReactDom from 'react-dom';
import {Main} from './components/main';
import electron from 'electron';
import SoundCloud from 'soundcloud';

electron.ipcRenderer.on('token', (event, token) => {
    event.sender.send('token', 'success');  // 送信元へレスポンスを返す
    SoundCloud.initialize({
        client_id: 'CLIENT-ID',
        oauth_token: token
    });

    SC.get('/me/favorites').then(function(tracks){
        console.log('Latest track: ' + tracks[0].title);
    });

    SoundCloud.stream('/tracks/293').then((player) => {
        player.play();
    });
});
```

# まとめ

認証~SDKの初期化さえできれば、曲の再生、フォローの取得やLikeなど、一通りの動作が簡単に実装できるようになります。

SoundCloudユーザの方は、Electronでクライアントを作ってみてはいかがでしょう?

# 参考

[HTTP API Guide](https://developers.soundcloud.com/docs/api/guide)

[SoundCloud JavaScript SDK](http://connect.soundcloud.com/examples/basic.html)

[日本語訳 Electron(旧atom-shell) Quick start](http://qiita.com/kitfactory/items/cc153cdfaf3d9fd70044)

[Electronでipcを使ってプロセス間通信を行う](http://qiita.com/Misumi_Rize/items/dde76dbf89abee13991c)

[electron/docs/api/web-contents.md](https://github.com/atom/electron/blob/master/docs/api/web-contents.md#webcontentssendchannel-arg1-arg2-)