"""Animily Music - YouTube 업로드 독립 YouTube 업로드 모듈. 기존 auto_shorts의 토큰을 공유하되 코드는 독립. """ import os import pickle from google.auth.transport.requests import Request from googleapiclient.discovery import build from googleapiclient.http import MediaFileUpload from config import TOKEN_PATH, YOUTUBE_CATEGORY_ID DESCRIPTION_TEMPLATE = """{title} 🎵 저작권 프리 음악 (Copyright Free) 본 음악은 AI로 생성된 저작권 프리 음악입니다. 개인/상업적 용도 모두 자유롭게 사용하실 수 있습니다. 출처 표기 없이 자유롭게 사용 가능합니다. 🎵 Copyright Free Music This music is AI-generated and copyright-free. Free to use for any personal or commercial purpose. No attribution required. ━━━━━━━━━━━━━━━━━━━━━━━━ 🔬 과학적 근거 • University of Glasgow + Scottish SPCA (2017): 레게/소프트록에서 개 스트레스 최소 • Through a Dog's Ear (2008): 50-60 BPM 솔로 피아노에서 70%+ 진정 반응 • Snowdon & Teie (2015): 고양이는 퍼링 주파수(25-50Hz)에 긍정 반응 ━━━━━━━━━━━━━━━━━━━━━━━━ 🐾 반려동물 행동교정 플랫폼 애니밀리 📲 앱 다운로드: 앱스토어/구글플레이에서 '애니밀리' 검색 🌐 https://conimals.co.kr/ 🤖 AI 생성 음악 (ACE-Step 1.5) #반려동물음악 #강아지수면음악 #고양이음악 #펫힐링 #저작권프리 #dogmusic #catmusic #petrelaxation #copyrightfree #royaltyfree""" FIRST_COMMENT = """🎵 저작권 프리 음악입니다! 개인/상업적 용도 모두 자유롭게 사용하세요. 출처 표기 불필요, 다운로드 자유 ✅ 🎵 This is copyright-free music! Free for personal and commercial use. No attribution needed ✅""" DEFAULT_TAGS = [ "반려동물음악", "강아지수면음악", "고양이음악", "펫힐링", "저작권프리", "수면음악", "분리불안", "반려동물", "dog music", "cat music", "pet relaxation", "copyright free", "royalty free", "sleep music", "calming music", "애니밀리", ] def _get_youtube_client(): """YouTube API 클라이언트 (토큰 자동 리프레시)""" with open(TOKEN_PATH, "rb") as f: creds = pickle.load(f) if creds.expired and creds.refresh_token: creds.refresh(Request()) with open(TOKEN_PATH, "wb") as f: pickle.dump(creds, f) print(" [YT] 토큰 리프레시 완료", flush=True) return build("youtube", "v3", credentials=creds) def upload_video(video_path, title, thumbnail_path=None, extra_tags=None, privacy="public"): """YouTube 영상 업로드 Args: video_path: 영상 파일 경로 title: 영상 제목 thumbnail_path: 썸네일 이미지 경로 extra_tags: 추가 태그 privacy: "public", "unlisted", "private" Returns: str: video_id or None """ youtube = _get_youtube_client() description = DESCRIPTION_TEMPLATE.format(title=title) tags = list(DEFAULT_TAGS) if extra_tags: tags.extend(extra_tags) body = { "snippet": { "title": title[:100], "description": description, "tags": tags[:30], "categoryId": YOUTUBE_CATEGORY_ID, "defaultLanguage": "ko", }, "status": { "privacyStatus": privacy, "madeForKids": False, "selfDeclaredMadeForKids": False, }, } print(f" [YT] 업로드 시작: {title}", flush=True) print(f" [YT] 파일: {video_path} ({os.path.getsize(video_path) / (1024*1024):.0f}MB)", flush=True) media = MediaFileUpload( video_path, mimetype="video/mp4", resumable=True, chunksize=50 * 1024 * 1024, # 50MB chunks ) request = youtube.videos().insert( part="snippet,status", body=body, media_body=media, ) video_id = None response = None while response is None: status, response = request.next_chunk() if status: progress = int(status.progress() * 100) if progress % 20 == 0: print(f" [YT] 업로드 진행: {progress}%", flush=True) video_id = response.get("id") print(f" [YT] 업로드 완료: https://youtu.be/{video_id}", flush=True) # 썸네일 설정 if thumbnail_path and os.path.exists(thumbnail_path): try: # 2MB 제한 체크 if os.path.getsize(thumbnail_path) > 2 * 1024 * 1024: from PIL import Image img = Image.open(thumbnail_path).convert("RGB") compressed = thumbnail_path.replace(".png", "_thumb.jpg") img.save(compressed, "JPEG", quality=85) thumbnail_path = compressed youtube.thumbnails().set( videoId=video_id, media_body=MediaFileUpload(thumbnail_path, mimetype="image/jpeg"), ).execute() print(f" [YT] 썸네일 설정 완료", flush=True) except Exception as e: print(f" [YT] 썸네일 실패: {e}", flush=True) # 고정 댓글 if video_id: try: youtube.commentThreads().insert( part="snippet", body={ "snippet": { "videoId": video_id, "topLevelComment": { "snippet": {"textOriginal": FIRST_COMMENT} }, } }, ).execute() print(f" [YT] 고정 댓글 등록 완료", flush=True) except Exception as e: print(f" [YT] 고정 댓글 실패: {e}", flush=True) return video_id # 뮤직큐우 재생목록 (기존) PLAYLIST_ID = "PLr8dPYZT-hCUjL-OgPxJdF81Dvn_g8Vbg" def create_or_get_playlist(title="뮤직큐우"): """재생목록 ID 반환 (기존 '뮤직큐우' 사용)""" return PLAYLIST_ID def add_to_playlist(video_id, playlist_id): """영상을 재생목록에 추가""" if not playlist_id or not video_id: return youtube = _get_youtube_client() try: youtube.playlistItems().insert( part="snippet", body={ "snippet": { "playlistId": playlist_id, "resourceId": { "kind": "youtube#video", "videoId": video_id, }, } }, ).execute() print(f" [YT] 재생목록 추가 완료", flush=True) except Exception as e: print(f" [YT] 재생목록 추가 실패: {e}", flush=True) if __name__ == "__main__": print("upload_youtube.py - scheduler.py를 통해 사용하세요")