Chienomi

コンテンツを整理して検索できるようにする

Live With Linux::script

本記事は題材としてアダルトコンテンツが含まれている。

私は音声作品を嗜んでいるのだが、(DLsiteユーザーの方であればきっと共感していただけると思うが)セールの時に気になったものを買う、という雑さだとコンテンツが把握できないまま溜まっていく。

基本的にデータは量が増えると単純なヒエラルキー整理では追いつかなくなっていき、検索が必要になる。 写真や動画などカメラ撮影したものもそうだろうし、ゲームプレイも常時録画しているとやはり発生する。

ゲーム動画はジャンル/タイトル/年月でおよそ絞り込んで探すことができるが、音声作品は難しく、多角的な検索が欲しくなる。 基本はサークルごとの分類だと思うが、実際はサークル指名というのは結構少ないだろう。 どちらかといえばキャストやジャンル、作品傾向などから探したいことのほうが多いはずだ。

ここでは音声作品をターゲットにするが、同じような考え方は単一の軸で分類できず、個々の作品が単発になるようなもの(例えば映画)などにも適用できるだろう。

ベースづくり

作品の格納

作品はVoice配下に$title, _$circle/$title, _$circle/_$series/$titleのいずれかとして格納する。

同一サークルの作品を複数購入したら_$circleを作ればいいし、同一サークル内にシリーズ作品がありまとめたければもう一段掘れる。

これはどちらかというと「データ整頓」にフォーカスしているため、検索性はそれほど考えなくて良い。

DLsiteの場合、作品はzipまたはrarアーカイブとして配信される。 この中身に関してはサークルが主体的にアップロードするためであり、サークルごとに大きく異なっており、サークル内でも全く違うこともある。 このため、中身を統一的に扱うためには展開したアーカイブを一定の規則で整頓する必要があり、手間がかなり大きくなってしまう。

だが、$titleディレクトリに関してはある程度気を遣う必要がある。 これは、ちゃんとタイトルになっていることもあれば、そもそもディレクトリが掘られていないこともあるし、「本編」みたいなディレクトリであることもあるし、“RJ1000000”というようなDLsite側の作品IDになっていることもある。 これを、全体でユニークなタイトルのディレクトリをちゃんと作るようにしなければならない。 音声作品のタイトルは全体に長い傾向であるため、タイトルの重複はあまり起きないが、一応気を遣う必要がある。

また、タイトルの変更は大変な作業になるため、タイトル変更を生じないように注意しなければならない。 特に注意が必要なのは、Macを使って仕上げてアップロードしているサークルの中には、アーカイブ内のファイルにWindowsでは使えない文字(特に?)が含まれていることがあるということだ。 スマートフォンで再生しようとしたりするとリネームを余儀なくされることがある。

また、(ありえないとは思うが)$title_で始まってはならない。

by Cast

作品を演じたキャストベースのリンクファームを作る。 _VoiceByActress配下に$name/$titleというシンボリックリンクを貼る。

シンボリックリンクを貼る作業は手作業だ。 このリンクは一対多になる可能性がある。作品の出演者が一人であるとは限らないためだ。

これは、ln -s ../../Voice/$titleのようなコマンドを打つことで、タイトル名を維持したリンクを作ることができる。

キャスト名義が単一でない場合(例えば紅月ことねさん=琴音有波さん)、$nameのシンボリックリンクを作る。

これを作るだけでキャストベースで探すことができるため、かなり実用的。

では、_$circle_$seriesが切られてリンクが切れた場合はどうするのか。 これは、Zshスクリプトで処理する。

#!/bin/zsh
setopt EXTENDED_GLOB

print -l -- **/*(-@D) | while read blink
do
  (cd ../Voice; find . -name "${blink:t}") | read vlink
  if [[ -z vlink ]]
  then
    print "${blink} no alternative found." >&2
    continue
  fi
  rm -v "$blink"
  ln -sv "../../Voice/${vlink#./}" "$blink"
done

**/*(-@D)でリンク切れになったシンボリックリンクを探すことができる。 $titleはユニークかつイミュータブルであるため、Voiceディレクトリ配下を探せばどこに行ったかがみつかるので、リンクを書き換えれば良い。

なお、あるシリーズが同一キャストによって演じられている場合はシリーズに対して_$seriesとして、あるサークルの作品がすべて同一キャストによって演じられている場合は__$circleとして登録するようにした。 この場合、$series$circleも変更は制限される。

by Date

「最近買った作品で見てないもの」などという探し方をすることがあるため、日付ベースでも探せるようにする。 これは、_VoiceByDate/$titleという自動生成されたリンクファームだ。

ZshとRubyを使って簡単に書いてある。

#!/bin/zsh
setopt bare_glob_qual

for i in ../Voice/[^_]*(/) ../Voice/_*/[^_]*(/) ../Voice/_*/_*/*(/)
do
  typeset date=$(stat -c "%w" $i | sed 's/ .*//')
  print $date "$i"
done | ruby ./create.rb

%wはbtime(Birth Time)であり、ノードが生成された時刻を示す。利用できるかどうかはファイルシステムに依存する(ちなみに、私はこのディスクはEXT4である)。

