CUI 版Dropbox?

あったよあった。CUIからでもDropbox使う方法。

Linux CLI in python

どうやら、この python スクリプト使って、X システムが無いCUIオンリーの環境でも、Dropbox のインストール・実行できるよってことらしい。

使ってみた。

$ wget https://dl.getdropbox.com/u/43645/dbcli.py
$ python dbcli.py 
Usage: dbcli.py <command> [options] ... <command> [options]
Available commands:
   status - Get overall status for the daemon.
   copypublic <file> - Copies the url to the clipboard (? I guess)
   install <x86/x86_64> - Tries to download the daemon and install it in your home directory.
   browse <folder> - Browse the specified folder in your browser.
   copygallery - Copies the gallery url to the clipboard.
   share <file> - Open the browser and shows share-information.
   file <file> - Get information about a file/folder.
   folder <folder> - Get information about a file/folder.
   help - Gives this.
   revisions <file> - Opens the revisions page in your browser for the specified file.

なるほどー取りあえず install コマンド使えば良い感じ?

$ python dbcli.py install x86
error: couldn't connect to the daemond.
Trying to start the daemon.
The daemon does not exist (try dbcli.py install)
rm: cannot remove `/home/kui/.dropbox-dist/': そのようなファイルやディレクトリはありません
Starting to download dropbox-lnx.x86-0.6.382.tar.gz...
  ... 1 kB of 17149 kB
  ... 1716 kB of 17149 kB
  ... 3431 kB of 17149 kB
  ... 5146 kB of 17149 kB
  ... 6861 kB of 17149 kB
  ... 8576 kB of 17149 kB
  ... 10291 kB of 17149 kB
  ... 12006 kB of 17149 kB
  ... 13721 kB of 17149 kB
  ... 15436 kB of 17149 kB
  ... 17151 kB of 17149 kB     # この辺で「おおおこれはひょっとして、dropboxdとかダウンロードしてる!?wktk」ってなってる。
Done. Unpacking...
Trying to start the daemon.
Traceback (most recent call last):
  File "<string>", line 6, in <module>
  File "__main__.py", line 105, in <module>
  File "__main__dropbox__.py", line 174, in <module>
  File "__main__dropbox__.py", line 169, in main_startup
  File "arch/linux/startup.py", line 131, in main_thread_loop
AttributeError: 'NoneType' object has no attribute 'MainLoop'
Exception in thread CONTROL (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "threading.py", line 486, in __bootstrap_inner
  File "common_util/trace.py", line 31, in intwatched
<type 'exceptions.TypeError'>: 'NoneType' object is not callable
Unhandled exception in thread started by <bound method WatchedThread.__bootstrap of <WatchedThread(CONTROL, started)>>
Error in sys.excepthook:
TypeError: 'NoneType' object is not callable

Original exception was:
Traceback (most recent call last):
  File "threading.py", line 462, in __bootstrap
  File "threading.py", line 527, in __bootstrap_inner
AttributeError: 'NoneType' object has no attribute 'acquire'
Exception in thread AUTHENTICATE (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "threading.py", line 486, in __bootstrap_inner
  File "client_api/authenticate.py", line 207, in run
<type 'exceptions.TypeError'>: 'NoneType' object is not callable
Unhandled exception in thread started by <bound method AuthenticationThread.__bootstrap of <AuthenticationThread(AUTHENTICATE, started)>>
Error in sys.excepthook:
TypeError: 'NoneType' object is not callable

Original exception was:
Traceback (most recent call last):
  File "threading.py", line 462, in __bootstrap
  File "threading.py", line 527, in __bootstrap_inner
AttributeError: 'NoneType' object has no attribute 'acquire'
Exception in thread RTRACE (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "threading.py", line 486, in __bootstrap_inner
  File "common_util/trace.py", line 486, in run
  File "common_util/trace.py", line 412, in rtrace_thread_func
<type 'exceptions.TypeError'>: 'NoneType' object is not callable
Unhandled exception in thread started by <bound method RtraceThread.__bootstrap of <RtraceThread(RTRACE, started)>>
Error in sys.excepthook:
TypeError: 'NoneType' object is not callable

Original exception was:
Traceback (most recent call last):
  File "threading.py", line 462, in __bootstrap
  File "threading.py", line 527, in __bootstrap_inner
AttributeError: 'NoneType' object has no attribute 'acquire'

何やらダメだった。pythonはちょっとしかかじった事無いので、何が書いてあるか今イチピンとこない。どうやら lenny な debian ではダメだったようだ。あとで原因追及したいな。

追記
どうやら

 :
Trying to start the daemon.
 :

から下のメッセージは、dropboxd からのメッセージみたい。直接 dropboxd を動かしてみて分かった。Ubuntu は良くても debian はダメってことかしら。残念。

Dropbox使ってみた & ssh ログイン時にDropbox使う方法

話題になってただけあって便利な感じ.気になるのは,Gnome (正確には nautilus?)を起動するようなログインをしないと自動で同期してくれない点.

実際,Linuxssh でリモートログインして使うことの多い僕には,Dropbox縁無いかなー残念かなー

と思いつつ,そもそもこれ,誰が同期してるんだろうなーと思い探し出す.

$ ps x | grep drop
19109 ?        Ssl    0:07 /home/kui/.dropbox-dist/dropboxd

それっぽい名前...
ssh ログインしてこいつを動かしてみる.

$ ~/.drop-dist/dropboxd
[プロンプト帰ってこない]

う,,ごいた?

別端末から確認してみたところうまく機能しているようです.ノーティファー無くても,同期さえしてくれていればいいのさ!って人は,これだけ動かせばいいのかもしれないですね.

これで,このデーモンのスタート&ストップスクリプトを誰かが書いてくれたら幸せになれる!

ちなみに「.drop-dist/dropboxd」でぐぐるとなにやらたくさん出てきているのでもっと便利な方法あるかもしれないです.

もごもごに Autopagerize を

最近になってAutopagerize使いはじめた。今まで手をつけてなかったのは、AutoPagerizeを絶賛する記事が多かったため天邪鬼根性発動していたから。
いいかんじですねー。ホイールまわすのが楽しくなってしまう。
難点は、

  • フッターを見る機会がほとんど無くなってしまうため、サイトデザイナーさん泣かせな点
  • AutopagerizeのON/OFFやりにくい
  • 調子に乗ってホイール回す → トップに戻るの大変(Homeキー押すなりすれば解決

って感じでしょうか。
でもやっぱりホイール回すの楽しい。
んでタイトルの通り、いつも遊びに行ってるもごもごautopagerizeできないので自分でsiteinfo書いた。

var SITEINFO = [

   // みんなのひろば、マイホーム
   {
        url:          'http://(www.)?mogo2.jp/(top|home(/archive_(all|res)|/res)?|(comment|keyword/search)/[1-9][0-9]*)',
        nextLink:     '//div[@id="t_all_read"]/p/a[last()]',
        pageElement:  'id("newest_comment")',
        exampleUrl:   'http://mogo2.jp/top',
    },

    // もごリンク、XXさんの部屋 (こっちの表示納得いかない)
    {
        url:          'http://(www.)?mogo2.jp/(member/(keyword/)?)?[1-9][0-9]*',
        nextLink:     '//div[@id="t_all_read"]/p/a[last()]',
        pageElement:  'id("hot_kuchikomi")',
        exampleUrl:   'http://mogo2.jp/99',
    },
]

こんな感じで書いた。
しかしなんだ、urlをあんなに気合い入れて書く必要なかった様子。もったいないからそのまま。
wedata に登録しておいた。もごもご(みんなのひろば、マイホーム)もごもご(もごリンク、XX さんの部屋)
登録してから気がついたけど、これ、もごもごで使う人いるのかなぁ。。。
なんか久しぶりにXPath書いたー

んー登録したけど上のデータを一つに纏められてしまったので,みんなのひろばでの挙動がおかしくなってしまった.何かにコミットする経験って初めてだからどうしたらいいかわからないなあ.

Last.fmラジオの自作

http://code.google.com/p/thelastripper/wiki/LastFM12UnofficialDocumentation
Last.fm のラジオを自分で作る際に必要な WebAPI のドキュメント.ただ...これをみて堂々とプレイヤーを公開するのは気が引けますね.再生までの簡単な手順はこんな感じ*1.最後の方に "まとめ" として簡単なRubyスクリプトも載せてみた.これくらいなら大丈夫だよね...きっと..

手順

ラジオとして機能させるにはざっくり分けると四つのステップを踏むことになります.具体的には,

Handshake
セッションIDの取得,リクエスト先のホスト名の取得
Adjusting Radio Station
ラジオの選択
Requesting an XSPF
プレイリストの取得
Play
音楽再生

となります.しかし,Handshake で取得する情報は,2回目以降でも使い回しができるみたいです.

Handshake

セッションを張る

要求

以下の URL に GET リクエストを送る.

http://ws.audioscrobbler.com/radio/handshake.php?version=1.3.1.1&username=[USERNAME]&passwordmd5=[PASSWORDMD5_HASH]
[USERNAME]
Last.fm に登録してあるアカウントのユーザ名
[PASSWORDMD5_HASH]
Last.fm に登録してあるアカウントのパスワードのMD5ハッシュ関数を通した文字列(md5sum コマンドとか,Rubyだったら"digest/md5"とか?)
応答

すると,ws.audioscrobbler.com からはこんな感じのが返ってくる

session=ae1eb54a11615e605d61d6e83dde71bc
stream_url=http://87.117.229.85:80/last.mp3?Session=ae1eb54a11615e605d61d6e83dde71bc
subscriber=0
framehack=0
base_url=ws.audioscrobbler.com
base_path=/radio
info_message=
fingerprint_upload_url=http://ws.audioscrobbler.com/fingerprint/upload.php

実際に必要なのは session, base_url, base_path の値だけ.

session
以降リクエストする時のセッションID
base_url
以降のリクエストを送るホスト
base_path
以降のりくs(ry

これら三つを保存しておく.

Adjusting Radio Station

ラジオ局の指定

要求

以下の URL に GET リクエストを送る.

http://[base_url][base_path]/adjust.php?session=[sessionID]&url=[LASTFMURI]
[base_url],[base_path],[sessionID]
先ほどの Handshake で取得した値
[LASTFMURI]
ラジオを選択する時のLast.fm独自のURI
Last.fm独自URI

僕が良く使うやつだけ挙げておく.他のは元のを参照してください.

lastfm://artist/$artistname
$artistname で指定されるアーティストに似た楽曲が集まった局を指定
lastfm://globaltags/$tag
$tag で指定されるタグがついた楽曲が集まった局を指定

応答

例えば, lastfm://globaltags/8bit とラジオ局を選択した時に,選択が成功するとこんな感じの応答が返ってくる.

response=OK
url=lastfm://globaltags/8bit
stationname= 8bit Tag Radio
discovery=true

ここで特に保存すべき内容は無い.

Requesting an XSPF

プレイリストを取得する.XSPF とはプレイリストのファイル形式の一つで,XML の形式をとっています.

要求

以下のように GET 要求を送る

http://[base_url][base_path]/xspf.php?sk=[SESSIONID]&desktop=1.3.1.1
[base_url],[base_path],[sessionID]
先ほどの Handshake で取得した値
応答

すると以下のXSPF形式で応答がある.(先ほどと同じ様に 8bit というタグで指定した場合)

<playlist version="1" xmlns:lastfm="http://www.audioscrobbler.net/dtd/xspf-lastfm">
<title>+8bit+Tag+Radio</title>
<creator>Last.fm</creator>
<link rel="http://www.last.fm/skipsLeft">6</link>
<trackList>
    <track>
        <location>http://play.last.fm/user/da4d7abd06d00e21b9b387b491cbf1fe.mp3</locatio
n>
        <title>Marbles</title>
        <id>36732</id>
        <album>Swarm &amp; Dither</album>
        <creator>Hrvatski</creator>
        <duration>227000</duration>
        <image>http://cdn.last.fm/coverart/130x130/7099.jpg</image>
        <lastfm:trackauth>92c11</lastfm:trackauth>
        <lastfm:albumId>7099</lastfm:albumId>
        <lastfm:artistId>2308</lastfm:artistId>
                <link rel="http://www.last.fm/artistpage">http://www.last.fm/music/Hrvat
ski</link>
        <link rel="http://www.last.fm/albumpage">http://www.last.fm/music/Hrvatski/Swarm
%2B%2526%2BDither</link>
        <link rel="http://www.last.fm/trackpage">http://www.last.fm/music/Hrvatski/_/Mar
bles</link>
        <link rel="http://www.last.fm/buyTrackURL"></link>
        <link rel="http://www.last.fm/buyAlbumURL">http://www.last.fm/affiliate_sendto.p
hp?link=catch&amp;prod=7099&amp;pos=65633c2c6d40fbe9c8bf27ce82d2ca5a</link>
        <link rel="http://www.last.fm/freeTrackURL"></link>
    </track>
    <track>
        <location>http://play.last.fm/user/f1702fa3845bec7817a62749a1fbc49a.mp3</location>
        <title>Pioneer</title>
        <id>69590509</id>
        <album>Pioneer</album>
        <creator>she</creator>
        <duration>204000</duration>
        <image>http://cdn.last.fm/coverart/130x130/3287460.jpg</image>
        <lastfm:trackauth>35a7f</lastfm:trackauth>
        <lastfm:albumId>3287460</lastfm:albumId>
        <lastfm:artistId>1083333</lastfm:artistId>
                <link rel="http://www.last.fm/artistpage">http://www.last.fm/music/she</link>
        <link rel="http://www.last.fm/albumpage">http://www.last.fm/music/she/Pioneer</link>
        <link rel="http://www.last.fm/trackpage">http://www.last.fm/music/she/_/Pioneer</link>
        <link rel="http://www.last.fm/buyTrackURL"></link>
        <link rel="http://www.last.fm/buyAlbumURL"></link>
        <link rel="http://www.last.fm/freeTrackURL">http://freedownloads.last.fm/download/69590509/Pioneer.mp3</link>
    </track>
</trackList>
</playlist>

とこんな感じ.

Play

先のレスポンスの中の,"/playlist/trackList/track/location" で指定される文字列を,mpg123 などに渡す.

まとめ

再生までの最小限のコードはこんな感じ(?)になるのかな.

*1:ひょっとしたら,他のトコロでも過去に取り上げられているかも知れませんが,'08年の2〜4月あたりに少しAPIの仕様が変わっているようです.この記事は'08年5月5日現在のこのAPI紹介です.

続きを読む

net/http で Keep-Alive なやりとり

Ruby でよく使うライブラリ net/http なんですが,コネクション張り続けて通信するにはどうしたらいいんだろう.という話.

いろいろ弄った結果

ポイントは二つくらい


  1. 念のため Net::HTTP:Get のインスタンスに以下のようなヘッダエンティティtをくっつける.

    Net::HTTP::Get#['Connection'] = 'Keep-Alive'
    


  2. Net::HTTP.start や,Net::HTTP#start を使ってコネクションを張る

  3. 2回目以降の要求をする時において,前回の要求から時間が空いていると,サーバがコネクションを断ち切ってしまうので,コネクションの張り直し手続きが必要になる.
これらを踏まえて,3回ほど d.hatena.ne.jp に要求を送るスクリプトを書く.
また3回目の要求の際は,意図的に間隔を空けて,サーバからコネクションを断ち切られてしまった場合を再現している.

#!/usr/bin/ruby -Ku

require 'net/http'
require 'uri'

host = 'd.hatena.ne.jp'
port = 80
path = '/'
sleep_time = 7

# 要求・応答ヘッダ出力
def print_key_and_val(hash)
  puts "-- #{hash.class} --"
  hash.each do|k,v| puts "#{k} => \t#{v}" end
  puts ''
end

http = Net::HTTP.new(host,port)
req = Net::HTTP::Get.new(path)

# ポイント1
req['Connection'] = 'Keep-Alive'

# コネクションを張る(ポイント2
http.start

# 1回目の要求
puts '## First'
print_key_and_val req
print_key_and_val http.request(req)

# 2回目の要求
puts '## Second'
print_key_and_val req
print_key_and_val http.request(req)

# 間隔を空ける
puts "\n## sleep #{sleep_time}s\n\n"
sleep sleep_time # この間に,d.hatena サーバからコネクションを切られてしまう

# 3回目の要求(ポイント3
#  sleep 中に,サーバからコネクションを切られているので,うまくいかない.
#  EOFError が発生してしまう.この例外を捕捉し,コネクションを張りなおす
#  事で対処をした.
puts '## Third'
retry_flag = true # 張りなおしてもダメだった時を判別するためのフラグ
begin
  print_key_and_val req
  print_key_and_val http.request(req)
rescue EOFError => e
  puts '********************'
  puts '** Catch EOFError **'
  puts '********************'
  if retry_flag
    http.finish # 一度こちらからも切って,
    http.start  # コネクション張り張り直し
    retry_flag = false
    retry
  else
    raise EOFError, e.message
  end
end

# コネクションを切る
http.finish

実行結果

$ ruby http_timeout.rb
## First
-- Net::HTTP::Get --
connection =>   Keep-Alive
accept =>       */*

-- Net::HTTPOK --
vary =>         Accept-Encoding
connection =>   Keep-Alive
content-type =>         text/html; charset=euc-jp
date =>         Fri, 21 Mar 2008 09:06:09 GMT
server =>       Apache
keep-alive =>   timeout=7, max=5
transfer-encoding =>    chunked

## Second
-- Net::HTTP::Get --
connection =>   Keep-Alive
accept =>       */*
host =>         d.hatena.ne.jp

-- Net::HTTPOK --
vary =>         Accept-Encoding
connection =>   Keep-Alive
content-type =>         text/html; charset=euc-jp
date =>         Fri, 21 Mar 2008 09:06:09 GMT
server =>       Apache
keep-alive =>   timeout=7, max=4
transfer-encoding =>    chunked


## sleep 7s

## Third
-- Net::HTTP::Get --
connection =>   Keep-Alive
accept =>       */*
host =>         d.hatena.ne.jp

********************
** Catch EOFError **
********************
-- Net::HTTP::Get --
connection =>   Keep-Alive
accept =>       */*
host =>         d.hatena.ne.jp

-- Net::HTTPOK --
vary =>         Accept-Encoding
connection =>   Keep-Alive
content-type =>         text/html; charset=euc-jp
date =>         Fri, 21 Mar 2008 09:06:17 GMT
server =>       Apache
keep-alive =>   timeout=7, max=5
transfer-encoding =>    chunked
$

その他

実際,書く時は,イチイチ begin ... rescue ... end で EOFError を補足するよりも,def で EOFError 捕捉機能つきの要求メソッドを用意するか,下記のようにしてしまった方が,賢く見えるかも.

class Net::HTTP
  alias_method :old_request, :request
  def request(req, body = nil, &block)

    retry_flag = true

    begin
      old_request(req, body, &block)
    rescue EOFError => e
      if retry_flag
        retry_flag = false
        finish
        start
        retry
      else
        raise EOFError, e.message
      end
    end

  end
end

こうすれば,再接続を意識せずにコードが書ける.本当は
レスポンスヘッダ Keep-Alive の max値も考慮に入れなければならないんでしょうね...

Ruby で音楽再生

Ruby で mp3 ファイルを再生する方法について.ただし mpg123 など音楽再生部分は外部コマンドに任せてる.
RubyForge とか,RAAみてもピンとくるのがなかったので書く.

音楽再生に関するノウハウはさっぱりなので,誰かいい方法知ってたらへるぷみー

目的

Ruby で音楽再生をしたい.
しかしながら RubyForge とか,RAAみてもピンとくるのがなかった.
ピンとくるのがなかっただけで,Ruby/SDL とか,ruby-audiere があります.
しかし,これらはムダに大掛かりだったり,Audiere というソフト(ライブラリ?)をインストールする必要があったりするので使いたくありませんでした.
そこで,手元の自宅サーバに既にインストールされてる mpg123 を使えないかと考えました.
それに mpg123 は CUI の音楽再生ソフトの中では有名な方だと思うので.

やり方

この方法では mpg123 が必要になる.
Siena.さんからのアドバイスから,popen3, open 以下の内容をブロックに入れました.ありがとうございます.

#!/usr/bin/env ruby -Ku

require 'open3'

mp3filename = ARGV.shift
mpg123 = '/usr/bin/mpg123'
command = "#{mpg123} -b 1000 -q -"
buffer_size = 1024

Open3.popen3(command) do | pipes |
  sound = pipes[0]
  pipes[1].close    # これと
  pipes[2].close    # これって,要らないかな.
  open(mp3filename) do | mp3file |
    while !(buf = mp3file.read(buffer_size)).nil?
      sound.write buf
    end
  end
end

問題点

厳密な再生終了がわからない.
実際に上のコードを動かすとわかるけれど,上の while ループ部分が終了しても音楽の再生が終わったとは限らない.
mpg123 のオプションの -b を小さく(あるいは取っ払う)ことで幾分かマシにはなるものの,それでも両終了の時差は 1 秒程度発生している.
マイッタネ.