SDK Building Principles

江品陞  Vincent Chiang

vincentchiang@kkbox.com

Lecturer

Developer Advocate @

  • Partner & developers engagement
  • Open API showcase development & articles writing

In the past, we care

Now, we should

KKBOX 是一間軟體公司

我們並不擅長

  • 硬體研發設計
  • 蓋工廠製造產品
  • 處理通路庫存

KKBOX 是一間軟體公司

我們並不擅長

  • 硬體研發設計
  • 蓋工廠製造產品
  • 處理通路庫存
  • 怕.jpg

從軟體的思維

Topic

Experiences about how KKBOX

  • Release their API
  • Build a developer site
  • Provide SDK
  • Help people make their music product

When we meet the hardware company......

我照你們文件說的規則生成網址結果是 404

因為沒有做 URL encoding

API 回傳格式為什麼要用 JSON

不然要用什麼?

API 回傳的字串為什麼是 UTF-8  Encoding

............

我同事剛到職的時候表情是這樣

經過一個月完成受訓後

實際接觸 Partners 一個月後

再過一個月......

Again

從軟體的思維

Open API

  • 全世界的人都可以找得到
  • 想接的會先自己接接看
  • 想要更多功能會來跟我們說

Open API

  • 全世界的人都可以找得到
  • 想接的會先自己接接看
  • 想要更多功能會來跟我們說
  • 最重要的是,那些接不起來的會自行放棄

Open API

  • 全世界的人都可以找得到
  • 想接的會先自己接接看
  • 想要更多功能會來跟我們說
  • 最重要的是,那些接不起來的會自行放棄

有沒有很聰明?

(ゝ∀・)b​

Why developers like SDK?

Why developers like SDK?

Because we are very lazy!

使用 SDK 的好處

  • 就不用看 RESTful API Document
  • 不用處理複雜的認證流程
  • IDE 有 Autocomplete
  • 寫出來的程式不會有 Bug

使用 SDK 的好處

  • 就不用看 RESTful API Document
  • 不用處理複雜的認證流程
  • IDE 有 Autocomplete
  • 寫出來的程式不會有 Bug(因為是別人寫的)

Which language of SDK should we develop first?

Which language of SDK should we develop first?

Of course JavaScript!

SDK Development Principles

  1. 化零為整
  2. 化繁為簡
  3. 化無為有

化零為整

化零為整

import { ARTISTS as ENDPOINT } from '../Endpoint';
import Fetcher from './Fetcher';

export default class ArtistFetcher extends Fetcher {
  constructor(http, territory = 'TW') {
    super(http, territory);
    this.artistID = undefined;
  }

  setArtistID(artistID) {
    this.artistID = artistID;
    return this;
  }

  // @example api.artistFetcher.setArtistID('Cnv_K6i5Ft4y41SxLy').fetchMetadata();
  fetchMetadata() {
    return this.http.get(ENDPOINT + '/' + this.artistID, {
      territory: this.territory
    });
  }

  // @example api.artistFetcher.setArtistID('Cnv_K6i5Ft4y41SxLy').fetchAlbums();
  fetchAlbums(limit = undefined, offset = undefined) {
    return this.http.get(ENDPOINT + '/' + this.artistID + '/albums', {
      territory: this.territory,
      limit: limit,
      offset: offset
    });
  }

  // @example api.artistFetcher.setArtistID('Cnv_K6i5Ft4y41SxLy').fetchTopTracks();
  fetchTopTracks(limit = undefined, offset = undefined) {
    return this.http.get(ENDPOINT + '/' + this.artistID + '/top-tracks', {
      territory: this.territory,
      limit: limit,
      offset: offset
    });
  }

  // @example api.artistFetcher.setArtistID('Cnv_K6i5Ft4y41SxLy').fetchRelatedArtists();
  fetchRelatedArtists(limit = undefined, offset = undefined) {
    return this.http.get(ENDPOINT + '/' + this.artistID + '/related-artists', {
      territory: this.territory,
      limit: limit,
      offset: offset
    });
  }
}

