最近Notionよりも軽くて好きな機能だけプラグインで入れられるobsidianを多用しています。

同期はObsidian Self-hosted LiveSyncを使用しているのでスマホともサクサク同期が出来て良いです。

Obsidian Self-hosted LiveSyncはこちらのサイトを参考に自宅鯖にDockerでCouchDBサーバを立てて運用しています。

obsidianテーマはPrimaryを使用しています。

obsidianで読書管理

obsidianのBook-Searchプラグインにタイトルなどを入力すると自動で書籍情報をネットから拾ってきてくれるので、それを自動集計プラグインDataview使って取り込んだ本データを一覧に出せるようにしています

各書籍のページはデフォルトでこんな感じ。(発行日は邦訳でなく原作の初版発行日を入れています)

フォルダ構成はこんな感じで、booksフォルダ内に各書籍のページデータ、本棚ファイルにはDateviewのコードを記述しています。

書籍データはBook-searchプラグインがネット上から取得してくれますが、発行日など上手く取れなかった場合は手動で入力しています。

プラグインのインストール

設定→コミュニティプラグインからBook SearchDataviewプラグインを検索してインストールし、それぞれ有効化します。

Book Searchの設定

適当なフォルダにbook-template.mdを作成します(名前は何でもいいです)。自分の場合templetesフォルダを作成したのでtempletes/book-template.mdになります。

book-template.md
---
title: "{{title}}"
author: "{{author}}"
publisher: "{{publisher}}"
publishedDate: "{{publishDate}}"
isbn: "{{isbn13}}"
series:
genre:
tags:
  - "読書"
  - "{{author}}"
cover: "{{coverUrl}}"
---

# {{title}}

<img src="{{coverUrl}}" alt="{{title}}" width="200px">

**著者:** {{author}}  
**出版社:** {{publisher}}  
**発行日:** {{publishDate}}  

---

## 概要
{{description}}

---

## 備考・メモ
- 

必要な情報があればテンプレートに追加します。(translator:で翻訳者やreadDate:で読書完了日、未読/既読フラグなど)isbnは古い本だとisbn10なので二項目作ってもいいかも

また、Book-Searchプラグインでwebから取得できる情報はこちらを参照

設定→Book SearchからTemplete fileの項目に上記のテンプレートファイルを設定します。ついでにNew File locationに書籍ファイルのデフォルトの作成場所を指定しておきます。

また可能であればGoogle CloudにてAPIキーを作成し、APIライブラリからBooks APIをONにしてAPIキー欄に打ち込みます。これでGoogle Booksからデータを取得出来るようになります。

書籍の登録

Ctrl+Pを押してBook Search: Create new book noteを選択し、出てきたウィンドウにタイトルなどを打ち込むと指定したフォルダに書影のファイルが作成されます。

取得出来ない情報は手打ちで入力していきます。

読み終えた感想なども手打ちで最後の備考メモに書いていきます。

このメニュー(Book Search: Create new book note)はobsidianの設定→ホットキーからCtrl+Bなどに設定しておくと便利です。

Dateviewで一覧画面を作成

書籍一覧を表示したいページを作成し、下記を入力します。

FROMには書影ファイルの入っているフォルダを指定してください。

本棚
```dataview
TABLE WITHOUT ID
"[[" + file.path + "|" + title + "]]" AS "タイトル",
author AS "著者",
dateformat(publishedDate, "yyyy/MM/dd") AS "発行日",
series
FROM "読書/books"
SORT author ASC, publishedDate ASC
```

上記では発行日→著者名の順でソートします。SORT部分をpublishedDate ASCのみにすれば発行日順となります。

WHERE genre = "ミステリ"をFROMの行の下に入れるとジャンルだけで絞り込めます。

作成したファイルはForce note view modeプラグインで閲覧モード固定にしておくと便利です。

また、Dataviewの設定でJavascriptを有効化するとさらに色々できます。

下記では作者の一番古い本を参照し、作者ごとに年代順に並べることができます。