#!/bin/ruby
require 'yaml'

db = if File.exist? ".linkdb.yaml"
  YAML.load(File.read(".linkdb.yaml"))
else
  {}
end
 

ARGF.each do |line|
  date, path = line.chomp.split(" ", 2)
  title = path.sub(%r:.*/:, "")
  if db[title]
    unless db[title][] == path
      system("ln", "-sfv", path, "#{db[title][]}-#{title}")
      db[title][] = path
    else
      STDERR.puts "..........UNCHANGED"
    end
  else
    db[title] = {
      path,
      date
    }
    unless File.exist? "#{date}-#{title}"
      system("ln", "-sv", path, "#{date}-#{title}")
    else
#      system("rm", "-v", "#{date}-#{title}")
#      system("ln", "-sv", path, "#{date}-#{title}")
       STDERR.puts "#{date}-#{title} is ALREADY EXIST, SKIPPING..........."
    end
  end
end

File.open(".linkdb.yaml", "w") {|f| YAML.dump(db, f)}

btimeはディスク換装を行った場合など変わりうるので、データベースに取っておく必要がある。 dateはこのdateを保存するためのもので、dateの書き換えは行わない。

このスクリプトは、自動的にDateリンクを生成し、また更新する。

何らかの理由でDateが切れてデッドリンクになってしまった場合は、rm *(-@D)で良い。

Tad

タブフィールド

「これ!」と決まっている場合はいいのだが、今の気分にあった作品を探すなどするにはこれら多角的な分類をひとつにまとめて閲覧したいところだ。 また、違う分類(例えば単純にタイトル)で探した上で、他の要素(例えばキャスト)などを確認したいこともある。

これは、手入力したデータと自動的に取得したデータとをアッセンブルしてひとつにまとめれば良いのだが、まとめるものはどんなデータベースでも良く、選択肢は広い。

こういうときに非常に役立つのがタブフィールドテキストだ。 Microsoftの信者であればTSVなどと呼ぶこともあるが、Unixとしては非常に伝統的なデータベースである。

テキストは改行とタブ文字を潰せばタブ結合することで表現できるために扱いやすく、出力後もcut(1)などでも扱いやすい、利便性の高いフォーマットだ。

加えて言うならば、タブフィールドテキストはCSVの一種として見ることもでき、CSVを扱うことができるアプリケーション(例えばLibreoffice)でも扱うことができる。

こうしたケースにはうってつけだ。

Tadが便利

Tadはこのようなタブフィールドテキストに特化したビューワである。 これが猛烈に便利だ。

  • フィールドごとにまとめて表にする
  • カラムごとにソートできる
  • 特定のフィールドでまとめることができる
  • フィールドごとに「含む」「等しい」などのフィルタをかけることができる等しいものについては選択もできる

LinuxのほかWindows, Macも存在する。 TypeScript/Reactで作られているようだ。Expressを使ったローカルウェブアプリケーションになっている。 クライアントアプリケーションの外殻はElectron。 ライセンスはMIT.

データベースを作る

基本方針

スクリプトを実行したらここまで準備したものを参照し、完全なデータベースを作るようにしたい。 そのようにすれば、パス変更などが発生したとしても通常の手順で更新できる。

Dateは階層化されておらず、タイトルと一対一で結合する。 そのため、Dateをベースとし、キャストを埋めるのが妥当だ。

さらに、絞り込みができるのであれば、ジャンルや作品傾向なども記録したい。 個々の情報ごとにリンクファームを増やすのは大変だし、リンクファームが適切でないケースも多いため、作品に対する付加情報を_VoiceExtendedInfo/$title/info.yamlとして付加できるようにする。

$title.yamlでない理由は、上海飯店さんの作品は既に展開すると256バイトに迫る長さのタイトルであるため、.yamlをつける余裕がない可能性があるからだ。

suffixから推測できるようにYAML形式である。 これは、文字列のクォートがいらず、インラインで書けるために楽だからだ。

初期化はスクリプト。作品傾向はタグにまとめた。

#!/bin/zsh
setopt GLOB_BARE_QUAL

for i in ../Voice/[^_]*(/) ../Voice/_*/[^_]*(/) ../Voice/_*/_*/[^_]*(/)
do
  typeset dest="${i:t}/info.yaml"
  if [[ ! -e "${dest:h}" ]]
  then
    mkdir -v "${dest:h}"
    cat > "$dest" << EOF
---
tags: []
rate: 3
writer: ~
duration: ~
description: ~
r18: yes
EOF
    ln -s ../"$i" "${dest:h}/link"
  fi
done

Descriptionの存在意義だが、READMEがある場合はそれを写しても良いのだが(それをできるように改行潰ししているわけだし)、それよりはREADMEもジャケットも含まれてないタイプの作品でサイトから概要をコピってくるためにある。

Duration(単位は分)の計算は面倒なので(そもそも対象に入るオーディオファイルが何かというのは手動で指定しないといけないので)、補助スクリプト。

#!/bin/zsh

