X Tweet CSV Generator

X (Twitter) tweet linklerini CSV olarak dışa aktarır. (x_likes_manager formatında)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         X Tweet CSV Generator
// @namespace    https://x.com/
// @version      1.0.0
// @description  X (Twitter) tweet linklerini CSV olarak dışa aktarır. (x_likes_manager formatında)
// @author       XLikesManager
// @match        https://x.com/*
// @match        https://twitter.com/*
// @icon         https://abs.twimg.com/favicons/twitter.3.ico
// @license      MIT
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    const BEARER = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';

    const state = {
        queryId: null,
        csrfToken: null,
        tweets: []
    };

    const $ = (sel, ctx = document) => ctx.querySelector(sel);
    const sleep = ms => new Promise(r => setTimeout(r, ms));

    function getCsrf() {
        const m = document.cookie.match(/ct0=([^;]+)/);
        return m ? m[1] : null;
    }

    async function discoverQueryId() {
        try {
            const links = [...document.querySelectorAll('link[href*="main."], script[src*="main."]')];
            let mainUrl = null;
            for (const l of links) {
                const url = l.href || l.src;
                if (url && /main\.[a-f0-9]+\.[a-z]+\.js/i.test(url)) { mainUrl = url; break; }
            }
            if (!mainUrl) {
                const scripts = document.querySelectorAll('script[src]');
                for (const s of scripts) {
                    if (/\/main\.[a-f0-9]+/i.test(s.src)) { mainUrl = s.src; break; }
                }
            }
            if (!mainUrl) return 'V3vfsYzNEyD9tsf4xoFRgw';

            const js = await (await fetch(mainUrl)).text();
            const m = js.match(/queryId:"([^"]+)",operationName:"TweetResultByRestId"/);
            return m ? m[1] : 'V3vfsYzNEyD9tsf4xoFRgw';
        } catch (e) {
            return 'V3vfsYzNEyD9tsf4xoFRgw';
        }
    }

    async function apiFetch(url, opts = {}) {
        const headers = {
            'authorization': `Bearer ${BEARER}`,
            'x-csrf-token': getCsrf() || '',
            'x-twitter-auth-type': 'OAuth2Session',
            'x-twitter-active-user': 'yes',
            'content-type': 'application/json',
            ...(opts.headers || {})
        };
        const resp = await fetch(url, { ...opts, headers, credentials: 'include' });
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        return await resp.json();
    }

    async function fetchTweetDetail(tweetId) {
        const qid = state.queryId;
        const variables = { tweetId, withCommunity: false, includePromotedContent: false, withVoice: false };
        const features = {
            responsive_web_graphql_exclude_directive_enabled: true,
            verified_phone_label_enabled: false,
            responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
            responsive_web_graphql_timeline_navigation_enabled: true,
            creator_subscriptions_tweet_preview_api_enabled: true,
            freedom_of_speech_not_reach_fetch_enabled: true,
            tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true
        };
        const url = `https://x.com/i/api/graphql/${qid}/TweetResultByRestId?variables=${encodeURIComponent(JSON.stringify(variables))}&features=${encodeURIComponent(JSON.stringify(features))}`;
        const data = await apiFetch(url);
        const result = data?.data?.tweetResult?.result;
        if (!result) return null;

        let tweet = result.tweet || result;
        if (tweet.__typename === 'TweetTombstone') return null;

        const legacy = tweet.legacy || {};
        const userResults = tweet.core?.user_results?.result || {};
        const userLegacy = userResults.legacy || {};

        const mediaEntities = legacy.extended_entities?.media || legacy.entities?.media || [];
        const mediaUrls = mediaEntities.map(m => {
            if (m.video_info?.variants) {
                const mp4s = m.video_info.variants.filter(v => v.content_type === 'video/mp4');
                if (mp4s.length) return mp4s.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0].url.split('?')[0];
            }
            return m.media_url_https || m.media_url;
        }).filter(Boolean);

        return {
            id: tweetId,
            text: legacy.full_text || '',
            username: userLegacy.screen_name || '',
            displayName: userLegacy.name || '',
            date: legacy.created_at || '',
            likes: legacy.favorite_count || 0,
            retweets: legacy.retweet_count || 0,
            replies: legacy.reply_count || 0,
            mediaUrls,
            url: `https://x.com/${userLegacy.screen_name || 'i'}/status/${tweetId}`
        };
    }

    function generateCSV(tweets) {
        const BOM = '\uFEFF';
        const headers = ['Sıra No', 'Tweet ID', 'Tweet Metni', 'Tweet Yazarı', 'Yazar Görünen Adı',
            'Tweet Tarihi', 'Beğeni Sayısı', 'RT Sayısı', 'Yanıt Sayısı', 'Medya URL\'leri', 'Tweet URL\'si'];

        const escapeCSV = (val) => {
            const s = String(val ?? '').replace(/"/g, '""');
            return s.includes(';') || s.includes('"') || s.includes('\n') || s.includes('\r') ? `"${s}"` : s;
        };

        const rows = tweets.map((t, i) => [
            i + 1, t.id, escapeCSV(t.text), t.username ? `@${t.username}` : '', escapeCSV(t.displayName),
            t.date ? new Date(t.date).toISOString() : '', t.likes, t.retweets, t.replies,
            t.mediaUrls.join(', '), t.url
        ].join(';'));

        return BOM + headers.join(';') + '\n' + rows.join('\n');
    }

    function injectStyles() {
        const css = document.createElement('style');
        css.textContent = `
            #xcsv-fab {
                position: fixed; bottom: 80px; right: 20px; width: 48px; height: 48px;
                background: #1d9bf0; border-radius: 50%; display: flex; align-items: center; justify-content: center;
                cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 9999; border: none; transition: transform 0.2s;
            }
            #xcsv-fab:hover { transform: scale(1.1); }
            #xcsv-fab svg { width: 24px; height: 24px; fill: white; }
            #xcsv-panel-wrapper {
                position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: none;
                align-items: center; justify-content: center; z-index: 10000;
            }
            #xcsv-panel {
                width: 500px; background: #16181c; color: #e7e9ea; padding: 24px; border-radius: 16px;
                border: 1px solid #333; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            }
            #xcsv-panel textarea {
                width: 100%; height: 200px; background: #000; color: #fff; border: 1px solid #333;
                border-radius: 12px; padding: 12px; font-family: monospace; resize: none; margin: 12px 0;
            }
            #xcsv-panel textarea:focus { outline: 1px solid #1d9bf0; }
            #xcsv-panel button.primary {
                width: 100%; padding: 12px; background: #1d9bf0; color: #fff; border: none;
                border-radius: 999px; font-weight: bold; cursor: pointer; font-size: 15px;
            }
            #xcsv-panel button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
            #xcsv-panel .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
            #xcsv-panel h3 { margin: 0; font-size: 18px; }
            #xcsv-panel .close { background: none; border: none; color: #71767b; cursor: pointer; font-size: 20px; }
        `;
        document.head.appendChild(css);
    }

    function createUI() {
        injectStyles();

        // FAB
        const fab = document.createElement('button');
        fab.id = 'xcsv-fab';
        fab.title = 'X Tweet to CSV Generator';
        fab.innerHTML = `<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></svg>`;
        document.body.appendChild(fab);

        // Wrapper
        const wrapper = document.createElement('div');
        wrapper.id = 'xcsv-panel-wrapper';
        wrapper.innerHTML = `
            <div id="xcsv-panel">
                <div class="header">
                    <h3>X Tweet to CSV Generator</h3>
                    <button class="close">✕</button>
                </div>
                <p style="font-size: 13px; color: #71767b; line-height: 1.4;">Linkleri alt alta girin. Sadece tweet linkleri işlenecektir.</p>
                <textarea id="xcsv-links" placeholder="https://x.com/user/status/123456789..."></textarea>
                <div id="xcsv-status" style="font-size: 13px; margin-bottom: 12px; color: #1d9bf0; min-height: 18px;"></div>
                <button id="xcsv-generate" class="primary">CSV Oluştur ve İndir</button>
            </div>
        `;
        document.body.appendChild(wrapper);

        fab.onclick = () => { wrapper.style.display = 'flex'; };
        wrapper.querySelector('.close').onclick = () => { wrapper.style.display = 'none'; };
        wrapper.onclick = (e) => { if (e.target === wrapper) wrapper.style.display = 'none'; };

        const btn = $('#xcsv-generate');
        const status = $('#xcsv-status');
        const textarea = $('#xcsv-links');

        btn.onclick = async () => {
            const lines = textarea.value.split('\n').map(l => l.trim()).filter(Boolean);
            const ids = lines.map(l => { const m = l.match(/status\/(\d+)/); return m ? m[1] : null; }).filter(Boolean);

            if (ids.length === 0) return alert('Lütfen geçerli tweet linkleri girin.');

            btn.disabled = true;
            textarea.disabled = true;
            state.queryId = await discoverQueryId();
            state.tweets = [];

            for (let i = 0; i < ids.length; i++) {
                status.textContent = `İşleniyor: ${i + 1} / ${ids.length}`;
                try {
                    const tweet = await fetchTweetDetail(ids[i]);
                    if (tweet) state.tweets.push(tweet);
                } catch (e) {}
                await sleep(800);
            }

            if (state.tweets.length > 0) {
                const csv = generateCSV(state.tweets);
                const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `x_links_${new Date().toISOString().split('T')[0]}.csv`;
                a.click();
                URL.revokeObjectURL(url);
                status.textContent = `Tamamlandı! ${state.tweets.length} tweet indirildi.`;
            } else {
                status.textContent = 'Veri çekilemedi. Bağlantınızı kontrol edin.';
            }

            btn.disabled = false;
            textarea.disabled = false;
        };
    }

    // Initialize with delay for X.com
    setTimeout(() => {
        if (!location.hostname.includes('x.com') && !location.hostname.includes('twitter.com')) return;
        createUI();
    }, 2000);

})();