peertube-plugin-sponsorblock 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 Jean-Baptiste L.
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published
8
+ by the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ ---
20
+
21
+ Full license text: https://www.gnu.org/licenses/agpl-3.0.txt
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # PeerTube Plugin SponsorBlock
2
+
3
+ PeerTube plugin to integrate SponsorBlock and automatically skip (or permanently remove) sponsor segments from videos imported from YouTube.
4
+
5
+ ## Goal
6
+
7
+ Allow PeerTube instances to leverage the crowdsourced SponsorBlock database to improve the viewing experience of videos imported from YouTube.
8
+
9
+ ## Project Status
10
+
11
+ **Under active development**
12
+
13
+ Phase 1 (client-side skip), Phase 2 (admin dashboard & periodic sync), and Phase 3 (permanent removal) are implemented.
14
+
15
+ See the [User Guide](USER_GUIDE.md) for installation and usage instructions.
16
+
17
+ ## Features
18
+
19
+ ### Phase 1: MVP (Client-side skip)
20
+ - Automatic YouTube ID detection on import
21
+ - SponsorBlock segment retrieval via API
22
+ - Automatic segment skipping in the video player
23
+ - Local segment caching
24
+ - Per-category configuration (sponsor, intro, outro, etc.)
25
+
26
+ ### Phase 2: Admin dashboard & sync
27
+ - Admin dashboard with statistics and mappings table
28
+ - Periodic sync with SponsorBlock (configurable interval)
29
+ - Manual "Scan Imports" to map existing videos
30
+ - Per-row Sync / Process / Delete actions
31
+ - Color-coded category markers on the progress bar
32
+
33
+ ### Phase 3: Permanent removal
34
+ - Background processing worker (30s polling)
35
+ - Priority queue with retries
36
+ - FFmpeg cutting (`-c copy`) and concatenation
37
+ - Support for web-videos, HLS, and original files
38
+ - API routes: single and bulk processing
39
+ - Automatic processing on import (in `remove` mode)
40
+ - Configurable `storage_path` setting
41
+
42
+ ## Architecture
43
+
44
+ ### Main components
45
+
46
+ 1. **YouTube-to-PeerTube mapping table**
47
+ ```sql
48
+ plugin_sponsorblock_mapping (peertube_uuid, youtube_id)
49
+ ```
50
+
51
+ 2. **SponsorBlock segments cache**
52
+ ```sql
53
+ plugin_sponsorblock_segments (youtube_id, start_time, end_time, category)
54
+ ```
55
+
56
+ 3. **FFmpeg processing queue**
57
+ ```sql
58
+ plugin_sponsorblock_processing_queue (video_uuid, segments, status, priority)
59
+ ```
60
+
61
+ 4. **Import hooks**
62
+ - Captures the YouTube ID on import
63
+ - Automatic segment retrieval
64
+ - Automatic queue insertion in `remove` mode
65
+
66
+ 5. **Processing worker**
67
+ - 30s polling (active only in `remove` mode)
68
+ - Optimistic locking (`FOR UPDATE SKIP LOCKED`)
69
+ - Automatic retry (3 attempts max)
70
+
71
+ 6. **Video player integration**
72
+ - Automatic segment skipping during playback
73
+ - Visual notifications
74
+
75
+ 7. **Admin dashboard**
76
+ - Stats cards (mapped videos, segments, time saved, queue pending)
77
+ - Mappings table with per-row actions
78
+ - Bulk actions (Scan Imports, Sync All, Process All)
79
+ - Periodic sync timer (configurable)
80
+
81
+ ## Documentation
82
+
83
+ - [User Guide](USER_GUIDE.md) — Installation, configuration, and usage guide for instance administrators
84
+ - [Development Guide](DEVELOPMENT.md) — Development setup, project structure, testing, and contributing
85
+ - [Changelog](CHANGELOG.md) — Version history and release notes
86
+ - [TODO](TODO.md) — Roadmap, planned features, and known issues
87
+ - [Research](RESEARCH.md) — State-of-the-art research and PeerTube plugin capabilities
88
+ - [Technical Analysis](TECHNICAL_ANALYSIS.md) — Technical analysis of permanent segment removal with FFmpeg
89
+
90
+ ## Research highlights
91
+
92
+ ### State of the art
93
+
94
+ **No native SponsorBlock plugin for PeerTube currently exists.**
95
+
96
+ Similar projects:
97
+ - **peertube-plugin-chapters**: Manual chapters (not crowdsourced)
98
+ - **Tubular**: Android app with SponsorBlock + PeerTube support
99
+
100
+ Open feature requests since 2020:
101
+ - [ajayyy/SponsorBlock#1209](https://github.com/ajayyy/SponsorBlock/issues/1209)
102
+ - [ajayyy/SponsorBlock#1938](https://github.com/ajayyy/SponsorBlock/issues/1938)
103
+ - [ajayyy/SponsorBlock#993](https://github.com/ajayyy/SponsorBlock/issues/993)
104
+
105
+ ### PeerTube capabilities
106
+
107
+ The PeerTube plugin system supports:
108
+ - Import hooks (`filter:api.video.post-import-url.accept.result`)
109
+ - Video player hooks (`action:video-watch.video.loaded`)
110
+ - Database access (custom table creation)
111
+ - External HTTP requests (SponsorBlock API)
112
+ - UI modification
113
+
114
+ ## Technologies
115
+
116
+ - **PeerTube**: Decentralized video platform
117
+ - **SponsorBlock API**: https://sponsor.ajay.app/api/
118
+ - **FFmpeg/ffprobe**: For permanent segment removal
119
+ - **PostgreSQL**: PeerTube database
120
+ - **Node.js**: Plugin runtime
121
+
122
+ ## Resources
123
+
124
+ ### PeerTube documentation
125
+ - [Plugin Guide](https://docs.joinpeertube.org/contribute/plugins)
126
+ - [Plugin API](https://docs.joinpeertube.org/api/plugins)
127
+ - [Architecture](https://docs.joinpeertube.org/contribute/architecture)
128
+
129
+ ### SponsorBlock
130
+ - [API Documentation](https://wiki.sponsor.ajay.app/w/API_Docs)
131
+ - [Source Code](https://github.com/ajayyy/SponsorBlock)
132
+
133
+ ## Contributing
134
+
135
+ Contributions are welcome:
136
+ - PeerTube experience and feedback
137
+ - FFmpeg expertise
138
+ - Testing on development PeerTube instances
139
+
140
+ ## License
141
+
142
+ AGPL-3.0 (for compatibility with PeerTube)
143
+
144
+ ## Warnings
145
+
146
+ ### "Skip" mode (Phase 1)
147
+ - Segments are still downloaded (no bandwidth savings)
148
+ - Works only in the PeerTube web player
149
+
150
+ ### "Permanent removal" mode
151
+ - Irreversible modification of video files
152
+ - Uses `ffmpeg -c copy` (remuxing without re-encoding, fast and lossless)
153
+ - Comment timestamps will be shifted after removal
154
+ - Automatic retry (3 attempts) on error
155
+ - **Recommended only with automatic backups**
156
+ - Requires `ffmpeg` and `ffprobe` in the `PATH`
157
+
158
+ ---
159
+
160
+ **Author**: Jean-Baptiste L.
161
+ **Created**: 2026-01-31
@@ -0,0 +1,409 @@
1
+ /**
2
+ * PeerTube Plugin SponsorBlock
3
+ * Custom styles
4
+ */
5
+
6
+ /* Segment markers on progress bar */
7
+ .sponsorblock-marker {
8
+ position: absolute;
9
+ top: 0;
10
+ bottom: 0;
11
+ background-color: rgba(0, 255, 0, 0.6);
12
+ pointer-events: none;
13
+ z-index: 30;
14
+ transition: background-color 0.2s ease;
15
+ }
16
+
17
+ .sponsorblock-marker:hover {
18
+ background-color: rgba(0, 255, 0, 0.8);
19
+ }
20
+
21
+ /* Sponsor category colors */
22
+ .sponsorblock-marker[data-category="sponsor"] {
23
+ background-color: rgba(0, 255, 0, 0.6);
24
+ }
25
+
26
+ .sponsorblock-marker[data-category="selfpromo"] {
27
+ background-color: rgba(255, 255, 0, 0.6);
28
+ }
29
+
30
+ .sponsorblock-marker[data-category="interaction"] {
31
+ background-color: rgba(0, 191, 255, 0.6);
32
+ }
33
+
34
+ .sponsorblock-marker[data-category="intro"] {
35
+ background-color: rgba(0, 255, 255, 0.6);
36
+ }
37
+
38
+ .sponsorblock-marker[data-category="outro"] {
39
+ background-color: rgba(0, 32, 255, 0.6);
40
+ }
41
+
42
+ .sponsorblock-marker[data-category="preview"] {
43
+ background-color: rgba(255, 128, 0, 0.6);
44
+ }
45
+
46
+ .sponsorblock-marker[data-category="music_offtopic"] {
47
+ background-color: rgba(255, 0, 255, 0.6);
48
+ }
49
+
50
+ .sponsorblock-marker[data-category="filler"] {
51
+ background-color: rgba(127, 0, 255, 0.6);
52
+ }
53
+
54
+ /* Skip notification styling */
55
+ .sponsorblock-notification {
56
+ position: fixed;
57
+ bottom: 80px;
58
+ right: 20px;
59
+ background-color: rgba(0, 0, 0, 0.8);
60
+ color: white;
61
+ padding: 10px 20px;
62
+ border-radius: 5px;
63
+ font-size: 14px;
64
+ z-index: 9999;
65
+ animation: fadeInOut 3s ease-in-out;
66
+ }
67
+
68
+ @keyframes fadeInOut {
69
+ 0% {
70
+ opacity: 0;
71
+ transform: translateY(10px);
72
+ }
73
+ 10% {
74
+ opacity: 1;
75
+ transform: translateY(0);
76
+ }
77
+ 90% {
78
+ opacity: 1;
79
+ transform: translateY(0);
80
+ }
81
+ 100% {
82
+ opacity: 0;
83
+ transform: translateY(-10px);
84
+ }
85
+ }
86
+
87
+ /* SponsorBlock mapping widget */
88
+ .sponsorblock-widget {
89
+ margin-top: 10px;
90
+ font-size: 13px;
91
+ }
92
+
93
+ .sponsorblock-widget-toggle {
94
+ cursor: pointer;
95
+ color: #999;
96
+ user-select: none;
97
+ display: inline-block;
98
+ font-weight: 600;
99
+ }
100
+
101
+ .sponsorblock-widget-toggle:hover {
102
+ color: #ccc;
103
+ }
104
+
105
+ .sponsorblock-widget-form {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 8px;
109
+ margin-top: 6px;
110
+ }
111
+
112
+ .sponsorblock-widget-input {
113
+ font-family: monospace;
114
+ font-size: 13px;
115
+ padding: 4px 8px;
116
+ border: 1px solid #555;
117
+ border-radius: 4px;
118
+ background: #1a1a1a;
119
+ color: #eee;
120
+ width: 280px;
121
+ }
122
+
123
+ .sponsorblock-widget-input:focus {
124
+ outline: none;
125
+ border-color: #00b300;
126
+ }
127
+
128
+ .sponsorblock-widget-btn {
129
+ padding: 4px 14px;
130
+ border: none;
131
+ border-radius: 4px;
132
+ background: #00b300;
133
+ color: #fff;
134
+ font-size: 13px;
135
+ font-weight: 600;
136
+ cursor: pointer;
137
+ }
138
+
139
+ .sponsorblock-widget-btn:hover {
140
+ background: #009a00;
141
+ }
142
+
143
+ .sponsorblock-widget-btn:disabled {
144
+ background: #555;
145
+ cursor: not-allowed;
146
+ }
147
+
148
+ .sponsorblock-widget-status {
149
+ margin-top: 4px;
150
+ font-size: 12px;
151
+ }
152
+
153
+ .sponsorblock-widget-status.success {
154
+ color: #00b300;
155
+ }
156
+
157
+ .sponsorblock-widget-status.error {
158
+ color: #e74c3c;
159
+ }
160
+
161
+ .sponsorblock-widget-current {
162
+ margin-top: 6px;
163
+ color: #999;
164
+ font-size: 12px;
165
+ }
166
+
167
+ .sponsorblock-widget-current code {
168
+ background: #2a2a2a;
169
+ padding: 1px 5px;
170
+ border-radius: 3px;
171
+ font-family: monospace;
172
+ color: #ccc;
173
+ }
174
+
175
+ /* ==========================================
176
+ Admin Dashboard
177
+ ========================================== */
178
+
179
+ .sponsorblock-admin-root {
180
+ padding: 20px 0;
181
+ font-size: 14px;
182
+ color: #eee;
183
+ }
184
+
185
+ /* Stats row */
186
+ .sponsorblock-admin-stats {
187
+ display: flex;
188
+ gap: 16px;
189
+ flex-wrap: wrap;
190
+ margin-bottom: 24px;
191
+ }
192
+
193
+ .sponsorblock-admin-stat {
194
+ background: #1a1a1a;
195
+ border: 1px solid #333;
196
+ border-radius: 8px;
197
+ padding: 16px 20px;
198
+ min-width: 160px;
199
+ flex: 1;
200
+ }
201
+
202
+ .sponsorblock-admin-stat-value {
203
+ font-size: 28px;
204
+ font-weight: 700;
205
+ color: #00b300;
206
+ line-height: 1.2;
207
+ }
208
+
209
+ .sponsorblock-admin-stat-label {
210
+ font-size: 12px;
211
+ color: #999;
212
+ margin-top: 4px;
213
+ text-transform: uppercase;
214
+ letter-spacing: 0.5px;
215
+ }
216
+
217
+ /* Action buttons bar */
218
+ .sponsorblock-admin-actions {
219
+ display: flex;
220
+ gap: 10px;
221
+ flex-wrap: wrap;
222
+ margin-bottom: 24px;
223
+ }
224
+
225
+ .sponsorblock-admin-btn {
226
+ padding: 8px 18px;
227
+ border: none;
228
+ border-radius: 4px;
229
+ font-size: 13px;
230
+ font-weight: 600;
231
+ cursor: pointer;
232
+ color: #fff;
233
+ background: #00b300;
234
+ transition: background 0.15s;
235
+ }
236
+
237
+ .sponsorblock-admin-btn:hover {
238
+ background: #009a00;
239
+ }
240
+
241
+ .sponsorblock-admin-btn:disabled {
242
+ background: #555;
243
+ cursor: not-allowed;
244
+ }
245
+
246
+ .sponsorblock-admin-btn--secondary {
247
+ background: #2a6dd4;
248
+ }
249
+
250
+ .sponsorblock-admin-btn--secondary:hover {
251
+ background: #1f5bb8;
252
+ }
253
+
254
+ .sponsorblock-admin-btn--danger {
255
+ background: #e74c3c;
256
+ }
257
+
258
+ .sponsorblock-admin-btn--danger:hover {
259
+ background: #c0392b;
260
+ }
261
+
262
+ /* Status message */
263
+ .sponsorblock-admin-message {
264
+ margin-bottom: 16px;
265
+ padding: 10px 14px;
266
+ border-radius: 4px;
267
+ font-size: 13px;
268
+ }
269
+
270
+ .sponsorblock-admin-message--success {
271
+ background: rgba(0, 179, 0, 0.15);
272
+ color: #00b300;
273
+ border: 1px solid rgba(0, 179, 0, 0.3);
274
+ }
275
+
276
+ .sponsorblock-admin-message--error {
277
+ background: rgba(231, 76, 60, 0.15);
278
+ color: #e74c3c;
279
+ border: 1px solid rgba(231, 76, 60, 0.3);
280
+ }
281
+
282
+ /* Mappings table */
283
+ .sponsorblock-admin-table-wrap {
284
+ overflow-x: auto;
285
+ }
286
+
287
+ .sponsorblock-admin-table {
288
+ width: 100%;
289
+ border-collapse: collapse;
290
+ font-size: 13px;
291
+ }
292
+
293
+ .sponsorblock-admin-table th {
294
+ text-align: left;
295
+ padding: 10px 12px;
296
+ border-bottom: 2px solid #333;
297
+ color: #999;
298
+ font-size: 11px;
299
+ text-transform: uppercase;
300
+ letter-spacing: 0.5px;
301
+ white-space: nowrap;
302
+ }
303
+
304
+ .sponsorblock-admin-table td {
305
+ padding: 10px 12px;
306
+ border-bottom: 1px solid #222;
307
+ vertical-align: middle;
308
+ }
309
+
310
+ .sponsorblock-admin-table tr:hover td {
311
+ background: rgba(255, 255, 255, 0.03);
312
+ }
313
+
314
+ .sponsorblock-admin-table code {
315
+ background: #2a2a2a;
316
+ padding: 2px 6px;
317
+ border-radius: 3px;
318
+ font-family: monospace;
319
+ font-size: 12px;
320
+ color: #ccc;
321
+ }
322
+
323
+ /* Row action buttons */
324
+ .sponsorblock-admin-row-actions {
325
+ display: flex;
326
+ gap: 6px;
327
+ }
328
+
329
+ .sponsorblock-admin-row-btn {
330
+ padding: 4px 10px;
331
+ border: none;
332
+ border-radius: 3px;
333
+ font-size: 11px;
334
+ font-weight: 600;
335
+ cursor: pointer;
336
+ color: #fff;
337
+ background: #444;
338
+ transition: background 0.15s;
339
+ }
340
+
341
+ .sponsorblock-admin-row-btn:hover {
342
+ background: #555;
343
+ }
344
+
345
+ .sponsorblock-admin-row-btn--sync {
346
+ background: #2a6dd4;
347
+ }
348
+
349
+ .sponsorblock-admin-row-btn--sync:hover {
350
+ background: #1f5bb8;
351
+ }
352
+
353
+ .sponsorblock-admin-row-btn--process {
354
+ background: #00b300;
355
+ }
356
+
357
+ .sponsorblock-admin-row-btn--process:hover {
358
+ background: #009a00;
359
+ }
360
+
361
+ .sponsorblock-admin-row-btn--delete {
362
+ background: #e74c3c;
363
+ }
364
+
365
+ .sponsorblock-admin-row-btn--delete:hover {
366
+ background: #c0392b;
367
+ }
368
+
369
+ .sponsorblock-admin-row-btn:disabled {
370
+ background: #555;
371
+ cursor: not-allowed;
372
+ }
373
+
374
+ /* Queue status badges */
375
+ .sponsorblock-admin-badge {
376
+ display: inline-block;
377
+ padding: 2px 8px;
378
+ border-radius: 10px;
379
+ font-size: 11px;
380
+ font-weight: 600;
381
+ }
382
+
383
+ .sponsorblock-admin-badge--pending {
384
+ background: rgba(255, 193, 7, 0.2);
385
+ color: #ffc107;
386
+ }
387
+
388
+ .sponsorblock-admin-badge--processing {
389
+ background: rgba(42, 109, 212, 0.2);
390
+ color: #5b9aef;
391
+ }
392
+
393
+ .sponsorblock-admin-badge--done {
394
+ background: rgba(0, 179, 0, 0.2);
395
+ color: #00b300;
396
+ }
397
+
398
+ .sponsorblock-admin-badge--error {
399
+ background: rgba(231, 76, 60, 0.2);
400
+ color: #e74c3c;
401
+ }
402
+
403
+ /* Empty state */
404
+ .sponsorblock-admin-empty {
405
+ text-align: center;
406
+ color: #666;
407
+ padding: 40px 20px;
408
+ font-size: 14px;
409
+ }