From ebf36a73a014faa3e824e878edcbed9a8e94d188 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 17 Sep 2026 22:23:52 +0000 Subject: [PATCH] Teach this on backtoschool.help: a class title that fits, walked up from the lecture to its collection A folder channel names its entry by the whole served path, and a course is deep (collection / part / week / lecture), so the one-click class from a live room sent a title past the school's 160 and got "title is too long". classTitle() in web/src/live-context.ts keeps the lecture (cleaned by liveTitle) and the outermost folder, fills the folders between back in from the lecture upward while they fit, and as a last resort cuts the collection at a word so what is playing is never the part that is lost. A title with no path in it is cut at a word. Titles that already fit are left alone. The school's own form has maxlength=160, so this was the only sender of an oversize title. Co-Authored-By: Claude Fable 5.1 --- web/src/app.ts | 4 ++-- web/src/live-context.ts | Bin 2432 -> 4667 bytes web/test/live-context.test.ts | 28 +++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/web/src/app.ts b/web/src/app.ts index 5ebe7fc..a5a61f6 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -12,7 +12,7 @@ import { CONTROL_ICONS, drawControlIcon } from "./control-icons.ts"; import { displayName, formatTime } from "./format.ts"; import { beginListNavigation, replaceList } from "./accessibility.ts"; import { updateLiveList } from "./live-list.ts"; -import { liveContext, liveTitle } from "./live-context.ts"; +import { classTitle, liveContext, liveTitle } from "./live-context.ts"; import { BrowserPlayer, revoke, tracksFromFiles, type LocalTrack, @@ -4827,7 +4827,7 @@ export function start(): void { try { const made = await fetch("/api/v1/events", { method: "POST", headers, - body: JSON.stringify({ kind: "class", title: currentShareTitle() || "Live class", broadcastUrl: link, visibility: "public" }), + body: JSON.stringify({ kind: "class", title: classTitle(currentShareTitle()) || "Live class", broadcastUrl: link, visibility: "public" }), }); const body = (await made.json().catch(() => ({}))) as { event?: { id: string; slug: string; version: number }; error?: string }; if (!made.ok || !body.event) throw new Error(body.error ?? "the class could not be made"); diff --git a/web/src/live-context.ts b/web/src/live-context.ts index 6333d1df0f708c3f61fc8406b1e84b3723da507d..e6395135ed09996d6bd82071aaf04ec40d6b209f 100644 GIT binary patch literal 4667 zcma)9U2fyZ5zezOks4vJAZ?klCyPAD@fgnRJPa_|9b^qG5Lq*Bku8Za$!6G1YaC0+ z9wKMR6$0#IjrRrS?ZUlsGRHjN|qSZjKsKkL#sebcD)o_4fs zOi808nc$yo(n;MIXHrun&W$Dm zNuz7n(h(e+eNN^luP0*~jbgrSGNJFxrYele09c_@7y}ZiDN9ookF-_~nij^bNl~pb z-o$uP)s_q00PT#FzXkwWz~mPax)vzOwkeKUdUJMmPPxS$$go-BHVJ4_{s;;-LD2JG8;0=@-T_^`ykUtOJe6JsXifx-E@MbQm zg*8R%G$L%U1A<+&XWvQ-T(YbG$b_b!PLPH*COyIDXG_;vlt7H_;!B;aU=curWE5SX1gqe#dnA z)@Sna@{+4^^xRm;p+6XIeRlMf97U2!RX@A-f8`R+AbX)G^9pVX#2*3wJA=osPI7fn zn?i6X!w<~lMyrCXX`2*E;KLs?@Typ}C8WZsOgVKym85Xhg3e5K)}26Bkp(G0HrKJG zGxEt;XbLKTRVGKlu0eNiXX4FH#FVGJ1EDr+z>LIx<;u$!jcOi)i8|ahw6Sc9;)eC7 zf7t`FXEm!-z$0?3op#ERXHv`qNuABdlvkPlIt>*IOjJ=USZ$F~hINEGcn+@@AGsLY zyn&hABODX?lee*Cqf;x%3J$P$5<|2%hT06)7|4C~KXv*5iyrOKXiPmW{4_ws8`$v` zJNyrBGFqLR35bspqAR+4{Uk1iH#_?E=YP`cr`|WeDp$t4JDLvM9xkylZ^&o4FRM=? zDt@nDV=)ej>yNA&)7J3KFAop+6~90EtI4Zq1P_0Kob?8!u5fymBuNww&`f^J;qPo7 zj|oe`uR-|-G;#E0X@U?^*g0`kpRsE4CgMVs-Lsrd>3kj^dd2$!o?J=2lQv%zXpjtx ztXp5{(6ccqC#Iq&mN$N+T^uK!P{>=nfy@XeB5X>AxQK_I--qwN`KCW`fc)^p;X-%B z1AX{u|qktNF~rG>JVqB}0rD6i_)C2}cxu>3`(p8x(V zzKe%rm9K(#AFqG>@ypGJn~(3meEjgI4>#Zn_WFVDh>kHu_}jggZVkAWxwD9qb$P?j zJX8R2FpYvs!A8cbDt%g;LCL7nn&)Xz4%I3&OmRs1`BzZK}olb)qHKHbv?y% z!DA7W9XO*NkojZUXnjBK#-%Z7g(-(^WyqxH(ip8q>`1R~+KNXXhLqvW%iLkoIp&*w zQpG(%3M3$2!bJ<^y+%KbGK`yFDd*fZV$X9@%*ZGj_W)Fp)AT3c^alfgZn39<0V>Dk9#VVDMw z__B_S5uaxCVxE#7&=1%qh$P1aV9JcfD0Lm^$L$nTqT~qZ@@0q%n53BGk$*)S0~c~k z#prVk>&w|)NOU8^d!sg814RNk6X4tn9_~j#X=9$ODbGXz% zT&>&;!u|2~F(ITS zto6OmtKj|vBJ`|&2n>Nphv=hd-L?A(S%~<)0P8><2bAm@k6q_W7sapo$ivu#ZyG2K zM&B+=XlC6{7R*_Hne;fnnQKuk=ZBq%@5b!@y=P&l6sT6-SWIsoY48ZVmLi4!x)JWy zIi{dJ*D-~IRm@>;eA#mMn#HjkH zqnVtUpwnD+yae79YOfi&VvWdqygrSAS0 ztmMT2-unk#4JCy&GJGXsaz=k7Xd4-P+Dze-t`F3~54bqu6Tt<0s8E7-r)@#SxwQQZ z&U-mD9P&C}K=ArhGzMZ63JQY>6QGOe6rX=GGldr%3`Z?Tkc#s+d+y%7eu7_tzC&*D z-ni?ReEC5S?#~=je)xsi?-_nCg!1U-?w;IF!AaiS1@a;_yvWgbHm;#~fIv>zBF88G zStn}Xog^CtfF>%)1H=N!;7`gGJJPP?$9MzoaL)TG&KmbG^=3I*8E}XP0qZ0 z@=Pd03_iIrUb9b~`_u4*&L^YK4@(TQevl2b4B(Iaoq^>vp*P(v`;cTujLDWS_n-Ce Rt1OClzyAC$Oh&t-{{lDW27&+p delta 7 Ocmdn3(jdH{ffE1=;R37x diff --git a/web/test/live-context.test.ts b/web/test/live-context.test.ts index 8dfd11a..6fe34ab 100644 --- a/web/test/live-context.test.ts +++ b/web/test/live-context.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { liveContext, liveTitle } from "../src/live-context.ts"; +import { classTitle, liveContext, liveTitle } from "../src/live-context.ts"; import { parseSnapshot } from "../src/remote.ts"; import { emptySnapshot, merge, type RemoteTrack } from "../../src/protocol.ts"; @@ -68,3 +68,29 @@ test("real wire snapshots keep the course when a later SSE frame changes only th assert.equal(liveContext(snapshot.tracks, snapshot.index).fullTitle, "Course › Section › Second"); assert.equal(liveContext(snapshot.tracks, snapshot.index).position, "Playlist · 2 of 2"); }); + +test("a class title walks up from the lecture to the collection and fits the school's limit", () => { + const lecture = "Coursera - Deep Learning Specialization/1. Neural Networks and Deep Learning/Week 1 - Introduction to Deep Learning/Neural Networks and Deep Learning Basics/03_what-is-a-neural-network-and-how-does-it-learn.mp4"; + assert.ok(lecture.length > 160); + const fitted = classTitle(lecture); + assert.ok(fitted.length <= 160, fitted); + // The collection and the lecture are kept; the nearest folder (the section) + // fits and stays, the week above it would run over and goes. + assert.equal(fitted, "Coursera - Deep Learning Specialization › Neural Networks and Deep Learning Basics › what-is-a-neural-network-and-how-does-it-learn"); + assert.equal(classTitle(lecture, 200), "Coursera - Deep Learning Specialization › Week 1 - Introduction to Deep Learning › Neural Networks and Deep Learning Basics › what-is-a-neural-network-and-how-does-it-learn"); + assert.equal(classTitle(lecture, 90), "Coursera - Deep Learning Specialization › what-is-a-neural-network-and-how-does-it-learn"); + // With no room for both, the lecture stays whole and the collection is cut at a word. + assert.equal(classTitle(lecture, 70), "Coursera - Deep… › what-is-a-neural-network-and-how-does-it-learn"); + assert.equal(classTitle(lecture, 40), "what-is-a-neural-network-and-how-does-i…"); +}); + +test("a class title that already fits, or has no path, is left alone or cut at a word", () => { + assert.equal(classTitle("Coursera - Deep Learning Specialization/Week 1/03_intro.mp4"), "Coursera - Deep Learning Specialization/Week 1/03_intro.mp4"); + assert.equal(classTitle(" Live class  "), "Live class"); + const long = "word ".repeat(50).trim(); + const cut = classTitle(long, 30); + assert.ok(cut.length <= 30); + assert.equal(cut, "word word word word word word…"); + // The player's own live title (context › title) shortens the same way. + assert.equal(classTitle("Microservices › 01 Getting Started › 02 Building › Maven", 40), "Microservices › 02 Building › Maven"); +});