X Video Editor Job Finder

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})();