YouTube Metadata Fetcher
Fetch metadata video dan channel YouTube tanpa API key. Supports URL, handle (@), dan ID langsung.
15 Juni 20261023 file
yt-video.jsjavascript · 130 baris
const axios = require('axios');const VIDEO_ID_REGEX = /(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})|^([a-zA-Z0-9_-]{11})$/;function extractVideoId(input) { const match = input.match(VIDEO_ID_REGEX); return match ? (match[1] || match[2]) : null;}async function getVideoMeta(input) { const videoId = extractVideoId(input); if (!videoId) throw new Error('Invalid YouTube URL or video ID'); const quotaUser = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); const ts = Date.now(); const parts = [ 'snippet', 'statistics', 'contentDetails', 'status', 'liveStreamingDetails', 'localizations', 'recordingDetails', 'topicDetails', 'player', ].join(','); const { data } = await axios.get('https://ytapi.apps.mattw.io/v3/videos', { params: { key: 'foo1', quotaUser, part: parts, id: videoId, _: ts, }, headers: { accept: 'application/json, text/javascript, */*; q=0.01', origin: 'https://mattw.io', referer: 'https://mattw.io/', 'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36', }, }); if (!data.items || data.items.length === 0) throw new Error('Video not found'); const item = data.items[0]; const s = item.snippet || {}; const st = item.statistics || {}; const cd = item.contentDetails || {}; const status = item.status || {}; const live = item.liveStreamingDetails || {}; const topic = item.topicDetails || {}; const thumbnails = s.thumbnails || {}; const bestThumb = thumbnails.maxres || thumbnails.standard || thumbnails.high || thumbnails.medium || thumbnails.default || null; const result = { id: item.id, url: `https://www.youtube.com/watch?v=${item.id}`, title: s.title || null, description: s.description || null, publishedAt: s.publishedAt || null, channel: { id: s.channelId || null, title: s.channelTitle || null, url: s.channelId ? `https://www.youtube.com/channel/${s.channelId}` : null, }, category: { id: s.categoryId || null, }, tags: s.tags || [], defaultLanguage: s.defaultLanguage || null, defaultAudioLanguage: s.defaultAudioLanguage || null, liveBroadcastContent: s.liveBroadcastContent || null, thumbnails: { default: thumbnails.default || null, medium: thumbnails.medium || null, high: thumbnails.high || null, standard: thumbnails.standard || null, maxres: thumbnails.maxres || null, best: bestThumb ? bestThumb.url : null, }, statistics: { views: st.viewCount ? parseInt(st.viewCount) : null, likes: st.likeCount ? parseInt(st.likeCount) : null, comments: st.commentCount ? parseInt(st.commentCount) : null, favorites: st.favoriteCount ? parseInt(st.favoriteCount) : null, }, contentDetails: { duration: cd.duration || null, dimension: cd.dimension || null, definition: cd.definition || null, caption: cd.caption || null, licensedContent: cd.licensedContent ?? null, projection: cd.projection || null, contentRating: cd.contentRating || {}, }, status: { uploadStatus: status.uploadStatus || null, privacyStatus: status.privacyStatus || null, license: status.license || null, embeddable: status.embeddable ?? null, publicStatsViewable: status.publicStatsViewable ?? null, madeForKids: status.madeForKids ?? null, }, liveStreaming: Object.keys(live).length > 0 ? live : null, topicCategories: topic.topicCategories || [], topicIds: topic.topicIds || [], player: item.player || null, localizations: item.localizations || null, recordingDetails: item.recordingDetails || null, }; return result;}const input = process.argv[2];if (!input) { process.stderr.write('Usage: node yt-meta.js <video_url_or_id>\n'); process.exit(1);}getVideoMeta(input) .then(r => process.stdout.write(JSON.stringify(r, null, 2) + '\n')) .catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });yt-channel.jsjavascript · 119 baris
const axios = require('axios');const HEADERS = { accept: 'application/json, text/javascript, */*; q=0.01', origin: 'https://mattw.io', referer: 'https://mattw.io/', 'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',};const CHANNEL_ID_REGEX = /^UC[a-zA-Z0-9_-]{22}$/;const CHANNEL_URL_REGEX = /youtube\.com\/(?:channel\/(UC[a-zA-Z0-9_-]{22})|(?:@|user\/|c\/)([^/?&\s]+))/;async function resolveChannelId(input) { const trimmed = input.trim(); if (CHANNEL_ID_REGEX.test(trimmed)) return trimmed; const urlMatch = trimmed.match(CHANNEL_URL_REGEX); if (urlMatch && urlMatch[1]) return urlMatch[1]; const resolveUrl = urlMatch ? trimmed : trimmed.startsWith('@') ? `https://youtube.com/${trimmed}` : `https://youtube.com/@${trimmed}`; const { data } = await axios.get('https://ytapi.apps.mattw.io/v1/resolve_url', { params: { url: resolveUrl }, headers: HEADERS, }); if (!data || !data.channelId) throw new Error('Could not resolve channel ID'); return data.channelId;}async function getChannelMeta(input) { const channelId = await resolveChannelId(input); const quotaUser = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); const { data } = await axios.get('https://ytapi.apps.mattw.io/v3/channels', { params: { key: 'foo1', quotaUser, part: 'id,snippet,statistics,brandingSettings,contentDetails,localizations,status,topicDetails', id: channelId, _: Date.now(), }, headers: HEADERS, }); if (!data.items || data.items.length === 0) throw new Error('Channel not found'); const item = data.items[0]; const s = item.snippet || {}; const st = item.statistics || {}; const bs = item.brandingSettings || {}; const cd = item.contentDetails || {}; const status = item.status || {}; const topic = item.topicDetails || {}; const thumbnails = s.thumbnails || {}; const bestThumb = thumbnails.high || thumbnails.medium || thumbnails.default || null; return { id: item.id, url: `https://www.youtube.com/channel/${item.id}`, title: s.title || null, description: s.description || null, customUrl: s.customUrl || null, publishedAt: s.publishedAt || null, country: s.country || null, defaultLanguage: s.defaultLanguage || null, thumbnails: { default: thumbnails.default || null, medium: thumbnails.medium || null, high: thumbnails.high || null, best: bestThumb ? bestThumb.url : null, }, statistics: { subscribers: st.subscriberCount ? parseInt(st.subscriberCount) : null, hiddenSubscriberCount: st.hiddenSubscriberCount ?? null, views: st.viewCount ? parseInt(st.viewCount) : null, videoCount: st.videoCount ? parseInt(st.videoCount) : null, }, branding: { title: bs.channel?.title || null, description: bs.channel?.description || null, keywords: bs.channel?.keywords || null, country: bs.channel?.country || null, bannerExternalUrl: bs.image?.bannerExternalUrl || null, }, contentDetails: { uploadsPlaylistId: cd.relatedPlaylists?.uploads || null, }, status: { privacyStatus: status.privacyStatus || null, isLinked: status.isLinked ?? null, longUploadsStatus: status.longUploadsStatus || null, madeForKids: status.madeForKids ?? null, }, topicCategories: topic.topicCategories || [], localizations: item.localizations || null, };}const input = process.argv[2];if (!input) { process.stderr.write('Usage: node yt-channel.js <channel_url|handle|@handle|channel_id>\n'); process.exit(1);}getChannelMeta(input) .then(r => process.stdout.write(JSON.stringify(r, null, 2) + '\n')) .catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });README.mdmarkdown · 178 baris
Komentar
Masuk untuk ikut berkomentar.
Belum ada komentar.