Compare commits

..

17 Commits

Author SHA1 Message Date
5a3159d4cb chore: bump version to 1.1.6 2024-08-07 17:08:15 +08:00
38b99bf4dc feat: import export ui layout 2024-08-07 17:07:18 +08:00
0cd9c071d7 chore: bump version to 1.1.5 2024-08-03 23:23:33 +08:00
bf582d0194 fix: update color 2024-08-03 23:19:49 +08:00
5dec6a8273 chore: bump version to 1.1.4 2024-07-29 15:44:10 +08:00
0e429a4762 fix: OS servers log path 2024-07-29 15:43:25 +08:00
5164a17dca style: pylint 2024-07-28 10:13:21 +08:00
a660c03bb5 docs: fix old project name 2024-07-27 15:56:13 +08:00
d7457f2bfb chore: bump version to 1.1.3 2024-07-27 15:50:31 +08:00
223ab899e0 fix(uigf): add bangboo to ID table
close: #5
2024-07-27 15:50:02 +08:00
f62ca1d7e7 chore: bump version to 1.1.2 2024-07-26 10:06:44 +08:00
c034b2e70a feat(uigf): metadata from local db 2024-07-26 10:05:02 +08:00
a2faa86f0c chore: bump version to 1.1.1 2024-07-25 21:45:45 +08:00
510bfdab7a feat: add support for Europe, America, TW,HK,MO servers 2024-07-25 21:44:38 +08:00
f616944755 chore: bump version to 1.1.0 2024-07-25 13:47:37 +08:00
7300c6e719 refactor(i18n): UIGF v4.0 is no longer in beta 2024-07-25 13:26:57 +08:00
6fe12da9be feat(uigf): import UIGFv4.0
BREAKING CHANGES: `region` is no longer stored in/read from local file
2024-07-25 13:25:26 +08:00
13 changed files with 2744 additions and 48 deletions

View File