化繁為簡

化繁為簡

/**
 * Fetch KKBOX resources.
 */
export default class Api {
  /**
   * Need access token to initialize.
   *
   * @param {string} token - Get via Auth.
   * @param {string} [territory = 'TW'] - ['TW', 'HK', 'SG', 'MY', 'JP'] The territory for the fetcher.
   * @example new Api(token);
   * @example new Api(token, 'TW');
   */
  constructor(token, territory = 'TW') {
    this.territory = territory;
    this.httpClient = undefined;
    this.setToken(token);
  }

  /**
   * Set new token and create fetchers with the new token.
   *
   * @param {string} token - Get via Auth.
   * @example api.setToken(token);
   */
  setToken(token) {
    this.httpClient = new HttpClient(token);

    /**
     * @type {SearchFetcher}
     */
    this.searchFetcher = new SearchFetcher(this.httpClient, this.territory);

    /**
     * @type {TrackFetcher}
     */
    this.trackFetcher = new TrackFetcher(this.httpClient, this.territory);

    /**
     * @type {AlbumFetcher}
     */
    this.albumFetcher = new AlbumFetcher(this.httpClient, this.territory);

    /**
     * @type {ArtistFetcher}
     */
    this.artistFetcher = new ArtistFetcher(this.httpClient, this.territory);

    /**
     * @type {FeaturedPlaylistFetcher}
     */
    this.featuredPlaylistFetcher = new FeaturedPlaylistFetcher(
      this.httpClient,
      this.territory
    );

    /**
     * @type {FeaturedPlaylistCategoryFetcher}
     */
    this.featuredPlaylistCategoryFetcher = new FeaturedPlaylistCategoryFetcher(
      this.httpClient,
      this.territory
    );

    /**
     * @type {NewReleaseCategoryFetcher}
     */
    this.newReleaseCategoryFetcher = new NewReleaseCategoryFetcher(
      this.httpClient,
      this.territory
    );

    /**
     * @type {NewHitsPlaylistFetcher}
     */
    this.newHitsPlaylistFetcher = new NewHitsPlaylistFetcher(
      this.httpClient,
      this.territory
    );

    /**
     * @type {GenreStationFetcher}
     */
    this.genreStationFetcher = new GenreStationFetcher(
      this.httpClient,
      this.territory
    );

    /**
     * @type {MoodStationFetcher}
     */
    this.moodStationFetcher = new MoodStationFetcher(
      this.httpClient,
      this.territory
    );

    /**
     * @type {ChartFetcher}
     */
    this.chartFetcher = new ChartFetcher(this.httpClient, this.territory);

    /**
     * @type {SharedPlaylistFetcher}
     */
    this.sharedPlaylistFetcher = new SharedPlaylistFetcher(
      this.httpClient,
      this.territory
    );
  }
}

化無為有

SDK 可以實現原本 API 做不到的功能

化無為有

漂向北方有很多人唱過

但是我只想要黃明志和鄧紫棋合唱的

import { SEARCH as ENDPOINT } from '../Endpoint';
import Fetcher from './Fetcher';

/**
 * Search API.
 * @see https://docs-en.kkbox.codes/v1.1/reference#search
 */
export default class SearchFetcher extends Fetcher {

/**
   * Filter what you don't want when search.
   *
   * @param {Object} [conditions] - search conditions.
   * @param {string} conditions.track - track's name.
   * @param {string} conditions.album - album's name.
   * @param {string} conditions.artist - artist's name.
   * @param {string} conditions.playlist - playlist's title.
   * @param {string} conditions.availableTerritory - tracks and albums available territory.
   * @return {Search}
   * @example
   * api.searchFetcher
   *  .setSearchCriteria(q = '飄向北方', type = 'track')
   *  .filter({artist: '黃明志, G.E.M.鄧紫棋'})
   *  .fetchSearchResult();
   */
  filter(conditions = {}) {
    this.filterConditions = conditions;
    return this;
  }
}

