Files
minecraft_launcher/src/installer-rp/images.ts
claude-bot 6cd402121b i18n: 리소스팩 설치기 UI 문구를 locales/installer-rp/ko-kr.json 으로 분리
- main/preload/ytdlp/ffmpeg/music/images/pack/renderer 전반에서 로그·에러·진행
  메시지 문자열을 locales/installer-rp/ko-kr.json 사전 키로 교체
- preload 에 loadLocale 추가, main 에 rp:i18n:dict IPC 핸들러 추가
- 패키징된 .exe 에서도 한국어 사전이 적용되도록 electron-builder.yml 의
  extraResources 에 locales/ 폴더 추가

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 04:00:31 +09:00

113 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { promises as fs } from 'node:fs'
import path from 'node:path'
import http from 'node:http'
import https from 'node:https'
import { URL } from 'node:url'
import sharp from 'sharp'
import { loadComponentI18n } from '../shared/i18n.js'
const { t } = loadComponentI18n('installer-rp')
/** painting variant 텍스처의 최대 변 길이(px). 슬롯 4x4 × 256px. */
const MAX_SIDE = 1024
/** 유튜브 URL 에서 영상 ID 만 뽑아낸다. 못 찾으면 빈 문자열. */
export function ytIdFromUrl(url: string): string {
try {
const u = new URL(url)
if (u.hostname === 'youtu.be') return u.pathname.replace(/^\//, '')
if (/youtube\.com$/i.test(u.hostname) || /^(www\.|m\.)?youtube\.com$/i.test(u.hostname)) {
const v = u.searchParams.get('v')
if (v) return v
// shorts/<id>, embed/<id> 형태도 대응
const m = u.pathname.match(/\/(?:shorts|embed)\/([^/]+)/)
if (m) return m[1]
}
return ''
} catch {
return ''
}
}
/** 단순 HTTP/HTTPS GET (302 따라감, 4xx/5xx 는 reject). */
function fetchBuffer(url: string, redirects = 0): Promise<Buffer> {
return new Promise((resolve, reject) => {
if (redirects > 8) {
reject(new Error(t('common.tooManyRedirects')))
return
}
const target = new URL(url)
const lib = target.protocol === 'https:' ? https : http
const req = lib.get(target, {
timeout: 30000,
headers: { 'user-agent': 'mc-music-quiz-rp-installer' }
}, (res) => {
const code = res.statusCode || 0
if (code >= 300 && code < 400 && res.headers.location) {
res.resume()
fetchBuffer(new URL(res.headers.location, target).toString(), redirects + 1)
.then(resolve, reject)
return
}
if (code !== 200) {
res.resume()
reject(new Error(`HTTP ${code}`))
return
}
const chunks: Buffer[] = []
res.on('data', (c: Buffer) => chunks.push(c))
res.on('end', () => resolve(Buffer.concat(chunks)))
})
req.on('error', reject)
req.on('timeout', () => req.destroy(new Error(t('common.requestTimeout'))))
})
}
/**
* 이미지 URL 을 다운로드해 Buffer 로 돌려준다.
* - 유튜브 영상 URL 이면 `i.ytimg.com/vi/<id>/maxresdefault.jpg` 1차 →
* 실패하면 `hqdefault.jpg` 로 폴백.
* - 그 외 URL 은 HTTP GET 으로 그대로 받음.
*/
export async function downloadImage(rawUrl: string): Promise<Buffer> {
const ytId = ytIdFromUrl(rawUrl)
if (ytId) {
try {
return await fetchBuffer(`https://i.ytimg.com/vi/${ytId}/maxresdefault.jpg`)
} catch {
return await fetchBuffer(`https://i.ytimg.com/vi/${ytId}/hqdefault.jpg`)
}
}
return fetchBuffer(rawUrl)
}
/**
* painting variant 슬롯 규격(정사각 1:1, ≤1024×1024)에 맞춰 정규화.
* 알고리즘 (docs/add.md):
* 1) s = min(가로, 세로) → 가운데 정사각 크롭 (s×s)
* 2) s > 1024 이면 1024×1024 로 축소 (Lanczos)
* 3) s ≤ 1024 이면 그대로 (업스케일 없음)
* 결과를 PNG 로 outPath 에 저장.
*/
export async function normalizeToCover(buffer: Buffer, outPath: string): Promise<void> {
const img = sharp(buffer)
const meta = await img.metadata()
const w = meta.width ?? 0
const h = meta.height ?? 0
if (w <= 0 || h <= 0) throw new Error(t('errors.imageMetaUnknown'))
const s = Math.min(w, h)
const left = Math.floor((w - s) / 2)
const top = Math.floor((h - s) / 2)
let pipeline = img.extract({ left, top, width: s, height: s })
if (s > MAX_SIDE) {
pipeline = pipeline.resize(MAX_SIDE, MAX_SIDE, { kernel: 'lanczos3' })
}
await fs.mkdir(path.dirname(outPath), { recursive: true })
await pipeline.png().toFile(outPath)
}
/** cover_NN.png 파일명을 만든다 (NN 2자리 0패딩). */
export function coverFileName(index: number): string {
return `cover_${String(index).padStart(2, '0')}.png`
}