コンテンツを整理して検索できるようにする
Live With Linux::script
- TOP
- Articles
- Live With Linux
- コンテンツを整理して検索できるようにする
序
本記事は題材としてアダルトコンテンツが含まれている。
私は音声作品を嗜んでいるのだが、(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'
= if File.exist? ".linkdb.yaml"
db YAML.load(File.read(".linkdb.yaml"))
else
{}
end
ARGF.each do |line|
= line.chomp.split(" ", 2)
date, path = path.sub(%r:.*/:, "")
title if db[title]
unless db[title][:path] == path
system("ln", "-sfv", path, "#{db[title][:date]}-#{title}")
[title][:path] = path
dbelse
STDERR.puts "..........UNCHANGED"
end
else
[title] = {
dbpath: path,
date: 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
["circle"] = links.shift.sub(/^_/, "")
this["series"] = links.shift.sub(/^_/, "")
thisend
if links.length == 2
["circle"] = links.shift.sub(/^_/, "")
thisend
["title"] = links.shift
this
thisend
Dir.glob("_VoiceByDate/*").each do |i|
begin
= File.readlink i
link rescue
next
end
= i[/\d\d\d\d-\d\d-\d\d/]
date .sub!(%r:.*Voice/:, "")
link= link.split("/")
links = fromlinks(links)
this ["date"] = date
this[this["title"]] = this
filehashend
Dir.glob("_VoiceByActress/*/*").each do |i|
begin
= File.readlink i
link rescue
next
end
.sub!(%r:.*Voice/:, "")
link= i.split("/")
path next if File.symlink? "#{path[0]}/#{path[1]}"
= link.sub(%r:.*/:, "")
title if path[-1][0, 2] == "__"
= path[-1].sub(/^__/, "")
circle .each do |k, v|
filehashif v["circle"] == circle
if v["actress"]
["actress"].push path[1]
velse
["actress"] = [path[1]]
vend
end
end
elsif path[-1][0] == "_"
= path[-1].sub(/^_/, "")
series .each do |k, v|
filehashif v["series"] == series
if v["actress"]
["actress"].push path[1]
velse
["actress"] = [path[1]]
vend
end
end
else
begin
if filehash[link.sub(%r:.*/:, "")]["actress"]
[link.sub(%r:.*/:, "")]["actress"].push path[1]
filehashelse
[link.sub(%r:.*/:, "")]["actress"] = [path[1]]
filehashend
rescue
abort "????? - #{i}"
end
end
end
Dir.glob("_VoiceExtendedInfo/*/info.yaml").each do |i|
= i.split("/")
path = path[1]
title = YAML.load(File.read(i))
info [title].merge! info
filehash[title]["tags"] = filehash[title]["tags"].map {|x| "##{x}" }.join(" ")
filehash[title]["description"] = filehash[title]["description"]&.gsub("\t", " ")&.gsub("\n", " ")
filehashend
.each do |k ,v|
filehashputs [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
として簡単に出すことができる。
ちなみに、ちなみにだが。音声作品を嗜む人には分かってもらえると思うのだが。
好みにあまり関係ないと思うのだが、常に陽向葵ゅかさんが圧倒的に多くなるのは、やはり出演作品がそれだけ多いということなのだろうか……