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.
@@ -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 {