LoginSignup
5
0

More than 5 years have passed since last update.

MacのiTunes向けにD言語でkqueueを使っていい感じのNow Playingツールを作る。

Last updated at Posted at 2016-12-20

はいどうも。なんか20日の枠が空いてたので書くことにしました...(D言語Advent Calendarを埋めたかったので)
それで、ネタとして最近ちょろっと書いたMacのiTunes向けのNow Playingツールをkqueueを使って作ったのでそれについて書こうと思います。

なお、今回作るツール、nplyはGitHubにて公開しています: GitHub - alphaKAI/nply

kqueueとは。

kqueueとはFreeBSDにFreeBSD 4.1-RELEASEから追加されたシステムコールです。
OS XはBSD系なので、利用可能です。
D言語で使う場合は、本来core.sys.darwin.sys.eventをimportするだけで使えるはずなのになんかリンクに失敗するので全く同じ内容のファイルを別途作ってそれをimportする必要があります...たぶんなんかcore.sys.darwin.sys.eventがビルド時になんかdmdから読まれないっぽいですね(それはそう)

とりあえず、使うDバインディングをkqueuez.dというファイル名でcore.sys.darwin.sys.eventの中身を保存します。

kqueuez.d
/**
 * D header file for Darwin.
 *
 * Copyright: Copyright Martin Nowak 2012. Etienne Cimon 2015.
 * License:   $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
 * Authors:   Martin Nowak
 */

/*          Copyright Martin Nowak 2012. Etienne Cimon 2015.
 * Distributed under the Boost Software License, Version 1.0.
 *    (See accompanying file LICENSE or copy at
 *          http://www.boost.org/LICENSE_1_0.txt)
 */

version (OSX)
    version = Darwin;
else version (iOS)
    version = Darwin;
else version (TVOS)
    version = Darwin;
else version (WatchOS)
    version = Darwin;

version (Darwin):
extern (C):
nothrow:
@nogc:

import core.stdc.stdint;    // intptr_t, uintptr_t
import core.sys.posix.time; // timespec

enum : short
{
    EVFILT_READ     =  -1,
    EVFILT_WRITE    =  -2,
    EVFILT_AIO      =  -3, /* attached to aio requests */
    EVFILT_VNODE    =  -4, /* attached to vnodes */
    EVFILT_PROC     =  -5, /* attached to struct proc */
    EVFILT_SIGNAL   =  -6, /* attached to struct proc */
    EVFILT_TIMER    =  -7, /* timers */
    EVFILT_MACHPORT =  -8, /* Mach portsets */
    EVFILT_FS       =  -9, /* filesystem events */
    EVFILT_USER     = -10, /* User events */
    EVFILT_VM       = -12, /* virtual memory events */
    EVFILT_SYSCOUNT =  11
}

extern(D) void EV_SET(kevent_t* kevp, typeof(kevent_t.tupleof) args)
{
    *kevp = kevent_t(args);
}

struct kevent_t
{
    uintptr_t    ident; /* identifier for this event */
    short       filter; /* filter for event */
    ushort       flags;
    uint        fflags;
    intptr_t      data;
    void        *udata; /* opaque user data identifier */
}

enum
{
    /* actions */
    EV_ADD      = 0x0001, /* add event to kq (implies enable) */
    EV_DELETE   = 0x0002, /* delete event from kq */
    EV_ENABLE   = 0x0004, /* enable event */
    EV_DISABLE  = 0x0008, /* disable event (not reported) */

    /* flags */
    EV_ONESHOT  = 0x0010, /* only report one occurrence */
    EV_CLEAR    = 0x0020, /* clear event state after reporting */
    EV_RECEIPT  = 0x0040, /* force EV_ERROR on success, data=0 */
    EV_DISPATCH = 0x0080, /* disable event after reporting */

    EV_SYSFLAGS = 0xF000, /* reserved by system */
    EV_FLAG1    = 0x2000, /* filter-specific flag */

    /* returned values */
    EV_EOF      = 0x8000, /* EOF detected */
    EV_ERROR    = 0x4000, /* error, data contains errno */
}

