@tacone/prosey 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/prosey CHANGED
@@ -103177,14 +103177,21 @@ var LOGO_DATA_URI = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(LOGO_
103177
103177
  function escapeHtml(text) {
103178
103178
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
103179
103179
  }
103180
- function watchMetaHtml(duration, wordCount, videoId) {
103180
+ function watchMetaHtml(duration, wordCount, videoId, channelName, channelId, channelDescription) {
103181
+ const sep = '<span style="opacity: 0.4">&nbsp;&nbsp;|&nbsp;&nbsp;</span>';
103181
103182
  const h2 = Math.floor(duration / 3600);
103182
103183
  const m3 = Math.round(duration % 3600 / 60);
103183
103184
  const durStr = h2 > 0 ? `${h2} h ${m3} min` : `${m3} min`;
103184
103185
  const readTime = Math.ceil(wordCount / 200);
103185
- const url = `https://youtube.com/watch?v=${videoId}`;
103186
- const separator = '<span style="opacity: 0.4">&nbsp;&nbsp;|&nbsp;&nbsp;</span>';
103187
- return `<a href="${url}" class="watch-link">${durStr} watch</a> ${separator} ${readTime} min read`;
103186
+ const watchUrl = `https://youtube.com/watch?v=${videoId}`;
103187
+ let parts = "";
103188
+ if (channelName) {
103189
+ const chUrl = channelId ? `https://youtube.com/channel/${channelId}` : `https://youtube.com/@${encodeURIComponent(channelName)}`;
103190
+ const chDesc = channelDescription || channelName;
103191
+ parts += `<a href="${chUrl}" class="watch-link" title="${escapeHtml(chDesc)}">${escapeHtml(channelName)}</a>${sep}`;
103192
+ }
103193
+ parts += `<a href="${watchUrl}" class="watch-link">${durStr} watch</a>${sep}${readTime} min read`;
103194
+ return parts;
103188
103195
  }
103189
103196
  async function generateHtml(markdown, title, watchMeta) {
103190
103197
  const [css, body] = await Promise.all([getPicoCss(), g2.parse(markdown)]);
@@ -103275,7 +103282,7 @@ img[alt="Prosey"]:hover, img[alt="Prosey"]:active, img[alt="Prosey"]:focus { fil
103275
103282
  <body>
103276
103283
  <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem 0">
103277
103284
  <img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top;flex-shrink:0">
103278
- <div style="flex:1;text-align:center;font-size:.85rem;color:var(--pico-muted-color)">${watchMeta ? watchMetaHtml(watchMeta.duration, watchMeta.wordCount, watchMeta.videoId) : ""}</div>
103285
+ <div style="flex:1;text-align:center;font-size:.85rem;color:var(--pico-muted-color)">${watchMeta ? watchMetaHtml(watchMeta.duration, watchMeta.wordCount, watchMeta.videoId, watchMeta.channelName, watchMeta.channelId, watchMeta.channelDescription) : ""}</div>
103279
103286
  <button id="theme-btn" type="button" style="background:none;border:none;cursor:pointer;padding:0;line-height:1;opacity:.5;filter:grayscale(100%);transition:all 0.3s;flex-shrink:0">\uD83D\uDCA1</button>
103280
103287
  </div>
103281
103288
  <main class="content">
@@ -103313,7 +103320,7 @@ function openInBrowser(htmlPath) {
103313
103320
  // package.json
103314
103321
  var package_default = {
103315
103322
  name: "@tacone/prosey",
103316
- version: "0.8.0",
103323
+ version: "0.9.0",
103317
103324
  description: "Download YouTube video transcripts from the CLI",
103318
103325
  module: "src/index.ts",
103319
103326
  type: "module",
@@ -121940,6 +121947,25 @@ var debugApis = {
121940
121947
  };
121941
121948
 
121942
121949
  // src/index.ts
121950
+ var YT_INNERTUBE_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
121951
+ async function fetchChannelDescription(channelId) {
121952
+ try {
121953
+ const resp = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${YT_INNERTUBE_API_KEY}`, {
121954
+ method: "POST",
121955
+ headers: { "Content-Type": "application/json" },
121956
+ body: JSON.stringify({
121957
+ context: {
121958
+ client: { clientName: "WEB", clientVersion: "2.20240101.00.00" }
121959
+ },
121960
+ browseId: channelId
121961
+ })
121962
+ });
121963
+ const data = await resp.json();
121964
+ return data?.metadata?.channelMetadataRenderer?.description;
121965
+ } catch {
121966
+ return;
121967
+ }
121968
+ }
121943
121969
  process.stdout.on("error", (err) => {
121944
121970
  if (err.code === "EPIPE")
121945
121971
  process.exit(0);
@@ -122340,15 +122366,23 @@ try {
122340
122366
  let structuredContent;
122341
122367
  let videoTitle;
122342
122368
  let videoDuration = 0;
122369
+ let channelName;
122370
+ let channelId;
122371
+ let channelDescription;
122343
122372
  if (!segments2) {
122344
122373
  info("Fetching transcript...");
122345
122374
  const opts = lang ? { lang, videoDetails: true } : { videoDetails: true };
122346
122375
  const result = await fetchTranscript(videoId, opts);
122347
122376
  segments2 = result.segments;
122348
122377
  videoDuration = result.videoDetails.lengthSeconds;
122378
+ channelName = result.videoDetails.author;
122379
+ channelId = result.videoDetails.channelId;
122380
+ channelDescription = await fetchChannelDescription(channelId);
122349
122381
  const infoJson = JSON.stringify({
122350
122382
  title: result.videoDetails.title,
122351
- channel: result.videoDetails.author,
122383
+ channel: channelName,
122384
+ channelId,
122385
+ channelDescription,
122352
122386
  description: result.videoDetails.description,
122353
122387
  duration: videoDuration
122354
122388
  });
@@ -122385,9 +122419,14 @@ ${structuredContent}
122385
122419
  const fallbackOpts = lang ? { lang, videoDetails: true } : { videoDetails: true };
122386
122420
  const fallbackResult = await fetchTranscript(videoId, fallbackOpts);
122387
122421
  videoDuration = fallbackResult.videoDetails.lengthSeconds;
122422
+ channelName = fallbackResult.videoDetails.author;
122423
+ channelId = fallbackResult.videoDetails.channelId;
122424
+ channelDescription = await fetchChannelDescription(channelId);
122388
122425
  cachedInfo = JSON.stringify({
122389
122426
  title: fallbackResult.videoDetails.title,
122390
122427
  channel: fallbackResult.videoDetails.author,
122428
+ channelId,
122429
+ channelDescription,
122391
122430
  description: fallbackResult.videoDetails.description,
122392
122431
  duration: videoDuration
122393
122432
  });
@@ -122403,6 +122442,9 @@ ${structuredContent}
122403
122442
  const cachedInfoObj = JSON.parse(cachedInfo);
122404
122443
  videoTitle = cachedInfoObj.title;
122405
122444
  videoDuration = cachedInfoObj.duration ?? 0;
122445
+ channelName = cachedInfoObj.channel;
122446
+ channelId = cachedInfoObj.channelId;
122447
+ channelDescription = cachedInfoObj.channelDescription;
122406
122448
  const truncatedInfo = JSON.stringify({
122407
122449
  title: cachedInfoObj.title,
122408
122450
  channel: cachedInfoObj.channel,
@@ -122439,7 +122481,10 @@ ${structuredContent}
122439
122481
  const htmlContent = await generateHtml(formatted, videoTitle, {
122440
122482
  videoId,
122441
122483
  duration: videoDuration,
122442
- wordCount
122484
+ wordCount,
122485
+ channelName,
122486
+ channelId,
122487
+ channelDescription
122443
122488
  });
122444
122489
  const htmlPath = join5(dir2, "summary.html");
122445
122490
  await writeFile3(htmlPath, htmlContent, "utf8");
@@ -122473,6 +122518,9 @@ ${structuredContent}
122473
122518
  let md = null;
122474
122519
  let videoTitle;
122475
122520
  let videoDuration = 0;
122521
+ let channelName;
122522
+ let channelId;
122523
+ let channelDescription;
122476
122524
  startTimer();
122477
122525
  let cachedInfo = null;
122478
122526
  if (!noCache) {
@@ -122501,9 +122549,14 @@ ${structuredContent}
122501
122549
  const result = await fetchTranscript(videoId, opts);
122502
122550
  segments2 = result.segments;
122503
122551
  videoDuration = result.videoDetails.lengthSeconds;
122552
+ channelName = result.videoDetails.author;
122553
+ channelId = result.videoDetails.channelId;
122554
+ channelDescription = await fetchChannelDescription(channelId);
122504
122555
  const infoJson = JSON.stringify({
122505
122556
  title: result.videoDetails.title,
122506
- channel: result.videoDetails.author,
122557
+ channel: channelName,
122558
+ channelId,
122559
+ channelDescription,
122507
122560
  description: result.videoDetails.description,
122508
122561
  duration: videoDuration
122509
122562
  });
@@ -122555,9 +122608,14 @@ ${structuredContent}
122555
122608
  const fallbackOpts = lang ? { lang, videoDetails: true } : { videoDetails: true };
122556
122609
  const fallbackResult = await fetchTranscript(videoId, fallbackOpts);
122557
122610
  videoDuration = fallbackResult.videoDetails.lengthSeconds;
122611
+ channelName = fallbackResult.videoDetails.author;
122612
+ channelId = fallbackResult.videoDetails.channelId;
122613
+ channelDescription = await fetchChannelDescription(channelId);
122558
122614
  cachedInfo = JSON.stringify({
122559
122615
  title: fallbackResult.videoDetails.title,
122560
122616
  channel: fallbackResult.videoDetails.author,
122617
+ channelId,
122618
+ channelDescription,
122561
122619
  description: fallbackResult.videoDetails.description,
122562
122620
  duration: videoDuration
122563
122621
  });
@@ -122573,6 +122631,9 @@ ${structuredContent}
122573
122631
  const cachedInfoObj = JSON.parse(cachedInfo);
122574
122632
  videoTitle = cachedInfoObj.title;
122575
122633
  videoDuration = cachedInfoObj.duration ?? 0;
122634
+ channelName = cachedInfoObj.channel;
122635
+ channelId = cachedInfoObj.channelId;
122636
+ channelDescription = cachedInfoObj.channelDescription;
122576
122637
  const truncatedInfo = JSON.stringify({
122577
122638
  title: cachedInfoObj.title,
122578
122639
  channel: cachedInfoObj.channel,
@@ -122605,7 +122666,10 @@ ${transcriptText}`;
122605
122666
  const htmlContent = await generateHtml(formatted, videoTitle, {
122606
122667
  videoId,
122607
122668
  duration: videoDuration,
122608
- wordCount
122669
+ wordCount,
122670
+ channelName,
122671
+ channelId,
122672
+ channelDescription
122609
122673
  });
122610
122674
  const htmlPath = join5(dir2, "transcript.html");
122611
122675
  await writeFile3(htmlPath, htmlContent, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacone/prosey",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Download YouTube video transcripts from the CLI",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -25,7 +25,7 @@ hints = true
25
25
  # OPENCODE_PERMISSION='{"read":"allow","write":"deny","edit":"deny","bash":"deny","glob":"deny","grep":"deny","webfetch":\"deny\",\"task\":\"deny\",\"todowrite\":\"deny\",\"websearch\":\"deny","lsp":"deny"}' opencode run --pure
26
26
  # copilot -s --deny-all-tools --read-only
27
27
 
28
- command = "OPENCODE_PERMISSION='{\"read\":\"allow\",\"write\":\"deny\",\"edit\":\"deny\",\"bash\":\"deny\",\"glob\":\"deny\",\"grep\":\"deny\",\"webfetch\":\"deny\",\"task\":\"deny\",\"todowrite\":\"deny\",\"websearch\":\"deny\",\"lsp\":\"deny\"}' opencode run --pure --variant low"
28
+ command = "OPENCODE_PERMISSION='{\"read\":\"allow\",\"write\":\"deny\",\"edit\":\"deny\",\"bash\":\"deny\",\"glob\":\"deny\",\"grep\":\"deny\",\"webfetch\":\"deny\",\"task\":\"deny\",\"todowrite\":\"deny\",\"websearch\":\"deny\",\"lsp\":\"deny\"}' opencode run --pure --model opencode/deepseek-v4-flash-free --variant low"
29
29
 
30
30
  [summarize]
31
31
 
@@ -41,7 +41,7 @@ Guidelines:
41
41
  - use the TIMESTAMPS only to understand the structure, don't output time markers
42
42
  - content length: aim to be the same range as the original text or more.
43
43
  - section titles should be h2 headers
44
- - paragraph length soft-limit. 250 characters
44
+ - paragraph length: soft-limit 250 characters, hard-limit 350 characters.
45
45
 
46
46
  Content considerations:
47
47
 
package/src/html.ts CHANGED
@@ -34,20 +34,43 @@ function escapeHtml(text: string): string {
34
34
  .replace(/"/g, "&quot;");
35
35
  }
36
36
 
37
- function watchMetaHtml(duration: number, wordCount: number, videoId: string): string {
37
+ function watchMetaHtml(
38
+ duration: number,
39
+ wordCount: number,
40
+ videoId: string,
41
+ channelName?: string,
42
+ channelId?: string,
43
+ channelDescription?: string,
44
+ ): string {
45
+ const sep = '<span style="opacity: 0.4">&nbsp;&nbsp;|&nbsp;&nbsp;</span>';
38
46
  const h = Math.floor(duration / 3600);
39
47
  const m = Math.round((duration % 3600) / 60);
40
48
  const durStr = h > 0 ? `${h} h ${m} min` : `${m} min`;
41
49
  const readTime = Math.ceil(wordCount / 200);
42
- const url = `https://youtube.com/watch?v=${videoId}`;
43
- const separator = '<span style="opacity: 0.4">&nbsp;&nbsp;|&nbsp;&nbsp;</span>';
44
- return `<a href="${url}" class="watch-link">${durStr} watch</a> ${separator} ${readTime} min read`;
50
+ const watchUrl = `https://youtube.com/watch?v=${videoId}`;
51
+ let parts = "";
52
+ if (channelName) {
53
+ const chUrl = channelId
54
+ ? `https://youtube.com/channel/${channelId}`
55
+ : `https://youtube.com/@${encodeURIComponent(channelName)}`;
56
+ const chDesc = channelDescription || channelName;
57
+ parts += `<a href="${chUrl}" class="watch-link" title="${escapeHtml(chDesc)}">${escapeHtml(channelName)}</a>${sep}`;
58
+ }
59
+ parts += `<a href="${watchUrl}" class="watch-link">${durStr} watch</a>${sep}${readTime} min read`;
60
+ return parts;
45
61
  }
46
62
 
47
63
  export async function generateHtml(
48
64
  markdown: string,
49
65
  title?: string,
50
- watchMeta?: { videoId: string; duration: number; wordCount: number },
66
+ watchMeta?: {
67
+ videoId: string;
68
+ duration: number;
69
+ wordCount: number;
70
+ channelName?: string;
71
+ channelId?: string;
72
+ channelDescription?: string;
73
+ },
51
74
  ): Promise<string> {
52
75
  const [css, body] = await Promise.all([getPicoCss(), marked.parse(markdown)]);
53
76
 
@@ -138,7 +161,7 @@ img[alt="Prosey"]:hover, img[alt="Prosey"]:active, img[alt="Prosey"]:focus { fil
138
161
  <body>
139
162
  <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem 0">
140
163
  <img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top;flex-shrink:0">
141
- <div style="flex:1;text-align:center;font-size:.85rem;color:var(--pico-muted-color)">${watchMeta ? watchMetaHtml(watchMeta.duration, watchMeta.wordCount, watchMeta.videoId) : ""}</div>
164
+ <div style="flex:1;text-align:center;font-size:.85rem;color:var(--pico-muted-color)">${watchMeta ? watchMetaHtml(watchMeta.duration, watchMeta.wordCount, watchMeta.videoId, watchMeta.channelName, watchMeta.channelId, watchMeta.channelDescription) : ""}</div>
142
165
  <button id="theme-btn" type="button" style="background:none;border:none;cursor:pointer;padding:0;line-height:1;opacity:.5;filter:grayscale(100%);transition:all 0.3s;flex-shrink:0">💡</button>
143
166
  </div>
144
167
  <main class="content">
package/src/index.ts CHANGED
@@ -25,6 +25,30 @@ import { checkVersion } from "./version-check";
25
25
  import pkg from "../package.json";
26
26
  import prettier from "prettier";
27
27
 
28
+ const YT_INNERTUBE_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
29
+
30
+ async function fetchChannelDescription(channelId: string): Promise<string | undefined> {
31
+ try {
32
+ const resp = await fetch(
33
+ `https://www.youtube.com/youtubei/v1/browse?key=${YT_INNERTUBE_API_KEY}`,
34
+ {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({
38
+ context: {
39
+ client: { clientName: "WEB", clientVersion: "2.20240101.00.00" },
40
+ },
41
+ browseId: channelId,
42
+ }),
43
+ },
44
+ );
45
+ const data: any = await resp.json();
46
+ return data?.metadata?.channelMetadataRenderer?.description;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
28
52
  process.stdout.on("error", (err: NodeJS.ErrnoException) => {
29
53
  if (err.code === "EPIPE") process.exit(0);
30
54
  });
@@ -484,6 +508,9 @@ try {
484
508
  let structuredContent: string;
485
509
  let videoTitle: string | undefined;
486
510
  let videoDuration = 0;
511
+ let channelName: string | undefined;
512
+ let channelId: string | undefined;
513
+ let channelDescription: string | undefined;
487
514
 
488
515
  if (!segments) {
489
516
  info("Fetching transcript...");
@@ -494,9 +521,14 @@ try {
494
521
  };
495
522
  segments = result.segments;
496
523
  videoDuration = result.videoDetails.lengthSeconds;
524
+ channelName = result.videoDetails.author;
525
+ channelId = result.videoDetails.channelId;
526
+ channelDescription = await fetchChannelDescription(channelId);
497
527
  const infoJson = JSON.stringify({
498
528
  title: result.videoDetails.title,
499
- channel: result.videoDetails.author,
529
+ channel: channelName,
530
+ channelId: channelId,
531
+ channelDescription: channelDescription,
500
532
  description: result.videoDetails.description,
501
533
  duration: videoDuration,
502
534
  });
@@ -533,9 +565,14 @@ try {
533
565
  segments: TranscriptSegment[];
534
566
  };
535
567
  videoDuration = fallbackResult.videoDetails.lengthSeconds;
568
+ channelName = fallbackResult.videoDetails.author;
569
+ channelId = fallbackResult.videoDetails.channelId;
570
+ channelDescription = await fetchChannelDescription(channelId);
536
571
  cachedInfo = JSON.stringify({
537
572
  title: fallbackResult.videoDetails.title,
538
573
  channel: fallbackResult.videoDetails.author,
574
+ channelId: channelId,
575
+ channelDescription: channelDescription,
539
576
  description: fallbackResult.videoDetails.description,
540
577
  duration: videoDuration,
541
578
  });
@@ -553,6 +590,9 @@ try {
553
590
  const cachedInfoObj = JSON.parse(cachedInfo);
554
591
  videoTitle = cachedInfoObj.title;
555
592
  videoDuration = cachedInfoObj.duration ?? 0;
593
+ channelName = cachedInfoObj.channel;
594
+ channelId = cachedInfoObj.channelId;
595
+ channelDescription = cachedInfoObj.channelDescription;
556
596
  const truncatedInfo = JSON.stringify({
557
597
  title: cachedInfoObj.title,
558
598
  channel: cachedInfoObj.channel,
@@ -586,6 +626,9 @@ try {
586
626
  videoId,
587
627
  duration: videoDuration,
588
628
  wordCount,
629
+ channelName,
630
+ channelId,
631
+ channelDescription,
589
632
  });
590
633
  const htmlPath = join(dir, "summary.html");
591
634
  await writeFile(htmlPath, htmlContent, "utf8");
@@ -622,6 +665,9 @@ try {
622
665
  let md: string | null = null;
623
666
  let videoTitle: string | undefined;
624
667
  let videoDuration = 0;
668
+ let channelName: string | undefined;
669
+ let channelId: string | undefined;
670
+ let channelDescription: string | undefined;
625
671
 
626
672
  startTimer();
627
673
 
@@ -660,9 +706,14 @@ try {
660
706
  };
661
707
  segments = result.segments;
662
708
  videoDuration = result.videoDetails.lengthSeconds;
709
+ channelName = result.videoDetails.author;
710
+ channelId = result.videoDetails.channelId;
711
+ channelDescription = await fetchChannelDescription(channelId);
663
712
  const infoJson = JSON.stringify({
664
713
  title: result.videoDetails.title,
665
- channel: result.videoDetails.author,
714
+ channel: channelName,
715
+ channelId: channelId,
716
+ channelDescription: channelDescription,
666
717
  description: result.videoDetails.description,
667
718
  duration: videoDuration,
668
719
  });
@@ -712,9 +763,14 @@ try {
712
763
  segments: TranscriptSegment[];
713
764
  };
714
765
  videoDuration = fallbackResult.videoDetails.lengthSeconds;
766
+ channelName = fallbackResult.videoDetails.author;
767
+ channelId = fallbackResult.videoDetails.channelId;
768
+ channelDescription = await fetchChannelDescription(channelId);
715
769
  cachedInfo = JSON.stringify({
716
770
  title: fallbackResult.videoDetails.title,
717
771
  channel: fallbackResult.videoDetails.author,
772
+ channelId: channelId,
773
+ channelDescription: channelDescription,
718
774
  description: fallbackResult.videoDetails.description,
719
775
  duration: videoDuration,
720
776
  });
@@ -732,6 +788,9 @@ try {
732
788
  const cachedInfoObj = JSON.parse(cachedInfo);
733
789
  videoTitle = cachedInfoObj.title;
734
790
  videoDuration = cachedInfoObj.duration ?? 0;
791
+ channelName = cachedInfoObj.channel;
792
+ channelId = cachedInfoObj.channelId;
793
+ channelDescription = cachedInfoObj.channelDescription;
735
794
  const truncatedInfo = JSON.stringify({
736
795
  title: cachedInfoObj.title,
737
796
  channel: cachedInfoObj.channel,
@@ -760,6 +819,9 @@ try {
760
819
  videoId,
761
820
  duration: videoDuration,
762
821
  wordCount,
822
+ channelName,
823
+ channelId,
824
+ channelDescription,
763
825
  });
764
826
  const htmlPath = join(dir, "transcript.html");
765
827
  await writeFile(htmlPath, htmlContent, "utf8");