本棚-作者順
```dataviewjs
const P = dv.pages('"読書/books"')
  .where(p => p.author && p.publishedDate)
  .array();

// 著者ごとの最古発行日を計算(ソート用)
const oldestByAuthor = {};
for (const p of P) {
  const author = p.author;
  const dateMs = dv.date(p.publishedDate).toMillis();
  if (!(author in oldestByAuthor) || dateMs < oldestByAuthor[author]) {
    oldestByAuthor[author] = dateMs;
  }
}

const rows = P
  .map(r => ({
    author: r.author,
    title: r.title ?? r.file.name,
    path: r.file.path,
    published: dv.date(r.publishedDate).toMillis(),
    series: r.series ?? "",
    sortKey: oldestByAuthor[r.author]
  }))
  .sort((a, b) =>
    a.sortKey - b.sortKey || a.published - b.published
  );

dv.table(
  ["タイトル", "著者", "発行日", "シリーズ"],
  rows.map(r => [
    `[[${r.path}|${r.title}]]`,
    r.author,
    dv.luxon.DateTime.fromMillis(r.published).toFormat("yyyy/MM/dd"),
    r.series
  ])
);

最初のコードでは著者名順でしたが、こちらでは作者の年代ごとに並べることができます。

あとは書籍を追加する度に自動で更新してくれるので楽ちん!だいぶ便利です

家のサーバーにgitのリモートリポジトリを置いて小説をgit管理しています。

リポジトリは自宅鯖(123.456.789.01)の/home/owner/novel/mynovel.gitにあると想定して記述します。

(もちろんGitHubで非公開のリポジトリを作成してOK、ただしGithubは性的な内容は置けないので注意)

iPhoneで編集したりするので設定メモです。

AndroidでもTermuxなどのLinuxエミュを使えば同様に行えます。(Turmuxの場合aptコマンド、ストレージはtermux-setup-storageコマンドで連携)

続きを読む

画像をMiniOサーバにアップロードするアプリを作成してビルド後自鯖にデプロイしてみたところ、Server Actionsの1MB制限に引っかかってしまいアップロードできない事態に見舞われました。開発環境だと問題なく上げられたのでちょっと困りました。

Next.js13以下ではnext.config.tsに下記を追加することで回避できたようですが、Next.js15 App Routerでは使用できません。

next.config.ts
// Next.js13以前の設定
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // これを追記
  experimental: {
    serverActions: {
      bodySizeLimit: '500mb',
    },
  },
};

export default nextConfig;

Next.js15ではスタンドアロンモード(最小限の構成でビルド)でデプロイすることでExpressのserver.js側でアップロード上限を設定できるようなのでやってみました。next.config.tsに下記を追記します。

next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // スタンドアロンモードで出力
  output: "standalone",
};

export default nextConfig;

設定系は/myapp/.env.productionに書いています。

ディレクトリ構成が下記になっているので適宜変更してください。

/
├─ myapp/ (Next.jsアプリ)
│  ├─ app/
│  │  └─ page.tsx
│  ├─ .env.production
│  ├─ next.config.ts
│  ├─ package.json
│  ├─ package.lock.json
│  ├─ server.js
│  └─ tsconfig.json
└─ docker-compose.yml

自前でserver.jsを用意します

server.js
/* eslint-disable @typescript-eslint/no-require-imports */

// server.js
const express = require("express");
const next = require("next");
const path = require("path");

const port = parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";

const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  // 最大アップロードサイズを設定
  server.use(express.json({ limit: "500mb" }));
  server.use(express.urlencoded({ extended: true, limit: "500mb" }));
  
  server.use(
    "/_next/static",
    express.static(path.join(__dirname, ".next/static"))
  );

  server.all("*", (req, res) => {
    return handle(req, res);
  });

  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});
Dockerfile
# Dockerfile

FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=base /app/node_modules ./node_modules
COPY . .
RUN npm run build

# 本番用
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /server.js ./server.js

# Next standalone実行に必要
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]
docker-compose.yml
services:
  app:
    build:
      context: ./myapp
      dockerfile: Dockerfile
    container_name: myapp-app
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    env_file:
      - ./myapp/.env.production
    depends_on:
      - db

volumes:
  postgres_data:

あとはNginx側でもclient_max_body_sizeを設定してあげます。

Nginx
server {
    listen 32222 ssl;
    ssl_certificate /home/owner/system/pem/server_cert.pem;
    ssl_certificate_key /home/owner/system/pem/server_key.pem;
    client_max_body_size 500m; # ここで上限設定
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Next.jsのApp routerの同じファイル名だらけでタブの視認性が悪い問題解決!

globパターンが分かってなくてちょっと戸惑いました

VScodeのsetteing.jsonに下記

setting.json
  // タブ名カスタム
  "workbench.editor.customLabels.enabled": true,
  "workbench.editor.customLabels.patterns": {
    "**/admin/*/*/page.tsx": "${dirname(1)}${dirname(0)} - Page",
    "**/admin/*/page.tsx": "${dirname} - Page",
    "**/api/*/*/route.ts": "${dirname(1)}${dirname(0)} - API",
    "**/api/*/route.ts": "${dirname} - API",
    "**/layout.tsx": "${dirname} - Layout",
    "**/page.tsx": "${dirname} - Page",
    "**/route.ts": "${dirname} - API"
  }

admin/*/*/page.tsxなどにすることで階層を指定できるようです。

admin/upload.tsxは「upload – Page」、admin/edit/[id]/page.tsxは「edit[id] – Page」と[slug]が分かるようにしたかったので

SNSに上げていた進捗をまとめてみました。NSFW(背後注意)な絵もあるのでご注意

お空のデート

続きを読む

メギド72、ついに完結しオフライン版となりました。

他のゲームとかだとなかなか触れない部分を埋めてくれたり、家族愛や同性間の恋愛など、様々な愛について人外視点で解体して再構築するシナリオと、奥深いゲーム性、ミルトンの失楽園や黙示録はじめループ重力量子論などの量子力学・物理学にも通じたライターのSF作家としての力量、本当に大好きです…

弊アジトには実装された272体が全員揃い、衣装もすべて購入済み、全員Lv70(ブフ様&サタン様はLv80)&専用霊宝も全作成済み、全金冠にミッションもソロモンLv以外はスタイル縛りミッションも全て埋めたり、本当にやりこんだゲームでした。

自作したメギド所持率チェッカーも自鯖がダメになるまではずっと置き続ける予定です。是非攻略の際の手持ち共有などにもご利用ください。

メギド72を永遠にしたい

そういうわけでオフライン版となったメギド72ですが、オフライン化後は通信が一切行われず、運営のサーバーも閉じ、端末が壊れるまではずっと遊べる仕様となりました。

いやだーーーーーー!!!!!壊れないで!!!!!!!!!

そういうわけで私は目論見ました。

Androidであればエミュレータもあるし、バックアップがうまくいけばメギドを永遠にできるのでは?

私はずっとiPhoneでメギドをやっていたのですが、このためだけにGoogle Pixel8aを買い、データ移行をしておきました。

ちなみにpixelにしたのはroot化がXperiaよりしやすそうとかなだったのですが結局仕事とかがバタついていてその辺やらずにオフライン化突入

iOSはシステムやアプリデータなどユーザーが触れないように割とガチガチなので、root化なしにアプリデータのバックアップできる可能性が割と低かったため

結論から言うと、いけました。

PCへバックアップを取ってそのデータでプレイできる感じになります。

現状rootを取らなくてもできるので良かったです😉

続きを読む

Nox playerを入れたかったのですが何やってもハイパーバイザー&仮想化ベースのセキュリティが無効化できず困った備忘録です。

Intel 14700KF+MSI Z790 TOMAHAWK WIFIでWindows11Proを使用しています。

VT-xの有効化の確認にはLeoMoon CPU-Vなどが簡単です。

こんな風になっちゃう…

またMicrosoftのcoreinfoでもチェックできます。コマンドプロンプトからソフトのあるフォルダに移動してcoreinfo64 -vで実行します。

続きを読む

最近のお絵描き関連のツールのメモをまとめてみました。

画像にはNSFWありますので注意

↓クリスタ自作素材です。あんまりないです

続きを読む

m.estampie.work, blog.estampie.work… と個別に証明書を取っていたのですが面倒になってきたのでワイルドカード証明書を取ることにしました。またHTTP-01チャレンジで行っていたのでDNS-01チャレンジで取得してみます。

DNS-01チャレンジで証明書取得

サンプルはexample.comです。

既にcerbotはインストール済みのため、追加で必要なパッケージを取得します。

sudo apt-get update
sudo apt-get install python3-certbot-dns-cloudflare

CloudflareのダッシュボードでAPIトークンを取得しておきます。テンプレートから「ゾーン DNS を編集する」を選び、対象ドメインを指定します。

取得したら/etc/letsencrypt/cloudflare.iniファイルを作成してAPIトークンを記述します。

sudo vi /etc/letsencrypt/cloudflare.ini
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
/etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = APIトークン

元々example.comで証明書を取得していたのでワイルドカード証明書もcert-nameを同名で取得します。

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.example.com" \
  -d "example.com" \
  --cert-name example.com

完了したら下記コマンドで証明書を確認します。

sudo certbot certificates

ワイルドカード証明書でカバーできる不要な証明書は下記で削除します。

sudo certbot delete --cert-name blog.example.com

Nginxに記述してある証明書ファイルの場所ssl_certificate, ssl_certificate_keyを統一したcert-name example.comのものに置き換えます。

default
server {
    listen 443 ssl;
    server_name blog.example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_verify_client off;
    ...
}

またHTTP-01チャレンジの際に必要だった/.well-known/へのアクセスの切り分けも不要になります。

書き換えたらNginxを再起動します。

sudo nginx -t
sudo systemctl reload nginx

以上で完了です。

IMAPメールの受信ボックスの未読メールから特定の件名&送信元のメールを取得→LINE通知→既読にするPythonスクリプトです。

前回書いたものはGASを使ってGmailのみ対応でしたが、こちらはIMAPメールなら何でも大丈夫です。

サンプルではBoothの購入とskebリクエストが入ったら通知します。

自鯖があればLinuxならCronで定期実行してあげます。

24時間動いているPCがないといけないので、AWSのLambdaを使って無料枠で実行したりも良いかもです。(無料枠だと実行間隔に注意かも)

続きを読む