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 +21 -0
- package/README.md +161 -0
- package/assets/style.css +409 -0
- package/client/admin.js +403 -0
- package/client/common.js +12 -0
- package/client/video-watch.js +383 -0
- package/languages/en.json +77 -0
- package/languages/fr.json +77 -0
- package/main.js +582 -0
- package/package.json +61 -0
- package/server/ffmpeg.js +234 -0
- package/server/routes.js +709 -0
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
|
package/assets/style.css
ADDED
|
@@ -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
|
+
}
|