aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/update-library-dates.mjs
blob: 2317b17841cb5a9ef55c3ab504d108e125e93a31 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env node
// 小平市立図書館のイベントカレンダーから開催日を取得し、
// 行事予定ページの図書館イベント行(日付セル)を更新する。
//
// 使い方:
//   node scripts/update-library-dates.mjs            … 変更内容を表示(書き換えない)
//   node scripts/update-library-dates.mjs --apply    … 実際に書き換える
//   node scripts/update-library-dates.mjs --months 4 … 当月から4か月分を取得(既定3)
//
// 行の追加・削除は自動では行わない(内容を確認して手で編集する)。
// 表にない新しいイベント、カレンダーから消えたイベントは一覧で表示する。

import fs from 'node:fs';
import path from 'node:path';

const BASE = 'https://library.kodaira.ed.jp/viewer/';
const UA =
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';

const args = process.argv.slice(2);
const apply = args.includes('--apply');
const monthsArg = args.indexOf('--months');
const months = monthsArg >= 0 ? Number(args[monthsArg + 1]) || 3 : 3;
const targetIdx = args.indexOf('--target');
const targetPath =
  targetIdx >= 0
    ? args[targetIdx + 1]
    : path.join('src', 'content', 'docs', 'kodaira-data', 'hp-archive', 'schedule.mdx');

async function fetchText(url) {
  const res = await fetch(url, { headers: { 'User-Agent': UA } });
  if (!res.ok) throw new Error(`${res.status} ${url}`);
  return res.text();
}

function normalize(html) {
  return html.replace(/\s+/g, ' ');
}

// カレンダーから { id: { title, lib, dates:Set } } を作る
async function collectEvents() {
  const now = new Date();
  const events = new Map();

  for (let i = 0; i < months; i += 1) {
    const d = new Date(now.getFullYear(), now.getMonth() + i, 1);
    const url = `${BASE}calendar-monthly.html?date=${d.getFullYear()}/${d.getMonth() + 1}/1`;
    const html = normalize(await fetchText(url));
    const rows = html.match(/<tr class="calendar-daily cal_event">.*?<\/tr>/g) ?? [];

    for (const row of rows) {
      const dm = row.match(/calendar-daily\.html\?date=(\d{4})\/(\d{2})\/(\d{2})/);
      if (!dm) continue;
      const key = `${dm[1]}-${dm[2]}-${dm[3]}`;
      const items =
        row.match(
          /<a target="_self" href="([^"]*)" class="cal_event_index_\d+">([^<]+)<\/a> <span class="icon[^"]*">([^<]*)<\/span>/g
        ) ?? [];

      for (const item of items) {
        const m = item.match(
          /href="([^"]*)" class="cal_event_index_\d+">([^<]+)<\/a> <span class="icon[^"]*">([^<]*)<\/span>/
        );
        if (!m) continue;
        const id = (m[1].match(/id=(\d+)/) ?? [])[1];
        if (!id) continue;
        if (!events.has(id)) {
          events.set(id, { id, title: m[2].trim(), lib: m[3].trim(), dates: new Set() });
        }
        events.get(id).dates.add(key);
      }
    }
  }
  return events;
}

// "2026-11-01" の集合を「11月1日・18日、12月6日・16日」の形にする
// 日付が1つだけのときは「11月19日(木)」のように曜日を付ける
function formatDates(dateKeys, withWeekday) {
  const sorted = [...dateKeys].sort();
  const [y0, m0, d0] = sorted[0].split('-').map(Number);
  if (withWeekday && sorted.length === 1) {
    const w = '日月火水木金土'[new Date(y0, m0 - 1, d0).getDay()];
    return `${m0}月${d0}日(${w})`;
  }
  const byMonth = new Map();
  for (const key of sorted) {
    const [y, m, d] = key.split('-').map(Number);
    const k = `${y}-${m}`;
    if (!byMonth.has(k)) byMonth.set(k, []);
    byMonth.get(k).push(d);
  }
  const parts = [];
  for (const [k, days] of byMonth) {
    const m = Number(k.split('-')[1]);
    parts.push(`${m}月${days[0]}日${days.slice(1).map((d) => `・${d}日`).join('')}`);
  }
  return parts.join('、');
}

// 図書館のページ(info.html?id=…)を出典に含む行を対象にする。
// 市報など他の出典と併記している行(例:「市報こだいら10月20日号3面/[図書館のページ](…)」)も拾う。
const ROW_RE =
  /^\| ([^|]*) \| ([^|]*) \| ([^|]*) \| ([^|]*) \| ([^|]*library\.kodaira\.ed\.jp\/viewer\/info\.html\?id=(\d+)[^|]*) \|$/;

async function main() {
  const events = await collectEvents();
  const src = fs.readFileSync(targetPath, 'utf8');
  const lines = src.split('\n');

  const inTable = new Map();
  const changes = [];

  lines.forEach((line, index) => {
    // Windows の CRLF でも一致するよう、照合時だけ行末の \r を除く
    const m = line.replace(/\r$/, '').match(ROW_RE);
    if (!m) return;
    const [, datesCell, name, , , , id] = m;
    inTable.set(id, { index, name, datesCell });

    const ev = events.get(id);
    if (!ev || ev.dates.size === 0) return;
    const next = formatDates(ev.dates, ev.dates.size === 1);
    if (next !== datesCell) changes.push({ index, name, id, from: datesCell, to: next });
  });

  console.log(`図書館の予定: カレンダー ${events.size}件 / 表 ${inTable.size}件(${months}か月分を取得)`);

  if (changes.length === 0) {
    console.log('日付の変更はありません。');
  } else {
    for (const c of changes) {
      console.log(`- ${c.name}`);
      console.log(`    現在: ${c.from}`);
      console.log(`    新規: ${c.to}`);
      if (apply) lines[c.index] = lines[c.index].replace(`| ${c.from} |`, `| ${c.to} |`);
    }
  }

  const missing = [...events.values()].filter((e) => !inTable.has(e.id));
  if (missing.length > 0) {
    console.log('\n表にないイベント(内容を確認して追加してください):');
    for (const e of missing) {
      console.log(`- ${e.title}(${e.lib}) ${formatDates(e.dates)}  ${BASE}info.html?id=${e.id}`);
    }
  }

  const gone = [...inTable.entries()].filter(([id]) => !events.has(id));
  if (gone.length > 0) {
    console.log('\nカレンダーにない行(終了した可能性。必要なら削除してください):');
    for (const [, row] of gone) console.log(`- ${row.name.replace(/^📚 /, '')}(${row.datesCell})`);
  }

  if (apply && changes.length > 0) {
    fs.writeFileSync(targetPath, lines.join('\n'), 'utf8');
    console.log(`\n${changes.length}件を ${targetPath} に反映しました。`);
  } else if (!apply && changes.length > 0) {
    console.log('\n(--apply を付けると反映します)');
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});