@dxos/plugin-youtube 0.8.3

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.
Files changed (199) hide show
  1. package/LICENSE +21 -0
  2. package/dist/lib/browser/ChannelArticle-CDQR4BBY.mjs +90 -0
  3. package/dist/lib/browser/ChannelArticle-CDQR4BBY.mjs.map +7 -0
  4. package/dist/lib/browser/ChannelSettings-ZYUNW3VS.mjs +28 -0
  5. package/dist/lib/browser/ChannelSettings-ZYUNW3VS.mjs.map +7 -0
  6. package/dist/lib/browser/VideoArticle-FC4A6E7B.mjs +76 -0
  7. package/dist/lib/browser/VideoArticle-FC4A6E7B.mjs.map +7 -0
  8. package/dist/lib/browser/VideoCard-CCPXDCB7.mjs +64 -0
  9. package/dist/lib/browser/VideoCard-CCPXDCB7.mjs.map +7 -0
  10. package/dist/lib/browser/app-graph-builder-MJY6A6SN.mjs +195 -0
  11. package/dist/lib/browser/app-graph-builder-MJY6A6SN.mjs.map +7 -0
  12. package/dist/lib/browser/blueprint-definition-FRYUYJ22.mjs +22 -0
  13. package/dist/lib/browser/blueprint-definition-FRYUYJ22.mjs.map +7 -0
  14. package/dist/lib/browser/blueprints/index.mjs +13 -0
  15. package/dist/lib/browser/blueprints/index.mjs.map +7 -0
  16. package/dist/lib/browser/chunk-C26XKDK2.mjs +355 -0
  17. package/dist/lib/browser/chunk-C26XKDK2.mjs.map +7 -0
  18. package/dist/lib/browser/chunk-DFRSBBSO.mjs +21 -0
  19. package/dist/lib/browser/chunk-DFRSBBSO.mjs.map +7 -0
  20. package/dist/lib/browser/chunk-GFRR4TTX.mjs +72 -0
  21. package/dist/lib/browser/chunk-GFRR4TTX.mjs.map +7 -0
  22. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  23. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  24. package/dist/lib/browser/chunk-MUE22YUM.mjs +57 -0
  25. package/dist/lib/browser/chunk-MUE22YUM.mjs.map +7 -0
  26. package/dist/lib/browser/chunk-P67QEKBQ.mjs +72 -0
  27. package/dist/lib/browser/chunk-P67QEKBQ.mjs.map +7 -0
  28. package/dist/lib/browser/chunk-YMDT37TA.mjs +62 -0
  29. package/dist/lib/browser/chunk-YMDT37TA.mjs.map +7 -0
  30. package/dist/lib/browser/chunk-Z3DGTMKC.mjs +8 -0
  31. package/dist/lib/browser/chunk-Z3DGTMKC.mjs.map +7 -0
  32. package/dist/lib/browser/clear-synced-videos-EVMJIZPD.mjs +66 -0
  33. package/dist/lib/browser/clear-synced-videos-EVMJIZPD.mjs.map +7 -0
  34. package/dist/lib/browser/index.mjs +153 -0
  35. package/dist/lib/browser/index.mjs.map +7 -0
  36. package/dist/lib/browser/meta.json +1 -0
  37. package/dist/lib/browser/react-surface-EDA5VYDC.mjs +77 -0
  38. package/dist/lib/browser/react-surface-EDA5VYDC.mjs.map +7 -0
  39. package/dist/lib/browser/sync-423Q4BDD.mjs +360 -0
  40. package/dist/lib/browser/sync-423Q4BDD.mjs.map +7 -0
  41. package/dist/lib/browser/types/index.mjs +14 -0
  42. package/dist/lib/browser/types/index.mjs.map +7 -0
  43. package/dist/lib/node-esm/ChannelArticle-GQ64BO7V.mjs +91 -0
  44. package/dist/lib/node-esm/ChannelArticle-GQ64BO7V.mjs.map +7 -0
  45. package/dist/lib/node-esm/ChannelSettings-DM2HWNKO.mjs +29 -0
  46. package/dist/lib/node-esm/ChannelSettings-DM2HWNKO.mjs.map +7 -0
  47. package/dist/lib/node-esm/VideoArticle-WLTWZO3K.mjs +77 -0
  48. package/dist/lib/node-esm/VideoArticle-WLTWZO3K.mjs.map +7 -0
  49. package/dist/lib/node-esm/VideoCard-FOWQZK75.mjs +65 -0
  50. package/dist/lib/node-esm/VideoCard-FOWQZK75.mjs.map +7 -0
  51. package/dist/lib/node-esm/app-graph-builder-IU5TBAXN.mjs +196 -0
  52. package/dist/lib/node-esm/app-graph-builder-IU5TBAXN.mjs.map +7 -0
  53. package/dist/lib/node-esm/blueprint-definition-W264MZ3D.mjs +23 -0
  54. package/dist/lib/node-esm/blueprint-definition-W264MZ3D.mjs.map +7 -0
  55. package/dist/lib/node-esm/blueprints/index.mjs +14 -0
  56. package/dist/lib/node-esm/blueprints/index.mjs.map +7 -0
  57. package/dist/lib/node-esm/chunk-5KNC2JMP.mjs +58 -0
  58. package/dist/lib/node-esm/chunk-5KNC2JMP.mjs.map +7 -0
  59. package/dist/lib/node-esm/chunk-6BUJ2DQX.mjs +73 -0
  60. package/dist/lib/node-esm/chunk-6BUJ2DQX.mjs.map +7 -0
  61. package/dist/lib/node-esm/chunk-CX6MV3QM.mjs +23 -0
  62. package/dist/lib/node-esm/chunk-CX6MV3QM.mjs.map +7 -0
  63. package/dist/lib/node-esm/chunk-CZSLL3XQ.mjs +63 -0
  64. package/dist/lib/node-esm/chunk-CZSLL3XQ.mjs.map +7 -0
  65. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  66. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  67. package/dist/lib/node-esm/chunk-JM5SBBP5.mjs +356 -0
  68. package/dist/lib/node-esm/chunk-JM5SBBP5.mjs.map +7 -0
  69. package/dist/lib/node-esm/chunk-JSGRZMG3.mjs +73 -0
  70. package/dist/lib/node-esm/chunk-JSGRZMG3.mjs.map +7 -0
  71. package/dist/lib/node-esm/chunk-M4S6BE47.mjs +10 -0
  72. package/dist/lib/node-esm/chunk-M4S6BE47.mjs.map +7 -0
  73. package/dist/lib/node-esm/clear-synced-videos-5UCH6XHL.mjs +67 -0
  74. package/dist/lib/node-esm/clear-synced-videos-5UCH6XHL.mjs.map +7 -0
  75. package/dist/lib/node-esm/index.mjs +154 -0
  76. package/dist/lib/node-esm/index.mjs.map +7 -0
  77. package/dist/lib/node-esm/meta.json +1 -0
  78. package/dist/lib/node-esm/react-surface-5DJAQPHJ.mjs +78 -0
  79. package/dist/lib/node-esm/react-surface-5DJAQPHJ.mjs.map +7 -0
  80. package/dist/lib/node-esm/sync-CEF5DX2J.mjs +361 -0
  81. package/dist/lib/node-esm/sync-CEF5DX2J.mjs.map +7 -0
  82. package/dist/lib/node-esm/types/index.mjs +15 -0
  83. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  84. package/dist/types/src/YouTubePlugin.d.ts +3 -0
  85. package/dist/types/src/YouTubePlugin.d.ts.map +1 -0
  86. package/dist/types/src/blueprints/index.d.ts +3 -0
  87. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  88. package/dist/types/src/blueprints/youtube.d.ts +4 -0
  89. package/dist/types/src/blueprints/youtube.d.ts.map +1 -0
  90. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +6 -0
  91. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -0
  92. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +3 -0
  93. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +1 -0
  94. package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts +6 -0
  95. package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts.map +1 -0
  96. package/dist/types/src/capabilities/blueprint-definition/index.d.ts +3 -0
  97. package/dist/types/src/capabilities/blueprint-definition/index.d.ts.map +1 -0
  98. package/dist/types/src/capabilities/index.d.ts +4 -0
  99. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  100. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  101. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  102. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  103. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  104. package/dist/types/src/containers/ChannelArticle/ChannelArticle.d.ts +8 -0
  105. package/dist/types/src/containers/ChannelArticle/ChannelArticle.d.ts.map +1 -0
  106. package/dist/types/src/containers/ChannelArticle/index.d.ts +3 -0
  107. package/dist/types/src/containers/ChannelArticle/index.d.ts.map +1 -0
  108. package/dist/types/src/containers/ChannelSettings/ChannelSettings.d.ts +7 -0
  109. package/dist/types/src/containers/ChannelSettings/ChannelSettings.d.ts.map +1 -0
  110. package/dist/types/src/containers/ChannelSettings/index.d.ts +3 -0
  111. package/dist/types/src/containers/ChannelSettings/index.d.ts.map +1 -0
  112. package/dist/types/src/containers/VideoArticle/VideoArticle.d.ts +11 -0
  113. package/dist/types/src/containers/VideoArticle/VideoArticle.d.ts.map +1 -0
  114. package/dist/types/src/containers/VideoArticle/index.d.ts +3 -0
  115. package/dist/types/src/containers/VideoArticle/index.d.ts.map +1 -0
  116. package/dist/types/src/containers/VideoCard/VideoCard.d.ts +9 -0
  117. package/dist/types/src/containers/VideoCard/VideoCard.d.ts.map +1 -0
  118. package/dist/types/src/containers/VideoCard/index.d.ts +3 -0
  119. package/dist/types/src/containers/VideoCard/index.d.ts.map +1 -0
  120. package/dist/types/src/containers/index.d.ts +11 -0
  121. package/dist/types/src/containers/index.d.ts.map +1 -0
  122. package/dist/types/src/index.d.ts +4 -0
  123. package/dist/types/src/index.d.ts.map +1 -0
  124. package/dist/types/src/meta.d.ts +3 -0
  125. package/dist/types/src/meta.d.ts.map +1 -0
  126. package/dist/types/src/operations/apis/index.d.ts +2 -0
  127. package/dist/types/src/operations/apis/index.d.ts.map +1 -0
  128. package/dist/types/src/operations/apis/youtube/api.d.ts +251 -0
  129. package/dist/types/src/operations/apis/youtube/api.d.ts.map +1 -0
  130. package/dist/types/src/operations/apis/youtube/index.d.ts +3 -0
  131. package/dist/types/src/operations/apis/youtube/index.d.ts.map +1 -0
  132. package/dist/types/src/operations/apis/youtube/types.d.ts +493 -0
  133. package/dist/types/src/operations/apis/youtube/types.d.ts.map +1 -0
  134. package/dist/types/src/operations/clear-synced-videos.d.ts +5 -0
  135. package/dist/types/src/operations/clear-synced-videos.d.ts.map +1 -0
  136. package/dist/types/src/operations/definitions.d.ts +45 -0
  137. package/dist/types/src/operations/definitions.d.ts.map +1 -0
  138. package/dist/types/src/operations/index.d.ts +5 -0
  139. package/dist/types/src/operations/index.d.ts.map +1 -0
  140. package/dist/types/src/operations/services/google-credentials.d.ts +35 -0
  141. package/dist/types/src/operations/services/google-credentials.d.ts.map +1 -0
  142. package/dist/types/src/operations/services/index.d.ts +2 -0
  143. package/dist/types/src/operations/services/index.d.ts.map +1 -0
  144. package/dist/types/src/operations/sync.d.ts +5 -0
  145. package/dist/types/src/operations/sync.d.ts.map +1 -0
  146. package/dist/types/src/operations/sync.test.d.ts +2 -0
  147. package/dist/types/src/operations/sync.test.d.ts.map +1 -0
  148. package/dist/types/src/operations/transcript.d.ts +12 -0
  149. package/dist/types/src/operations/transcript.d.ts.map +1 -0
  150. package/dist/types/src/translations.d.ts +49 -0
  151. package/dist/types/src/translations.d.ts.map +1 -0
  152. package/dist/types/src/types/Channel.d.ts +54 -0
  153. package/dist/types/src/types/Channel.d.ts.map +1 -0
  154. package/dist/types/src/types/Video.d.ts +41 -0
  155. package/dist/types/src/types/Video.d.ts.map +1 -0
  156. package/dist/types/src/types/index.d.ts +4 -0
  157. package/dist/types/src/types/index.d.ts.map +1 -0
  158. package/dist/types/src/types/types.d.ts +5 -0
  159. package/dist/types/src/types/types.d.ts.map +1 -0
  160. package/dist/types/tsconfig.tsbuildinfo +1 -0
  161. package/package.json +113 -0
  162. package/src/YouTubePlugin.tsx +66 -0
  163. package/src/blueprints/index.ts +7 -0
  164. package/src/blueprints/youtube.ts +52 -0
  165. package/src/capabilities/app-graph-builder/app-graph-builder.ts +148 -0
  166. package/src/capabilities/app-graph-builder/index.ts +7 -0
  167. package/src/capabilities/blueprint-definition/blueprint-definition.ts +17 -0
  168. package/src/capabilities/blueprint-definition/index.ts +7 -0
  169. package/src/capabilities/index.ts +7 -0
  170. package/src/capabilities/react-surface/index.ts +7 -0
  171. package/src/capabilities/react-surface/react-surface.tsx +54 -0
  172. package/src/containers/ChannelArticle/ChannelArticle.tsx +115 -0
  173. package/src/containers/ChannelArticle/index.ts +7 -0
  174. package/src/containers/ChannelSettings/ChannelSettings.tsx +34 -0
  175. package/src/containers/ChannelSettings/index.ts +7 -0
  176. package/src/containers/VideoArticle/VideoArticle.tsx +109 -0
  177. package/src/containers/VideoArticle/index.ts +7 -0
  178. package/src/containers/VideoCard/VideoCard.tsx +86 -0
  179. package/src/containers/VideoCard/index.ts +7 -0
  180. package/src/containers/index.ts +24 -0
  181. package/src/index.ts +8 -0
  182. package/src/meta.ts +19 -0
  183. package/src/operations/apis/index.ts +5 -0
  184. package/src/operations/apis/youtube/api.ts +149 -0
  185. package/src/operations/apis/youtube/index.ts +6 -0
  186. package/src/operations/apis/youtube/types.ts +254 -0
  187. package/src/operations/clear-synced-videos.ts +50 -0
  188. package/src/operations/definitions.ts +62 -0
  189. package/src/operations/index.ts +13 -0
  190. package/src/operations/services/google-credentials.ts +81 -0
  191. package/src/operations/services/index.ts +5 -0
  192. package/src/operations/sync.test.ts +114 -0
  193. package/src/operations/sync.ts +309 -0
  194. package/src/operations/transcript.ts +47 -0
  195. package/src/translations.ts +59 -0
  196. package/src/types/Channel.ts +80 -0
  197. package/src/types/Video.ts +67 -0
  198. package/src/types/index.ts +8 -0
  199. package/src/types/types.ts +10 -0
