@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.
- package/LICENSE +21 -0
- package/dist/lib/browser/ChannelArticle-CDQR4BBY.mjs +90 -0
- package/dist/lib/browser/ChannelArticle-CDQR4BBY.mjs.map +7 -0
- package/dist/lib/browser/ChannelSettings-ZYUNW3VS.mjs +28 -0
- package/dist/lib/browser/ChannelSettings-ZYUNW3VS.mjs.map +7 -0
- package/dist/lib/browser/VideoArticle-FC4A6E7B.mjs +76 -0
- package/dist/lib/browser/VideoArticle-FC4A6E7B.mjs.map +7 -0
- package/dist/lib/browser/VideoCard-CCPXDCB7.mjs +64 -0
- package/dist/lib/browser/VideoCard-CCPXDCB7.mjs.map +7 -0
- package/dist/lib/browser/app-graph-builder-MJY6A6SN.mjs +195 -0
- package/dist/lib/browser/app-graph-builder-MJY6A6SN.mjs.map +7 -0
- package/dist/lib/browser/blueprint-definition-FRYUYJ22.mjs +22 -0
- package/dist/lib/browser/blueprint-definition-FRYUYJ22.mjs.map +7 -0
- package/dist/lib/browser/blueprints/index.mjs +13 -0
- package/dist/lib/browser/blueprints/index.mjs.map +7 -0
- package/dist/lib/browser/chunk-C26XKDK2.mjs +355 -0
- package/dist/lib/browser/chunk-C26XKDK2.mjs.map +7 -0
- package/dist/lib/browser/chunk-DFRSBBSO.mjs +21 -0
- package/dist/lib/browser/chunk-DFRSBBSO.mjs.map +7 -0
- package/dist/lib/browser/chunk-GFRR4TTX.mjs +72 -0
- package/dist/lib/browser/chunk-GFRR4TTX.mjs.map +7 -0
- package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/browser/chunk-MUE22YUM.mjs +57 -0
- package/dist/lib/browser/chunk-MUE22YUM.mjs.map +7 -0
- package/dist/lib/browser/chunk-P67QEKBQ.mjs +72 -0
- package/dist/lib/browser/chunk-P67QEKBQ.mjs.map +7 -0
- package/dist/lib/browser/chunk-YMDT37TA.mjs +62 -0
- package/dist/lib/browser/chunk-YMDT37TA.mjs.map +7 -0
- package/dist/lib/browser/chunk-Z3DGTMKC.mjs +8 -0
- package/dist/lib/browser/chunk-Z3DGTMKC.mjs.map +7 -0
- package/dist/lib/browser/clear-synced-videos-EVMJIZPD.mjs +66 -0
- package/dist/lib/browser/clear-synced-videos-EVMJIZPD.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +153 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/react-surface-EDA5VYDC.mjs +77 -0
- package/dist/lib/browser/react-surface-EDA5VYDC.mjs.map +7 -0
- package/dist/lib/browser/sync-423Q4BDD.mjs +360 -0
- package/dist/lib/browser/sync-423Q4BDD.mjs.map +7 -0
- package/dist/lib/browser/types/index.mjs +14 -0
- package/dist/lib/browser/types/index.mjs.map +7 -0
- package/dist/lib/node-esm/ChannelArticle-GQ64BO7V.mjs +91 -0
- package/dist/lib/node-esm/ChannelArticle-GQ64BO7V.mjs.map +7 -0
- package/dist/lib/node-esm/ChannelSettings-DM2HWNKO.mjs +29 -0
- package/dist/lib/node-esm/ChannelSettings-DM2HWNKO.mjs.map +7 -0
- package/dist/lib/node-esm/VideoArticle-WLTWZO3K.mjs +77 -0
- package/dist/lib/node-esm/VideoArticle-WLTWZO3K.mjs.map +7 -0
- package/dist/lib/node-esm/VideoCard-FOWQZK75.mjs +65 -0
- package/dist/lib/node-esm/VideoCard-FOWQZK75.mjs.map +7 -0
- package/dist/lib/node-esm/app-graph-builder-IU5TBAXN.mjs +196 -0
- package/dist/lib/node-esm/app-graph-builder-IU5TBAXN.mjs.map +7 -0
- package/dist/lib/node-esm/blueprint-definition-W264MZ3D.mjs +23 -0
- package/dist/lib/node-esm/blueprint-definition-W264MZ3D.mjs.map +7 -0
- package/dist/lib/node-esm/blueprints/index.mjs +14 -0
- package/dist/lib/node-esm/blueprints/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-5KNC2JMP.mjs +58 -0
- package/dist/lib/node-esm/chunk-5KNC2JMP.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-6BUJ2DQX.mjs +73 -0
- package/dist/lib/node-esm/chunk-6BUJ2DQX.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-CX6MV3QM.mjs +23 -0
- package/dist/lib/node-esm/chunk-CX6MV3QM.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-CZSLL3XQ.mjs +63 -0
- package/dist/lib/node-esm/chunk-CZSLL3XQ.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-JM5SBBP5.mjs +356 -0
- package/dist/lib/node-esm/chunk-JM5SBBP5.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-JSGRZMG3.mjs +73 -0
- package/dist/lib/node-esm/chunk-JSGRZMG3.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-M4S6BE47.mjs +10 -0
- package/dist/lib/node-esm/chunk-M4S6BE47.mjs.map +7 -0
- package/dist/lib/node-esm/clear-synced-videos-5UCH6XHL.mjs +67 -0
- package/dist/lib/node-esm/clear-synced-videos-5UCH6XHL.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +154 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/react-surface-5DJAQPHJ.mjs +78 -0
- package/dist/lib/node-esm/react-surface-5DJAQPHJ.mjs.map +7 -0
- package/dist/lib/node-esm/sync-CEF5DX2J.mjs +361 -0
- package/dist/lib/node-esm/sync-CEF5DX2J.mjs.map +7 -0
- package/dist/lib/node-esm/types/index.mjs +15 -0
- package/dist/lib/node-esm/types/index.mjs.map +7 -0
- package/dist/types/src/YouTubePlugin.d.ts +3 -0
- package/dist/types/src/YouTubePlugin.d.ts.map +1 -0
- package/dist/types/src/blueprints/index.d.ts +3 -0
- package/dist/types/src/blueprints/index.d.ts.map +1 -0
- package/dist/types/src/blueprints/youtube.d.ts +4 -0
- package/dist/types/src/blueprints/youtube.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +6 -0
- package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder/index.d.ts +3 -0
- package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts +6 -0
- package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts.map +1 -0
- package/dist/types/src/capabilities/blueprint-definition/index.d.ts +3 -0
- package/dist/types/src/capabilities/blueprint-definition/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +4 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
- package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
- package/dist/types/src/containers/ChannelArticle/ChannelArticle.d.ts +8 -0
- package/dist/types/src/containers/ChannelArticle/ChannelArticle.d.ts.map +1 -0
- package/dist/types/src/containers/ChannelArticle/index.d.ts +3 -0
- package/dist/types/src/containers/ChannelArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/ChannelSettings/ChannelSettings.d.ts +7 -0
- package/dist/types/src/containers/ChannelSettings/ChannelSettings.d.ts.map +1 -0
- package/dist/types/src/containers/ChannelSettings/index.d.ts +3 -0
- package/dist/types/src/containers/ChannelSettings/index.d.ts.map +1 -0
- package/dist/types/src/containers/VideoArticle/VideoArticle.d.ts +11 -0
- package/dist/types/src/containers/VideoArticle/VideoArticle.d.ts.map +1 -0
- package/dist/types/src/containers/VideoArticle/index.d.ts +3 -0
- package/dist/types/src/containers/VideoArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/VideoCard/VideoCard.d.ts +9 -0
- package/dist/types/src/containers/VideoCard/VideoCard.d.ts.map +1 -0
- package/dist/types/src/containers/VideoCard/index.d.ts +3 -0
- package/dist/types/src/containers/VideoCard/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +11 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +3 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/operations/apis/index.d.ts +2 -0
- package/dist/types/src/operations/apis/index.d.ts.map +1 -0
- package/dist/types/src/operations/apis/youtube/api.d.ts +251 -0
- package/dist/types/src/operations/apis/youtube/api.d.ts.map +1 -0
- package/dist/types/src/operations/apis/youtube/index.d.ts +3 -0
- package/dist/types/src/operations/apis/youtube/index.d.ts.map +1 -0
- package/dist/types/src/operations/apis/youtube/types.d.ts +493 -0
- package/dist/types/src/operations/apis/youtube/types.d.ts.map +1 -0
- package/dist/types/src/operations/clear-synced-videos.d.ts +5 -0
- package/dist/types/src/operations/clear-synced-videos.d.ts.map +1 -0
- package/dist/types/src/operations/definitions.d.ts +45 -0
- package/dist/types/src/operations/definitions.d.ts.map +1 -0
- package/dist/types/src/operations/index.d.ts +5 -0
- package/dist/types/src/operations/index.d.ts.map +1 -0
- package/dist/types/src/operations/services/google-credentials.d.ts +35 -0
- package/dist/types/src/operations/services/google-credentials.d.ts.map +1 -0
- package/dist/types/src/operations/services/index.d.ts +2 -0
- package/dist/types/src/operations/services/index.d.ts.map +1 -0
- package/dist/types/src/operations/sync.d.ts +5 -0
- package/dist/types/src/operations/sync.d.ts.map +1 -0
- package/dist/types/src/operations/sync.test.d.ts +2 -0
- package/dist/types/src/operations/sync.test.d.ts.map +1 -0
- package/dist/types/src/operations/transcript.d.ts +12 -0
- package/dist/types/src/operations/transcript.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +49 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/Channel.d.ts +54 -0
- package/dist/types/src/types/Channel.d.ts.map +1 -0
- package/dist/types/src/types/Video.d.ts +41 -0
- package/dist/types/src/types/Video.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +4 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/types/types.d.ts +5 -0
- package/dist/types/src/types/types.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +113 -0
- package/src/YouTubePlugin.tsx +66 -0
- package/src/blueprints/index.ts +7 -0
- package/src/blueprints/youtube.ts +52 -0
- package/src/capabilities/app-graph-builder/app-graph-builder.ts +148 -0
- package/src/capabilities/app-graph-builder/index.ts +7 -0
- package/src/capabilities/blueprint-definition/blueprint-definition.ts +17 -0
- package/src/capabilities/blueprint-definition/index.ts +7 -0
- package/src/capabilities/index.ts +7 -0
- package/src/capabilities/react-surface/index.ts +7 -0
- package/src/capabilities/react-surface/react-surface.tsx +54 -0
- package/src/containers/ChannelArticle/ChannelArticle.tsx +115 -0
- package/src/containers/ChannelArticle/index.ts +7 -0
- package/src/containers/ChannelSettings/ChannelSettings.tsx +34 -0
- package/src/containers/ChannelSettings/index.ts +7 -0
- package/src/containers/VideoArticle/VideoArticle.tsx +109 -0
- package/src/containers/VideoArticle/index.ts +7 -0
- package/src/containers/VideoCard/VideoCard.tsx +86 -0
- package/src/containers/VideoCard/index.ts +7 -0
- package/src/containers/index.ts +24 -0
- package/src/index.ts +8 -0
- package/src/meta.ts +19 -0
- package/src/operations/apis/index.ts +5 -0
- package/src/operations/apis/youtube/api.ts +149 -0
- package/src/operations/apis/youtube/index.ts +6 -0
- package/src/operations/apis/youtube/types.ts +254 -0
- package/src/operations/clear-synced-videos.ts +50 -0
- package/src/operations/definitions.ts +62 -0
- package/src/operations/index.ts +13 -0
- package/src/operations/services/google-credentials.ts +81 -0
- package/src/operations/services/index.ts +5 -0
- package/src/operations/sync.test.ts +114 -0
- package/src/operations/sync.ts +309 -0
- package/src/operations/transcript.ts +47 -0
- package/src/translations.ts +59 -0
- package/src/types/Channel.ts +80 -0
- package/src/types/Video.ts +67 -0
- package/src/types/index.ts +8 -0
- 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,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,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
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,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,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>;
|