Created
August 29, 2025 16:01
-
-
Save kkeeth/91be59cc0097c9981ef0977120fbf2f6 to your computer and use it in GitHub Desktop.
YouTube の自分のチャンネルの各番組で非公開になっている動画を丸っと公開にするスクリプト
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// youtube_public_fixed.js | |
import { google } from 'googleapis'; | |
import fs from 'fs'; | |
import readline from 'readline'; | |
// --- 認証関連の基本設定(変更なし) --- | |
// 認証情報を読み込み | |
const credentials = JSON.parse(fs.readFileSync('client_secret.json')); | |
const { client_secret, client_id, redirect_uris } = credentials.installed; | |
const oAuth2Client = new google.auth.OAuth2( | |
client_id, | |
client_secret, | |
redirect_uris[0] | |
); | |
// 認証用のURL生成 | |
function getAuthUrl() { | |
const authUrl = oAuth2Client.generateAuthUrl({ | |
access_type: 'offline', | |
scope: ['https://www.googleapis.com/auth/youtube.readonly', 'https://www.googleapis.com/auth/youtube'], | |
}); | |
return authUrl; | |
} | |
// 認証コードから認証トークンを取得 | |
async function getAccessToken(code) { | |
const { tokens } = await oAuth2Client.getToken(code); | |
oAuth2Client.setCredentials(tokens); | |
// トークンを保存(次回使用のため) | |
fs.writeFileSync('token.json', JSON.stringify(tokens)); | |
return tokens; | |
} | |
// 保存されたトークンを読み込み | |
function loadTokens() { | |
try { | |
const tokens = JSON.parse(fs.readFileSync('token.json')); | |
oAuth2Client.setCredentials(tokens); | |
return true; | |
} catch (error) { | |
return false; | |
} | |
} | |
// --- ここからが修正・追加されたコード --- | |
/** | |
* 認証ユーザーが管理するチャンネルの一覧を取得して返す | |
*/ | |
async function listUserChannels(youtube) { | |
const response = await youtube.channels.list({ | |
part: 'id,snippet', | |
mine: true, | |
}); | |
if (!response.data.items || response.data.items.length === 0) { | |
console.log('管理しているYouTubeチャンネルが見つかりませんでした。'); | |
return []; | |
} | |
return response.data.items; | |
} | |
/** | |
* 指定されたチャンネルIDのプレイリスト一覧を取得 | |
*/ | |
async function listChannelPlaylists(youtube, channelId) { | |
try { | |
const response = await youtube.playlists.list({ | |
part: 'id,snippet,contentDetails', | |
channelId: channelId, | |
maxResults: 50, | |
}); | |
if (!response.data.items || response.data.items.length === 0) { | |
console.log('このチャンネルにはプレイリストが見つかりませんでした。'); | |
return []; | |
} | |
return response.data.items; | |
} catch (error) { | |
console.error('プレイリストの取得中にエラーが発生しました:', error.message); | |
return []; | |
} | |
} | |
/** | |
* ユーザー入力を取得する共通関数 | |
*/ | |
async function getUserInput(prompt) { | |
const rl = readline.createInterface({ | |
input: process.stdin, | |
output: process.stdout, | |
}); | |
return new Promise((resolve) => { | |
rl.question(prompt, (input) => { | |
rl.close(); | |
resolve(input); | |
}); | |
}); | |
} | |
/** | |
* プレイリスト選択UI | |
*/ | |
async function selectPlaylist(playlists) { | |
console.log('\n操作対象の番組(プレイリスト)を選択してください:'); | |
console.log('[0] 全ての動画(チャンネル全体)'); | |
playlists.forEach((playlist, index) => { | |
console.log(`[${index + 1}] ${playlist.snippet.title} (動画数: ${playlist.contentDetails.itemCount})`); | |
}); | |
const answer = await getUserInput('番号を入力してください: '); | |
return parseInt(answer, 10); | |
} | |
/** | |
* プレイリスト選択と実行を処理する関数 | |
*/ | |
async function handlePlaylistSelection(youtube, channelId) { | |
console.log('\nプレイリスト(番組)を取得中...'); | |
const playlists = await listChannelPlaylists(youtube, channelId); | |
if (playlists.length === 0) { | |
console.log('プレイリストが見つからないため、チャンネル全体の動画を処理します。'); | |
await publishPrivateVideosFromChannel(channelId); | |
return; | |
} | |
const playlistIndex = await selectPlaylist(playlists); | |
if (playlistIndex === 0) { | |
console.log('チャンネル全体の動画を処理します。'); | |
await publishPrivateVideosFromChannel(channelId); | |
} else if (playlistIndex > 0 && playlistIndex <= playlists.length) { | |
const selectedPlaylist = playlists[playlistIndex - 1]; | |
console.log(`選択された番組: ${selectedPlaylist.snippet.title}`); | |
await publishPrivateVideosFromPlaylist(selectedPlaylist.id); | |
} else { | |
console.log('無効な番号です。処理を終了します。'); | |
} | |
} | |
/** | |
* 指定されたプレイリストIDの非公開・限定公開動画を公開に変更 | |
* @param {string} playlistId - 操作対象のプレイリストID | |
*/ | |
async function publishPrivateVideosFromPlaylist(playlistId) { | |
const youtube = google.youtube({ version: 'v3', auth: oAuth2Client }); | |
try { | |
console.log(`\n指定されたプレイリストID: ${playlistId}`); | |
console.log('プレイリスト情報を取得中...'); | |
let nextPageToken = null; | |
let allVideoIds = []; | |
console.log('プレイリストから全ての動画IDを取得中...'); | |
do { | |
const playlistResponse = await youtube.playlistItems.list({ | |
part: 'contentDetails', | |
playlistId: playlistId, | |
maxResults: 50, | |
pageToken: nextPageToken || undefined | |
}); | |
const videoIds = playlistResponse.data.items.map(item => item.contentDetails.videoId); | |
allVideoIds = allVideoIds.concat(videoIds); | |
nextPageToken = playlistResponse.data.nextPageToken; | |
console.log(`読み込み済み動画ID数: ${allVideoIds.length}`); | |
} while (nextPageToken); | |
console.log(`合計動画数: ${allVideoIds.length}`); | |
let privateCount = 0; | |
let publishedCount = 0; | |
// 50件ずつのバッチ処理で動画詳細を取得・更新 | |
for (let i = 0; i < allVideoIds.length; i += 50) { | |
const batch = allVideoIds.slice(i, i + 50); | |
const videoIdsString = batch.join(','); | |
const details = await youtube.videos.list({ | |
part: 'status,snippet', | |
id: videoIdsString | |
}); | |
for (const video of details.data.items) { | |
const currentStatus = video.status.privacyStatus; | |
console.log(`動画: "${video.snippet.title}" - 現在のステータス: ${currentStatus}`); | |
if (currentStatus === 'private' || currentStatus === 'unlisted') { | |
privateCount++; | |
console.log(` -> 公開設定に変更します: "${video.snippet.title}"`); | |
await youtube.videos.update({ | |
part: 'status', | |
requestBody: { | |
id: video.id, | |
status: { | |
privacyStatus: 'public' | |
} | |
} | |
}); | |
publishedCount++; | |
await new Promise(resolve => setTimeout(resolve, 200)); // API制限を考慮 | |
} | |
} | |
} | |
console.log(`\n--- 処理完了 ---`); | |
console.log(`非公開・限定公開の動画数: ${privateCount}`); | |
console.log(`公開済みの動画数: ${publishedCount}`); | |
} catch (error) { | |
console.error('動画の公開処理中にエラーが発生しました:', error.message); | |
if (error.code === 403) { | |
console.error('権限がありません。OAuthスコープやAPIの割り当て量を確認してください。'); | |
} | |
} | |
} | |
/** | |
* 指定されたチャンネルIDの全動画を公開に変更 | |
* @param {string} channelId - 操作対象のチャンネルID | |
*/ | |
async function publishPrivateVideosFromChannel(channelId) { | |
const youtube = google.youtube({ version: 'v3', auth: oAuth2Client }); | |
try { | |
console.log(`\n指定されたチャンネルID: ${channelId}`); | |
console.log('チャンネル情報を取得中...'); | |
const channelResponse = await youtube.channels.list({ | |
part: 'contentDetails', | |
id: channelId | |
}); | |
if (!channelResponse.data.items || channelResponse.data.items.length === 0) { | |
console.log('指定されたIDのチャンネルが見つかりませんでした。'); | |
return; | |
} | |
const uploadsPlaylistId = channelResponse.data.items[0].contentDetails.relatedPlaylists.uploads; | |
console.log(`アップロード済み動画のプレイリストID: ${uploadsPlaylistId}`); | |
await publishPrivateVideosFromPlaylist(uploadsPlaylistId); | |
} catch (error) { | |
console.error('チャンネル動画の公開処理中にエラーが発生しました:', error.message); | |
} | |
} | |
/** | |
* メイン処理 | |
*/ | |
async function main() { | |
console.log('=== YouTube動画公開ツール ==='); | |
console.log('注意: 異なるYouTubeアカウントを使用したい場合は、token.jsonを削除してから実行してください。\n'); | |
if (!loadTokens()) { | |
console.log('初回実行のため、Googleアカウントでの認証が必要です。'); | |
console.log('*** 重要 *** 認証時に、目的のYouTubeアカウントでログインしてください。'); | |
console.log('以下のURLにアクセスして認証し、表示されたコードを貼り付けてください:'); | |
console.log(getAuthUrl()); | |
const code = await getUserInput('認証コードを入力してください: '); | |
await getAccessToken(code); | |
} else { | |
console.log('既存の認証情報を使用します。'); | |
} | |
// 【追加】チャンネル選択のロジック | |
const youtube = google.youtube({ version: 'v3', auth: oAuth2Client }); | |
const channels = await listUserChannels(youtube); | |
if (channels.length === 0) { | |
return; // チャンネルがない場合は終了 | |
} | |
let selectedChannelId; | |
if (channels.length === 1) { | |
// チャンネルが1つしかない場合は自動で選択 | |
selectedChannelId = channels[0].id; | |
console.log(`管理チャンネルが1つのため、自動的に選択しました: ${channels[0].snippet.title} (${selectedChannelId})`); | |
await handlePlaylistSelection(youtube, selectedChannelId); | |
return; | |
} else { | |
// 複数のチャンネルがある場合はユーザーに選択させる | |
console.log('\n操作対象のチャンネルを選択してください:'); | |
channels.forEach((channel, index) => { | |
console.log(`[${index + 1}] ${channel.snippet.title} (ID: ${channel.id})`); | |
}); | |
const answer = await getUserInput('番号を入力してください: '); | |
const selectedIndex = parseInt(answer, 10) - 1; | |
if (selectedIndex >= 0 && selectedIndex < channels.length) { | |
selectedChannelId = channels[selectedIndex].id; | |
} else { | |
console.log('無効な番号です。処理を終了します。'); | |
return; | |
} | |
} | |
await handlePlaylistSelection(youtube, selectedChannelId); | |
} | |
main().catch(console.error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment