Compare commits
3 Commits
86246a613e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b27a6740a | ||
|
|
65f1a984cf | ||
|
|
88f774ae76 |
@@ -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
2
run.sh
@@ -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
30
src/event.ts
Normal 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());
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
|
||||
@@ -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
57
src/util_string.ts
Normal 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
12
src/util_string_test.ts
Normal 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
41
src/write_descriptions.ts
Normal 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("
")}">🔴</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
19
src/write_gallery.ts
Normal 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
121
write.ts
@@ -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("
")}">🔴</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())
|
||||
}
|
||||
Reference in New Issue
Block a user