Chienomi

ブラウザからディレクトリを開く (+DLsite Voice Utils)

開発::util

  • TOP
  • Articles
  • 開発
  • ブラウザからディレクトリを開く (+DLsite Voice Utils)

この話は可能性を感じる「クライアント完結型ウェブアプリ」 & カスタム検索エンジンを自作する話で触れたDLsite Voice Utilsのアップデートの話なのだが、検索してもパッと出てこない話題として、「ブラウザからディレクトリをファイルマネージャで開く」というトピックスをまず単独で扱う。

ブラウザからディレクトリをファイルマネージャ開く

前述の記事ではタイトル部分のリンクは次のように作られていた。

title.innerHTML = `<a href="${meta[i].path.replace("?", "%3F").replace("#", "%23")}">${i}</a>`

これは、作品ディレクトリに対するリンクである。 これをクリックすると、当然ながらブラウザは作品ディレクトリを(同一ページで)開く。

しかし、だ。 私は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:)以外をハンドルする場合、それをして良いか確認する。

以上を踏まえると、

  1. ハンドルするためのプログラムを用意する
  2. ハンドルプログラムを起動する.desktopファイルを用意する
  3. ハンドラを登録する

の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
v["path"] = File.absolute_path v["path"]

app.js側でも変換する

title.innerHTML = `<a href="dlvfol://${meta[i].path.replace("?", "%3F").replace("#", "%23")}">${i}</a>`

検索ツールの改善

ソート

「ソートは日付だけで十分」と言ったものの、やっぱりできると便利ではあるため追加した。

まず、表を作るcreate_table()のソート部分を、ソート条件を参照するように変更した。

// Sorting
const desc_adjust = sort_status.desc ? -1 : 1
switch (sort_status.type) {
  case "str[]":
    entities = entities.sort((a,b) => {
    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 = entities.sort((a,b) => {
    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を呼ぶ」イベントリスナを追加する。

sorters.forEach(i => {
  i.addEventListener("click", e => {
    if ( sort_status.by === i.dataset.prop ) {
      sort_status.desc = !sort_status.desc
    } else {
      sort_status.desc = false
    }
    sort_status.by =  i.dataset.prop
    switch (i.dataset.type) {
      case "num":
        sort_status.type = null
        sort_status.default = 0
        break
        case "dstr":
          sort_status.type = null
          sort_status.default = "1970-01-01"
          break
        case "str[]":
          sort_status.type = "str[]"
          sort_status.default = []
          break
        default:
        sort_status.type = null
        sort_status.default = ""
    }
    show(e)
  })
})

うまく抽象化できたので、だいぶコンパクトに書けている。 switch内の重複が気になるが、重複している3行より少なくしようとすると処理量が増えるので妥協する。 条件がもうひとつ増えたら、ソート判定部分を関数にすれば短くなる。

画像

以前の記事では「画像はスクリプトで生成するからURLは固定で良い」だったが、タイミング的にやってないときもあるので、mkjson.rb側でフォローするようにした。

thumb.jpgが存在しない場合、その作品フォルダ内の最も小さい画像を探す。

unless File.exist?("#{v["path"]}/thumb.jpg")
  # Make image path.
  imgfiles = Dir.glob("#{v["path"]}/**/*.{jpg,jpeg,JPG,png,PNG,webp,avif}")
  unless imgfiles.empty?
    imgfiles.sort_by! {|i| File::Stat.new(i).size }
    v["imgpath"] = imgfiles[0][v["path"].length .. -1]
  end
end

app.jsも少し変更された。

cover.innerHTML = `<img src="${meta[i].path.replace("?", "%3F").replace("#", "%23")}/${meta[i].imgpath || "thumb.jpg"}" />`

さて、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>

とできるようにした。