@@ -0,0 +1,109 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useState } from 'react';
6
+
7
+ import { Icon, Panel } from '@dxos/react-ui';
8
+
9
+ import * as Channel from '../../types/Channel';
10
+ import * as Video from '../../types/Video';
11
+
12
+ export type VideoArticleProps = {
13
+ role: string | string[];
14
+ subject: Video.YouTubeVideo;
15
+ channel: Channel.YouTubeChannel;
16
+ attendableId?: string;
17
+ };
18
+
19
+ export const VideoArticle = ({ subject: video, role }: VideoArticleProps) => {
20
+ const [showPlayer, setShowPlayer] = useState(false);
21
+ const publishedDate = new Date(video.publishedAt).toLocaleDateString();
22
+ const hasTranscript = Boolean(video.transcript);
23
+ const embedUrl = `https://www.youtube.com/embed/${video.videoId}`;
24
+
25
+ return (
26
+ <Panel.Root>
27
+ <Panel.Content className={role === 'section' ? 'overflow-auto' : ''}>
28
+ <div className='flex flex-col gap-4 p-4 max-w-4xl'>
29
+ <div className='flex flex-col gap-2'>
30
+ <h2 className='text-lg font-semibold'>{video.title}</h2>
31
+ <div className='flex items-center gap-2 text-sm text-description'>
32
+ <span>{video.channelTitle}</span>
33
+ <span>•</span>
34
+ <span>{publishedDate}</span>
35
+ {video.viewCount !== undefined && (
36
+ <>
37
+ <span>•</span>
38
+ <span>{video.viewCount.toLocaleString()} views</span>
39
+ </>
40
+ )}
41
+ {video.likeCount !== undefined && (
42
+ <>
43
+ <span>•</span>
44
+ <span>{video.likeCount.toLocaleString()} likes</span>
45
+ </>
46
+ )}
47
+ </div>
48
+ </div>
49
+
50
+ <div className='aspect-video w-full max-w-2xl'>
51
+ {showPlayer ? (
52
+ <iframe
53
+ src={embedUrl}
54
+ title={video.title}
55
+ allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
56
+ allowFullScreen
57
+ className='h-full w-full rounded-lg'
58
+ />
59
+ ) : (
60
+ <button
61
+ type='button'
62
+ onClick={() => setShowPlayer(true)}
63
+ className='relative h-full w-full group cursor-pointer'
64
+ >
65
+ {video.thumbnailUrl ? (
66
+ <img src={video.thumbnailUrl} alt={video.title} className='h-full w-full object-cover rounded-lg' />
67
+ ) : (
68
+ <div className='h-full w-full bg-surface-hover rounded-lg flex items-center justify-center'>
69
+ <Icon icon='ph--play--fill' size={12} />
70
+ </div>
71
+ )}
72
+ <div className='absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/50 rounded-lg transition-colors'>
73
+ <div className='bg-red-600 text-white rounded-full p-4 group-hover:scale-110 transition-transform'>
74
+ <Icon icon='ph--play--fill' size={8} />
75
+ </div>
76
+ </div>
77
+ </button>
78
+ )}
79
+ </div>
80
+
81
+ {video.description && (
82
+ <div className='max-w-2xl'>
83
+ <h3 className='text-md font-medium mb-2'>Description</h3>
84
+ <p className='whitespace-pre-wrap text-sm'>{video.description}</p>
85
+ </div>
86
+ )}
87
+
88
+ {hasTranscript && (
89
+ <div className='max-w-2xl'>
90
+ <h3 className='text-md font-medium mb-2'>Transcript</h3>
91
+ <div className='bg-surface-input p-4 rounded-lg max-h-96 overflow-auto'>
92
+ <p className='whitespace-pre-wrap text-sm'>{video.transcript}</p>
93
+ </div>
94
+ </div>
95
+ )}
96
+
97
+ <a
98
+ href={video.url}
99
+ target='_blank'
100
+ rel='noopener noreferrer'
101
+ className='inline-flex items-center gap-2 text-sm text-primary hover:underline'
102
+ >
103
+ Watch on YouTube →
104
+ </a>
105
+ </div>
106
+ </Panel.Content>
107
+ </Panel.Root>
108
+ );
109
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { VideoArticle } from './VideoArticle';
6
+
7
+ export default VideoArticle;
@@ -0,0 +1,86 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useState } from 'react';
6
+
7
+ import { type SurfaceComponentProps } from '@dxos/app-toolkit/ui';
8
+ import { Card, Icon } from '@dxos/react-ui';
9
+
10
+ import * as Video from '../../types/Video';
11
+
12
+ export type VideoCardProps = SurfaceComponentProps<Video.YouTubeVideo>;
13
+
14
+ /**
15
+ * YouTube video card with embedded player.
16
+ */
17
+ export const VideoCard = ({ subject: video }: VideoCardProps) => {
18
+ const [showPlayer, setShowPlayer] = useState(false);
19
+ const publishedDate = new Date(video.publishedAt).toLocaleDateString();
20
+ const hasTranscript = Boolean(video.transcript);
21
+
22
+ const embedUrl = `https://www.youtube.com/embed/${video.videoId}?autoplay=1`;
23
+
24
+ return (
25
+ <Card.Content>
26
+ {showPlayer ? (
27
+ <div className='aspect-video w-full'>
28
+ <iframe
29
+ src={embedUrl}
30
+ title={video.title}
31
+ allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
32
+ allowFullScreen
33
+ className='h-full w-full rounded'
34
+ />
35
+ </div>
36
+ ) : (
37
+ <button
38
+ type='button'
39
+ onClick={() => setShowPlayer(true)}
40
+ className='relative aspect-video w-full group cursor-pointer'
41
+ >
42
+ {video.thumbnailUrl ? (
43
+ <img src={video.thumbnailUrl} alt={video.title} className='h-full w-full object-cover rounded' />
44
+ ) : (
45
+ <div className='h-full w-full bg-surface-hover rounded flex items-center justify-center'>
46
+ <Icon icon='ph--play--fill' size={12} />
47
+ </div>
48
+ )}
49
+ <div className='absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/50 rounded transition-colors'>
50
+ <div className='bg-red-600 text-white rounded-full p-3 group-hover:scale-110 transition-transform'>
51
+ <Icon icon='ph--play--fill' size={6} />
52
+ </div>
53
+ </div>
54
+ </button>
55
+ )}
56
+ <Card.Toolbar>
57
+ <Card.IconBlock>
58
+ <Icon icon='ph--youtube-logo--regular' size={5} />
59
+ </Card.IconBlock>
60
+ <div className='flex gap-3 items-center justify-between col-span-2'>
61
+ <span className='grow truncate font-medium'>{video.title}</span>
62
+ <span className='text-xs text-description text-right whitespace-nowrap pe-2'>{publishedDate}</span>
63
+ </div>
64
+ </Card.Toolbar>
65
+ <Card.Row>
66
+ <span className='text-xs text-description'>{video.channelTitle}</span>
67
+ </Card.Row>
68
+ {video.description && (
69
+ <Card.Row>
70
+ <Card.Text variant='description'>{video.description.slice(0, 150)}...</Card.Text>
71
+ </Card.Row>
72
+ )}
73
+ <Card.Row>
74
+ <div className='flex gap-2 items-center text-xs text-description'>
75
+ {video.viewCount !== undefined && <span>{video.viewCount.toLocaleString()} views</span>}
76
+ {hasTranscript && (
77
+ <>
78
+ <span>•</span>
79
+ <span className='text-green-600'>Transcript available</span>
80
+ </>
81
+ )}
82
+ </div>
83
+ </Card.Row>
84
+ </Card.Content>
85
+ );
86
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { VideoCard } from './VideoCard';
6
+
7
+ export default VideoCard;
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type ComponentType, lazy, type LazyExoticComponent } from 'react';
6
+
7
+ import type { ChannelArticleProps } from './ChannelArticle/ChannelArticle';
8
+ import type { ChannelSettingsProps } from './ChannelSettings/ChannelSettings';
9
+ import type { VideoArticleProps } from './VideoArticle/VideoArticle';
10
+ import type { VideoCardProps } from './VideoCard/VideoCard';
11
+
12
+ export type { ChannelArticleProps, ChannelSettingsProps, VideoArticleProps, VideoCardProps };
13
+
14
+ export const ChannelArticle: LazyExoticComponent<ComponentType<ChannelArticleProps>> = lazy(
15
+ () => import('./ChannelArticle'),
16
+ );
17
+
18
+ export const ChannelSettings: LazyExoticComponent<ComponentType<ChannelSettingsProps>> = lazy(
19
+ () => import('./ChannelSettings'),
20
+ );
21
+
22
+ export const VideoArticle: LazyExoticComponent<ComponentType<VideoArticleProps>> = lazy(() => import('./VideoArticle'));
23
+
24
+ export const VideoCard: LazyExoticComponent<ComponentType<VideoCardProps>> = lazy(() => import('./VideoCard'));
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './meta';
6
+ export * from './operations';
7
+
8
+ export * from './YouTubePlugin';
package/src/meta.ts ADDED
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Plugin } from '@dxos/app-framework';
6
+ import { trim } from '@dxos/util';
7
+
8
+ export const meta: Plugin.Meta = {
9
+ id: 'org.dxos.plugin.youtube',
10
+ name: 'YouTube',
11
+ description: trim`
12
+ YouTube channel subscription and video feed management.
13
+ Sync videos from channels and access transcripts for analysis.
14
+ `,
15
+ icon: 'ph--youtube-logo--regular',
16
+ iconHue: 'red',
17
+ source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-youtube',
18
+ tags: ['labs'],
19
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * as YouTube from './youtube';
@@ -0,0 +1,149 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import * as HttpClient from '@effect/platform/HttpClient';
6
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
7
+ import * as Effect from 'effect/Effect';
8
+ import type * as ParseResult from 'effect/ParseResult';
9
+ import * as Schedule from 'effect/Schedule';
10
+ import * as Schema from 'effect/Schema';
11
+
12
+ import { withAuthorization } from '@dxos/functions';
13
+ import { log } from '@dxos/log';
14
+
15
+ import { GoogleCredentials } from '../../services/google-credentials';
16
+
17
+ import {
18
+ ChannelsResponse,
19
+ ErrorResponse,
20
+ PlaylistItemsResponse,
21
+ SearchResponse,
22
+ VideosResponse,
23
+ YouTubeError,
24
+ } from './types';
25
+
26
+ const API_URL = 'https://www.googleapis.com/youtube/v3';
27
+
28
+ /**
29
+ * Creates a URL from path segments and query parameters.
30
+ */
31
+ const createUrl = (parts: (string | undefined)[], params?: Record<string, unknown>): URL => {
32
+ const url = new URL(parts.filter(Boolean).join('/'));
33
+ if (params) {
34
+ Object.entries(params)
35
+ .filter(([_, value]) => value != null)
36
+ .forEach(([key, value]) => url.searchParams.set(key, String(value)));
37
+ }
38
+ return url;
39
+ };
40
+
41
+ /**
42
+ * Decode response and handle YouTube API errors.
43
+ */
44
+ const decodeAndHandleErrors =
45
+ <S extends Schema.Schema.Any>(schema: S) =>
46
+ (
47
+ data: unknown,
48
+ ): Effect.Effect<Schema.Schema.Type<S>, YouTubeError | ParseResult.ParseError, Schema.Schema.Context<S>> =>
49
+ Schema.decodeUnknown(Schema.Union(schema, ErrorResponse))(data).pipe(
50
+ Effect.flatMap((response) => {
51
+ if ('error' in response) {
52
+ return Effect.fail(YouTubeError.fromErrorResponse(response));
53
+ } else {
54
+ return Effect.succeed(response);
55
+ }
56
+ }),
57
+ );
58
+
59
+ /**
60
+ * Makes an authenticated HTTP request to a YouTube API endpoint.
61
+ */
62
+ const makeYouTubeApiRequest = Effect.fn('makeYouTubeApiRequest')(function* (url: string) {
63
+ const token = yield* GoogleCredentials.get();
64
+
65
+ const httpClient = yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization(token, 'Bearer')));
66
+ const httpClientWithTracerDisabled = httpClient.pipe(HttpClient.withTracerDisabledWhen(() => true));
67
+
68
+ const request = HttpClientRequest.get(url);
69
+
70
+ const response = yield* request.pipe(
71
+ HttpClientRequest.setHeader('accept', 'application/json'),
72
+ httpClientWithTracerDisabled.execute,
73
+ Effect.flatMap((res) => res.json),
74
+ Effect.timeout('10 seconds'),
75
+ Effect.retry(Schedule.exponential(1_000).pipe(Schedule.compose(Schedule.recurs(3)))),
76
+ Effect.scoped,
77
+ Effect.withSpan('YouTubeApiRequest'),
78
+ );
79
+
80
+ if ((response as Record<string, unknown>).error) {
81
+ log.catch((response as Record<string, unknown>).error);
82
+ }
83
+
84
+ return response;
85
+ });
86
+
87
+ /**
88
+ * Get channel details by channel ID.
89
+ * https://developers.google.com/youtube/v3/docs/channels/list
90
+ */
91
+ export const getChannel = Effect.fn(function* (channelId: string) {
92
+ const url = createUrl([API_URL, 'channels'], {
93
+ part: 'snippet,contentDetails',
94
+ id: channelId,
95
+ }).toString();
96
+ return yield* makeYouTubeApiRequest(url).pipe(Effect.flatMap(decodeAndHandleErrors(ChannelsResponse)));
97
+ });
98
+
99
+ /**
100
+ * Get channel by username/handle.
101
+ * https://developers.google.com/youtube/v3/docs/channels/list
102
+ */
103
+ export const getChannelByHandle = Effect.fn(function* (handle: string) {
104
+ const url = createUrl([API_URL, 'channels'], {
105
+ part: 'snippet,contentDetails',
106
+ forHandle: handle.startsWith('@') ? handle.slice(1) : handle,
107
+ }).toString();
108
+ return yield* makeYouTubeApiRequest(url).pipe(Effect.flatMap(decodeAndHandleErrors(ChannelsResponse)));
109
+ });
110
+
111
+ /**
112
+ * Search for channels by query.
113
+ * https://developers.google.com/youtube/v3/docs/search/list
114
+ */
115
+ export const searchChannels = Effect.fn(function* (query: string, maxResults = 10) {
116
+ const url = createUrl([API_URL, 'search'], {
117
+ part: 'snippet',
118
+ type: 'channel',
119
+ q: query,
120
+ maxResults,
121
+ }).toString();
122
+ return yield* makeYouTubeApiRequest(url).pipe(Effect.flatMap(decodeAndHandleErrors(SearchResponse)));
123
+ });
124
+
125
+ /**
126
+ * List videos from a playlist (used to get uploads from a channel's uploads playlist).
127
+ * https://developers.google.com/youtube/v3/docs/playlistItems/list
128
+ */
129
+ export const listPlaylistItems = Effect.fn(function* (playlistId: string, maxResults = 50, pageToken?: string) {
130
+ const url = createUrl([API_URL, 'playlistItems'], {
131
+ part: 'snippet,contentDetails',
132
+ playlistId,
133
+ maxResults,
134
+ pageToken,
135
+ }).toString();
136
+ return yield* makeYouTubeApiRequest(url).pipe(Effect.flatMap(decodeAndHandleErrors(PlaylistItemsResponse)));
137
+ });
138
+
139
+ /**
140
+ * Get detailed video information including statistics and duration.
141
+ * https://developers.google.com/youtube/v3/docs/videos/list
142
+ */
143
+ export const getVideoDetails = Effect.fn(function* (videoIds: string[]) {
144
+ const url = createUrl([API_URL, 'videos'], {
145
+ part: 'snippet,contentDetails,statistics',
146
+ id: videoIds.join(','),
147
+ }).toString();
148
+ return yield* makeYouTubeApiRequest(url).pipe(Effect.flatMap(decodeAndHandleErrors(VideosResponse)));
149
+ });
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './api';
6
+ export * from './types';
@@ -0,0 +1,254 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+
7
+ /**
8
+ * YouTube API error response.
9
+ */
10
+ export class YouTubeError extends Schema.TaggedError<YouTubeError>()('YouTubeError', {
11
+ code: Schema.Number,
12
+ message: Schema.String,
13
+ status: Schema.optional(Schema.String),
14
+ }) {
15
+ static fromErrorResponse(response: ErrorResponse) {
16
+ return new YouTubeError({
17
+ code: response.error.code,
18
+ message: response.error.message,
19
+ status: response.error.status,
20
+ });
21
+ }
22
+ }
23
+
24
+ export const ErrorResponse = Schema.Struct({
25
+ error: Schema.Struct({
26
+ code: Schema.Number,
27
+ message: Schema.String,
28
+ status: Schema.optional(Schema.String),
29
+ }),
30
+ });
31
+
32
+ export type ErrorResponse = Schema.Schema.Type<typeof ErrorResponse>;
33
+
34
+ /**
35
+ * YouTube channel snippet from search/channel API.
36
+ */
37
+ export const ChannelSnippet = Schema.Struct({
38
+ title: Schema.String,
39
+ description: Schema.optional(Schema.String),
40
+ customUrl: Schema.optional(Schema.String),
41
+ publishedAt: Schema.optional(Schema.String),
42
+ thumbnails: Schema.optional(
43
+ Schema.Struct({
44
+ default: Schema.optional(Schema.Struct({ url: Schema.String })),
45
+ medium: Schema.optional(Schema.Struct({ url: Schema.String })),
46
+ high: Schema.optional(Schema.Struct({ url: Schema.String })),
47
+ }),
48
+ ),
49
+ });
50
+
51
+ /**
52
+ * YouTube channel item from channels API.
53
+ */
54
+ export const ChannelItem = Schema.Struct({
55
+ kind: Schema.String,
56
+ etag: Schema.String,
57
+ id: Schema.String,
58
+ snippet: Schema.optional(ChannelSnippet),
59
+ contentDetails: Schema.optional(
60
+ Schema.Struct({
61
+ relatedPlaylists: Schema.optional(
62
+ Schema.Struct({
63
+ uploads: Schema.optional(Schema.String),
64
+ }),
65
+ ),
66
+ }),
67
+ ),
68
+ });
69
+
70
+ /**
71
+ * YouTube channels list response.
72
+ */
73
+ export const ChannelsResponse = Schema.Struct({
74
+ kind: Schema.String,
75
+ etag: Schema.String,
76
+ pageInfo: Schema.optional(
77
+ Schema.Struct({
78
+ totalResults: Schema.Number,
79
+ resultsPerPage: Schema.Number,
80
+ }),
81
+ ),
82
+ items: Schema.Array(ChannelItem),
83
+ });
84
+
85
+ export type ChannelsResponse = Schema.Schema.Type<typeof ChannelsResponse>;
86
+
87
+ /**
88
+ * Video snippet from search/playlistItems API.
89
+ */
90
+ export const VideoSnippet = Schema.Struct({
91
+ publishedAt: Schema.String,
92
+ channelId: Schema.optional(Schema.String),
93
+ title: Schema.String,
94
+ description: Schema.optional(Schema.String),
95
+ thumbnails: Schema.optional(
96
+ Schema.Struct({
97
+ default: Schema.optional(Schema.Struct({ url: Schema.String })),
98
+ medium: Schema.optional(Schema.Struct({ url: Schema.String })),
99
+ high: Schema.optional(Schema.Struct({ url: Schema.String })),
100
+ standard: Schema.optional(Schema.Struct({ url: Schema.String })),
101
+ maxres: Schema.optional(Schema.Struct({ url: Schema.String })),
102
+ }),
103
+ ),
104
+ channelTitle: Schema.optional(Schema.String),
105
+ playlistId: Schema.optional(Schema.String),
106
+ position: Schema.optional(Schema.Number),
107
+ resourceId: Schema.optional(
108
+ Schema.Struct({
109
+ kind: Schema.String,
110
+ videoId: Schema.String,
111
+ }),
112
+ ),
113
+ });
114
+
115
+ /**
116
+ * Content details for a video.
117
+ */
118
+ export const VideoContentDetails = Schema.Struct({
119
+ videoId: Schema.optional(Schema.String),
120
+ videoPublishedAt: Schema.optional(Schema.String),
121
+ duration: Schema.optional(Schema.String),
122
+ });
123
+
124
+ /**
125
+ * Video statistics.
126
+ */
127
+ export const VideoStatistics = Schema.Struct({
128
+ viewCount: Schema.optional(Schema.String),
129
+ likeCount: Schema.optional(Schema.String),
130
+ commentCount: Schema.optional(Schema.String),
131
+ });
132
+
133
+ /**
134
+ * Video item from videos API.
135
+ */
136
+ export const VideoItem = Schema.Struct({
137
+ kind: Schema.String,
138
+ etag: Schema.String,
139
+ id: Schema.String,
140
+ snippet: Schema.optional(VideoSnippet),
141
+ contentDetails: Schema.optional(VideoContentDetails),
142
+ statistics: Schema.optional(VideoStatistics),
143
+ });
144
+
145
+ export type VideoItem = Schema.Schema.Type<typeof VideoItem>;
146
+
147
+ /**
148
+ * YouTube videos list response.
149
+ */
150
+ export const VideosResponse = Schema.Struct({
151
+ kind: Schema.String,
152
+ etag: Schema.String,
153
+ nextPageToken: Schema.optional(Schema.String),
154
+ prevPageToken: Schema.optional(Schema.String),
155
+ pageInfo: Schema.optional(
156
+ Schema.Struct({
157
+ totalResults: Schema.Number,
158
+ resultsPerPage: Schema.Number,
159
+ }),
160
+ ),
161
+ items: Schema.Array(VideoItem),
162
+ });
163
+
164
+ export type VideosResponse = Schema.Schema.Type<typeof VideosResponse>;
165
+
166
+ /**
167
+ * Playlist item from playlistItems API.
168
+ */
169
+ export const PlaylistItem = Schema.Struct({
170
+ kind: Schema.String,
171
+ etag: Schema.String,
172
+ id: Schema.String,
173
+ snippet: Schema.optional(VideoSnippet),
174
+ contentDetails: Schema.optional(VideoContentDetails),
175
+ });
176
+
177
+ /**
178
+ * YouTube playlist items response.
179
+ */
180
+ export const PlaylistItemsResponse = Schema.Struct({
181
+ kind: Schema.String,
182
+ etag: Schema.String,
183
+ nextPageToken: Schema.optional(Schema.String),
184
+ prevPageToken: Schema.optional(Schema.String),
185
+ pageInfo: Schema.optional(
186
+ Schema.Struct({
187
+ totalResults: Schema.Number,
188
+ resultsPerPage: Schema.Number,
189
+ }),
190
+ ),
191
+ items: Schema.Array(PlaylistItem),
192
+ });
193
+
194
+ export type PlaylistItemsResponse = Schema.Schema.Type<typeof PlaylistItemsResponse>;
195
+
196
+ /**
197
+ * Search result item.
198
+ */
199
+ export const SearchItem = Schema.Struct({
200
+ kind: Schema.String,
201
+ etag: Schema.String,
202
+ id: Schema.Struct({
203
+ kind: Schema.String,
204
+ videoId: Schema.optional(Schema.String),
205
+ channelId: Schema.optional(Schema.String),
206
+ playlistId: Schema.optional(Schema.String),
207
+ }),
208
+ snippet: Schema.optional(VideoSnippet),
209
+ });
210
+
211
+ /**
212
+ * YouTube search response.
213
+ */
214
+ export const SearchResponse = Schema.Struct({
215
+ kind: Schema.String,
216
+ etag: Schema.String,
217
+ nextPageToken: Schema.optional(Schema.String),
218
+ prevPageToken: Schema.optional(Schema.String),
219
+ pageInfo: Schema.optional(
220
+ Schema.Struct({
221
+ totalResults: Schema.Number,
222
+ resultsPerPage: Schema.Number,
223
+ }),
224
+ ),
225
+ items: Schema.Array(SearchItem),
226
+ });
227
+
228
+ export type SearchResponse = Schema.Schema.Type<typeof SearchResponse>;
229
+
230
+ /**
231
+ * Caption track from captions.list.
232
+ */
233
+ export const CaptionResource = Schema.Struct({
234
+ id: Schema.String,
235
+ snippet: Schema.optional(
236
+ Schema.Struct({
237
+ videoId: Schema.optional(Schema.String),
238
+ language: Schema.optional(Schema.String),
239
+ name: Schema.optional(Schema.String),
240
+ trackKind: Schema.optional(Schema.String),
241
+ }),
242
+ ),
243
+ });
244
+
245
+ /**
246
+ * YouTube captions.list response.
247
+ */
248
+ export const CaptionsListResponse = Schema.Struct({
249
+ kind: Schema.String,
250
+ etag: Schema.String,
251
+ items: Schema.optional(Schema.Array(CaptionResource)),
252
+ });
253
+
254
+ export type CaptionsListResponse = Schema.Schema.Type<typeof CaptionsListResponse>;