enum
{
    /*
     * data/hint flags/masks for EVFILT_USER, shared with userspace
     *
     * On input, the top two bits of fflags specifies how the lower twenty four
     * bits should be applied to the stored value of fflags.
     *
     * On output, the top two bits will always be set to NOTE_FFNOP and the
     * remaining twenty four bits will contain the stored fflags value.
     */
    NOTE_FFNOP      = 0x00000000, /* ignore input fflags */
    NOTE_FFAND      = 0x40000000, /* AND fflags */
    NOTE_FFOR       = 0x80000000, /* OR fflags */
    NOTE_FFCOPY     = 0xc0000000, /* copy fflags */
    NOTE_FFCTRLMASK = 0xc0000000, /* masks for operations */
    NOTE_FFLAGSMASK = 0x00ffffff,

    NOTE_TRIGGER    = 0x01000000, /* Cause the event to be
                                  triggered for output. */

    /*
     * data/hint flags for EVFILT_{READ|WRITE}, shared with userspace
     */
    NOTE_LOWAT      = 0x0001, /* low water mark */

    /*
     * data/hint flags for EVFILT_VNODE, shared with userspace
     */
    NOTE_DELETE     = 0x0001, /* vnode was removed */
    NOTE_WRITE      = 0x0002, /* data contents changed */
    NOTE_EXTEND     = 0x0004, /* size increased */
    NOTE_ATTRIB     = 0x0008, /* attributes changed */
    NOTE_LINK       = 0x0010, /* link count changed */
    NOTE_RENAME     = 0x0020, /* vnode was renamed */
    NOTE_REVOKE     = 0x0040, /* vnode access was revoked */

    /*
     * data/hint flags for EVFILT_PROC, shared with userspace
     */
    NOTE_EXIT       = 0x80000000, /* process exited */
    NOTE_FORK       = 0x40000000, /* process forked */
    NOTE_EXEC       = 0x20000000, /* process exec'd */
    NOTE_PCTRLMASK  = 0xf0000000, /* mask for hint bits */
    NOTE_PDATAMASK  = 0x000fffff, /* mask for pid */

    /* additional flags for EVFILT_PROC */
    NOTE_TRACK      = 0x00000001, /* follow across forks */
    NOTE_TRACKERR   = 0x00000002, /* could not track child */
    NOTE_CHILD      = 0x00000004, /* am a child process */
}

int kqueue();
int kevent(int kq, const kevent_t *changelist, int nchanges,
           kevent_t *eventlist, int nevents,
           const timespec *timeout);

kqueueの例

D言語でkqueueを使って見る例を、NetBSD WikiにあるC言語によるチュートリアル(kqueue turorial)を参考にして紹介します。
ここでは今回ののNow Playingツールでも使うtimerの例をご紹介します。
C言語での例は先のWikiを見てください。

ktimer.d
import kqueuez,
       core.sys.posix.sys.time,
       core.sys.posix.unistd;
import core.thread;
import core.stdc.stdlib,
       core.stdc.string;
import std.process,
       std.string,
       std.stdio;

void main() {
  kevent_t change,
           event;
  int kq,
      nev;

  if ((kq = kqueue()) == -1) {
    throw new Error("kqueue()");
  }

  // Set timer: 5000msec(5sec)
  EV_SET(&change, 1, EVFILT_TIMER, EV_ADD | EV_ENABLE, 0, 5000, null);

  // Event Loop
  for (;;) {
    nev = kevent(kq, &change, 1, &event, 1, null);

    if (nev < 0) {
      throw new Error("kevent()");
    } else if (nev > 0) {
      if (event.flags & EV_ERROR) {
        throw new Error("EV_ERROR: " ~ strerror(cast(int)event.data).fromStringz);
      }

      new Thread({
        spawnProcess("date");
      }).start;
    }
  }

  close(kq);
}

これで5秒おきにdateコマンドが実行されます。

今回はこれをもとにします。

D言語でiTunesで現在再生中の曲に関する情報を得る。

これは以前に別の記事で書いた書いたことを拡張する感じですので基本的に詳細については以下の記事を見てください。
OSXでObjective-Cを用いてiTunesが現在再生している楽曲情報を取得する方法(& D言語でNow Playingをツイートする方法)

