Created
December 3, 2025 02:14
-
-
Save kinneko/3e5508a877a16db1bb320ea364338ac6 to your computer and use it in GitHub Desktop.
fanbox_url_to_md.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 "\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 "\n" "$ymd" "$bn" | |
| else | |
| printf "\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 "" | |
| 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" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
fanboxの記事をZolaに移行するためのスクリプトです。
タグ処理はうまくいっていないけど、真面目につけてなかったので手動で対処することにした。
画像ファイルの位置がうまく取れないので、こちらも手動になった。