@ -2,7 +2,7 @@ const fs = require('fs-extra')
const path = require('path') const path = require('path')
const crypto = require('crypto') const crypto = require('crypto')
const AdmZip = require('adm-zip') const AdmZip = require('adm-zip')
const { version } = require('../package.json') const { version, autoUpdateActive, autoUpdateFrom } = require('../package.json')
const hash = (data, type = 'sha256') => { const hash = (data, type = 'sha256') => {
const hmac = crypto.createHmac(type, 'nap') const hmac = crypto.createHmac(type, 'nap')
@ -32,9 +32,9 @@ const start = async () => {
await fs.copy(zipPath, path.resolve(outputPath, `${hashName}.zip`)) await fs.copy(zipPath, path.resolve(outputPath, `${hashName}.zip`))
await fs.remove(zipPath) await fs.remove(zipPath)
await fs.outputJSON(path.join(outputPath, 'manifest.json'), { await fs.outputJSON(path.join(outputPath, 'manifest.json'), {
active: true, active: autoUpdateActive,
version, version: version,
from: '0.0.1', from: autoUpdateFrom,
name: `${hashName}.zip`, name: `${hashName}.zip`,
hash: sha256 hash: sha256
}) })

View File

@ -6,7 +6,7 @@
一个使用 Electron 制作的小工具,需要在 Windows 操作系统上运行。 一个使用 Electron 制作的小工具,需要在 Windows 操作系统上运行。
通过读取游戏日志或者代理模式获取访问游戏跃迁记录 API 所需的 authKey然后再使用获取到的 authKey 来读取游戏跃迁记录。 通过读取游戏日志或者代理模式获取访问游戏调频记录 API 所需的 authKey然后再使用获取到的 authKey 来读取游戏调频记录。
## 其它语言 ## 其它语言
@ -15,7 +15,7 @@
## 使用说明 ## 使用说明
1. 下载工具后解压 - 下载地址: [GitHub](https://github.com/earthjasonlin/zzz-signal-search-export/releases/latest/download/ZzzSignalSearchExport.zip) / [123云盘](https://www.123pan.com/s/Vs9uVv-ShhE.html) / [蓝奏云(密码:zzzz](https://www.lanzouh.com/b00eewtvxa) 1. 下载工具后解压 - 下载地址: [GitHub](https://github.com/earthjasonlin/zzz-signal-search-export/releases/latest/download/ZzzSignalSearchExport.zip) / [123云盘](https://www.123pan.com/s/Vs9uVv-ShhE.html) / [蓝奏云(密码:zzzz](https://www.lanzouh.com/b00eewtvxa)
2. 打开游戏的跃迁详情页面 2. 打开游戏的调频详情页面
![详情页面](/docs/wish-history.jpg) ![详情页面](/docs/wish-history.jpg)
@ -34,7 +34,7 @@
如果需要导出多个账号的数据,可以点击旁边的加号按钮。 如果需要导出多个账号的数据,可以点击旁边的加号按钮。
然后游戏切换的新账号,再打开跃迁历史记录,工具再点击“加载数据”按钮。 然后游戏切换的新账号,再打开调频历史记录,工具再点击“加载数据”按钮。
## Devlopment ## Devlopment

View File

@ -6,7 +6,7 @@ This project is modified from the [star-rail-warp-export](https://github.com/biu
A tool made from Electron that runs on the Windows operating system. A tool made from Electron that runs on the Windows operating system.
Read the game log or proxy to get the authKey needed to access the game warp history API, and then use the authKey to read the game wish history. Read the game log or proxy to get the authKey needed to access the game signal search history API, and then use the authKey to read the game wish history.
## Other languages ## Other languages
@ -18,7 +18,7 @@ If you feel that the existing translation is inappropriate, you can send a pull
1. Unzip after downloading the tool - [GitHub](https://github.com/earthjasonlin/zzz-signal-search-export/releases/latest/download/ZzzSignalSearchExport.zip) 1. Unzip after downloading the tool - [GitHub](https://github.com/earthjasonlin/zzz-signal-search-export/releases/latest/download/ZzzSignalSearchExport.zip)
2. Open the warp details page of the game 2. Open the signal search details page of the game
![warp details](/docs/wish-history-en.jpg) ![warp details](/docs/wish-history-en.jpg)

View File

@ -1,6 +1,8 @@
{ {
"name": "zzz-signal-search-export", "name": "zzz-signal-search-export",
"version": "1.0.10", "version": "1.1.6",
"autoUpdateActive": true,
"autoUpdateFrom": "1.1.0",
"main": "./dist/electron/main/main.js", "main": "./dist/electron/main/main.js",
"author": "earthjasonlin <https://git.loliquq.cn/earthjasonlin>", "author": "earthjasonlin <https://git.loliquq.cn/earthjasonlin>",
"homepage": "https://github.com/earthjasonlin/zzz-signal-search-export", "homepage": "https://github.com/earthjasonlin/zzz-signal-search-export",

View File

@ -3,9 +3,10 @@
"ui.button.load": "Load data", "ui.button.load": "Load data",
"ui.button.update": "Update", "ui.button.update": "Update",
"ui.button.directUpdate": "Direct update", "ui.button.directUpdate": "Direct update",
"ui.button.files": "Export Files", "ui.button.files": "Import/Export",
"ui.button.excel": "Export Excel", "ui.button.excel": "Export Excel",
"ui.button.uigf": "Export UIGF (Beta)", "ui.button.uigf": "Export UIGF",
"ui.button.import": "Import UIGF",
"ui.button.url": "Input URL", "ui.button.url": "Input URL",
"ui.button.setting": "Settings", "ui.button.setting": "Settings",
"ui.button.option": "Option", "ui.button.option": "Option",

View File

@ -3,9 +3,10 @@
"ui.button.load": "加载数据", "ui.button.load": "加载数据",
"ui.button.update": "更新数据", "ui.button.update": "更新数据",
"ui.button.directUpdate": "直接更新", "ui.button.directUpdate": "直接更新",
"ui.button.files": "导出文件", "ui.button.files": "导入/导出",
"ui.button.excel": "导出Excel", "ui.button.excel": "导出Excel",
"ui.button.uigf":"导出UIGF (Beta)", "ui.button.uigf":"导出UIGF",
"ui.button.import":"导入UIGF",
"ui.button.url": "输入URL", "ui.button.url": "输入URL",
"ui.button.setting": "设置", "ui.button.setting": "设置",
"ui.button.option": "选项", "ui.button.option": "选项",

View File

@ -3,9 +3,10 @@
"ui.button.load": "加載數據", "ui.button.load": "加載數據",
"ui.button.update": "更新數據", "ui.button.update": "更新數據",
"ui.button.directUpdate": "直接更新", "ui.button.directUpdate": "直接更新",
"ui.button.files": "導出文件", "ui.button.files": "導入/匯出",
"ui.button.excel": "導出Excel", "ui.button.excel": "導出Excel",
"ui.button.uigf":"導出UIGF (Beta)", "ui.button.uigf":"導出UIGF",
"ui.button.import":"導入UIGF",
"ui.button.url": "輸入URL", "ui.button.url": "輸入URL",
"ui.button.setting": "設置", "ui.button.setting": "設置",
"ui.button.option": "選項", "ui.button.option": "選項",

2487
src/idJson.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,13 @@
const { app, ipcMain, dialog } = require('electron') const { app, ipcMain, dialog } = require('electron')
const fs = require('fs-extra') const fs = require('fs-extra')
const path = require('path') const path = require('path')
const getData = require('./getData').getData const { getData, saveData, changeCurrent, convertTimeZone } = require('./getData')
const config = require('./config')
const { name, version } = require('../../package.json') const { name, version } = require('../../package.json')
const i18n = require('./i18n') const i18n = require('./i18n')
const { exit } = require('process') const { mergeData } = require('./utils/mergeData')
const { sendMsg } = require('./utils')
const idJson = require('../idJson.json')
const getTimeString = () => { const getTimeString = () => {
return new Date().toLocaleString('sv').replace(/[- :]/g, '').slice(0, -2) return new Date().toLocaleString('sv').replace(/[- :]/g, '').slice(0, -2)
@ -16,8 +19,7 @@ const formatDate = (date) => {
let d = `${date.getDate()}`.padStart(2, '0') let d = `${date.getDate()}`.padStart(2, '0')
return `${y}-${m}-${d} ${date.toLocaleString('zh-cn', { hour12: false }).slice(-8)}` return `${y}-${m}-${d} ${date.toLocaleString('zh-cn', { hour12: false }).slice(-8)}`
} }
const exportUIGF = async (uids) => {
const start = async (uids) => {
const result = { const result = {
info: { info: {
export_timestamp: Math.ceil(Date.now() / 1000), export_timestamp: Math.ceil(Date.now() / 1000),
@ -35,16 +37,7 @@ const start = async (uids) => {
if (!fulldata.length) { if (!fulldata.length) {
throw new Error('数据为空') throw new Error('数据为空')
} }
const serverTimeZone = new Map([
["prod_gf_cn", 8],
["prod_gf_jp", 8]
])
fulldata.forEach(data => { fulldata.forEach(data => {
let timezone
timezone = serverTimeZone.get(data.region)
if(!timezone) {
throw new Error('不支持此服务器')
}
const listTemp = [] const listTemp = []
for (let [type, arr] of data.result) { for (let [type, arr] of data.result) {
arr.forEach(log => { arr.forEach(log => {
@ -64,7 +57,7 @@ const start = async (uids) => {
listTemp.sort((a, b) => Number(BigInt(a.id) - BigInt(b.id))) listTemp.sort((a, b) => Number(BigInt(a.id) - BigInt(b.id)))
let dataTemp = { let dataTemp = {
uid: data.uid, uid: data.uid,
timezone: timezone, timezone: data.region_time_zone,
lang: data.lang, lang: data.lang,
list: [] list: []
} }
@ -87,6 +80,95 @@ const start = async (uids) => {
} }
} }
const importUIGF = async () => {
const filepath = await dialog.showOpenDialogSync({
properties: ['openFile'],
filters: [
{ name: i18n.uigf.fileType, extensions: ['json'] }
]
})
if (!filepath) return
const { dataMap, current } = await getData()
try {
const jsonData = fs.readJsonSync(filepath[0])
if('info' in jsonData && 'version' in jsonData.info) {
if (jsonData.info.version !== 'v4.0') {
sendMsg('不支持此版本UIGF')
console.error('不支持此版本UIGF')
return
}
} else {
sendMsg('UIGF格式错误')
console.error('UIGF格式错误')
return
}
jsonData.nap.forEach(uidData => {
const resultTemp = []
const isNew = !Boolean(dataMap.has(uidData.uid))
let region_time_zone
if (!isNew) region_time_zone = dataMap.get(uidData.uid).region_time_zone
else region_time_zone = uidData.timezone
let targetLang
if (!isNew) targetLang = dataMap.get(uidData.uid).lang
else targetLang = uidData.lang
if(!idJson[targetLang] && (!uidData.list[0].name || !uidData.list[0].item_type || !uidData.list[0].rank_type)) targetLang = config.lang
let idTargetLangJson = idJson[targetLang]
uidData.list.forEach(recordEntry => {
let rank_type
if (idTargetLangJson?.[recordEntry.item_id].rank_type) rank_type = String(idTargetLangJson[recordEntry.item_id].rank_type)
else rank_type = recordEntry.rank_type
resultTemp.push({
gacha_id: recordEntry.gacha_id,
gacha_type: recordEntry.gacha_type,
item_id: recordEntry.item_id,
count: recordEntry.count ?? "1",
time: convertTimeZone(recordEntry.time, uidData.timezone, region_time_zone),
name: idTargetLangJson?.[recordEntry.item_id].name ?? recordEntry.name,
item_type: idTargetLangJson?.[recordEntry.item_id].item_type ?? recordEntry.item_type,
rank_type: rank_type,
id: recordEntry.id
})
})
const resultTempGrouped = resultTemp.reduce((acc, curr) => {
if (!acc[curr.gacha_type]) {
acc[curr.gacha_type] = []
}
acc[curr.gacha_type].push(curr)
return acc;
}, {})
const resultTempMap = new Map(Object.entries(resultTempGrouped))
const resultMap = { result: resultTempMap, uid: uidData.uid}
let data
const mergedData = mergeData(dataMap.get(uidData.uid), resultMap)
if (isNew) {
data = { result: mergedData, time: Date.now(), uid: uidData.uid, lang: targetLang, region_time_zone: uidData.timezone, deleted: false }
} else {
data = { result: mergedData, time: Date.now(), uid: dataMap.get(uidData.uid).uid, lang: targetLang, region_time_zone: dataMap.get(uidData.uid).region_time_zone, deleted: dataMap.get(uidData.uid).deleted }
}
saveData(data, '')
changeCurrent(uidData.uid)
dataMap.set(uidData.uid, data)
})
return {
dataMap,
current: config.current
}
} catch (error) {
sendMsg(error, 'ERROR')
console.error(error)
}
}
ipcMain.handle('EXPORT_UIGF_JSON', async (event, uids) => { ipcMain.handle('EXPORT_UIGF_JSON', async (event, uids) => {
await start(uids) await exportUIGF(uids)
})
ipcMain.handle('IMPORT_UIGF_JSON', async () => {
return await importUIGF()
}) })

View File

@ -29,6 +29,27 @@ const defaultTypeMap = new Map([
['5', '邦布频段'] ['5', '邦布频段']
]) ])
const serverTimeZone = new Map([
["prod_gf_cn", 8],
["prod_gf_jp", 8],
["prod_gf_us", -5],
["prod_gf_eu", 1],
["prod_gf_sg", 8]
])
const convertTimeZone = (dateTimeStr, fromTimeZoneOffset, toTimeZoneOffset) => {
let date = new Date(dateTimeStr.replace(' ', 'T') + 'Z');
let utcDate = new Date(date.getTime() - fromTimeZoneOffset * 60 * 60 * 1000);
let targetDate = new Date(utcDate.getTime() + toTimeZoneOffset * 60 * 60 * 1000);
let year = targetDate.getUTCFullYear();
let month = String(targetDate.getUTCMonth() + 1).padStart(2, '0');
let day = String(targetDate.getUTCDate()).padStart(2, '0');
let hours = String(targetDate.getUTCHours()).padStart(2, '0');
let minutes = String(targetDate.getUTCMinutes()).padStart(2, '0');
let seconds = String(targetDate.getUTCSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
const findDataFiles = async (dataPath, fileMap) => { const findDataFiles = async (dataPath, fileMap) => {
const files = await readdir(dataPath) const files = await readdir(dataPath)
if (files?.length) { if (files?.length) {
@ -93,7 +114,7 @@ const changeCurrent = async (uid) => {
const detectGameLocale = async (userPath) => { const detectGameLocale = async (userPath) => {
let list = [] let list = []
const lang = app.getLocale() const lang = app.getLocale()
const arr = ['/miHoYo/绝区零/', '/Cognosphere/Zenless Zone Zero/'] const arr = ['/miHoYo/绝区零/', '/miHoYo/ZenlessZoneZero/']
arr.forEach(str => { arr.forEach(str => {
try { try {
const pathname = path.join(userPath, '/AppData/LocalLow/', str, 'Player.log') const pathname = path.join(userPath, '/AppData/LocalLow/', str, 'Player.log')
@ -203,7 +224,6 @@ const getGachaLogs = async ({ name, key }, queryString) => {
let logs = [] let logs = []
let uid = '' let uid = ''
let region = '' let region = ''
let region_time_zone = ''
let endId = '0' let endId = '0'
const url = `${apiDomain}/common/gacha_record/api/getGachaLog?${queryString}` const url = `${apiDomain}/common/gacha_record/api/getGachaLog?${queryString}`
do { do {
@ -221,9 +241,6 @@ const getGachaLogs = async ({ name, key }, queryString) => {
if (!region) { if (!region) {
region = res.region region = res.region
} }
if (!region_time_zone) {
region_time_zone = res.region_time_zone
}
list.push(...logs) list.push(...logs)
page += 1 page += 1
@ -252,7 +269,7 @@ const getGachaLogs = async ({ name, key }, queryString) => {
} }
} }
} while (logs.length > 0) } while (logs.length > 0)
return { list, uid, region, region_time_zone } return { list, uid, region }
} }
const checkResStatus = (res) => { const checkResStatus = (res) => {
@ -425,10 +442,25 @@ const fetchData = async (urlOverride) => {
const typeMap = new Map() const typeMap = new Map()
const lang = searchParams.get('lang') const lang = searchParams.get('lang')
let originUid = '' let originUid = ''
let originRegion = '' let localTimeZone
let originTimeZone = ''
for (const type of gachaType) { for (const type of gachaType) {
const { list, uid, region, region_time_zone } = await getGachaLogs(type, queryString) const { list, uid, region} = await getGachaLogs(type, queryString)
const region_time_zone = serverTimeZone.get(region)
if(!region_time_zone) {
sendMsg('不支持此服务器')
console.error('不支持此服务器')
return
}
if (localTimeZone === undefined) {
localTimeZone = dataMap.get(uid)?.region_time_zone
if (localTimeZone === undefined) {
localTimeZone = region_time_zone
}
}
localTimeZone === Number(localTimeZone)
list.forEach(item => {
item.time = convertTimeZone(item.time, region_time_zone, localTimeZone)
})
const logs = list.map((item) => { const logs = list.map((item) => {
const { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type, count} = item const { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type, count} = item
return { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type, count } return { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type, count }
@ -439,14 +471,8 @@ const fetchData = async (urlOverride) => {
if (!originUid) { if (!originUid) {
originUid = uid originUid = uid
} }
if (!originRegion) {
originRegion = region
} }
if (!originTimeZone) { const data = { result, typeMap, time: Date.now(), uid: originUid, lang, region_time_zone: localTimeZone }
originTimeZone = region_time_zone
}
}
const data = { result, typeMap, time: Date.now(), uid: originUid, lang, region: originRegion, region_time_zone: originTimeZone }
const localData = dataMap.get(originUid) const localData = dataMap.get(originUid)
const mergedResult = mergeData(localData, data) const mergedResult = mergeData(localData, data)
data.result = mergedResult data.result = mergedResult
@ -527,5 +553,9 @@ exports.getData = () => {
} }
} }
exports.serverTimeZone = serverTimeZone
exports.getUrl = getUrl exports.getUrl = getUrl
exports.deleteData = deleteData exports.deleteData = deleteData
exports.saveData = saveData
exports.changeCurrent = changeCurrent
exports.convertTimeZone = convertTimeZone

View File

@ -12,6 +12,7 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="excel">{{ui.button.excel}}</el-dropdown-item> <el-dropdown-item command="excel">{{ui.button.excel}}</el-dropdown-item>
<el-dropdown-item command="uigf-json">{{ui.button.uigf}}</el-dropdown-item> <el-dropdown-item command="uigf-json">{{ui.button.uigf}}</el-dropdown-item>
<el-dropdown-item command="import-json" divided>{{ui.button.import}}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@ -272,11 +273,25 @@ const exportUIGFJSON = () => {
}); });
} }
const importData = async () => {
state.status = 'loading'
const data = await ipcRenderer.invoke('IMPORT_UIGF_JSON')
if (data) {
state.dataMap = data.dataMap
state.current = data.current
state.status = 'loaded'
} else {
state.status = 'failed'
}
}
const exportCommand = (type) => { const exportCommand = (type) => {
if (type === 'excel') { if (type === 'excel') {
saveExcel() saveExcel()
} else if (type === 'uigf-json') { } else if (type === 'uigf-json') {
exportUIGFJSON() exportUIGFJSON()
} else if (type === 'import-json') {
importData()
} }
} }
const openCacheFolder = async () => { const openCacheFolder = async () => {

View File

@ -32,7 +32,7 @@ const props = defineProps({
const chart = ref(null); const chart = ref(null);
const colors = ["#eeaa66", "#fac858", "#ee6666", "#5470c6", "#ba66ee", "#91cc75", "#73c0de"]; const colors = ["#fac858", "#fac858", "#ee6666", "#5470c6", "#5470c6", "#91cc75", "#73c0de"];
const parseData = (detail, type) => { const parseData = (detail, type) => {
const text = props.i18n.ui.data; const text = props.i18n.ui.data;

77
tools/getIdMap.py Normal file
View File

@ -0,0 +1,77 @@
# pylint: disable=C0116, C0103, C0201
"""Download and process data from the Hakushin API"""
import json
import requests
from opencc import OpenCC
# 初始化 OpenCC 转换器
cc = OpenCC('s2t')
# 获取 JSON 数据
weapon_url = 'https://api.hakush.in/zzz/data/weapon.json'
character_url = 'https://api.hakush.in/zzz/data/character.json'
bangboo_url = 'https://api.hakush.in/zzz/data/bangboo.json'
# 语言映射配置
language_map = {
"zh-cn": "CHS",
"zh-tw": "CHS", # 简体转繁体
"en-us": "EN",
"ja-jp": "JA",
"ko-kr": "KO"
}
# 类型映射配置
type_map = {
"weapon": {"zh-cn": "音擎", "zh-tw": "音擎", "en-us": "W-Engines", "ja-jp": "音動機", "ko-kr": "W-엔진"},
"character": {"zh-cn": "代理人", "zh-tw": "代理人", "en-us": "Agents",
"ja-jp": "エージェント", "ko-kr": "에이전트"},
"bangboo": {"zh-cn": "邦布", "zh-tw": "邦布", "en-us": "Bangboo",
"ja-jp": "ボンプ", "ko-kr": "「Bangboo」"}
}
def fetch_json(url):
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
def transform_data(data, item_type):
transformed = {lang: {} for lang in language_map.keys()}
for id_, item in data.items():
for lang, key in language_map.items():
name = item[key] if lang != 'zh-tw' else cc.convert(item['CHS'])
transformed[lang][id_] = {
"name": name,
"item_type": type_map[item_type][lang],
"rank_type": item['rank']
}
return transformed
def main():
try:
weapon_data = fetch_json(weapon_url)
character_data = fetch_json(character_url)
bangboo_data = fetch_json(bangboo_url)
transformed_data = {lang: {} for lang in language_map.keys()}
weapon_transformed = transform_data(weapon_data, "weapon")
character_transformed = transform_data(character_data, "character")
bangboo_transformed = transform_data(bangboo_data, "bangboo")
for lang in language_map.keys():
transformed_data[lang].update(weapon_transformed[lang])
transformed_data[lang].update(character_transformed[lang])
transformed_data[lang].update(bangboo_transformed[lang])
with open('./src/idJson.json', 'w', encoding='utf-8') as f:
json.dump(transformed_data, f, ensure_ascii=False, indent=2)
print("Data successfully transformed and saved")
except requests.RequestException as e:
print(f"Error fetching data: {e}")
if __name__ == "__main__":
main()