Automatically scrolls through X feed and opens posts mentioning video editor needs in new tabs
Size
10.8 KB
Version
1.1.1
Created
Mar 16, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name X Video Editor Job Finder
3// @description Automatically scrolls through X feed and opens posts mentioning video editor needs in new tabs
4// @version 1.1.1
5// @match https://*.x.com/*
6// @icon https://abs.twimg.com/favicons/twitter.3.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 let isScanning = false;
12 let processedPosts = new Set();
13 let openedTabs = new Set(); // Track opened tabs to prevent duplicates
14 let scanButton = null;
15
16 // Debounce function to prevent excessive calls
17 function debounce(func, wait) {
18 let timeout;
19 return function executedFunction(...args) {
20 const later = () => {
21 clearTimeout(timeout);
22 func(...args);
23 };
24 clearTimeout(timeout);
25 timeout = setTimeout(later, wait);
26 };
27 }
28
29 // Create UI control button
30 function createControlButton() {
31 const button = document.createElement('button');
32 button.id = 'video-editor-scanner-btn';
33 button.textContent = '▶ Start Video Editor Scanner';
34 button.style.cssText = `
35 position: fixed;
36 bottom: 20px;
37 right: 20px;
38 z-index: 10000;
39 background: linear-gradient(135deg, #1DA1F2 0%, #0d8bd9 100%);
40 color: white;
41 border: none;
42 padding: 12px 20px;
43 border-radius: 25px;
44 font-size: 14px;
45 font-weight: bold;
46 cursor: pointer;
47 box-shadow: 0 4px 12px rgba(29, 161, 242, 0.4);
48 transition: all 0.3s ease;
49 `;
50
51 button.addEventListener('mouseenter', () => {
52 button.style.transform = 'translateY(-2px)';
53 button.style.boxShadow = '0 6px 16px rgba(29, 161, 242, 0.5)';
54 });
55
56 button.addEventListener('mouseleave', () => {
57 button.style.transform = 'translateY(0)';
58 button.style.boxShadow = '0 4px 12px rgba(29, 161, 242, 0.4)';
59 });
60
61 button.addEventListener('click', toggleScanning);
62 document.body.appendChild(button);
63 return button;
64 }
65
66 // Toggle scanning on/off
67 function toggleScanning() {
68 isScanning = !isScanning;
69
70 if (isScanning) {
71 scanButton.textContent = '⏸ Stop Scanner';
72 scanButton.style.background = 'linear-gradient(135deg, #f44336 0%, #d32f2f 100%)';
73 console.log('Video Editor Scanner: Started');
74 startScanning();
75 } else {
76 scanButton.textContent = '▶ Start Video Editor Scanner';
77 scanButton.style.background = 'linear-gradient(135deg, #1DA1F2 0%, #0d8bd9 100%)';
78 console.log('Video Editor Scanner: Stopped');
79 }
80 }
81
82 // Extract post URL from article element
83 function getPostUrl(article) {
84 try {
85 // Find the time element which contains the link to the post
86 const timeElement = article.querySelector('time');
87 if (timeElement) {
88 const linkElement = timeElement.closest('a[href*="/status/"]');
89 if (linkElement) {
90 const href = linkElement.getAttribute('href');
91 return 'https://x.com' + href;
92 }
93 }
94 } catch (error) {
95 console.error('Error extracting post URL:', error);
96 }
97 return null;
98 }
99
100 // Analyze post text using AI
101 async function analyzePostForVideoEditor(postText) {
102 try {
103 const prompt = `Analyze this social media post and determine if it's a CONTENT CREATOR or BUSINESS looking to HIRE a video editor.
104
105Post text: "${postText}"
106
107ONLY return true if:
108- The post is from someone SEEKING/HIRING/LOOKING FOR a video editor
109- They are asking for video editing help or services
110- They are posting a job opportunity for video editors
111- They need someone to edit their videos
112
113RETURN FALSE if:
114- The post is from a video editor offering their services
115- Someone is showcasing their video editing work
116- A video editor is looking for clients or work
117- The post says things like "I'm a video editor", "DM for commissions", "I can edit", "hire me", "I edited", "need a video editor? DM me"
118- The post is self-promotion from an editor
119
120The key distinction: We want people who NEED editors, not editors who OFFER services.`;
121
122 const result = await RM.aiCall(prompt, {
123 type: 'json_schema',
124 json_schema: {
125 name: 'video_editor_detection',
126 schema: {
127 type: 'object',
128 properties: {
129 isVideoEditorPost: { type: 'boolean' },
130 confidence: { type: 'number', minimum: 0, maximum: 1 },
131 reason: { type: 'string' },
132 isEditorOfferingServices: { type: 'boolean' }
133 },
134 required: ['isVideoEditorPost', 'confidence', 'isEditorOfferingServices']
135 }
136 }
137 });
138
139 console.log('AI Analysis Result:', result);
140
141 // Only return true if it's a video editor post AND not an editor offering services
142 return result.isVideoEditorPost && !result.isEditorOfferingServices && result.confidence > 0.7;
143 } catch (error) {
144 console.error('Error analyzing post with AI:', error);
145 return false;
146 }
147 }
148
149 // Process a single post
150 async function processPost(article) {
151 try {
152 // Get post URL to use as unique identifier
153 const postUrl = getPostUrl(article);
154 if (!postUrl || processedPosts.has(postUrl)) {
155 return;
156 }
157
158 // Find the post text
159 const textElement = article.querySelector('[data-testid="tweetText"]');
160 if (!textElement) {
161 return;
162 }
163
164 const postText = textElement.textContent.trim();
165 if (!postText || postText.length < 10) {
166 return;
167 }
168
169 console.log('Analyzing post:', postText.substring(0, 100) + '...');
170
171 // Mark as processed to avoid duplicate analysis
172 processedPosts.add(postUrl);
173
174 // Analyze with AI
175 const isVideoEditorPost = await analyzePostForVideoEditor(postText);
176
177 if (isVideoEditorPost) {
178 // Check if we already opened this tab
179 if (openedTabs.has(postUrl)) {
180 console.log('Tab already opened for:', postUrl);
181 return;
182 }
183
184 console.log('✅ Found video editor post! Opening:', postUrl);
185
186 // Mark as opened before opening to prevent race conditions
187 openedTabs.add(postUrl);
188
189 // Highlight the post
190 article.style.border = '3px solid #4CAF50';
191 article.style.boxShadow = '0 0 10px rgba(76, 175, 80, 0.5)';
192
193 // Open in new tab
194 await GM.openInTab(postUrl, true);
195
196 // Show notification
197 showNotification('Video Editor Post Found!', postText.substring(0, 100));
198 }
199 } catch (error) {
200 console.error('Error processing post:', error);
201 }
202 }
203
204 // Show notification
205 function showNotification(title, message) {
206 const notification = document.createElement('div');
207 notification.style.cssText = `
208 position: fixed;
209 top: 20px;
210 right: 20px;
211 z-index: 10001;
212 background: #4CAF50;
213 color: white;
214 padding: 15px 20px;
215 border-radius: 8px;
216 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
217 max-width: 300px;
218 animation: slideIn 0.3s ease;
219 `;
220
221 notification.innerHTML = `
222 <div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
223 <div style="font-size: 12px; opacity: 0.9;">${message}...</div>
224 `;
225
226 document.body.appendChild(notification);
227
228 setTimeout(() => {
229 notification.style.animation = 'slideOut 0.3s ease';
230 setTimeout(() => notification.remove(), 300);
231 }, 4000);
232 }
233
234 // Scan visible posts
235 const scanVisiblePosts = debounce(async function() {
236 if (!isScanning) return;
237
238 const articles = document.querySelectorAll('article[data-testid="tweet"]');
239 console.log(`Scanning ${articles.length} visible posts...`);
240
241 for (const article of articles) {
242 if (!isScanning) break;
243 await processPost(article);
244 }
245 }, 1000);
246
247 // Auto-scroll function
248 function autoScroll() {
249 if (!isScanning) return;
250
251 window.scrollBy({
252 top: 800,
253 behavior: 'smooth'
254 });
255
256 // Continue scrolling
257 setTimeout(() => {
258 if (isScanning) {
259 autoScroll();
260 }
261 }, 3000);
262 }
263
264 // Start scanning
265 function startScanning() {
266 // Initial scan
267 scanVisiblePosts();
268
269 // Start auto-scrolling
270 autoScroll();
271
272 // Observe DOM changes for new posts
273 const observer = new MutationObserver(debounce(() => {
274 if (isScanning) {
275 scanVisiblePosts();
276 }
277 }, 1000));
278
279 observer.observe(document.body, {
280 childList: true,
281 subtree: true
282 });
283 }
284
285 // Add CSS animations
286 TM_addStyle(`
287 @keyframes slideIn {
288 from {
289 transform: translateX(400px);
290 opacity: 0;
291 }
292 to {
293 transform: translateX(0);
294 opacity: 1;
295 }
296 }
297
298 @keyframes slideOut {
299 from {
300 transform: translateX(0);
301 opacity: 1;
302 }
303 to {
304 transform: translateX(400px);
305 opacity: 0;
306 }
307 }
308 `);
309
310 // Initialize when page is ready
311 function init() {
312 console.log('X Video Editor Job Finder: Initialized');
313
314 // Wait for the feed to load
315 const checkFeed = setInterval(() => {
316 const feed = document.querySelector('article[data-testid="tweet"]');
317 if (feed) {
318 clearInterval(checkFeed);
319 scanButton = createControlButton();
320 console.log('Control button created. Click to start scanning.');
321 }
322 }, 1000);
323 }
324
325 // Start the extension
326 if (document.readyState === 'loading') {
327 document.addEventListener('DOMContentLoaded', init);
328 } else {
329 init();
330 }
331})();