具体的な方法としては、D言語で直接iTunesの情報を得るのは無理なのでObjective-Cを使います。Objective-CでScripting Bindingを使って情報を得てそれをD言語がわから受け取る、そういう感じで情報を得ます。
また、今回のNow Playingツールは前回の記事からちゃんとパワーアップしています。というのも、アートワークが存在する場合にそのアートワークを取得し画像付ツイートをするようにしたというところです。

とりあえず、Objective-Cのコードを貼ります。

事前にXcodeをインストールし、sdefコマンドを使えるようにして
$ sdef /Applications/iTunes.app | sdp -fh --basename iTunes
コマンドでiTunes.hを生成してください。

iTunes.mm
#include <stdlib.h>
#include <stdbool.h>
#import "iTunes.h"

void* xmalloc(size_t size) {
  void* ret = malloc(size);

  if (ret == NULL) {
    fprintf(stderr, "FATAL ERROR - malloc failed to allocate the memory.\n");

    exit(EXIT_FAILURE);
  }

  return ret;
}

extern "C" {
  struct Artwork {
    unsigned char* data;
    size_t         length;
  };

  struct Music {
    const char* name;
    const char* album;
    const char* artist;
    Artwork*    artwork;
  };

  bool checkiTunesIsRunning() { return [[SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"] isRunning]; }

  struct Music* getCurrentiTunesPlay() {
    iTunesApplication* iTunes  = NULL;
    iTunesTrack*       current = NULL;
    struct Music*      music   = NULL;

    if (!checkiTunesIsRunning()) {
      return music;
    }

    music   = (Music*)xmalloc(sizeof(Music));
    iTunes  = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"];
    current = [iTunes currentTrack];

    music->name   = [[current name]   UTF8String];
    music->album  = [[current album]  UTF8String];
    music->artist = [[current artist] UTF8String];

    SBElementArray<iTunesArtwork*>* artworks = [current artworks];
    Artwork*                        artwork  = NULL;

    for (iTunesArtwork* _artwork in artworks) {
      artwork         = (Artwork*)xmalloc(sizeof(Artwork));
      artwork->length = [[_artwork rawData] length];

      if (artwork->length) {
        artwork->data = (unsigned char*)xmalloc(artwork->length);

        memcpy(artwork->data, [[_artwork rawData] bytes], artwork->length);
      } else {
        artwork->data = NULL;
      }
    }

    music->artwork = artwork;

    return music;
  }

  void freeMusic(Music* music) {
    if (music->artwork != NULL && music->artwork->length) {
      free(music->artwork->data);
      music->artwork->data = NULL;
    }
    free(music->artwork);
    music->artwork = NULL;
    free(music);
    music = NULL;
  }
}

これをD言語側から呼び出します。
呼び出すのは簡単です。次のようにすればいいです。

test.d
import std.string,
       std.stdio;

extern(C) {
  struct Artwork {
    ubyte* data;
    size_t length;
  };

  struct Music {
    const char* name;
    const char* album;
    const char* artist;
    Artwork*    artwork;
  }

  Music* getCurrentiTunesPlay();
  void freeMusic(Music* music);
}

void main() {
  Music* music = checkTrackChange;

  string name   = cast(string)fromStringz(music.name),
         album  = cast(string)fromStringz(music.album),
         artist = cast(string)fromStringz(music.artist);

  writeln("NowPlaying:");
  writeln("name   : ", name);
  writeln("album  : ", album);
  writeln("artist : ", artist);

  freeMusic(music);
}

これで

$ gcc -c iTunes.mm
$ dmd test iTunes.o -L-framework -LFoundation -L-framework -LiTunesLibrary -L-framework -LScriptingBridge

とするとリンク出来ます。

画像つきでツイートする。

今回画像つきツイートするにあたって拙作のTwitter4DにcustomUrlRequestという関数を追加しました。これはhttps://api.twitter.com/1.1/以外のベースurlのAPIをコールするための関数です。
Twitterで(公式のAPIのみを用いて)画像つきツイートをするにはhttps://upload.twitter.com/1.1/media/upload.jsonに画像をbase64エンコードしてmedia_dataというパラメータとしてPOSTして、アップロード&media_idを取得し
ツイートを行うstatuses/update.jsonmedia_idsというパラメータに取得してきたmedia_idを(複数枚のときはそれぞれmedia_idをカンマ区切りで結合して)渡すと画像つきツイートが出来ます。
次のような感じです。

import std.base64,
       std.json;
import twitter4d;

void main() {
  Twitter4D t4d = new Twitter4D([
            "consumerKey"       : "",
            "consumerSecret"    : "",
            "accessToken"       : "",
            "accessTokenSecret" : ""]);
  ubyte[] buf;//ここに画像のデータをいれる

  /* 中略(画像のデータを入れる) */

  string encoded = Base64.encode(buf);

  // upload a image and fetch a media_id
  auto parsed =   parseJSON((t4d.customUrlRequest("https://upload.twitter.com/1.1/", "POST", "media/upload.json", ["media_data": encoded])));
  string media_id = parsed.object["media_id_string"].str;

  // Tweet with a image!
  t4d.request("POST", "statuses/update.json", ["status" : "Tweet with a image!", "media_ids" : media_id]);
}

こんな感じで画像つきツイートをします。

ここまでで材料は揃いました。
今からこれらをくっつけてえいやっとして、Now Playingツールを作ります。

実際に作る。

仕様は次のようにします。

  1. 起動時に設定ファイルからConsumerKeyとかを読み込み、Twitter4Dのインスタンスを生成する(初期化)
  2. 5秒おきにイベントを実行するためにkqueueでタイマーイベントを作る。
  3. イベントループで5秒おきにループが回る。ここで、現在再生中の楽曲に関する情報を取得する。再生中でない場合や、前回取得時と変化がない場合はツイートせず、再生中かつ、前回取得時と違う楽曲が再生中にのみツイートをする。

こんな感じです。
これを実装してnplyコマンドを作ります。

nply.d
import kqueuez,
       core.sys.posix.sys.time,
       core.sys.posix.unistd;
import core.thread;
import core.stdc.stdlib,
       core.stdc.string;
import std.algorithm,
       std.base64,
       std.string,
       std.range,
       std.ascii,
       std.stdio,
       std.json,
       std.conv;
import twitter4d;

extern(C) {
  struct Artwork {
    ubyte* data;
    size_t length;
  };

  struct Music {
    const char* name;
    const char* album;
    const char* artist;
    Artwork*    artwork;
  }

  Music* getCurrentiTunesPlay();
  void freeMusic(Music* music);
}

struct Env {
  Twitter4D t4d;
  Music*    music;
  string currentMusic,
         previousMusic;
}

static Env E;

private static string getJsonData(JSONValue parsedJson, string key) {
  return parsedJson.object[key].str;
}

private static string readFile(string filePath) {
  auto file = File(filePath, "r");
  string buf;

  foreach(line; file.byLine) {
    buf = buf ~ cast(string)line;
  }

  return buf;
}

private static string[string] buildAuthHash(JSONValue parsed) {
  return [
            "consumerKey"       : getJsonData(parsed, "consumerKey"),
            "consumerSecret"    : getJsonData(parsed, "consumerSecret"),
            "accessToken"       : getJsonData(parsed, "accessToken"),
            "accessTokenSecret" : getJsonData(parsed, "accessTokenSecret")
  ];
}

private static void init() {
  auto keys = parseJSON(readFile("settings.json")).buildAuthHash;

  E.t4d = new Twitter4D(keys);
}

private static Music* checkTrackChange() {
  with (E) {
    music = getCurrentiTunesPlay;

    if (music is null) {
      writeln("iTunes is playing music.");

      return null;
    }

    currentMusic = cast(string)fromStringz(music.name).idup;

    if (currentMusic == "") {
      freeMusic(music);

      return null;
    }

    if (currentMusic != previousMusic) {
      return music;
    } else {
      freeMusic(music);

      return null;
    }
  }
}

private static void tweet() {
  with (E) {
    music = checkTrackChange;

    if (music is null) {
      return;
    }

    string name   = cast(string)fromStringz(music.name),
           album  = cast(string)fromStringz(music.album),
           artist = cast(string)fromStringz(music.artist);
    string nowPlayingString = "Now Playing: " ~ name ~ " from " ~ album ~ " (" ~ artist ~ ") #NowPlaying";

    currentMusic  = name.idup;
    previousMusic = name.idup;

    writeln("NowPlaying:");
    writefln("name   : %s", name);
    writefln("album  : %s", album);
    writefln("artist : %s", artist);
    writeln;

    writeln("[Tweet] - ", nowPlayingString);

    Artwork* artwork = music.artwork;

    if (artwork !is null) {
      ubyte[] buf;
      buf.length = artwork.length;
      memcpy(buf.ptr, artwork.data, buf.length);

      string encoded = Base64.encode(buf);

      auto parsed = parseJSON((t4d.customUrlRequest("https://upload.twitter.com/1.1/", "POST", "media/upload.json", ["media_data": encoded])));

      if ("media_id_string" in parsed.object) {
        string media_id = parsed.object["media_id_string"].str;

        t4d.request("POST", "statuses/update.json", ["status" : nowPlayingString, "media_ids" : media_id]);
      } else {
        t4d.request("POST", "statuses/update.json", ["status" : nowPlayingString]);
      }

    } else {
      t4d.request("POST", "statuses/update.json", ["status" : nowPlayingString]);
    }

    freeMusic(music);
  }
}

void diep(string msg) {
  stderr.writeln("[ERROR] ", msg);
  exit(EXIT_FAILURE);
}

void main() {
  init;

  kevent_t change,
           event;
  int kq, nev;

  if ((kq = kqueue()) == -1) {
    diep("kqueue()");
  }

  EV_SET(&change, 1, EVFILT_TIMER, EV_ADD | EV_ENABLE, 0, 5000, null);

  for (;;) {
    nev = kevent(kq, &change, 1, &event, 1, null);

    if (nev < 0) {
      diep("kevent()");
    } else if (nev > 0) {
      if (event.flags & EV_ERROR) {
        stderr.writefln("EV_ERROR: %s", strerror(cast(int)event.data).fromStringz);
        exit(EXIT_FAILURE);
      }

      tweet;
    }
  }

  close(kq);
  exit(EXIT_SUCCESS);
}

ここまでのファイルを同じディレクトリに保存した後に(同じディレクトリにtwitter4d.d, nply.d, kqueuez.d, iTunes.mm, iTunes.hが存在する状態で)

$ gcc -c iTunes.mm
$ dmd nply.d twitter4d.d kqueuez.d iTunes.o -L-framework -LFoundation -L-framework -LiTunesLibrary -L-framework -LScriptingBridge

これでnplyというバイナリが生成されます。

使う前にsettings.jsonというファイルを作りConsumerKeyとかを設定します。(これは自分で取得してください。)
また、AccessToken等は拙作のaccessTokenGetterを使うと簡単に取得できます(もっとも、ConsumerKeyを取得するときに、取得したアカウント向けのAccessTokenはそのコンソールで得ることが出来ますが...)

実際に動作するとこんな感じのツイートが投稿されます。

おわりに

私はこれまでなにかを待つというときに所謂busy waitをしていました...(while(true) {}で中でifで条件がtrueになるまで待つみたいなやつです)
だけど、それでは無限にCPUリソースを食ってしまいます。
なので、今回はkqueueを使っていい感じに曲が変わるのを待つことに成功しました。

余談ですが、私はTwitterのリアルタイム通知ツール(twitnotify)と今回のnplyなどTwitterに関するツールをD言語で整えています。また、今回のAdvent Calenderではテキストエディタdiloやプログラミング言語ChickenClispを紹介しましたし、今回紹介はしませんでしたが私は(すごくゆっくりですし機能はまだまだですが)シェル(もどき)dshもD言語で作っています。私はこれらを自分の生活環境というように呼んでいて、自分で生活環境を整えるのはとても楽しいです笑
実際に渡しが普段使ってる言語はD, エディタはVim(MacVim), シェルはzshであって、自分で作ったものを実際に使わなくても、開発することで普段使っている環境のようなものを自分で作るということはとても楽しいです。
みなさんも、D言語で(もしくは他の貴方の好きな言語で)生活環境を整えてみませんか?笑

ではでは、お疲れ様でした。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0