typeset -f duration=0

soxi -D "$@" | while read
do
  (( duration += REPLY ))
done

typeset -i di
(( di = duration / 60 ))

print $(( di ))

拡張情報は「常に入れる」だとしんどいので、再生時に入れる方針にしている。 「良かった」「良くなかった」を残しておくのはかなり有用。 特にすごく良かったものはまた再生したい可能性が高いし、何らかの理由で「あー、そういうのだったか……」となることは結構あるので、そういうのを除外するのも有用。

データベース生成スクリプト

Rubyで書いた。

#!/bin/ruby
require 'yaml'

filehash = {}

def fromlinks(links)
  this = {}
  if links.length == 3
    this["circle"] = links.shift.sub(/^_/, "")
    this["series"] = links.shift.sub(/^_/, "")
  end
  if links.length == 2
    this["circle"] = links.shift.sub(/^_/, "")
  end
  this["title"] =  links.shift
  this
end

Dir.glob("_VoiceByDate/*").each do |i|
  begin
    link = File.readlink i
  rescue
    next
  end
  date = i[/\d\d\d\d-\d\d-\d\d/]
  link.sub!(%r:.*Voice/:, "")
  links = link.split("/")
  this = fromlinks(links)
  this["date"] = date
  filehash[this["title"]] = this
end

Dir.glob("_VoiceByActress/*/*").each do |i|
  begin
    link = File.readlink i
  rescue
    next
  end
  link.sub!(%r:.*Voice/:, "")
  path = i.split("/")
  next if File.symlink? "#{path[0]}/#{path[1]}"
  title = link.sub(%r:.*/:, "")
  if path[-1][0, 2] == "__"
    circle = path[-1].sub(/^__/, "")
    filehash.each do |k, v|
      if v["circle"] == circle
        if v["actress"]
          v["actress"].push path[1]
        else
          v["actress"] = [path[1]]
        end
      end
    end
  elsif path[-1][0] == "_"
    series = path[-1].sub(/^_/, "")
    filehash.each do |k, v|
      if v["series"] == series
        if v["actress"]
          v["actress"].push path[1]
        else
          v["actress"] = [path[1]]
        end
      end
    end
  else
    begin
      if filehash[link.sub(%r:.*/:, "")]["actress"]
        filehash[link.sub(%r:.*/:, "")]["actress"].push path[1]
      else
        filehash[link.sub(%r:.*/:, "")]["actress"] = [path[1]]
      end
    rescue
      abort "????? - #{i}"
    end
  end
end

Dir.glob("_VoiceExtendedInfo/*/info.yaml").each do |i|
  path = i.split("/")
  title = path[1]
  info = YAML.load(File.read(i))
  filehash[title].merge! info
  filehash[title]["tags"] = filehash[title]["tags"].map {|x| "##{x}" }.join(" ")
  filehash[title]["description"] = filehash[title]["description"]&.gsub("\t", " ")&.gsub("\n", " ")
end

filehash.each do |k ,v|
  puts [v["title"], (v["actress"]&.join(", ") || "???"), v["date"], v["circle"], v["series"], v["tags"], v["rate"], v["duration"], v["writer"], v["r18"], v["description"] ].join("\t")
end

拡張情報を除いてタイトル, サークル, シリーズ, キャスト, 日を出力する。 Tadだとフィールドごとにまとめることができるが、日に関しては通常「購入日=ダウンロード日」で、ある程度まとまった単位になるため、単なるByDateよりも便利。

拡張情報は主にフィルタ用。

おまけ: プレイヤースクリプト

だいたい音声作品ってcoverがセットされてないけれどcover画像自体は同梱されていることが多いため、mpvの--cover-art-fileオプションを使うと快適なのだけど、やはりここは右クリックから行きたいのでますがはスクリプト。

Yadでファイル選択ダイアログを出してカバー画像を選択させる。

#!/bin/zsh

file_select() {
  (
    cd "$1"
    yad --file
  )
}

if [[ -d "$1" ]]
then
  cdir="$1"
else
  cdir="${1:h}"
fi

cover_file=$(file_select "${cdir}")
exec mpv --cover-art-file="$cover_file" "$1"

右クリックへの対応はNemo Action.

[Nemo Action]
Name=Play voice with cover
Comment=Play voice content with cover image.

EscapeSpaces=true
Exec=voicempv.zsh %P

Icon-Name=mpv

Selection=none
Extensions=any;
Dependencies=mpv;

おまけ2: お気に入りキャストを探す

誰が出演している作品をどれくらい持っているか、は_VoiceByActressでカウントすることもできるが、こちらはシリーズやサークルを指定することもできるようにしているため、正確な値が出ない。

生成したリストを使うとcut -f 2 voicelist.csv | sed 's/, /\n/g' | sort | uniq -cとして簡単に出すことができる。

ちなみに、ちなみにだが。音声作品を嗜む人には分かってもらえると思うのだが。

好みにあまり関係ないと思うのだが、常に陽向葵ゅかさんが圧倒的に多くなるのは、やはり出演作品がそれだけ多いということなのだろうか……