spectrawl 0.1.2 → 0.2.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/package.json +1 -1
- package/src/act/adapters/alternativeto.js +64 -0
- package/src/act/adapters/betalist.js +66 -0
- package/src/act/adapters/devhunt.js +56 -0
- package/src/act/adapters/directory.js +219 -0
- package/src/act/adapters/discord.js +131 -0
- package/src/act/adapters/github.js +152 -0
- package/src/act/adapters/hackernews.js +161 -0
- package/src/act/adapters/huggingface.js +103 -0
- package/src/act/adapters/medium.js +107 -0
- package/src/act/adapters/producthunt.js +143 -0
- package/src/act/adapters/quora.js +89 -0
- package/src/act/adapters/saashub.js +47 -0
- package/src/act/adapters/youtube.js +158 -0
- package/src/act/index.js +30 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const https = require('https')
|
|
2
|
+
const { URL } = require('url')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* YouTube platform adapter.
|
|
6
|
+
* Uses YouTube Data API v3 for video metadata, comments, playlists.
|
|
7
|
+
* Video uploads require OAuth2 + resumable upload (complex).
|
|
8
|
+
*
|
|
9
|
+
* Simpler actions (comment, playlist, community post) are API-based.
|
|
10
|
+
*/
|
|
11
|
+
class YouTubeAdapter {
|
|
12
|
+
async execute(action, params, ctx) {
|
|
13
|
+
switch (action) {
|
|
14
|
+
case 'comment':
|
|
15
|
+
return this._comment(params, ctx)
|
|
16
|
+
case 'reply':
|
|
17
|
+
return this._reply(params, ctx)
|
|
18
|
+
case 'create-playlist':
|
|
19
|
+
return this._createPlaylist(params, ctx)
|
|
20
|
+
case 'add-to-playlist':
|
|
21
|
+
return this._addToPlaylist(params, ctx)
|
|
22
|
+
case 'update-video':
|
|
23
|
+
return this._updateVideo(params, ctx)
|
|
24
|
+
default:
|
|
25
|
+
throw new Error(`Unsupported YouTube action: ${action}. Supported: comment, reply, create-playlist, add-to-playlist, update-video`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async _comment(params, ctx) {
|
|
30
|
+
const { videoId, text, account } = params
|
|
31
|
+
const token = this._getToken(account, ctx)
|
|
32
|
+
|
|
33
|
+
const data = await ytApi('POST', '/commentThreads?part=snippet', {
|
|
34
|
+
snippet: {
|
|
35
|
+
videoId,
|
|
36
|
+
topLevelComment: {
|
|
37
|
+
snippet: { textOriginal: text }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, token)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
commentId: data.id,
|
|
44
|
+
videoId
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async _reply(params, ctx) {
|
|
49
|
+
const { parentId, text, account } = params
|
|
50
|
+
const token = this._getToken(account, ctx)
|
|
51
|
+
|
|
52
|
+
const data = await ytApi('POST', '/comments?part=snippet', {
|
|
53
|
+
snippet: {
|
|
54
|
+
parentId,
|
|
55
|
+
textOriginal: text
|
|
56
|
+
}
|
|
57
|
+
}, token)
|
|
58
|
+
|
|
59
|
+
return { commentId: data.id }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async _createPlaylist(params, ctx) {
|
|
63
|
+
const { title, description, privacyStatus, account } = params
|
|
64
|
+
const token = this._getToken(account, ctx)
|
|
65
|
+
|
|
66
|
+
const data = await ytApi('POST', '/playlists?part=snippet,status', {
|
|
67
|
+
snippet: {
|
|
68
|
+
title,
|
|
69
|
+
description: description || ''
|
|
70
|
+
},
|
|
71
|
+
status: {
|
|
72
|
+
privacyStatus: privacyStatus || 'public'
|
|
73
|
+
}
|
|
74
|
+
}, token)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
playlistId: data.id,
|
|
78
|
+
url: `https://www.youtube.com/playlist?list=${data.id}`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async _addToPlaylist(params, ctx) {
|
|
83
|
+
const { playlistId, videoId, account } = params
|
|
84
|
+
const token = this._getToken(account, ctx)
|
|
85
|
+
|
|
86
|
+
const data = await ytApi('POST', '/playlistItems?part=snippet', {
|
|
87
|
+
snippet: {
|
|
88
|
+
playlistId,
|
|
89
|
+
resourceId: {
|
|
90
|
+
kind: 'youtube#video',
|
|
91
|
+
videoId
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, token)
|
|
95
|
+
|
|
96
|
+
return { itemId: data.id }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _updateVideo(params, ctx) {
|
|
100
|
+
const { videoId, title, description, tags, categoryId, account } = params
|
|
101
|
+
const token = this._getToken(account, ctx)
|
|
102
|
+
|
|
103
|
+
const snippet = { videoId }
|
|
104
|
+
if (title) snippet.title = title
|
|
105
|
+
if (description) snippet.description = description
|
|
106
|
+
if (tags) snippet.tags = tags
|
|
107
|
+
if (categoryId) snippet.categoryId = categoryId
|
|
108
|
+
|
|
109
|
+
const data = await ytApi('PUT', '/videos?part=snippet', {
|
|
110
|
+
id: videoId,
|
|
111
|
+
snippet
|
|
112
|
+
}, token)
|
|
113
|
+
|
|
114
|
+
return { videoId: data.id }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_getToken(account, ctx) {
|
|
118
|
+
if (account?.accessToken) return account.accessToken
|
|
119
|
+
const token = ctx?.config?.accounts?.youtube?.accessToken ||
|
|
120
|
+
process.env.YOUTUBE_ACCESS_TOKEN
|
|
121
|
+
if (!token) throw new Error('YouTube OAuth access token required')
|
|
122
|
+
return token
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ytApi(method, path, body, token) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const json = JSON.stringify(body)
|
|
129
|
+
const opts = {
|
|
130
|
+
hostname: 'www.googleapis.com',
|
|
131
|
+
path: `/youtube/v3${path}`,
|
|
132
|
+
method,
|
|
133
|
+
headers: {
|
|
134
|
+
'Authorization': `Bearer ${token}`,
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'Content-Length': Buffer.byteLength(json)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const req = https.request(opts, res => {
|
|
140
|
+
let data = ''
|
|
141
|
+
res.on('data', c => data += c)
|
|
142
|
+
res.on('end', () => {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(data)
|
|
145
|
+
if (res.statusCode >= 400) {
|
|
146
|
+
reject(new Error(`YouTube API ${res.statusCode}: ${parsed.error?.message || data.slice(0, 200)}`))
|
|
147
|
+
} else { resolve(parsed) }
|
|
148
|
+
} catch (e) { reject(new Error(`Invalid YouTube response: ${data.slice(0, 200)}`)) }
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
req.on('error', reject)
|
|
152
|
+
req.setTimeout(15000, () => { req.destroy(); reject(new Error('YouTube API timeout')) })
|
|
153
|
+
req.write(json)
|
|
154
|
+
req.end()
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { YouTubeAdapter }
|
package/src/act/index.js
CHANGED
|
@@ -11,6 +11,19 @@ const { DevtoAdapter } = require('./adapters/devto')
|
|
|
11
11
|
const { HashnodeAdapter } = require('./adapters/hashnode')
|
|
12
12
|
const { LinkedInAdapter } = require('./adapters/linkedin')
|
|
13
13
|
const { IHAdapter } = require('./adapters/ih')
|
|
14
|
+
const { MediumAdapter } = require('./adapters/medium')
|
|
15
|
+
const { GitHubAdapter } = require('./adapters/github')
|
|
16
|
+
const { DiscordAdapter } = require('./adapters/discord')
|
|
17
|
+
const { ProductHuntAdapter } = require('./adapters/producthunt')
|
|
18
|
+
const { HackerNewsAdapter } = require('./adapters/hackernews')
|
|
19
|
+
const { YouTubeAdapter } = require('./adapters/youtube')
|
|
20
|
+
const { QuoraAdapter } = require('./adapters/quora')
|
|
21
|
+
const { HuggingFaceAdapter } = require('./adapters/huggingface')
|
|
22
|
+
const { BetaListAdapter } = require('./adapters/betalist')
|
|
23
|
+
const { AlternativeToAdapter } = require('./adapters/alternativeto')
|
|
24
|
+
const { SaaSHubAdapter } = require('./adapters/saashub')
|
|
25
|
+
const { DevHuntAdapter } = require('./adapters/devhunt')
|
|
26
|
+
const { DirectoryAdapter } = require('./adapters/directory')
|
|
14
27
|
const { RateLimiter } = require('./rate-limiter')
|
|
15
28
|
|
|
16
29
|
const adapters = {
|
|
@@ -22,7 +35,23 @@ const adapters = {
|
|
|
22
35
|
hashnode: new HashnodeAdapter(),
|
|
23
36
|
linkedin: new LinkedInAdapter(),
|
|
24
37
|
ih: new IHAdapter(),
|
|
25
|
-
indiehackers: new IHAdapter()
|
|
38
|
+
indiehackers: new IHAdapter(),
|
|
39
|
+
medium: new MediumAdapter(),
|
|
40
|
+
github: new GitHubAdapter(),
|
|
41
|
+
discord: new DiscordAdapter(),
|
|
42
|
+
producthunt: new ProductHuntAdapter(),
|
|
43
|
+
'product-hunt': new ProductHuntAdapter(),
|
|
44
|
+
hackernews: new HackerNewsAdapter(),
|
|
45
|
+
hn: new HackerNewsAdapter(),
|
|
46
|
+
youtube: new YouTubeAdapter(),
|
|
47
|
+
quora: new QuoraAdapter(),
|
|
48
|
+
huggingface: new HuggingFaceAdapter(),
|
|
49
|
+
hf: new HuggingFaceAdapter(),
|
|
50
|
+
betalist: new BetaListAdapter(),
|
|
51
|
+
alternativeto: new AlternativeToAdapter(),
|
|
52
|
+
saashub: new SaaSHubAdapter(),
|
|
53
|
+
devhunt: new DevHuntAdapter(),
|
|
54
|
+
directory: new DirectoryAdapter()
|
|
26
55
|
}
|
|
27
56
|
|
|
28
57
|
class ActEngine {
|