Skip to content

Instantly share code, notes, and snippets.

@kinneko
Created December 3, 2025 02:14
Show Gist options
  • Select an option

  • Save kinneko/3e5508a877a16db1bb320ea364338ac6 to your computer and use it in GitHub Desktop.

Select an option

Save kinneko/3e5508a877a16db1bb320ea364338ac6 to your computer and use it in GitHub Desktop.
fanbox_url_to_md.sh
#!/usr/bin/env bash
set -euo pipefail
URL="${1:-}"
OUT_ROOT="out"
BROWSER="${FANBOX_BROWSER:-firefox}"
if [[ -z "$URL" ]]; then
echo "usage: $0 <fanbox_post_url>" >&2
exit 1
fi
need() { command -v "$1" >/dev/null 2>&1 || { echo "missing: $1" >&2; exit 1; }; }
need jq
need sed
need date
need gallery-dl
need find
need mv
need sort
need grep
need mkdir
need perl
b64dec() {
if base64 --decode >/dev/null 2>&1 <<<"Zg=="; then base64 --decode; else base64 -D; fi
}
is_gnu_date() { date --version >/dev/null 2>&1; }
normalize_iso_for_bsd() {
printf '%s' "$1" | sed -E \
-e 's/\.[0-9]+//' \
-e 's/Z$/+0000/' \
-e 's/([+-][0-9]{2}):([0-9]{2})$/\1\2/'
}
iso_to_local_datetime() {
local iso="$1"
if [[ -z "${iso}" || "${iso}" == "null" ]]; then
echo "1970-01-01 00:00:00"
return
fi
if is_gnu_date; then
TZ=Asia/Tokyo date -d "$iso" +"%Y-%m-%d %H:%M:%S"
else
local norm
norm="$(normalize_iso_for_bsd "$iso")"
TZ=Asia/Tokyo date -j -f "%Y-%m-%dT%H:%M:%S%z" "$norm" "+%Y-%m-%d %H:%M:%S"
fi
}
url_basename() {
printf '%s' "$1" | sed -E 's/[?#].*$//' | sed -E 's#.*/##'
}
# 本文の「オフセットされたURL行」を blockquote 化:
# (空白/全角空白で始まり、URL(または<URL>)だけの行) -> "> <URL>"
quote_offset_urls() {
perl -CS -pe '
s/\r$//;
# 全角スペース2つで始まる行だけ処理
if (/^\x{3000}\x{3000}(.*)$/) {
my $s = $1;
$s =~ s/\s+$//; # 行末空白除去
# URL行(URLだけ、<> 付きも許容)
if ($s =~ /^<?(https?:\/\/\S+?)>?\z/) {
$_ = "> <$1>\n";
} else {
# タイトル等:末尾に半角スペース2つ
$_ = "> $s \n";
}
}
'
}
mkdir -p "$OUT_ROOT"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
POST_JSON="$tmpdir/post.json"
USED_LIST="$tmpdir/used.txt"
: > "$USED_LIST"
# 1) JSON取得
gallery-dl --cookies-from-browser "$BROWSER" -j "$URL" > "$POST_JSON"
post_json="$(jq -r '.[0][1] | @base64' "$POST_JSON" | b64dec)"
title="$(jq -r '.title // ""' <<<"$post_json")"
published_iso="$(jq -r '.publishedDatetime // .updatedDatetime // empty' <<<"$post_json")"
zola_date="$(iso_to_local_datetime "$published_iso")"
ymd="${zola_date:0:10}"
creator_id="$(jq -r '.creatorId // .creator.creatorId // empty' <<<"$post_json")"
post_url="$(jq -r '.url // .postUrl // .canonicalUrl // empty' <<<"$post_json")"
[[ -n "$post_url" ]] || post_url="$URL"
post_dir="${OUT_ROOT}/${ymd}"
mkdir -p "$post_dir"
# 2) 画像/添付ダウンロード
gallery-dl --cookies-from-browser "$BROWSER" \
--directory "$post_dir" \
--filename '/O' \
"$URL"
# 3) out/YYYY-MM-DD/ 配下に掘られたものを直下へ集約
find "$post_dir" -mindepth 2 -type f 2>/dev/null | while IFS= read -r f; do
base="$(basename "$f")"
dest="$post_dir/$base"
if [[ -e "$dest" ]]; then
ext="${base##*.}"
stem="${base%.*}"
n=1
while [[ -e "$post_dir/${stem}_$n.${ext}" ]]; do n=$((n+1)); done
dest="$post_dir/${stem}_$n.${ext}"
fi
mv "$f" "$dest"
done
find "$post_dir" -type d -empty -delete 2>/dev/null || true
# 4) タグ抽出
tags_json="$(jq -c '
(
(.tags? // [])
+ (.tag? // [])
+ (.post?.tags? // [])
)
| map(tostring)
| map(select(length>0))
| . + ["blog","fanbox"]
| unique
' <<<"$post_json")"
tags_toml="$(jq -r -n --argjson t "$tags_json" '$t | map("\""+.+"\"") | join(", ")')"
title_escaped="$(printf '%s' "$title" | sed 's/\\/\\\\/g; s/"/\\"/g')"
out_md="${OUT_ROOT}/${ymd}.md" # 年月日のみ
render_blocks() {
jq -c '.articleBody.blocks? // [] | .[]' <<<"$post_json" | while IFS= read -r blk; do
btype="$(jq -r '.type // ""' <<<"$blk")"
if [[ "$btype" == "p" ]]; then
txt="$(jq -r '.text // ""' <<<"$blk")"
# 100円投げ銭行を削除(その行に両方含まれる場合)
if printf '%s' "$txt" | grep -q '100円' && printf '%s' "$txt" | grep -q '投げ銭'; then
continue
fi
# 空行はそのまま1行分だけ保持(余計な空行を足さない)
if [[ -z "$txt" ]]; then
echo ""
else
# URL引用化は維持
printf "%s\n" "$txt" | quote_offset_urls
fi
continue
fi
img_url="$(jq -r '
(
[.. | strings
| select(test("^https?://"))
| select(test("\\.(png|jpe?g|gif|webp)(\\?|$)";"i"))
] | .[0]
) // empty
' <<<"$blk")"
if [[ -n "$img_url" ]]; then
base="$(url_basename "$img_url")"
if [[ -f "${post_dir}/${base}" ]]; then
echo "$base" >> "$USED_LIST"
printf "![](/%s/%s)\n" "$ymd" "$base"
else
cand="$(find "$post_dir" -maxdepth 1 -type f -name "*${base}*" -print 2>/dev/null | head -n 1 || true)"
if [[ -n "$cand" ]]; then
bn="$(basename "$cand")"
echo "$bn" >> "$USED_LIST"
printf "![](/%s/%s)\n" "$ymd" "$bn"
else
printf "![](%s)\n" "$img_url"
fi
fi
echo ""
fi
done
}
{
echo "+++"
echo "date = \"${zola_date}\""
echo "draft = false"
echo "title = \"${title_escaped}\""
echo "description = \"\""
echo ""
echo "[taxonomies]"
echo "tags = [${tags_toml}]"
echo ""
echo "[extra]"
echo "#image = \"/event.png\""
echo "+++"
echo ""
render_blocks
# 本文中で拾えなかった画像を末尾に追記(先頭 /)
sort -u "$USED_LIST" > "${USED_LIST}.sorted" || true
find "$post_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.gif' -o -iname '*.webp' \) -print | sort | while IFS= read -r f; do
bn="$(basename "$f")"
if ! grep -Fxq "$bn" "${USED_LIST}.sorted" 2>/dev/null; then
echo "![](/${ymd}/${bn})"
echo ""
fi
done
echo "---"
echo "> オリジナル投稿:"
if [[ -n "$creator_id" ]]; then
# タイトル行頭に >、行末スペース2つ
echo "> ${title}|${creator_id}|pixivFANBOX "
else
echo "> ${title}|pixivFANBOX "
fi
# タイトルとURLの間の空行は入れない。URLは <> で囲う
echo "> <${post_url}>"
} > "$out_md"
echo "Wrote: $out_md"
echo "Media: $post_dir"
@kinneko

kinneko commented Dec 3, 2025

Copy link
Copy Markdown
Author

fanboxの記事をZolaに移行するためのスクリプトです。

タグ処理はうまくいっていないけど、真面目につけてなかったので手動で対処することにした。

画像ファイルの位置がうまく取れないので、こちらも手動になった。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment