ブラウザからディレクトリを開く (+DLsite Voice Utils)
開発::util
序
この話は可能性を感じる「クライアント完結型ウェブアプリ」 & カスタム検索エンジンを自作する話で触れたDLsite Voice Utilsのアップデートの話なのだが、検索してもパッと出てこない話題として、「ブラウザからディレクトリをファイルマネージャで開く」というトピックスをまず単独で扱う。
ブラウザからディレクトリをファイルマネージャ開く
前述の記事ではタイトル部分のリンクは次のように作られていた。
.innerHTML = `<a href="${meta[i].path.replace("?", "%3F").replace("#", "%23")}">${i}</a>` title
これは、作品ディレクトリに対するリンクである。 これをクリックすると、当然ながらブラウザは作品ディレクトリを(同一ページで)開く。
しかし、だ。 私はNemo Actionを駆使して再生しているので、ブラウザではなくNemoで開きたい。 ブラウザだとディレクトリ内の一連の音声ファイルを再生するだけでも大変だし、再生制御もしづらい。
だが、普通に考えればブラウザが外部アプリケーションを起動できてしまうと問題がある。 「だからできない」……というのはちょっと浅い話で、方法はあるのだが、それを「うまくやる」方法が思いついていなかった。 だが、よく考えてみれば単純な話だったのだ。
まず前提としてだが、Windowsのstart
に相当するような「開く」コマンドが、Linuxではxdg-open
というコマンドとして実装されている。
xdg-open
は引数にとったファイルのファイルタイプによって結び付けられた「アプリケーション」を起動する。
Windowsは拡張子によって判別するが、LinuxではMime
Typeによって判別される。
ここでxdg-open ~/
のようにした場合、ディレクトリはinode/directory
というMime
Typeを持っている。
そして、inode/directory
のハンドラとなっているアプリケーションによって開かれるわけだ。
そして、起動する「アプリケーション」は実行ファイルではなく、.desktop
ファイルである。
一般的にシステムワイドなものは/usr/share/applications
(もしくは/usr/local/share/applications
)に、ユーザーのものは~/.local/share/applications
にある。
この仕組みは「お気に入りのアプリケーション」あるいは「デフォルトアプリケーション」のように呼ばれる。 まずはこの仕組みを頭に入れてほしい。
次に、「開く」アクションが可能なのは、ローカルファイルパスに限らない。例えば
xdg-open https://chienomi.org/
のようなことが可能である。これでURLとかを開いているわけだ。
そして、これはxdg-open
に組み込まれているようなものではなく、設定可能である。
例えばdlvfol://home/jrh/
のようにして開くことができるわけだ。
ChromiumやFirefoxのようなブラウザは、このような未知のスキーマを与えられた場合、xdg-open
と同じような挙動でハンドラを起動するようになっている。これは、Linuxに限らない話で、Windowsなどでも同じだ。
ただし、ブラウザ自身が許可しているスキーマ(例えばhttp://
,
https://
, ftp://
, file:///
,
mailto:
)以外をハンドルする場合、それをして良いか確認する。
以上を踏まえると、
- ハンドルするためのプログラムを用意する
- ハンドルプログラムを起動する
.desktop
ファイルを用意する - ハンドラを登録する
の3ステップで取り扱うことができるようになるわけだ。 以下は、それを自動化する例である。
#!/bin/zsh
cat <<EOF > ~/.local/share/applications/dlsite_voice_folder.desktop
[Desktop Entry]
Type=Application
Name=dlvfol open folder
Exec=dlvfol.zsh %u
StartupNotify=false
MimeType=x-scheme-handler/dlvfol;
EOF
cat <<'EOF' > ~/.local/bin/dlvfol.zsh
#!/bin/zsh
xdg-open "file://${1##dlvfol://}"
EOF
chmod 755 ~/.local/bin/dlvfol.zsh
xdg-mime default dlsite_voice_folder.desktop x-scheme-handler/dlvfol
これでdlvfol:
スキーマを扱えるようになった。
(それだけだと分かりづらいので、ここではdlvfol://
スキーマにした)
プログラムにはスキーマ全体で渡されるため、先頭のdlvfol://
を削除する。
そうするとファイルパスになる……のだが、Nemoにそのまま渡すとURIエンコーディングがうまく機能せず、file:///
スキーマにするとうまくハンドルしてくれる。
そこで、file:///
スキーマに変換するようにした。
これをしたとき、ブラウザのページ上では「ページのURIからの相対」になるから、リンクは相対パスでよかったのだが、この方法で起動すると「ブラウザのカレントディレクトリからの相対」になるため、うまく機能しない。
そこで、mkjson.rb
でパスを展開するようにする。
(ベースパスが変更された場合、再生成する必要ができた)
# expand path
["path"] = File.absolute_path v["path"] v
app.js
側でも変換する
.innerHTML = `<a href="dlvfol://${meta[i].path.replace("?", "%3F").replace("#", "%23")}">${i}</a>` title
検索ツールの改善
ソート
「ソートは日付だけで十分」と言ったものの、やっぱりできると便利ではあるため追加した。
まず、表を作るcreate_table()
のソート部分を、ソート条件を参照するように変更した。
// Sorting
const desc_adjust = sort_status.desc ? -1 : 1
switch (sort_status.type) {
case "str[]":
= entities.sort((a,b) => {
entities const va = meta[a][sort_status.by]?.join(" ") || sort_status.default
const vb = meta[b][sort_status.by]?.join(" ") || sort_status.default
if (va < vb) { return -1 * desc_adjust }
else if (va > vb) { return 1 * desc_adjust }
else { return 0 }
})break
default:
= entities.sort((a,b) => {
entities const va = sort_status.by === "key" ? a : (meta[a][sort_status.by] || sort_status.default)
const vb = sort_status.by === "key" ? b: (meta[b][sort_status.by] || sort_status.default)
if (va < vb) { return -1 * desc_adjust }
else if (va > vb) { return 1 * desc_adjust }
else { return 0 }
}) }
つまり、sort_status
オブジェクトを書き換えてcreate_table
を呼べばソートされるようになる。
ソートを行うのはヘッダーカラムであり、これらにsorter
というnameを与えた。
そして
var sorters = document.getElementsByName("sorter")
とした上で、これらに「sort_status
を書き換えてcreate_table
を呼ぶ」イベントリスナを追加する。
.forEach(i => {
sorters.addEventListener("click", e => {
iif ( sort_status.by === i.dataset.prop ) {
.desc = !sort_status.desc
sort_statuselse {
} .desc = false
sort_status
}.by = i.dataset.prop
sort_statusswitch (i.dataset.type) {
case "num":
.type = null
sort_status.default = 0
sort_statusbreak
case "dstr":
.type = null
sort_status.default = "1970-01-01"
sort_statusbreak
case "str[]":
.type = "str[]"
sort_status.default = []
sort_statusbreak
default:
.type = null
sort_status.default = ""
sort_status
}show(e)
}) })
うまく抽象化できたので、だいぶコンパクトに書けている。
switch
内の重複が気になるが、重複している3行より少なくしようとすると処理量が増えるので妥協する。
条件がもうひとつ増えたら、ソート判定部分を関数にすれば短くなる。
画像
以前の記事では「画像はスクリプトで生成するからURLは固定で良い」だったが、タイミング的にやってないときもあるので、mkjson.rb
側でフォローするようにした。
thumb.jpg
が存在しない場合、その作品フォルダ内の最も小さい画像を探す。
unless File.exist?("#{v["path"]}/thumb.jpg")
# Make image path.
= Dir.glob("#{v["path"]}/**/*.{jpg,jpeg,JPG,png,PNG,webp,avif}")
imgfiles unless imgfiles.empty?
.sort_by! {|i| File::Stat.new(i).size }
imgfiles["imgpath"] = imgfiles[0][v["path"].length .. -1]
vend
end
app.js
も少し変更された。
.innerHTML = `<img src="${meta[i].path.replace("?", "%3F").replace("#", "%23")}/${meta[i].imgpath || "thumb.jpg"}" />` cover
さて、thumb.jpg
は最も大きいファイルを使っているのに、mkjson.rb
は最も小さいファイルを使う。
これは、「最も大きいファイルは原寸大アートワークである可能性が高い」(確度高め)であり、変換してしまう場合元のファイルサイズは気にならない。
対して、巨大画像をブラウザで大量に読むというのはちょっと厳しいものがあるので、mkjson.rb
では最も小さいファイルを使う。
これは、「最も小さいファイルは販売ページ用のサムネイルである可能性が高い」(確度低め)であることと、単純に読み込むファイルサイズを減らすためだ。
ライブラリツール
概要
このようなDLsiteライブラリを取り扱うツールだが、私は一定のルールのもとにライブラリを構成している。
さらに、バックアップを容易にする(rsync
しやすくする)ため、.library/$titleid
以下にストアして、シンボリックリンクを貼るような運用に変更した。
だが、単なる運用ルールだと、手順が多くて結構めんどい。 そこで、コマンドで簡単に済ませられるようにした。
ライブラリの展開
dlsite_extract [-u|--unconvert] [-i|--insjis] [-o|--outsjis] [-7|--7z] <source_file>
アーカイブファイルを展開して.library
以下に格納すること、そして$dexname
と$dexfile
がセットされるため
ln -vs ../../.library/$dexfile $dexname
や
ln -vs ../../.library/$dexfile/$dexname
のようにして簡単にリンクを貼れるのがポイント。
結構苦心の作である。
ライブラリの移動と削除
ライブラリの移動は、リンクの貼り直しが必要なので、mv
の代わりに使えるように
dlsite_movelink <source> <dest>
削除はリンクを消したら本体も消したいのでrm
の代わりに使えるように
dlsite_remove_from_library <link>
とできるようにした。