Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

they work as icecast servers and no longer depend on icecaast

  • Loading branch information...
commit 487f7c18aeb4d980b57afc7506b9fed7c8909b2a 1 parent d602c34
@drumsoft authored
View
3  .gitignore
@@ -0,0 +1,3 @@
+*~
+obsolete
+my-*
View
18 README
@@ -1,10 +1,18 @@
-CamTwist "icecast Song" plugin
+Traktor::SongNameServer, tweet-from-traktor.pl and CamTwist "Traktor Song" plugin
-"icecast Song" plugin (with a little hack to icecast2) displays track names playing on Traktor PCDJ to CamTwist virtual camera.
+日本語の説明は readme-j.txt を読んで下さい。
-other PCDJ products that supports icecast streaming will work with this plugin.
-Additionally, TweetFromTraktor.pl post the song names to twitter.
+[Traktor::SongNameServer]
+Traktor::SongNameServer is a server, receives song names from Traktor and provides them for other applications.
+It will work with other PCDJ softwares if they send icecast streaming protocols.
+To launch server, use songnameserver.pl .
-日本語の説明は readme-j.txt を読んで下さい。
+[tweet-from-traktor.pl]
+a script to post song names played by Traktor to twitter.
+This script works as server using Traktor::SongNameServer.
+
+[CamTwist "Traktor Song" plugin]
+a CamTwist plugin to display song names played by Traktor.
+This plugin fetches song names from Traktor::SongNameServer. then you should run it by songnameserver.pl or tweet-from-traktor.pl.
View
0  icecast Song.qtz → Traktor Song.qtz
File renamed without changes
View
257 Traktor/SongNameServer.pm
@@ -0,0 +1,257 @@
+use strict;
+use warnings;
+use utf8;
+
+package Traktor::SongNameServer;
+
+use IO::Socket;
+use IO::Select;
+
+sub new {
+ my $class = shift;
+ my %options = @_;
+ return bless {
+ host => $options{host} || 'localhost',
+ port => $options{port} || 8000,
+ buffer_size => $options{buffer_size} || 8192,
+ timeout => $options{timeout} || 1,
+ cueoptions => $options{cue} || {},
+ callback => $options{callback} || sub{},
+ song_artist => '',
+ song_title => '',
+ }, $class;
+}
+
+sub run {
+ my $self = shift;
+
+ my $cue = Traktor::SongNameServer::SongCue->new(
+ %{$self->{cueoptions}},
+ callback => sub {
+ my $song = shift;
+ $self->{song_artist} = $song->{ARTIST};
+ $self->{song_title} = $song->{TITLE};
+ $self->{callback}->($song);
+ }
+ );
+
+ my $sock_listen = new IO::Socket::INET(Listen=>5,
+ LocalAddr => $self->{host},
+ LocalPort => $self->{port},
+ Proto => 'tcp',
+ Reuse => 1);
+
+ die "IO::Socket : $!" unless $sock_listen;
+
+ my %workers;
+
+ my $selector = new IO::Select( $sock_listen );
+
+ while(1) {
+ my (@ready) = $selector->can_read($self->{timeout});
+ foreach my $sock (@ready) {
+ if($sock == $sock_listen) {
+ my $newsock = $sock_listen->accept;
+ $selector->add($newsock);
+ $workers{$newsock} = Traktor::SongNameServer::Worker->new($newsock, $self, $cue, $self->{buffer_size});
+ } else {
+ if ( ! $workers{$sock}->read() ) {
+ $workers{$sock}->finalize();
+ delete $workers{$sock};
+ $selector->remove($sock);
+ $sock->close();
+ }
+ }
+ }
+ $cue->noop();
+ }
+
+ close($sock_listen);
+}
+
+
+package Traktor::SongNameServer::Worker;
+use IO::Socket;
+use Encode qw/encode decode/;
+
+sub new {
+ my $class = shift;
+ my $sock = shift;
+ my $server = shift;
+ my $cue = shift;
+ my $buffer_size = shift;
+ my ($cl_port,$cl_iaddr) = unpack_sockaddr_in($sock->peername());
+
+ my $self = bless {
+ sock => $sock,
+ server => $server,
+ cue => $cue ,
+ buffer_size => $buffer_size,
+ client => inet_ntoa($cl_iaddr) . ':' . $cl_port,
+ input => '',
+ status => 'start', # 'start', 'header', 'data', 'meta', 'end'
+ mode => 'get' , # 'source', 'get'
+ }, $class;
+ return $self;
+}
+
+sub read {
+ my $self = shift;
+ my $buf;
+ my $length = sysread($self->{sock}, $buf, $self->{buffer_size});
+ if ( $length > 0 ) {
+ $self->{input} .= $buf;
+ }
+ my $keepconnect = $self->process();
+
+ return $length && $keepconnect;
+}
+
+sub finalize {
+ my $self = shift;
+ $self->process();
+ if ( $self->{mode} eq 'source' ) {
+ print "[source disconnected from $self->{client}]\n";
+ }
+}
+
+sub escape {
+ my $s = shift;
+ $s =~ s/&/&/;
+ $s =~ s/"/"/; #"
+ $s =~ s/</&lt;/;
+ $s =~ s/>/&gt;/;
+ $s;
+}
+
+sub response_songname {
+ my $self = shift;
+ my $out = $self->{sock};
+ print $out "HTTP/1.0 200 OK\r\n\r\n";
+ print $out qq{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n};
+ printf $out qq{<status><source><artist>%s</artist><title>%s</title></source></status>\r\n},
+ encode('utf8',escape($self->{server}->{song_artist})),
+ encode('utf8',escape($self->{server}->{song_title}));
+}
+
+sub response_ok {
+ my $self = shift;
+ my $out = $self->{sock};
+ print $out "HTTP/1.0 200 OK\r\n\r\n";
+}
+
+sub process {
+ my $self = shift;
+ while( 1 ) {
+ if ( $self->{status} eq 'start' || $self->{status} eq 'header' ) {
+ if ($self->{input} =~ s/^(.*?)\r\n//) {
+ # print $1, "\n";
+ if ( $self->{status} eq 'start' ) {
+ $self->{mode} = ($1 =~ /^SOURCE /i) ? 'source' : 'get';
+ $self->{status} = 'header';
+ if ( $self->{mode} eq 'source' ) {
+ print "[source connected from $self->{client}]\n";
+ }
+ } elsif ( ! defined $1 || $1 eq '' ) { # end of header
+ if ( $self->{mode} eq 'source' ) {
+ $self->response_ok();
+ $self->{status} = 'data';
+ } else {
+ $self->response_songname();
+ $self->{status} = 'end';
+ return 0;
+ }
+ }
+ } else {
+ last;
+ }
+ } elsif ( $self->{status} eq 'data' ) {
+ if ($self->{input} =~ s/^(?:.*?)(....)(ARTIST|TITLE)=//s) {
+ $self->{status} = 'meta';
+ $self->{metaname} = decode('utf8', $2);
+ $self->{metalength} = unpack('V',$1) - length($self->{metaname}) - 1;
+ } else {
+ if ( length($self->{input}) > 11 ) {
+ $self->{input} = substr($self->{input}, -11);
+ $self->{cue}->finish();
+ }
+ last;
+ }
+ } elsif ( $self->{status} eq 'meta' ) {
+ if ( length($self->{input}) >= $self->{metalength} ) {
+ my $metatext = decode('utf8', substr($self->{input}, 0, $self->{metalength}));
+ $self->{input} = substr($self->{input}, $self->{metalength});
+ $self->{status} = 'data';
+ $self->{cue}->add($self->{metaname}, $metatext);
+ } else {
+ last;
+ }
+ } else { # end
+ return 0;
+ }
+ }
+ return 1;
+ # (4byte/文字数 リトルエンディアン)(文字数byte/"(ARTIST|TITLE)=文字列")
+}
+
+
+package Traktor::SongNameServer::SongCue;
+
+sub new {
+ my $class = shift;
+ my %options = @_;
+ my $self = bless {
+ bysong => $options{bysong} || 0,
+ bytime => $options{bytime} || 0,
+ nextsongtimeout => $options{nextsongtimeout} || 0,
+ ignoreshortplay => $options{ignoreshortplay} || 0,
+ callback => $options{callback} || undef,
+ cue => [],
+ new => {},
+ }, $class;
+ return $self;
+}
+
+sub add {
+ my ($self, $name, $text) = @_;
+ $self->{new}->{$name} = $text;
+}
+
+sub finish {
+ my $self = shift;
+ if (keys %{$self->{new}}) {
+ my $now = time();
+ if ( $self->{ignoreshortplay} && @{$self->{cue}} &&
+ $self->{ignoreshortplay} > $now - $self->{cue}->[-1]->{time} ) {
+ pop @{$self->{cue}};
+ }
+ $self->{new}->{time} = $now;
+ push @{$self->{cue}}, $self->{new};
+ $self->{new} = {};
+ }
+}
+
+sub noop {
+ my $self = shift;
+ my $now = time();
+ if ( @{$self->{cue}} ) {
+ if ( $self->{bysong} < @{$self->{cue}} ) {
+ if (!$self->{bytime} ||
+ $self->{bytime} < $now - $self->{cue}->[$self->{bysong}]->{time} ) {
+ $self->fire();
+ }
+ }elsif ($self->{nextsongtimeout} &&
+ $self->{nextsongtimeout} < $now - $self->{cue}->[0]->{time} ) {
+ $self->fire();
+ }
+ }
+}
+
+sub fire {
+ my $self = shift;
+ $self->{callback}->(shift @{$self->{cue}});
+}
+
+1;
+
+
View
85 TweetFromTraktor.pl
@@ -1,85 +0,0 @@
-#!/usr/bin/perl
-
-use LWP::Simple;
-use Net::Twitter;
-use Data::Dumper;
-
-my $delay = 1; # tweet will be delayed for N song loadings.
-my $pollinginterval = 15; # in seconds
-my $postfix = " #HashTagOfMyTraktorPlay"; # postfix of tweets
-my $url = "http://localhost:8000/"; # stream url
-
-# create your new application and
-# get information from https://dev.twitter.com/apps
-my $nt = Net::Twitter->new(
- traits => [qw/OAuth API::REST/],
- consumer_key => '',
- consumer_secret => '',
- access_token => '',
- access_token_secret => '',
-);
-
-sub main {
- my $prev = "";
- my @cur = ();
- while(1) {
- my $tr = getTrackName();
- if ( defined $tr ) {
- if ($prev ne $tr) {
- unshift @cue, $tr;
- $prev = $tr;
- }
- if (@cue > $delay) {
- $tr = pop @cue;
- tweet($tr);
- }
- }
- sleep($pollinginterval);
- }
-}
-
-sub getTrackName {
- my $src = get($url);
- my $artist = $src =~ /<artist>([^<>]+)<\/artist>/ ?
- $1 : undef;
- my $title = $src =~ /<title>([^<>]+)<\/title>/ ?
- $1 : undef;
- return $artist && $title ?
- qq{$artist / "$title"} : undef;
-}
-
-sub tweet {
- my $tw = (shift) . $postfix;
- my $result = $nt->update($tw);
- print $tw . "\n";
-}
-
-main();
-
-__END__
-
-TweetFromTraktor.pl
-
-[概要]
-twitterに、Traktorでプレイ中の曲名をポストするスクリプトです。
-
-TweetFromTraktor.pl は起動しっぱなしになり、定期的に Traktor からのストリームの曲名をチェックし、変更があったら Twitter にポストします。
-
-曲名の変更はデフォルトで1曲分遅れるので、デッキにセットしてチェック・調整中の曲名が流される事はありません。その曲のプレイが始まって、次にかける曲をチェック・調整し始めたあたりで Tweet が行われます。
-
-
-[使い方]
-1. https://dev.twitter.com/apps にアクセスして、Twitterへポストするための新しいアプリケーションを登録する
-
-2. 同サイトから下記を取得して設定に記入
- Consumer key
- Consumer secret
- Access token
- Access token secret
-
-3. $postfix 等を調整する(イベントのハッシュタグにするとか)
-
-4. CamTwist "icecast Song" plugin のマニュアル記載の方法で修正した icecast を起動し Traktor からストリームを行う( readme-j.txt 記載の C-1, C-2 の手順)
-
-5. TweetFromTraktor.pl を起動する(起動しっぱなしになる)
-
View
161 readme-j.txt
@@ -1,106 +1,125 @@
-CamTwist "icecast Song" plugin
-
-Traktor でプレイ中の曲名を CamTwist に表示させる事ができます。
+Traktor::SongNameServer, tweet-from-traktor.pl and CamTwist "Traktor Song" plugin
+[Traktor::SongNameServer]
+Traktor でプレイ中の曲名を受信し、他のソフトから利用できる様にするサーバです。
他の PCDJ ソフトでも、icecastストリーミングプロトコルに対応しているものであれば動作する筈です。
+起動するには songnameserver.pl を使ってください。
-TweetFromTraktor.pl を追加しました。これはプレイ中の曲名をさらに twitter にポストする為のスクリプトです。
-
-
-[概要]
-
-"icecast Song" は TRAKTOR(等のPCDJ)で再生中の曲名情報を icecast2 サーバから取得します。
-TRAKTORはプレイ中のサウンドを曲名情報と一緒に icecast2 サーバにストリーミング送信します。
+[tweet-from-traktor.pl]
+Traktor::SongNameServer を使って、Traktor でプレイ中の曲名を Twitter にポストします。
-      ┌───────┐
-      │icecast2サーバ│
-      └───────┘
-UPSTREAM DJing↑     ↓FETCH song info
-    ┌──┴─┐ ┌─┴────────┐
-    │TRAKTOR │ │icecast Song plugin │
-    └────┘ └──────────┘
+[CamTwist "Traktor Song" plugin]
+Traktor::SongNameServer と通信して、Traktor でプレイ中の曲名を CamTwist に表示します。
-icecast2 は MacPorts からインストールすると簡単です。
-MacPortsの使い方は検索して下さい。
+[概要]
-[マニュアル]
-下記 [Adv.] がついている項目は、 icecast2 を Macports を使わずにインストールしたり、サーバのポート番号を変更したりといった事をする「よくわかってる人」向けの説明です。
+Traktor::SongNameServer を起動すると icecast サーバとして動作します。
+Traktor でこのサーバを Broadcasting Server に設定する事で、プレイした曲名データが Traktor::SongNameServer に送信されます。
-A.インストール
+CamTwist "Traktor Song" plugin は Traktor::SongNameServer から曲名情報を取得します。
-A-1. icecast2 をインストールする
- MacPorts でインストールする場合
- sudo port install icecast2
+tweet-from-traktor.pl は Traktor::SongNameServer からコールバックされ、Twitterに曲名情報を含むツイートをポストします。
- [Adv.]ソースからインストールした場合等は、以下の /opt/local/.. を適宜 /usr/local/.. 等に読み替える事
+      ┌────────────┐Callback┌───────────┐
+      │Traktor::SongNameServer │------→│tweet-from-traktor.pl │
+      └────────────┘    └───────────┘
+UPSTREAM DJing↑     ↓FETCH song info
+    ┌──┴─┐ ┌─┴───────┐
+    │Traktor │ │Traktor Song plugin│
+    └────┘ └─────────┘
-A-2. icecast2 の status.xsl を同梱の物に置き換える
- status.xsl は以下にインストールされているので、これを置き換える
- /opt/local/share/icecast/web/
-A-3. icecast2 の設定を行う
- /opt/local/etc/icecast.xml を変更する
- authenticationブロックのパスワードを設定する(必須)
+ cue.bysong: 曲名の更新を、n曲分遅らせる
+ cue.bytime: 曲名の更新を、n秒遅らせる
+ cue.nextsongtimeout: 次の曲がn秒プレイされなかったら、bysongの設定を無視して曲名を更新する
+ cue.ignoreshortplay: n秒未満しか再生されなかった曲は、プレイされなかった事にする
- [Adv.]もしも必要なら以下も変更する
- hostname
- listen-socket ブロックの port (ポート番号)
- ここを変更した場合は以下の "localhost" "8000" を変更した物に読み替える事
-A-4. icecast2 のログ出力先を作る
- icecast2のログファイルの出力先
- /opt/local/var/log/icecast/
- が必要なのでこれを作る
- mkdir -p /opt/local/var/log/icecast
- 自分が書き込めるパーミッションを設定する。
+[マニュアル]
+下記 [Adv.] がついている項目は、「よくわかってる人」向けの説明です。
-A-5. icecast Song.qtz をインストールする
- 下記のどちらかに icecast Song.qtz ファイルを置く
- CamTwist インストールフォルダの Effects フォルダ
- ~/Library/Application Support/CamTwist/Effects
+[A: Traktor::SongNameServer を使える様にしよう]
-B.icecast2 のテスト
+A-1. インストールと設定
+ このフォルダを任意の場所に置く
+ [Adv.] songnameserver.pl を開いて、設定を変更する
+ host, port: サーバの待ち受けアドレスとポート番号
+ ※ここを変更した場合は以下の "localhost" "8000" を変更した物に読み替える
+ (WindowsやLinuxの人は、Perlをインストールしておく)
-B-1. Traktor のストリーミング設定を行う
+A-2. Traktor のストリーミング設定を行う
Preference > Broadcasting > Server Settings
Address: localhost Port: 8000
- Password: A-3で設定したもの
- Format: 音は聴かないので何でも良い
+ Password: 空白
+ Format: Ogg Vorbis, 11025 Hz, 32kBit/s
-B-2. icecast2 を起動する
- icecast -c /opt/local/etc/icecast.xml
+A-3. Traktor::SongNameServer を起動
+ ターミナルで perl songnameserver.pl を実行
-B-3. Traktor でストリーミングを開始する
+A-4. Traktor でストリーミングを開始する
AUDIO RECORDER ペインを開き、STREAMINGボタンを押す
ストリーミングに成功するとボタンが光る
+ (失敗すると、ボタンは点滅する)
+
+A-5. 曲名を更新させて、表示を確認する
+ 曲を 2, 3 曲再生すると、曲名が更新される。
+ 更新された曲名は songnameserver.pl を実行中のターミナルに表示される。
+ また、ブラウザで http://localhost:8000/ にアクセスすると曲名が表示される。
+
+
+[B: CamTwist "Traktor Song" plugin を使おう]
+
+B-1. Traktor Song.qtz をインストール
+ 下記のどちらかに Traktor Song.qtz ファイルを置く
+ ・ CamTwist インストールフォルダの Effects フォルダ
+ ・ ~/Library/Application Support/CamTwist/Effects
+
+B-2. Traktor からストリーミングを開始する
+ A-3, A-4 を行ってください。
+
+B-3. CamTwist で "Traktor Song" エフェクトをADDして、好みの表示設定にする
+ [Adv.] ホストやポート名の設定を変えた場合は "icecast URL" を変更する
+ 文字サイズや表示位置などを調整する
-B-4. ブラウザでストリーミングの曲名を確認する
- ブラウザで下記のアドレスを開く
- http://localhost:8000/
- 下記の様な曲名を載せたXMLファイルが見えたら成功
- <status>
- <source>
- <mountpoint>/</mountpoint>
- <artist>takuya</artist>
- <title>364 Nights</title>
- </source>
- </status>
+曲名が更新されると CamTwist の画面に反映されます。
+二回目以降は B-2 から行ってください。
- なお、曲名が更新されるタイミングは「新しく曲をデッキにロードして、デッキを再生してしばらく後」なので、テストの際はストリーミング開始後に必ず曲をデッキにロードしてから再生する必要がある
+[C: tweet-from-traktor.pl を使おう]
-C. "icecast Song" プラグインの使用手順
+C-1. Net::Twitter モジュールのインストール
+ cpan や cpanm を使い Net::Twitter をインストールする
-C-1. icecast2 を起動する(参照:B-2)
+C-2. Twitter アプリケーションを登録
+ https://dev.twitter.com/apps にアクセスして、Twitterへポスト
+ するための新しいアプリケーションを登録する
+ 登録したアプリケーションのページから、以下を取得する
+ Consumer key
+ Consumer secret
+ Access token
+ Access token secret
-C-2. Traktor からストリーミングを開始する(参照:B-3)
+C-2. tweet-from-traktor.pl を設定
+ tweet-from-traktor.pl を開き、取得した4つの文字列を
+ consumer_key => '',
+ consumer_secret => '',
+ access_token => '',
+ access_token_secret => '',
+ に設定する。
+ また A-1 で行った設定があれば、 songnameserver.pl にも同じ設定を行う。
+ 【重要】 $postfix をいい感じにする(イベントのハッシュタグとか)
-C-3. CamTwist で "icecast Song" エフェクトをADDする
+C-3. tweet-from-traktor.pl を起動
+ ターミナルで perl tweet-from-traktor.pl を実行
+ (tweet-from-traktor.pl が Traktor::SongNameServer を起動するので、
+ songnameserver.pl は使いません)
-C-4. [Adv.] A-3でホストやポート名の設定を変えた場合は "icecast URL" を変更する
+C-4. Traktor からストリーミングを開始する
+ A-4 を行ってください。
-C-5. 必要なら "icecast Song" の文字サイズや表示位置などを調整する
+曲名が更新されると、Twitterにポストされます。
+二回目以降は C-3 から行ってください。
View
39 songnameserver.pl
@@ -0,0 +1,39 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Traktor::SongNameServer;
+use Encode qw/encode/;
+
+my %option = (
+ host => 'localhost', # server address
+ port => 8000, # server port
+
+ cue => {
+ # Song Name Cue Setting - song names are cued and displayed after delay.
+ # delay <bysong> songs to display song names.
+ bysong => 1,
+ # delay <bytime> seconds to display song names.
+ bytime => 0,
+ # when both <bysong> and <bytime> are not zero, they are summed.
+ # when <bysong> is not zero and next song are not coming,
+ # cued songname will be displayed after <nextsongtimeout> seconds.
+ # 0 means no timeout.
+ nextsongtimeout => 180,
+ # when song played shorter than <ignoreshortplay> seconds, ignore it.
+ # 0 means nothing ignored.
+ ignoreshortplay => 5,
+ },
+
+ buffer_size => 8192, # stream receive buffer size
+ timeout => 1, # stream receive loop timeout in second
+ callback => sub {
+ my $song = shift;
+ print encode('utf8', qq{[song changed: $song->{ARTIST} / "$song->{TITLE}"]\n});
+ }, # callback for song name changed.
+ # an argument is {ARTIST=>'...', TITLE=>'...'}.
+);
+
+my $server = Traktor::SongNameServer->new(%option);
+$server->run();
+
View
20 status.xsl
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" version = "1.0" >
-<xsl:template match = "/icestats" >
-
-<status>
-<xsl:for-each select="source">
- <source>
- <mountpoint><xsl:value-of select="@mount" /></mountpoint>
- <xsl:if test="artist">
- <artist><xsl:value-of select="artist" /></artist>
- </xsl:if>
- <xsl:if test="title">
- <title><xsl:value-of select="title" /></title>
- </xsl:if>
- </source>
-</xsl:for-each>
-</status>
-
-</xsl:template>
-</xsl:stylesheet>
View
55 tweet-from-traktor.pl
@@ -0,0 +1,55 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use utf8;
+use Net::Twitter;
+use Traktor::SongNameServer;
+use Encode qw/encode/;
+
+my $postfix = " #HashTagOfMyTraktorPlay"; # postfix of tweets
+
+# create your new application and
+# get information from https://dev.twitter.com/apps
+my $nt = Net::Twitter->new(
+ traits => [qw/OAuth API::REST/],
+ consumer_key => '',
+ consumer_secret => '',
+ access_token => '',
+ access_token_secret => '',
+);
+
+my %option = (
+ host => 'localhost', # server address
+ port => 8000, # server port
+
+ cue => {
+ # Song Name Cue Setting - song names are cued and displayed after delay.
+ # delay <bysong> songs to display song names.
+ bysong => 1,
+ # delay <bytime> seconds to display song names.
+ bytime => 0,
+ # when both <bysong> and <bytime> are not zero, they are summed.
+ # when <bysong> is not zero and next song are not coming,
+ # cued songname will be displayed after <nextsongtimeout> seconds.
+ # 0 means no timeout.
+ nextsongtimeout => 180,
+ # when song played shorter than <ignoreshortplay> seconds, ignore it.
+ # 0 means nothing ignored.
+ ignoreshortplay => 5,
+ },
+
+ buffer_size => 8192, # stream receive buffer size
+ timeout => 1, # stream receive loop timeout in second
+ callback => sub {
+ my $song = shift;
+ my $result = $nt->update(sprintf '%s / "%s" %s',
+ $song->{ARTIST}, $song->{TITLE}, $postfix);
+ print encode('utf8', qq{[song tweeted: $song->{ARTIST} / "$song->{TITLE}"]\n});
+ }, # callback for song name changed.
+ # an argument is {ARTIST=>'...', TITLE=>'...'}.
+);
+
+my $server = Traktor::SongNameServer->new(%option);
+$server->run();
+
Please sign in to comment.
Something went wrong with that request. Please try again.