@yeongjaeyou/claude-code-config 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/code-review-handler.md +203 -0
- package/.claude/agents/issue-resolver.md +123 -0
- package/.claude/agents/python-pro.md +7 -2
- package/.claude/agents/web-researcher.md +5 -1
- package/.claude/commands/ask-deepwiki.md +46 -11
- package/.claude/commands/gh/auto-review-loop.md +201 -0
- package/.claude/commands/gh/create-issue-label.md +4 -0
- package/.claude/commands/gh/decompose-issue.md +24 -2
- package/.claude/commands/gh/post-merge.md +52 -10
- package/.claude/commands/gh/resolve-and-review.md +69 -0
- package/.claude/commands/gh/resolve-issue.md +3 -0
- package/.claude/commands/tm/convert-prd.md +4 -0
- package/.claude/commands/tm/post-merge.md +7 -1
- package/.claude/commands/tm/resolve-issue.md +4 -0
- package/.claude/commands/tm/sync-to-github.md +4 -0
- package/.claude/settings.json +15 -0
- package/.claude/skills/claude-md-generator/SKILL.md +130 -0
- package/.claude/skills/claude-md-generator/references/examples.md +261 -0
- package/.claude/skills/claude-md-generator/references/templates.md +156 -0
- package/.claude/skills/hook-creator/SKILL.md +88 -0
- package/.claude/skills/hook-creator/references/examples.md +339 -0
- package/.claude/skills/hook-creator/references/hook-events.md +193 -0
- package/.claude/skills/skill-creator/SKILL.md +160 -13
- package/.claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/.claude/skills/skill-creator/references/workflows.md +28 -0
- package/.claude/skills/skill-creator/scripts/package_skill.py +10 -10
- package/.claude/skills/skill-creator/scripts/quick_validate.py +45 -15
- package/.claude/skills/slash-command-creator/SKILL.md +108 -0
- package/.claude/skills/slash-command-creator/references/examples.md +161 -0
- package/.claude/skills/slash-command-creator/references/frontmatter.md +74 -0
- package/.claude/skills/slash-command-creator/scripts/init_command.py +221 -0
- package/.claude/skills/subagent-creator/SKILL.md +127 -0
- package/.claude/skills/subagent-creator/assets/subagent-template.md +31 -0
- package/.claude/skills/subagent-creator/references/available-tools.md +63 -0
- package/.claude/skills/subagent-creator/references/examples.md +213 -0
- package/.claude/skills/youtube-collector/README.md +107 -0
- package/.claude/skills/youtube-collector/SKILL.md +158 -0
- package/.claude/skills/youtube-collector/references/data-schema.md +110 -0
- package/.claude/skills/youtube-collector/scripts/collect_videos.py +304 -0
- package/.claude/skills/youtube-collector/scripts/fetch_transcript.py +138 -0
- package/.claude/skills/youtube-collector/scripts/fetch_videos.py +229 -0
- package/.claude/skills/youtube-collector/scripts/register_channel.py +247 -0
- package/.claude/skills/youtube-collector/scripts/setup_api_key.py +151 -0
- package/package.json +1 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
YouTube 채널의 최신 영상 목록을 가져오는 스크립트
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python fetch_videos.py --channel-id UC... [--api-key YOUR_API_KEY] [--max-results 10]
|
|
7
|
+
python fetch_videos.py --channel-handle @channelname
|
|
8
|
+
|
|
9
|
+
API 키는 다음 위치에서 자동으로 로드됩니다:
|
|
10
|
+
- macOS/Linux: ~/.config/youtube-collector/config.yaml
|
|
11
|
+
- Windows: %APPDATA%\\youtube-collector\\config.yaml
|
|
12
|
+
|
|
13
|
+
Output:
|
|
14
|
+
JSON 형식으로 영상 목록 출력
|
|
15
|
+
|
|
16
|
+
Requirements:
|
|
17
|
+
pip install google-api-python-client pyyaml
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import platform
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from googleapiclient.discovery import build
|
|
29
|
+
from googleapiclient.errors import HttpError
|
|
30
|
+
except ImportError:
|
|
31
|
+
print(json.dumps({
|
|
32
|
+
"error": "google-api-python-client가 설치되어 있지 않습니다.",
|
|
33
|
+
"install": "pip install google-api-python-client"
|
|
34
|
+
}))
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import yaml
|
|
39
|
+
except ImportError:
|
|
40
|
+
print(json.dumps({
|
|
41
|
+
"error": "pyyaml이 설치되어 있지 않습니다.",
|
|
42
|
+
"install": "pip install pyyaml"
|
|
43
|
+
}))
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_api_key_config_path() -> str:
|
|
48
|
+
"""OS별 API 키 설정 파일 경로 반환"""
|
|
49
|
+
system = platform.system()
|
|
50
|
+
if system == "Windows":
|
|
51
|
+
base = os.environ.get("APPDATA", os.path.expanduser("~"))
|
|
52
|
+
return os.path.join(base, "youtube-collector", "config.yaml")
|
|
53
|
+
else: # macOS, Linux
|
|
54
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
55
|
+
return os.path.join(xdg_config, "youtube-collector", "config.yaml")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_api_key() -> str:
|
|
59
|
+
"""설정 파일에서 API 키 로드"""
|
|
60
|
+
config_path = get_api_key_config_path()
|
|
61
|
+
|
|
62
|
+
if not os.path.exists(config_path):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
67
|
+
config = yaml.safe_load(f)
|
|
68
|
+
return config.get('api_key') if config else None
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_channel_id_from_handle(youtube, handle: str) -> str:
|
|
74
|
+
"""채널 핸들(@username)로 채널 ID 조회"""
|
|
75
|
+
# @를 제거
|
|
76
|
+
handle = handle.lstrip('@')
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
response = youtube.search().list(
|
|
80
|
+
part="snippet",
|
|
81
|
+
q=f"@{handle}",
|
|
82
|
+
type="channel",
|
|
83
|
+
maxResults=1
|
|
84
|
+
).execute()
|
|
85
|
+
|
|
86
|
+
if response.get('items'):
|
|
87
|
+
return response['items'][0]['snippet']['channelId']
|
|
88
|
+
return None
|
|
89
|
+
except HttpError as e:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_channel_uploads_playlist_id(youtube, channel_id: str) -> str:
|
|
94
|
+
"""채널의 업로드 재생목록 ID 조회 (UC... -> UU...)"""
|
|
95
|
+
try:
|
|
96
|
+
response = youtube.channels().list(
|
|
97
|
+
part="contentDetails",
|
|
98
|
+
id=channel_id
|
|
99
|
+
).execute()
|
|
100
|
+
|
|
101
|
+
if response.get('items'):
|
|
102
|
+
return response['items'][0]['contentDetails']['relatedPlaylists']['uploads']
|
|
103
|
+
return None
|
|
104
|
+
except HttpError:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def fetch_videos(youtube, channel_id: str, max_results: int = 10) -> list:
|
|
109
|
+
"""채널의 최신 영상 목록 조회"""
|
|
110
|
+
videos = []
|
|
111
|
+
|
|
112
|
+
# 업로드 재생목록 ID 가져오기
|
|
113
|
+
uploads_playlist_id = get_channel_uploads_playlist_id(youtube, channel_id)
|
|
114
|
+
if not uploads_playlist_id:
|
|
115
|
+
return videos
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# 재생목록에서 영상 ID 목록 가져오기
|
|
119
|
+
response = youtube.playlistItems().list(
|
|
120
|
+
part="snippet,contentDetails",
|
|
121
|
+
playlistId=uploads_playlist_id,
|
|
122
|
+
maxResults=min(max_results, 50)
|
|
123
|
+
).execute()
|
|
124
|
+
|
|
125
|
+
video_ids = [item['contentDetails']['videoId'] for item in response.get('items', [])]
|
|
126
|
+
|
|
127
|
+
if not video_ids:
|
|
128
|
+
return videos
|
|
129
|
+
|
|
130
|
+
# 영상 상세 정보 가져오기
|
|
131
|
+
videos_response = youtube.videos().list(
|
|
132
|
+
part="snippet,contentDetails",
|
|
133
|
+
id=','.join(video_ids)
|
|
134
|
+
).execute()
|
|
135
|
+
|
|
136
|
+
for item in videos_response.get('items', []):
|
|
137
|
+
snippet = item['snippet']
|
|
138
|
+
content_details = item['contentDetails']
|
|
139
|
+
|
|
140
|
+
# 썸네일 URL (최대 해상도 우선)
|
|
141
|
+
thumbnails = snippet.get('thumbnails', {})
|
|
142
|
+
thumbnail_url = (
|
|
143
|
+
thumbnails.get('maxres', {}).get('url') or
|
|
144
|
+
thumbnails.get('high', {}).get('url') or
|
|
145
|
+
thumbnails.get('medium', {}).get('url') or
|
|
146
|
+
thumbnails.get('default', {}).get('url', '')
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
videos.append({
|
|
150
|
+
'video_id': item['id'],
|
|
151
|
+
'title': snippet.get('title', ''),
|
|
152
|
+
'description': snippet.get('description', ''),
|
|
153
|
+
'published_at': snippet.get('publishedAt', ''),
|
|
154
|
+
'channel_id': snippet.get('channelId', ''),
|
|
155
|
+
'channel_title': snippet.get('channelTitle', ''),
|
|
156
|
+
'thumbnail': thumbnail_url,
|
|
157
|
+
'duration': content_details.get('duration', ''),
|
|
158
|
+
'url': f"https://youtube.com/watch?v={item['id']}"
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return videos
|
|
162
|
+
|
|
163
|
+
except HttpError as e:
|
|
164
|
+
print(json.dumps({
|
|
165
|
+
"error": f"YouTube API 오류: {e.resp.status}",
|
|
166
|
+
"message": str(e)
|
|
167
|
+
}), file=sys.stderr)
|
|
168
|
+
return videos
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main():
|
|
172
|
+
parser = argparse.ArgumentParser(description='YouTube 채널의 최신 영상 목록 조회')
|
|
173
|
+
parser.add_argument('--channel-id', help='채널 ID (UC...)')
|
|
174
|
+
parser.add_argument('--channel-handle', help='채널 핸들 (@username)')
|
|
175
|
+
parser.add_argument('--api-key', help='YouTube Data API 키 (미지정시 설정 파일에서 로드)')
|
|
176
|
+
parser.add_argument('--max-results', type=int, default=10, help='최대 결과 수 (기본: 10)')
|
|
177
|
+
|
|
178
|
+
args = parser.parse_args()
|
|
179
|
+
|
|
180
|
+
if not args.channel_id and not args.channel_handle:
|
|
181
|
+
print(json.dumps({
|
|
182
|
+
"error": "--channel-id 또는 --channel-handle 중 하나를 지정해야 합니다."
|
|
183
|
+
}))
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
|
|
186
|
+
# API 키 결정: 인자 > 설정 파일
|
|
187
|
+
api_key = args.api_key or load_api_key()
|
|
188
|
+
|
|
189
|
+
if not api_key:
|
|
190
|
+
config_path = get_api_key_config_path()
|
|
191
|
+
print(json.dumps({
|
|
192
|
+
"error": "YouTube Data API 키가 설정되지 않았습니다.",
|
|
193
|
+
"help": f"다음 명령으로 API 키를 설정하세요: python3 setup_api_key.py",
|
|
194
|
+
"config_path": config_path
|
|
195
|
+
}))
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
youtube = build('youtube', 'v3', developerKey=api_key)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(json.dumps({
|
|
202
|
+
"error": "YouTube API 초기화 실패",
|
|
203
|
+
"message": str(e)
|
|
204
|
+
}))
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
|
|
207
|
+
# 채널 ID 결정
|
|
208
|
+
channel_id = args.channel_id
|
|
209
|
+
if not channel_id and args.channel_handle:
|
|
210
|
+
channel_id = get_channel_id_from_handle(youtube, args.channel_handle)
|
|
211
|
+
if not channel_id:
|
|
212
|
+
print(json.dumps({
|
|
213
|
+
"error": f"채널을 찾을 수 없습니다: {args.channel_handle}"
|
|
214
|
+
}))
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
# 영상 목록 조회
|
|
218
|
+
videos = fetch_videos(youtube, channel_id, args.max_results)
|
|
219
|
+
|
|
220
|
+
print(json.dumps({
|
|
221
|
+
"channel_id": channel_id,
|
|
222
|
+
"fetched_at": datetime.utcnow().isoformat() + "Z",
|
|
223
|
+
"count": len(videos),
|
|
224
|
+
"videos": videos
|
|
225
|
+
}, ensure_ascii=False, indent=2))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
main()
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
YouTube 채널을 channels.yaml에 등록하는 스크립트
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python register_channel.py --channel-handle @channelname --output-dir .reference/
|
|
7
|
+
python register_channel.py --channel-url https://youtube.com/@channelname --output-dir .reference/
|
|
8
|
+
|
|
9
|
+
Output:
|
|
10
|
+
JSON 형식으로 등록 결과 출력
|
|
11
|
+
|
|
12
|
+
Requirements:
|
|
13
|
+
pip install google-api-python-client pyyaml
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from urllib.parse import urlparse, unquote
|
|
24
|
+
|
|
25
|
+
# 같은 디렉토리의 모듈 import
|
|
26
|
+
script_dir = Path(__file__).parent
|
|
27
|
+
sys.path.insert(0, str(script_dir))
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from fetch_videos import (
|
|
31
|
+
load_api_key,
|
|
32
|
+
get_channel_id_from_handle,
|
|
33
|
+
)
|
|
34
|
+
from googleapiclient.discovery import build
|
|
35
|
+
except ImportError as e:
|
|
36
|
+
print(json.dumps({
|
|
37
|
+
"error": "필요한 모듈을 import할 수 없습니다.",
|
|
38
|
+
"detail": str(e),
|
|
39
|
+
"install": "pip install google-api-python-client pyyaml"
|
|
40
|
+
}))
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
import yaml
|
|
45
|
+
except ImportError:
|
|
46
|
+
print(json.dumps({
|
|
47
|
+
"error": "pyyaml이 설치되어 있지 않습니다.",
|
|
48
|
+
"install": "pip install pyyaml"
|
|
49
|
+
}))
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_handle_from_url(url: str) -> str:
|
|
54
|
+
"""YouTube URL에서 채널 핸들 추출"""
|
|
55
|
+
# URL 디코딩
|
|
56
|
+
url = unquote(url)
|
|
57
|
+
|
|
58
|
+
# URL 파싱
|
|
59
|
+
parsed = urlparse(url)
|
|
60
|
+
path = parsed.path
|
|
61
|
+
|
|
62
|
+
# @username 형식 (예: youtube.com/@channelname)
|
|
63
|
+
match = re.match(r'^/@([^/]+)', path)
|
|
64
|
+
if match:
|
|
65
|
+
return '@' + match.group(1)
|
|
66
|
+
|
|
67
|
+
# /channel/UC... 형식
|
|
68
|
+
match = re.match(r'^/channel/(UC[a-zA-Z0-9_-]+)', path)
|
|
69
|
+
if match:
|
|
70
|
+
return match.group(1) # 채널 ID 반환
|
|
71
|
+
|
|
72
|
+
# /c/customname 형식
|
|
73
|
+
match = re.match(r'^/c/([^/]+)', path)
|
|
74
|
+
if match:
|
|
75
|
+
return '@' + match.group(1)
|
|
76
|
+
|
|
77
|
+
# /user/username 형식
|
|
78
|
+
match = re.match(r'^/user/([^/]+)', path)
|
|
79
|
+
if match:
|
|
80
|
+
return '@' + match.group(1)
|
|
81
|
+
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_channel_info(youtube, channel_id: str) -> dict:
|
|
86
|
+
"""채널 ID로 채널 상세 정보 조회"""
|
|
87
|
+
try:
|
|
88
|
+
response = youtube.channels().list(
|
|
89
|
+
part="snippet",
|
|
90
|
+
id=channel_id
|
|
91
|
+
).execute()
|
|
92
|
+
|
|
93
|
+
if response.get('items'):
|
|
94
|
+
snippet = response['items'][0]['snippet']
|
|
95
|
+
return {
|
|
96
|
+
'id': channel_id,
|
|
97
|
+
'name': snippet.get('title', ''),
|
|
98
|
+
'description': snippet.get('description', ''),
|
|
99
|
+
'custom_url': snippet.get('customUrl', '')
|
|
100
|
+
}
|
|
101
|
+
return None
|
|
102
|
+
except Exception:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_channels(channels_file: Path) -> dict:
|
|
107
|
+
"""channels.yaml 로드"""
|
|
108
|
+
if not channels_file.exists():
|
|
109
|
+
return {'channels': []}
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
with open(channels_file, 'r', encoding='utf-8') as f:
|
|
113
|
+
data = yaml.safe_load(f)
|
|
114
|
+
return data if data else {'channels': []}
|
|
115
|
+
except Exception:
|
|
116
|
+
return {'channels': []}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def save_channels(channels_file: Path, data: dict):
|
|
120
|
+
"""channels.yaml 저장"""
|
|
121
|
+
# 부모 폴더 생성
|
|
122
|
+
channels_file.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
with open(channels_file, 'w', encoding='utf-8') as f:
|
|
125
|
+
yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main():
|
|
129
|
+
parser = argparse.ArgumentParser(description='YouTube 채널 등록')
|
|
130
|
+
parser.add_argument('--channel-handle', help='채널 핸들 (@username)')
|
|
131
|
+
parser.add_argument('--channel-url', help='채널 URL')
|
|
132
|
+
parser.add_argument('--channel-id', help='채널 ID (UC...)')
|
|
133
|
+
parser.add_argument('--output-dir', default='.reference', help='저장 디렉토리 (기본: .reference)')
|
|
134
|
+
parser.add_argument('--api-key', help='YouTube Data API 키 (미지정시 설정 파일에서 로드)')
|
|
135
|
+
|
|
136
|
+
args = parser.parse_args()
|
|
137
|
+
|
|
138
|
+
# 옵션 검증
|
|
139
|
+
if not args.channel_handle and not args.channel_url and not args.channel_id:
|
|
140
|
+
print(json.dumps({
|
|
141
|
+
"error": "--channel-handle, --channel-url, --channel-id 중 하나를 지정해야 합니다."
|
|
142
|
+
}))
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
# API 키 로드
|
|
146
|
+
api_key = args.api_key or load_api_key()
|
|
147
|
+
if not api_key:
|
|
148
|
+
print(json.dumps({
|
|
149
|
+
"error": "YouTube Data API 키가 설정되지 않았습니다.",
|
|
150
|
+
"help": "python3 scripts/setup_api_key.py로 설정해주세요."
|
|
151
|
+
}))
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
# YouTube API 초기화
|
|
155
|
+
try:
|
|
156
|
+
youtube = build('youtube', 'v3', developerKey=api_key)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(json.dumps({
|
|
159
|
+
"error": "YouTube API 초기화 실패",
|
|
160
|
+
"message": str(e)
|
|
161
|
+
}))
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
|
|
164
|
+
# 채널 핸들/ID 결정
|
|
165
|
+
channel_handle = args.channel_handle
|
|
166
|
+
channel_id = args.channel_id
|
|
167
|
+
|
|
168
|
+
if args.channel_url:
|
|
169
|
+
extracted = extract_handle_from_url(args.channel_url)
|
|
170
|
+
if not extracted:
|
|
171
|
+
print(json.dumps({
|
|
172
|
+
"error": "URL에서 채널 정보를 추출할 수 없습니다.",
|
|
173
|
+
"url": args.channel_url
|
|
174
|
+
}))
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
if extracted.startswith('UC'):
|
|
178
|
+
channel_id = extracted
|
|
179
|
+
else:
|
|
180
|
+
channel_handle = extracted
|
|
181
|
+
|
|
182
|
+
# 채널 ID 조회
|
|
183
|
+
if not channel_id and channel_handle:
|
|
184
|
+
channel_id = get_channel_id_from_handle(youtube, channel_handle)
|
|
185
|
+
if not channel_id:
|
|
186
|
+
print(json.dumps({
|
|
187
|
+
"error": f"채널을 찾을 수 없습니다: {channel_handle}"
|
|
188
|
+
}))
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
# 채널 상세 정보 조회
|
|
192
|
+
channel_info = get_channel_info(youtube, channel_id)
|
|
193
|
+
if not channel_info:
|
|
194
|
+
print(json.dumps({
|
|
195
|
+
"error": f"채널 정보를 가져올 수 없습니다: {channel_id}"
|
|
196
|
+
}))
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
# 핸들 결정 (없으면 custom_url 사용)
|
|
200
|
+
if not channel_handle:
|
|
201
|
+
custom_url = channel_info.get('custom_url', '')
|
|
202
|
+
if custom_url:
|
|
203
|
+
channel_handle = custom_url if custom_url.startswith('@') else '@' + custom_url
|
|
204
|
+
else:
|
|
205
|
+
channel_handle = '@' + channel_id # fallback
|
|
206
|
+
|
|
207
|
+
# channels.yaml 로드
|
|
208
|
+
channels_file = Path(args.output_dir) / "channels.yaml"
|
|
209
|
+
data = load_channels(channels_file)
|
|
210
|
+
|
|
211
|
+
# 중복 체크
|
|
212
|
+
existing_ids = {ch.get('id') for ch in data.get('channels', [])}
|
|
213
|
+
if channel_id in existing_ids:
|
|
214
|
+
print(json.dumps({
|
|
215
|
+
"status": "already_registered",
|
|
216
|
+
"message": f"채널이 이미 등록되어 있습니다: {channel_info['name']}",
|
|
217
|
+
"channel": {
|
|
218
|
+
"id": channel_id,
|
|
219
|
+
"handle": channel_handle,
|
|
220
|
+
"name": channel_info['name']
|
|
221
|
+
}
|
|
222
|
+
}))
|
|
223
|
+
sys.exit(0)
|
|
224
|
+
|
|
225
|
+
# 채널 추가
|
|
226
|
+
new_channel = {
|
|
227
|
+
'id': channel_id,
|
|
228
|
+
'handle': channel_handle,
|
|
229
|
+
'name': channel_info['name'],
|
|
230
|
+
'added_at': datetime.now().strftime('%Y-%m-%d')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
data['channels'].append(new_channel)
|
|
234
|
+
|
|
235
|
+
# 저장
|
|
236
|
+
save_channels(channels_file, data)
|
|
237
|
+
|
|
238
|
+
print(json.dumps({
|
|
239
|
+
"status": "registered",
|
|
240
|
+
"message": f"채널이 등록되었습니다: {channel_info['name']}",
|
|
241
|
+
"channel": new_channel,
|
|
242
|
+
"file": str(channels_file)
|
|
243
|
+
}, ensure_ascii=False, indent=2))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
main()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
YouTube Data API 키를 설정하는 스크립트
|
|
4
|
+
|
|
5
|
+
사용자로부터 API 키를 입력받아 OS별 적절한 경로에 저장합니다.
|
|
6
|
+
|
|
7
|
+
저장 경로:
|
|
8
|
+
- macOS/Linux: ~/.config/youtube-collector/config.yaml
|
|
9
|
+
- Windows: %APPDATA%\\youtube-collector\\config.yaml
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python setup_api_key.py # 대화형 입력
|
|
13
|
+
python setup_api_key.py --api-key KEY # 직접 지정
|
|
14
|
+
python setup_api_key.py --show # 현재 설정 확인
|
|
15
|
+
python setup_api_key.py --path # 설정 파일 경로 출력
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
import platform
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_config_path() -> str:
|
|
25
|
+
"""OS별 설정 파일 경로 반환"""
|
|
26
|
+
system = platform.system()
|
|
27
|
+
if system == "Windows":
|
|
28
|
+
base = os.environ.get("APPDATA", os.path.expanduser("~"))
|
|
29
|
+
return os.path.join(base, "youtube-collector", "config.yaml")
|
|
30
|
+
else: # macOS, Linux
|
|
31
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
32
|
+
return os.path.join(xdg_config, "youtube-collector", "config.yaml")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_config(config_path: str) -> dict:
|
|
36
|
+
"""기존 설정 로드"""
|
|
37
|
+
if not os.path.exists(config_path):
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import yaml
|
|
42
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
43
|
+
return yaml.safe_load(f) or {}
|
|
44
|
+
except ImportError:
|
|
45
|
+
# yaml 없으면 간단히 파싱
|
|
46
|
+
config = {}
|
|
47
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
48
|
+
for line in f:
|
|
49
|
+
line = line.strip()
|
|
50
|
+
if line and not line.startswith('#') and ':' in line:
|
|
51
|
+
key, value = line.split(':', 1)
|
|
52
|
+
config[key.strip()] = value.strip().strip('"\'')
|
|
53
|
+
return config
|
|
54
|
+
except Exception:
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_config(config_path: str, config: dict):
|
|
59
|
+
"""설정 저장"""
|
|
60
|
+
# 디렉토리 생성
|
|
61
|
+
config_dir = os.path.dirname(config_path)
|
|
62
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
import yaml
|
|
66
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
67
|
+
f.write("# YouTube Collector API 설정\n")
|
|
68
|
+
f.write("# 이 파일은 자동 생성되었습니다.\n\n")
|
|
69
|
+
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
|
70
|
+
except ImportError:
|
|
71
|
+
# yaml 없으면 수동 작성
|
|
72
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
73
|
+
f.write("# YouTube Collector API 설정\n")
|
|
74
|
+
f.write("# 이 파일은 자동 생성되었습니다.\n\n")
|
|
75
|
+
for key, value in config.items():
|
|
76
|
+
f.write(f'{key}: "{value}"\n')
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def mask_api_key(key: str) -> str:
|
|
80
|
+
"""API 키 마스킹 (앞 8자, 뒤 4자만 표시)"""
|
|
81
|
+
if not key or len(key) < 16:
|
|
82
|
+
return "***"
|
|
83
|
+
return f"{key[:8]}...{key[-4:]}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main():
|
|
87
|
+
parser = argparse.ArgumentParser(description='YouTube Data API 키 설정')
|
|
88
|
+
parser.add_argument('--api-key', help='설정할 API 키')
|
|
89
|
+
parser.add_argument('--show', action='store_true', help='현재 설정 확인')
|
|
90
|
+
parser.add_argument('--path', action='store_true', help='설정 파일 경로 출력')
|
|
91
|
+
|
|
92
|
+
args = parser.parse_args()
|
|
93
|
+
|
|
94
|
+
config_path = get_config_path()
|
|
95
|
+
|
|
96
|
+
# 경로만 출력
|
|
97
|
+
if args.path:
|
|
98
|
+
print(config_path)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# 현재 설정 확인
|
|
102
|
+
if args.show:
|
|
103
|
+
if os.path.exists(config_path):
|
|
104
|
+
config = load_config(config_path)
|
|
105
|
+
api_key = config.get('api_key', '')
|
|
106
|
+
print(f"설정 파일: {config_path}")
|
|
107
|
+
print(f"API 키: {mask_api_key(api_key) if api_key else '(설정되지 않음)'}")
|
|
108
|
+
else:
|
|
109
|
+
print(f"설정 파일이 없습니다: {config_path}")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# API 키 설정
|
|
113
|
+
api_key = args.api_key
|
|
114
|
+
|
|
115
|
+
if not api_key:
|
|
116
|
+
# 대화형 입력
|
|
117
|
+
print("YouTube Data API 키 설정")
|
|
118
|
+
print("-" * 40)
|
|
119
|
+
print(f"설정 파일 경로: {config_path}")
|
|
120
|
+
print()
|
|
121
|
+
print("API 키 발급 방법:")
|
|
122
|
+
print("1. https://console.cloud.google.com/ 접속")
|
|
123
|
+
print("2. 프로젝트 생성/선택")
|
|
124
|
+
print("3. YouTube Data API v3 활성화")
|
|
125
|
+
print("4. 사용자 인증 정보 > API 키 생성")
|
|
126
|
+
print()
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
api_key = input("API 키를 입력하세요: ").strip()
|
|
130
|
+
except (KeyboardInterrupt, EOFError):
|
|
131
|
+
print("\n취소되었습니다.")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
if not api_key:
|
|
135
|
+
print("API 키가 입력되지 않았습니다.")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
# 기존 설정 로드 및 업데이트
|
|
139
|
+
config = load_config(config_path)
|
|
140
|
+
config['api_key'] = api_key
|
|
141
|
+
|
|
142
|
+
# 저장
|
|
143
|
+
save_config(config_path, config)
|
|
144
|
+
|
|
145
|
+
print()
|
|
146
|
+
print(f"API 키가 저장되었습니다: {config_path}")
|
|
147
|
+
print(f"저장된 키: {mask_api_key(api_key)}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|