-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
youtubeBatchEdit.js
211 lines (182 loc) · 8.04 KB
/
youtubeBatchEdit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { getVideosLast72Hours, initYoutube, updateVideo, updateVideoThumbnail } from './youtubeAPI.js'
import fs from 'fs'
/**
* This script allow for batch edit of youtube metadata (title, desc, tags, date, thumbnail).
* The youtube video should be uploaded separately with the title matching the one from OpenPlanner.
*
* It can update:
* - title
* - description (it will format the author name + social links + abstract)
* - tags
* - recorded date
* - thumbnail from thumnails generate using https://fill-my-slides.web.app/ or your own thumbnails, located in the "miniature" folder, with the file name being the index of the talk from openplanner.json
*
* Pre-requisites:
* - Upload videos to youtube with the same title as in OpenPlanner
* - Generate thumbnails using https://fill-my-slides.web.app/ (you can get the correct format by uncomenting the line in the main : "return formatFillMySlidesData(openPlannerContent)", then put them in the "miniature" folder on the scripts folder. Don't forget to set Two images as avatar for co-speakers cases and specify which track was filmed in the "captedTrackIds" array.
* - change the playlist id "playlistId" to the one you want to edit (create it on youtube first)
* - get the full data from open planner as "openplanner.json" in scripts/openplanner.json
* - get the `scripts/client_secret.json` from https://console.developers.google.com/
* - node 18
*
* Usage:
* - cd scripts
* - npm install
* - node youtubeBatchEdit.js
*
* Help:
* - the youtube credential code looks like 4/0AcvDMrBXVxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx in the url
*/
const joinYoutubeAndOpenPlannerData = (youtubeVideos, openPlannerData) => {
// Check if all videos exist in OpenPlanner
const videosWithOpenPlannerData = youtubeVideos.map((video) => {
const videoTitle = video.snippet.title
// find session details in openplanner.json
const session = openPlannerData.sessions.find(
(session) => videoTitle.includes(session.title) || session.title.includes(videoTitle)
)
return {
...video,
session,
speakers:
session && session.speakerIds
? session.speakerIds.map((speakerId) => {
const speaker = openPlannerData.speakers.find((speaker) => speaker.id === speakerId)
return speaker
})
: [],
}
})
const videosWithValidSession = videosWithOpenPlannerData.filter(
(video) => video.session && video.speakers.length > 0
)
console.log('Matching videos: ' + videosWithValidSession.length)
console.log(
'Non matching video title or no speakers: ' +
videosWithOpenPlannerData
.filter((video) => !video.session || video.speakers.length === 0)
.map((video) => video.snippet.title)
)
return videosWithValidSession
}
const formatYoutubeDescription = (video, openPlannerContent) => {
const { session, speakers } = video
if (!session) {
return null
}
const speakersText = speakers.map((speaker) => {
const socialLink = speaker.socials.map((social) => {
if (!social.link) {
return ''
}
if (social.link.startsWith('http')) {
return social.link
}
const accountName = social.link.replace('@', '')
if (social.name === 'Twitter') {
return `https://twitter.com/${accountName}`
}
if (social.name === 'Linkedin') {
return `https://linkedin.com/in/${accountName}`
}
if (social.name === 'GitHub') {
return `https://github.com/${accountName}`
}
})[0]
return `${speaker.name} - ${socialLink}`
})
const desc = session.abstract + '\n\n' + openPlannerContent.event.name
return `${speakersText.join('\n')}\n${desc.replace(/<[^>]*>?/gm, '')}`
}
const formatFillMySlidesData = (openPlannerContent) => {
const captedTrackIds = ['lavande-lamour', 'coquelicot-amphi-106', 'olivier-amphi-108']
const keepedSessions = openPlannerContent.sessions.filter((session) => {
return session.speakerIds.length > 0 && captedTrackIds.includes(session.trackId)
})
const result = keepedSessions
.map((session) => {
const speakers = session.speakerIds.map((speakerId) => {
const speaker = openPlannerContent.speakers.find((speaker) => speaker.id === speakerId)
return speaker.name
})
const speakersAvatar = session.speakerIds.map((speakerId) => {
const speaker = openPlannerContent.speakers.find((speaker) => speaker.id === speakerId)
return speaker.photoUrl
})
if (!speakers || speakers.length === 0) {
return null
}
return {
0: session.title,
1: speakers.reverse().join(', '),
2: speakersAvatar[0],
3: speakersAvatar[1] || 'https://upload.wikimedia.org/wikipedia/commons/d/d0/Clear.gif',
}
})
.filter((session) => !!session)
console.log(JSON.stringify(result))
}
const main = async () => {
const { auth, channelId } = await initYoutube()
const playlistId = 'PLz7aCyCbFOu8_3w6EydaKkjHDiZ9Az1XR'
const videoCategoryId = '27' // use await listVideoCategories(auth)
const openPlannerFileName = 'openplanner.json'
const openPlannerContent = JSON.parse(fs.readFileSync(openPlannerFileName))
// Generate thumbnails using https://fill-my-slides.web.app/
// return formatFillMySlidesData(openPlannerContent)
const videos = await getVideosLast72Hours(auth, channelId, playlistId)
console.log('Retrieved videos: ' + videos.length)
return
const videosWithValidSession = joinYoutubeAndOpenPlannerData(videos, openPlannerContent)
const videosWithValidSessionAndDescription = videosWithValidSession
.map((video) => {
return {
...video,
description: formatYoutubeDescription(video, openPlannerContent),
}
})
.filter((video) => !!video)
// Update video metadata
for (const video of videosWithValidSessionAndDescription) {
const tagBasedOnOpenPlannerCategory = openPlannerContent.event.categories.find((category) => {
return category.id === video.session.categoryId
}).name
const updateModel = {
description: video.description,
categoryId: videoCategoryId,
defaultLanguage: 'fr',
tags: ['sunnytech', tagBasedOnOpenPlannerCategory],
recordingDetails: {
recordingDate: video.session.dateStart,
},
}
const videoId = video.snippet.resourceId.videoId
const result = await updateVideo(auth, videoId, video.snippet.title, updateModel)
if (result) {
console.log('Updated video: ' + video.snippet.title)
}
}
// Update video thumbnails
let i = 0
const videosWithValidSessionAndDescriptionOrderedFromOpenPlannerData = videosWithValidSessionAndDescription.sort(
(a, b) => {
// order by order of index in its respective array in openplanner.json
const aIndex = openPlannerContent.sessions.findIndex((session) => session.id === a.session.id)
const bIndex = openPlannerContent.sessions.findIndex((session) => session.id === b.session.id)
if (aIndex === -1 || bIndex === -1) {
return 0
}
return aIndex - bIndex
}
)
for (const video of videosWithValidSessionAndDescriptionOrderedFromOpenPlannerData) {
let thumbnailPath = `./miniature/${i}.png`
const videoId = video.snippet.resourceId.videoId
const result = await updateVideoThumbnail(auth, videoId, thumbnailPath)
if (result) {
console.log('Updated video thumbnail: ' + video.snippet.title)
}
i++
}
}
main()