Compare commits

...

3 Commits

Author SHA1 Message Date
Lukas Wölfer
5b27a6740a Add gallery creation 2025-06-11 23:47:42 +02:00
Lukas Wölfer
65f1a984cf Moved files 2025-06-11 23:09:19 +02:00
Lukas Wölfer
88f774ae76 Fixed title casing 2025-06-05 23:25:27 +02:00
11 changed files with 194 additions and 131 deletions

View File

@@ -1,6 +1,6 @@
{
"tasks": {
"dev": "deno run --watch main.ts"
"dev": "deno run --watch src/main.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@1",

2
run.sh
View File

@@ -1 +1 @@
deno run --env-file=.env --allow-net=dancing.thasky.one:443 -E main.ts
deno run --env-file=.env --allow-net=dancing.thasky.one:443 -E src/main.ts

30
src/event.ts Normal file
View File

@@ -0,0 +1,30 @@
import { VideoDescription } from "./main.ts";
export function eventTitle(token: VideoDescription) {
const event_name = token.event === token.location ? token.event : token.event + " " + token.location;
// FIXME: This will break with videos in 75 years
const event_contains_year = event_name.match(/\d{2}|20\d{2}/) !== null;
const event_date = event_contains_year ? "" : new Date(token.date).getFullYear().toString();
const event_title = `${event_name} ${event_date}`.trim();
return { event_name, event_date, event_title };
}
/**
*
* @param videos
* @returns Bucket of videos for of each event, grouped by `name`, `location` and `year`
*/
export function bucketEvents(videos: VideoDescription[]): VideoDescription[][] {
const buckets = Object.groupBy(videos, (video) => {
return `${video.event}${video.location}${new Date(video.date).getFullYear()}`;
});
const sortedBuckets = Object.values(buckets)
.filter(v => v !== undefined)
.map(b => b.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()));
return sortedBuckets
.sort((a, b) => new Date(a[0].date).getTime() - new Date(b[0].date).getTime());
}

View File

@@ -1,7 +1,9 @@
import { Mwn, RecentChange } from 'npm:mwn'
import process from "node:process";
import { parseSummary } from "./parse.ts";
import { bucketEvents, writeSections } from "./write.ts";
import { writeSections } from "./write_descriptions.ts";
import { bucketEvents } from "./event.ts";
import { writeGallery } from "./write_gallery.ts";
async function getWorkshopVideos(bot: Mwn): Promise<string[]> {
const response = await bot.request({
@@ -104,6 +106,30 @@ async function queryChanges(bot: Mwn, last_change: string | undefined): Promise<
return { last_change: changes[0].timestamp, new_file_changes };
}
async function changeDescriptionPage(bucketedEvents: VideoDescription[][], paths: string[], parseErrors: string[], bot: Mwn, title: string) {
const t = writeSections(bucketedEvents);
const trigger_summary = 'Triggered by changes to ' + paths.map(v => `[[${v}]]`).join(", ");
const error_summary = parseErrors.join("\n");
const summary = [trigger_summary, error_summary].filter(v => v !== undefined && v.length > 0).join("\n");
const response = await bot.save(title, "{{TOC|limit=3}}\n\n" + t, summary);
console.log(response);
}
async function changeGalleryPage(bucketedEvents: VideoDescription[][], paths: string[], parseErrors: string[], bot: Mwn, title: string) {
const t = writeGallery(bucketedEvents);
const trigger_summary = 'Triggered by changes to ' + paths.map(v => `[[${v}]]`).join(", ");
const error_summary = parseErrors.join("\n");
const summary = [trigger_summary, error_summary].filter(v => v !== undefined && v.length > 0).join("\n");
const response = await bot.save(title, "\n" + t, summary);
console.log(response);
}
async function main() {
const bot = new Mwn({
apiUrl: process.env.URL || 'https://dancing.thasky.one/api.php',
@@ -112,19 +138,17 @@ async function main() {
userAgent: 'mwn bot',
});
const title = 'Video Descriptions (Automated)'
const description_title = 'Video Descriptions (Automated)'
const gallery_title = 'West Coast Swing/Video Gallery (Automated)'
try {
await bot.login();
await watchdog(bot, async (paths) => {
const relevantFiles = await getWorkshopVideos(bot)
const { pages: d, errors: parseErrors } = await fetchPages(relevantFiles, bot)
const t = writeSections(bucketEvents(d))
const trigger_summary = 'Triggered by changes to ' + paths.map(v => `[[${v}]]`).join(", ")
const error_summary = parseErrors.join("\n")
const summary = [trigger_summary, error_summary].filter(v => v !== undefined && v.length > 0).join("\n")
const response = await bot.save(title, "{{TOC|limit=3}}\n\n" + t, summary);
console.log(response)
const bucketedEvents = bucketEvents(d)
await changeDescriptionPage(bucketedEvents, paths, parseErrors, bot, description_title);
await changeGalleryPage(bucketedEvents, paths, parseErrors, bot, gallery_title);
})

View File

@@ -79,5 +79,6 @@ export function parseSummary(description: string, name: string): VideoDescriptio
b.path = name
const titleMatch = name.match(/File:WCS \w+ (?<title>.+)\.[^.]+/)
b.title = titleMatch?.groups ? titleMatch.groups['title'] : name
b.title = b.title.trim()
return (b as VideoDescription)
}

57
src/util_string.ts Normal file
View File

@@ -0,0 +1,57 @@
export const SMALL_WORDS = new Set([
"a",
"an",
"and",
"as",
"at",
"because",
"but",
"by",
"en",
"for",
"if",
"in",
"neither",
"nor",
"of",
"on",
"only",
"or",
"over",
"per",
"so",
"some",
"than",
"that",
"the",
"to",
"up",
"upon",
"v",
"versus",
"via",
"vs",
"when",
"with",
"without",
"yet",
]);
export function capitalize(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1);
}
export function camelToTitleCase(camelCaseStr: string): string {
const titleCaseStr = camelCaseStr.replace(/([A-Z])/g, ' $1');
const words = titleCaseStr.split(' ')
.map(v => v.toLowerCase())
.map(word => {
if (SMALL_WORDS.has(word)) {
return word;
}
return capitalize(word)
});
return capitalize(words.join(' '));
}

12
src/util_string_test.ts Normal file
View File

@@ -0,0 +1,12 @@
import { assertEquals } from "@std/assert/equals";
import { camelToTitleCase } from "./util_string.ts";
Deno.test("camelToTitleCase transforms camelCase to Title Case", () => {
assertEquals(camelToTitleCase("thisIsATest"), "This Is a Test");
assertEquals(camelToTitleCase("denoIsAwesome"), "Deno Is Awesome");
assertEquals(camelToTitleCase("helloWorld"), "Hello World");
});
Deno.test("camelToTitleCase handles empty string", () => {
assertEquals(camelToTitleCase(""), "");
});

41
src/write_descriptions.ts Normal file
View File

@@ -0,0 +1,41 @@
import { eventTitle } from "./event.ts";
import { VideoDescription } from "./main.ts";
import { camelToTitleCase } from "./util_string.ts";
export function singleVideoDescription(video: VideoDescription): string {
const teachersList = video.teachers.map(v => "[[" + v + "]]").join(" & ");
const nagElement = video.nags.length > 0 ? `<span title="${video.nags.join("&#010;")}">🔴</span>` : "";
return `=== ${camelToTitleCase(video.title)} ===
Date: {{#time: Y-m-d (D) | ${video.date}}} ${nagElement}<br>
Teachers: ${teachersList}<br>
Level: ${video.level}
[[${video.path}|left|400px|thumb|${video.title}]]
<div style="float:left">
==== Shown Patterns ====
${video.patterns}
==== Notes ====
${video.notes}
</div>
<br clear=all>`;
}
export function writeSections(events: VideoDescription[][]): string {
return events.map(v => {
const token = v[0]
const { event_title } = eventTitle(token);
let r = `== ${event_title} ==\n`
r += `${v.length} Video${v.length <= 1 ? "" : "s"}\n`
r += v.map(video => singleVideoDescription(video)).join("\n\n")
return r
}).join("\n\n\n")
}

19
src/write_gallery.ts Normal file
View File

@@ -0,0 +1,19 @@
import { eventTitle } from "./event.ts";
import { VideoDescription } from "./main.ts";
export function writeGallery(events: VideoDescription[][]): string {
return events.map(v => {
const token = v[0]
const { event_title } = eventTitle(token);
let r = `=== ${event_title} ===\n`
r += `<gallery>\n`
r += v.map(video => `${video.path}|[[:${video.path}|${video.title}]]`).join("\n")
r += `</gallery>`
return r
}).join("\n\n")
}

121
write.ts
View File

@@ -1,121 +0,0 @@
import { VideoDescription } from "./main.ts";
export const SMALL_WORDS = new Set([
"a",
"an",
"and",
"as",
"at",
"because",
"but",
"by",
"en",
"for",
"if",
"in",
"neither",
"nor",
"of",
"on",
"only",
"or",
"over",
"per",
"so",
"some",
"than",
"that",
"the",
"to",
"up",
"upon",
"v",
"versus",
"via",
"vs",
"when",
"with",
"without",
"yet",
]);
function capitalize(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
function camelToTitleCase(camelCaseStr: string): string {
// Insert a space before each uppercase letter
const titleCaseStr = camelCaseStr.replace(/([A-Z])/g, ' $1');
// Split the string into words and process each word using map
const words = titleCaseStr.split(' ')
.map(v => v.toLowerCase())
.map(word => {
if (SMALL_WORDS.has(word)) {
return word;
}
return capitalize(word)
});
return capitalize(words.join(' '));
}
export function singleVideoDescription(video: VideoDescription): string {
const teachersList = video.teachers.map(v => "[[" + v + "]]").join(" & ");
const nagElement = video.nags.length > 0 ? `<span title="${video.nags.join("&#010;")}">🔴</span>` : "";
return `=== ${camelToTitleCase(video.title)} ===
Date: {{#time: Y-m-d (D) | ${video.date}}} ${nagElement}<br>
Teachers: ${teachersList}<br>
Level: ${video.level}
[[${video.path}|left|400px|thumb|${video.title}]]
<div style="float:left">
==== Shown Patterns ====
${video.patterns}
==== Notes ====
${video.notes}
</div>
<br clear=all>`;
}
export function writeSections(events: VideoDescription[][]): string {
return events.map(v => {
const token = v[0]
const event_name = token.event === token.location ? token.event : token.event + " " + token.location
// FIXME: This will break with videos in 75 years
const event_contains_year = event_name.match(/\d{2}|20\d{2}/) !== null
const event_date = event_contains_year ? "" : new Date(token.date).getFullYear().toString()
let r = `== ${event_name} ${event_date} ==\n`
r += `${v.length} Video${v.length <= 1 ? "" : "s"}\n`
r += v.map(video => singleVideoDescription(video)).join("\n\n")
return r
}).join("\n\n\n")
}
/**
*
* @param videos
* @returns Bucket of videos for of each event, grouped by `name`, `location` and `year`
*/
export function bucketEvents(videos: VideoDescription[]): VideoDescription[][] {
const buckets = Object.groupBy(videos, (video) => {
return `${video.event}${video.location}${new Date(video.date).getFullYear()}`;
})
const sortedBuckets = Object.values(buckets)
.filter(v => v !== undefined)
.map(b =>
b.sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime()))
return sortedBuckets
.sort((a, b) => new Date(a[0].date).getTime() - new Date(b[0].date).getTime())
}