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