化無為有

不能牴觸 API 原來的設計理念

KKBOX Open API 要怎麼播歌?

URI Rule
https://widget.kkbox.com/v1/?id={id}&type={type}&terr={territory}&lang={lang}&autoplay={boolean}&loop={boolean}

export default class TrackFetcher extends Fetcher {
  /**
   * Get KKBOX web widget uri of the track.
   * @example https://widget.kkbox.com/v1/?id=8sD5pE4dV0Zqmmler6&type=song
   * @return {string}
   */
  getWidgetUri() {
    return `https://widget.kkbox.com/v1/?id=${this.trackID}&type=song`;
  }
}
export default class SharedPlaylistFetcher extends Fetcher {
  /**
   * Get KKBOX web widget uri of the playlist.
   * @example https://widget.kkbox.com/v1/?id=KmjwNXizu5MxHFSloP&type=playlist
   * @return {string}
   */
  getWidgetUri() {
    return `https://widget.kkbox.com/v1/?id=${this.playlistID}&type=playlist`;
  }
}

三大心法

  1. 化零為整
  2. 化繁為簡
  3. 化無為有

Testing

Testing

import SearchFetcher from "../api/SearchFetcher";

describe('Search', () => {
    const searchFetcher = new SearchFetcher(
        httpClient
    ).setSearchCriteria('Linkin Park');
    describe('#fetchSearchResult()', () => {
        it('should response status 200', () => {
            return searchFetcher.fetchSearchResult().then(response => {
                response.status.should.be.exactly(200),
                    reject => should.not.exists(reject);
            });
        });
    });

    describe('#filter()', () => {
        it('should get result', () => {
            return searchFetcher
                .filter({
                    artist: 'Linkin Park',
                    album: 'One More Light',
                    available_territory: 'TW'
                })
                .fetchSearchResult()
                .then(response => {
                    response.data.tracks.data.length.should.be.greaterThan(0);
                });
        });
    });
});

Outside Monitoring

Documentation

import { SEARCH as ENDPOINT } from '../Endpoint';
import Fetcher from './Fetcher';

/**
 * Search API.
 * @see https://docs-en.kkbox.codes/v1.1/reference#search
 */
export default class SearchFetcher extends Fetcher {

/**
   * Filter what you don't want when search.
   *
   * @param {Object} [conditions] - search conditions.
   * @param {string} conditions.track - track's name.
   * @param {string} conditions.album - album's name.
   * @param {string} conditions.artist - artist's name.
   * @param {string} conditions.playlist - playlist's title.
   * @param {string} conditions.availableTerritory - tracks and albums available territory.
   * @return {Search}
   * @example
   * api.searchFetcher
   *  .setSearchCriteria('五月天 好好')
   *  .filter({artist: '五月天'})
   *  .fetchSearchResult();
   */
  filter(conditions = {}) {
    this.filterConditions = conditions;
    return this;
  }
}

Write Annotation

Generate doc automatically

Operation

  • Deploy to NPM
  • Deploy to GitHub

JavaScript 什麼不多,就洞最多

更新一下版號

或者不要用那個 Library 就可以了ξ( ✿>◡❛)

Marketing

你沒事會去 Google

KKBOX 有沒有 Open API

裡面有什麼資料嗎?

不會嘛!

因為你只想到你自己

What developers like?

  • 看到炫炮的東西,會想研究它是怎麼做出來的
  • 喜愛潮流、追求新知

For junior developers

For senior developers

澄清一下,三上悠亞是音樂家

不信你看 Wiki

Music Everywhere

請上網搜尋

KKBOX Open API

KKBOX JS SDK

SDK Building Principles

By zaoldyeck

SDK Building Principles

  • 2,105