unsubscribe.js
1 /** 2 * YouTube unsubscribe — unsubscribe from a channel via InnerTube subscription API. 3 */ 4 import { cli, Strategy } from '@jackwener/opencli/registry'; 5 import { prepareYoutubeApiPage, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js'; 6 import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors'; 7 8 cli({ 9 site: 'youtube', 10 name: 'unsubscribe', 11 description: 'Unsubscribe from a YouTube channel', 12 domain: 'www.youtube.com', 13 strategy: Strategy.COOKIE, 14 args: [ 15 { name: 'channel', required: true, positional: true, help: 'Channel ID (UCxxxx) or handle (@name)' }, 16 ], 17 columns: ['status', 'message'], 18 func: async (page, kwargs) => { 19 const channelInput = String(kwargs.channel); 20 await prepareYoutubeApiPage(page); 21 const result = await page.evaluate(` 22 (async () => { 23 ${SAPISID_HASH_FN} 24 25 const cfg = window.ytcfg?.data_ || {}; 26 const apiKey = cfg.INNERTUBE_API_KEY; 27 const context = cfg.INNERTUBE_CONTEXT; 28 if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' }; 29 30 const authHash = await getSapisidHash('https://www.youtube.com'); 31 if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' }; 32 33 ${RESOLVE_CHANNEL_HANDLE_FN} 34 35 let channelId = ${JSON.stringify(channelInput)}; 36 channelId = await resolveChannelHandle(channelId, apiKey, context); 37 38 if (!channelId.startsWith('UC')) { 39 return { error: 'arg', message: 'Could not resolve channel ID from: ' + ${JSON.stringify(channelInput)} }; 40 } 41 42 const resp = await fetch('/youtubei/v1/subscription/unsubscribe?key=' + apiKey + '&prettyPrint=false', { 43 method: 'POST', 44 credentials: 'include', 45 headers: { 46 'Content-Type': 'application/json', 47 'Authorization': authHash, 48 'X-Origin': 'https://www.youtube.com', 49 }, 50 body: JSON.stringify({ context, channelIds: [channelId] }), 51 }); 52 53 if (resp.status === 401 || resp.status === 403) return { error: 'auth', message: 'Not logged in' }; 54 if (!resp.ok) { 55 const body = await resp.json().catch(() => ({})); 56 const errStatus = body?.error?.status || ''; 57 if (errStatus === 'UNAUTHENTICATED') return { error: 'auth', message: 'Not logged in' }; 58 return { error: 'http', message: 'HTTP ' + resp.status + (errStatus ? ' ' + errStatus : '') }; 59 } 60 return { ok: true, channelId }; 61 })() 62 `); 63 if (result?.error === 'auth') { 64 throw new AuthRequiredError('www.youtube.com'); 65 } 66 if (result?.error) { 67 throw new CommandExecutionError(result.message || 'Failed to unsubscribe'); 68 } 69 return [{ status: 'success', message: 'Unsubscribed from: ' + (result.channelId || channelInput) }]; 70 }, 71 });