From 2296d453b18b514aa49cbd6d4a4ca664c81677c0 Mon Sep 17 00:00:00 2001 From: exqt <8168124+exqt@users.noreply.github.com> Date: Sun, 3 Nov 2024 21:46:46 +0900 Subject: [PATCH 1/7] refactor: rating upload page --- src/components/Rating/RatingApp.svelte | 237 ++++++++++--------------- src/components/Rating/clearData.ts | 15 ++ 2 files changed, 112 insertions(+), 140 deletions(-) create mode 100644 src/components/Rating/clearData.ts diff --git a/src/components/Rating/RatingApp.svelte b/src/components/Rating/RatingApp.svelte index 90fedb9..5f8311f 100644 --- a/src/components/Rating/RatingApp.svelte +++ b/src/components/Rating/RatingApp.svelte @@ -3,6 +3,8 @@ import Profile from './RatingProfile.svelte' import type { CardData, ScoreData } from 'node-hiroba/types' import lzutf8 from 'lzutf8' + import type { ClearData } from './clearData' + import { waitFor } from '../../lib/utils' const wikiOrigin = 'https://taiko.wiki' @@ -16,12 +18,77 @@ // ready let sendType: 'clear' | 'score' = 'clear' + + // 새로운 유틸리티 함수들 + async function checkWikiLogin (): Promise { + const wikiUser = await (await fetch(wikiOrigin + '/api/user', { credentials: 'include' })).json() + return wikiUser.logined === true + } + + async function fetchScoreForSong (songNo: string, clearData: ClearData): Promise { + const response: { songNo: string, body: { oni?: string, ura?: string } } = + { songNo, body: {} } + + while (true) { + try { + if (clearData.difficulty.oni === undefined) break + response.body.oni = await (await fetch( + `https://donderhiroba.jp/score_detail.php?song_no=${songNo}&level=${4}` + )).text() + break + } catch { + console.log('fetching error try in 3 seconds') + await waitFor(3000) + } + } + + while (true) { + try { + if (clearData.difficulty.ura === undefined) break + response.body.ura = await (await fetch( + `https://donderhiroba.jp/score_detail.php?song_no=${songNo}&level=${5}` + )).text() + break + } catch { + console.log('fetching error try in 3 seconds') + await waitFor(3000) + } + } + + console.log(response) + if (response.body.oni === undefined && response.body.ura === undefined) return null + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const parsed = parse.parseScoreData(response as any) + return parsed ?? null + } + + async function uploadToWiki (donderData: CardData, clearData: ClearData[], scoreData: ScoreData[]): Promise { + const data = JSON.stringify({ + donderData, + clearData, + scoreData + }) + + console.log(data) + + const compressedBody = lzutf8.compress(data, { + outputEncoding: 'ByteArray' + }) as Uint8Array + + await fetch(wikiOrigin + '/api/user/donder', { + credentials: 'include', + method: 'POST', + body: compressedBody, + headers: { + 'Content-Type': 'application/json' + } + }) + } + + // 리팩토링된 send 함수 async function send (cardData: CardData, sendType: 'clear' | 'score'): Promise { - if ( - !confirm( - `Send your donderhiroba datas to ${wikiOrigin}. It will be deleted together when you delete your account. Do you agree?` - ) - ) { + if (!confirm(`Send your donderhiroba datas to ${wikiOrigin}. It will be deleted together when you delete your account. Do you agree?`)) { alert('Canceled.') message = '' uploadMessage = '' @@ -29,12 +96,7 @@ return } - const wikiUser = await ( - await fetch(wikiOrigin + '/api/user', { - credentials: 'include' - }) - ).json() - if (wikiUser.logined === false) { + if (!await checkWikiLogin()) { message = 'Wiki Not Logined' return } @@ -42,144 +104,39 @@ try { scene = 'upload' uploadMessage = '' - // 클리어 데이터만 + + uploadMessage = 'Fetching clear data...' + let clearData = await hiroba.getClearData(null) + clearData = clearData.filter((song) => song.songNo.length < 3) // TODO + if (sendType === 'clear') { - uploadMessage = 'Fetching clear data...' - const clearData = await hiroba.getClearData(null) uploadMessage = 'Uploading clear data...' - const data = JSON.stringify({ - donderData: cardData, - clearData - }) - const body = lzutf8.compress(data, { - outputEncoding: 'ByteArray' - }) as Uint8Array - await fetch(wikiOrigin + '/api/user/donder', { - credentials: 'include', - method: 'POST', - body, - headers: { - 'Content-Type': 'application/json' - } - }) - } else { // 점수 데이터 + await uploadToWiki(cardData, clearData, []) + } else { uploadMessage = 'Updating score...' - await hiroba.updateScore(null) - uploadMessage = 'Fetching clear data...' - const clearData = await hiroba.getClearData(null) - const songNoDatas: Array<{ songNo: string, hasUra: boolean }> = - [] - clearData.forEach((e) => { - const hasUra = !(e.difficulty.ura === undefined) - songNoDatas.push({ - songNo: e.songNo, - hasUra - }) - }) - - counts = clearData.length - complete = 0 - uploadMessage = `Fetch score data... (0/${counts})` + // await hiroba.updateScore(null) - const grouped: Array< - Array<{ songNo: string, hasUra: boolean }> - > = [[], [], [], [], [], [], [], [], [], []] - songNoDatas.forEach((song, index) => { - grouped[Number(index.toString().at(-1))].push(song) - }) - const scoreData: Record = {} - const errors: Array<{ songNo: string, hasUra: boolean }> = [] - await Promise.all( - grouped.map(async (group) => { - for (const song of group) { - /** eslint-disable @typescript-eslint/no-unsafe-argument */ - const response: any = { - songNo: song.songNo, - body: {} - } - - try { - const fetched = await fetch( - `https://donderhiroba.jp/score_detail.php?song_no=${song.songNo}&level=4` - ) - const body = await fetched.text() - response.body.oni = body - if (song.hasUra) { - const fetched = await fetch( - `https://donderhiroba.jp/score_detail.php?song_no=${song.songNo}&level=5` - ) - const body = await fetched.text() - response.body.ura = body - } - } catch { - errors.push(song) - continue - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const parsed = parse.parseScoreData(response) - - if (parsed !== null) { - scoreData[song.songNo] = parsed - complete++ - uploadMessage = `Fetch score data... (${complete}/${counts})` - } - } - }) - ) - - for (const song of errors) { - const response: any = { - songNo: song.songNo, - body: {} + const scoresToFetch: Array> = [] + for (const song of clearData) { + if (song.difficulty.oni !== undefined || song.difficulty.ura !== undefined) { + scoresToFetch.push(fetchScoreForSong(song.songNo, song)) } + } - try { - const fetched = await fetch( - `https://donderhiroba.jp/score_detail.php?song_no=${song.songNo}&level=4` - ) - const body = await fetched.text() - response.body.oni = body - if (song.hasUra) { - const fetched = await fetch( - `https://donderhiroba.jp/score_detail.php?song_no=${song.songNo}&level=5` - ) - const body = await fetched.text() - response.body.ura = body - } - } catch { - errors.push(song) - continue - } + counts = scoresToFetch.length + complete = 0 + uploadMessage = `Fetch score data... (0/${counts})` - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const parsed = parse.parseScoreData(response) + const scoreData = await Promise.all(scoresToFetch.map(async (promise) => { + const result = await promise + complete++ + uploadMessage = `Fetch score data... (${complete}/${counts})` + return result + })) - if (parsed !== null) { - scoreData[song.songNo] = parsed - complete++ - uploadMessage = `Fetch score data... (${complete}/${counts})` - } - } + console.log(scoreData, clearData) - const data = JSON.stringify({ - donderData: cardData, - clearData, - scoreData - }) - - const body = lzutf8.compress(data, { - outputEncoding: 'ByteArray' - }) as Uint8Array - - await fetch(wikiOrigin + '/api/user/donder', { - credentials: 'include', - method: 'POST', - body, - headers: { - 'Content-Type': 'application/json' - } - }) + await uploadToWiki(cardData, clearData, scoreData.filter((score) => score !== null)) } message = 'Upload completed' diff --git a/src/components/Rating/clearData.ts b/src/components/Rating/clearData.ts new file mode 100644 index 0000000..75f9f5c --- /dev/null +++ b/src/components/Rating/clearData.ts @@ -0,0 +1,15 @@ +export type Difficulty = 'easy' | 'normal' | 'hard' | 'oni' | 'ura' + +export type Crown = 'played' | 'silver' | 'gold' | 'donderfull' | null +export type Badge = 'rainbow' | 'purple' | 'pink' | 'gold' | 'silver' | 'bronze' | 'white' | null + +export interface Clear { + crown: Crown + badge: Badge +} + +export interface ClearData { + title: string + songNo: string + difficulty: Partial> +} From cb471365ddee1b6ce6ef6c35088d632e5e507164 Mon Sep 17 00:00:00 2001 From: exqt <8168124+exqt@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:31:22 +0900 Subject: [PATCH 2/7] feat: recent score upload --- package-lock.json | 2 + package.json | 1 + public/rating.html | 3 + src/components/Rating/RatingApp.svelte | 208 +++++++++++++++++--- src/components/Rating/clearData.ts | 15 -- src/components/Rating/ratingTypes.ts | 46 +++++ src/components/Rating/recentScore.ts | 101 ++++++++++ src/components/Rating/recentScoreStorage.ts | 70 +++++++ src/components/Table/styles.css | 1 + 9 files changed, 406 insertions(+), 41 deletions(-) delete mode 100644 src/components/Rating/clearData.ts create mode 100644 src/components/Rating/ratingTypes.ts create mode 100644 src/components/Rating/recentScore.ts create mode 100644 src/components/Rating/recentScoreStorage.ts create mode 100644 src/components/Table/styles.css diff --git a/package-lock.json b/package-lock.json index 4dbaaae..2388fd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "donder-hiroba-plus", "version": "0.2.8.1", "dependencies": { + "cheerio": "^1.0.0", "hangul-js": "^0.2.6", "lzutf8": "^0.6.3", "node-hiroba": "^1.7.3", @@ -7214,6 +7215,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", diff --git a/package.json b/package.json index 15dfd2f..9642a94 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "vite": "^5.4.10" }, "dependencies": { + "cheerio": "^1.0.0", "hangul-js": "^0.2.6", "lzutf8": "^0.6.3", "node-hiroba": "^1.7.3", diff --git a/public/rating.html b/public/rating.html index c066c36..b3bea4c 100644 --- a/public/rating.html +++ b/public/rating.html @@ -1,4 +1,7 @@ + + +
diff --git a/src/components/Rating/RatingApp.svelte b/src/components/Rating/RatingApp.svelte index 5f8311f..9910212 100644 --- a/src/components/Rating/RatingApp.svelte +++ b/src/components/Rating/RatingApp.svelte @@ -3,8 +3,11 @@ import Profile from './RatingProfile.svelte' import type { CardData, ScoreData } from 'node-hiroba/types' import lzutf8 from 'lzutf8' - import type { ClearData } from './clearData' + import type { ClearData, DifficultyScoreData } from './ratingTypes' import { waitFor } from '../../lib/utils' + import { onMount } from 'svelte'; + import { getRecentScoreData, type RecentScoreData } from './recentScore'; + import RecentScoreStorage from './recentScoreStorage'; const wikiOrigin = 'https://taiko.wiki' @@ -16,8 +19,46 @@ let counts: number = 0 let complete: number = 0 + let nRecentPageToFetch: number = 20 + const storage = new RecentScoreStorage() + let storageLoaded = false + let lastUpdated: string | null = null + let scoreDataSorted: {songName: string, difficulty: string, score: DifficultyScoreData}[] = [] + let totalPlayCount: string = '0 / 0 / 0 / 0' + + onMount(async () => { + await storage.loadFromChromeStorage() + storageLoaded = true + lastUpdated = storage.getLastUpdated() + updateScoreDataSorted() + }) + + const updateScoreDataSorted = () => { + scoreDataSorted = [] + let totalPlay = 0 + let totalClear = 0 + let totalFullcombo = 0 + let totalDonderfullcombo = 0 + for (const [songNo, scoreData] of Object.entries(storage.getMap())) { + for (const [difficulty, score] of Object.entries(scoreData.difficulty)) { + scoreDataSorted.push({ + songName: scoreData.title, + difficulty, + score + }) + totalPlay += score.count.play + totalClear += score.count.clear + totalFullcombo += score.count.fullcombo + totalDonderfullcombo += score.count.donderfullcombo + } + } + scoreDataSorted.sort((a, b) => b.score.count.play - a.score.count.play) + totalPlayCount = `${totalPlay} / ${totalClear} / ${totalFullcombo} / ${totalDonderfullcombo}` + } + + // ready - let sendType: 'clear' | 'score' = 'clear' + let sendType: 'clear' | 'score' | 'recent' = 'clear' // 새로운 유틸리티 함수들 async function checkWikiLogin (): Promise { @@ -29,6 +70,7 @@ const response: { songNo: string, body: { oni?: string, ura?: string } } = { songNo, body: {} } + let waitTime = 3000 while (true) { try { if (clearData.difficulty.oni === undefined) break @@ -37,8 +79,8 @@ )).text() break } catch { - console.log('fetching error try in 3 seconds') - await waitFor(3000) + await waitFor(waitTime) + waitTime *= 2 } } @@ -50,12 +92,11 @@ )).text() break } catch { - console.log('fetching error try in 3 seconds') - await waitFor(3000) + await waitFor(waitTime) + waitTime *= 2 } } - console.log(response) if (response.body.oni === undefined && response.body.ura === undefined) return null // eslint-disable-next-line @typescript-eslint/no-unsafe-argument @@ -63,15 +104,14 @@ return parsed ?? null } - async function uploadToWiki (donderData: CardData, clearData: ClearData[], scoreData: ScoreData[]): Promise { + async function uploadToWiki (donderData: CardData, clearData: ClearData[], scoreDataMap: Record): Promise { + const data = JSON.stringify({ donderData, clearData, - scoreData + scoreData: scoreDataMap }) - console.log(data) - const compressedBody = lzutf8.compress(data, { outputEncoding: 'ByteArray' }) as Uint8Array @@ -84,10 +124,13 @@ 'Content-Type': 'application/json' } }) + + await storage.mergeMap(scoreDataMap) + updateScoreDataSorted() } // 리팩토링된 send 함수 - async function send (cardData: CardData, sendType: 'clear' | 'score'): Promise { + async function send (cardData: CardData, sendType: 'clear' | 'score' | 'recent'): Promise { if (!confirm(`Send your donderhiroba datas to ${wikiOrigin}. It will be deleted together when you delete your account. Do you agree?`)) { alert('Canceled.') message = '' @@ -106,18 +149,64 @@ uploadMessage = '' uploadMessage = 'Fetching clear data...' - let clearData = await hiroba.getClearData(null) - clearData = clearData.filter((song) => song.songNo.length < 3) // TODO + await hiroba.updateScore(null) + let clearData = await hiroba.getClearData(null) + if (sendType === 'clear') { uploadMessage = 'Uploading clear data...' - await uploadToWiki(cardData, clearData, []) - } else { + await uploadToWiki(cardData, clearData, {}) + + } else if (sendType === 'recent') { + uploadMessage = 'Fetching recent score data...' + const songNameToSongNo = new Map() + for (const song of clearData) { + songNameToSongNo.set(song.title, song.songNo) + } + + counts = nRecentPageToFetch + complete = 0 + uploadMessage = `Fetch score data... (0/${counts})` + + const recentScoreData: RecentScoreData[] = [] + await Promise.all( + Array.from({ length: nRecentPageToFetch }, (_, i) => i + 1).map(async (page) => { + recentScoreData.push(...(await getRecentScoreData(page))) + complete++ + uploadMessage = `Fetch score data... (${complete}/${counts})` + }) + ) + + const scoreDataMap: Record = {} + for (const recentScore of recentScoreData) { + const songNo = songNameToSongNo.get(recentScore.songName) + if (songNo === undefined) continue + const score = recentScore.scoreData + if (scoreDataMap[songNo] === undefined) { + scoreDataMap[songNo] = { + title: recentScore.songName, + songNo, + difficulty: {} + } + } + scoreDataMap[songNo].difficulty[recentScore.difficulty] = score + } + + console.log(nRecentPageToFetch) + console.log(recentScoreData) + console.log(songNameToSongNo) + console.log(scoreDataMap) + + await uploadToWiki(cardData, clearData, scoreDataMap) + + message = 'Upload completed' + scene = 'ready' + } else if (sendType === 'score') { uploadMessage = 'Updating score...' - // await hiroba.updateScore(null) const scoresToFetch: Array> = [] for (const song of clearData) { + // if (song.songNo.length >= 3 || song.songNo >= '30') continue // for debug if (song.difficulty.oni !== undefined || song.difficulty.ura !== undefined) { scoresToFetch.push(fetchScoreForSong(song.songNo, song)) } @@ -134,9 +223,16 @@ return result })) - console.log(scoreData, clearData) + const scoreDataMap: Record = {} + for (const score of scoreData) { + if (score !== null) { + scoreDataMap[score.songNo] = score + } + } - await uploadToWiki(cardData, clearData, scoreData.filter((score) => score !== null)) + console.log(scoreDataMap, clearData) + + await uploadToWiki(cardData, clearData, scoreDataMap) } message = 'Upload completed' @@ -147,8 +243,6 @@ scene = 'ready' } } - - // upload
@@ -161,20 +255,64 @@ {/if} - + {#if storageLoaded} +
+ Last Score Updated:
{lastUpdated}
+
+ Total Play Count: {totalPlayCount} +
+ List of Scores + + + + + + + + + + {#each scoreDataSorted as score} + + + + + + {/each} + +
NameDiffCount
+ {score.songName} + {score.difficulty}{score.score.count.play} / {score.score.count.clear} / {score.score.count.fullcombo} / {score.score.count.donderfullcombo}
+
+ {/if} {:else if scene === 'upload'} {uploadMessage} {/if} @@ -189,10 +327,28 @@ display: flex; flex-direction: column; align-items: center; + font-size: 1rem; + text-align: center; + line-height: 1.5; } .error_display { color: red; font-weight: bold; } + + .play-count-table { + width: 100%; + max-width: 600px; + border: 1px solid black; + border-collapse: collapse; + table-layout: fixed; + } + + .play-count-table th, + .play-count-table td { + border: 1px solid black; + padding: 5px; + text-align: center; + } diff --git a/src/components/Rating/clearData.ts b/src/components/Rating/clearData.ts deleted file mode 100644 index 75f9f5c..0000000 --- a/src/components/Rating/clearData.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Difficulty = 'easy' | 'normal' | 'hard' | 'oni' | 'ura' - -export type Crown = 'played' | 'silver' | 'gold' | 'donderfull' | null -export type Badge = 'rainbow' | 'purple' | 'pink' | 'gold' | 'silver' | 'bronze' | 'white' | null - -export interface Clear { - crown: Crown - badge: Badge -} - -export interface ClearData { - title: string - songNo: string - difficulty: Partial> -} diff --git a/src/components/Rating/ratingTypes.ts b/src/components/Rating/ratingTypes.ts new file mode 100644 index 0000000..75dd67e --- /dev/null +++ b/src/components/Rating/ratingTypes.ts @@ -0,0 +1,46 @@ +export type Difficulty = 'easy' | 'normal' | 'hard' | 'oni' | 'ura' + +export type Crown = 'played' | 'silver' | 'gold' | 'donderfull' | null +export type Badge = 'rainbow' | 'purple' | 'pink' | 'gold' | 'silver' | 'bronze' | 'white' | null + +export interface Clear { + crown: Crown + badge: Badge +} + +export interface ClearData { + title: string + songNo: string + difficulty: Partial> +} + +export interface ScoreData { + title: string + songNo: string + difficulty: Partial> +} + +export interface ScoreResponseData { + songNo: string + body: Record +} + +export interface DifficultyScoreData { + crown: Crown + badge: Badge + score: number + ranking: number + good: number + ok: number + bad: number + maxCombo: number + roll: number + count: Count +} + +export interface Count { + play: number + clear: number + fullcombo: number + donderfullcombo: number +} diff --git a/src/components/Rating/recentScore.ts b/src/components/Rating/recentScore.ts new file mode 100644 index 0000000..b41ebf3 --- /dev/null +++ b/src/components/Rating/recentScore.ts @@ -0,0 +1,101 @@ +import { load } from 'cheerio' +import * as cheerio from 'cheerio' +import type { Difficulty, ScoreData, ScoreResponseData, DifficultyScoreData, Crown, Badge } from './ratingTypes' + +const CROWN_MAP: Record = { + '01': 'played', + '03': 'silver', + '02': 'gold', + '04': 'donderfull' +} + +const getCrown = (src: string | undefined) => { + return src ? CROWN_MAP[src] ?? null : null +} + +const BADGE_MAP: Record = { + '8': 'rainbow', + '7': 'purple', + '6': 'pink', + '5': 'gold', + '4': 'silver', + '3': 'bronze', + '2': 'white' +} + +const getBadge = (element: any) => { + if (!element?.length) return null + const badgeId = element.attr('src')?.replace('image/sp/640/best_score_rank_', '').replace('_640.png', '') + return BADGE_MAP[badgeId] ?? null +} + +const diffCodeToDifficulty = (code: string): Difficulty => { + if (code === '5') return 'ura' + if (code === '4') return 'oni' + if (code === '3') return 'hard' + if (code === '2') return 'normal' + return 'easy' +} + +export type RecentScoreData = { + songName: string + difficulty: Difficulty + scoreData: DifficultyScoreData +} + +function parseDifficultyScoreData(body: any): RecentScoreData | null { + const $ = load(body) + + const songName = $('.songNameTitleScore h2').text().trim() + + const diffCode = $('.levelIcon').first().attr('src')?.match(/icon_course02_(\d+)_640\.png/)?.[1] + const difficulty = diffCodeToDifficulty(diffCode ?? '1') + const crownElements = $('.crownIcon') + const crownCode = crownElements.first().attr('src')?.match(/crown_(\d+)_640\.png/)?.[1] + const crown = getCrown(crownCode) + const badge = getBadge(crownElements.eq(1)) + + // 점수 파싱 + const score = parseInt($('.scoreScore').text().replace('点', ''), 10) + + // 상세 데이터 파싱 + const scoreDataElements = $('.scoreDataArea .playDataScore') + + const scoreData: DifficultyScoreData = { + crown, + badge, + score, + ranking: 0, + good: parseInt(scoreDataElements.eq(0).text(), 10), + maxCombo: parseInt(scoreDataElements.eq(1).text(), 10), + ok: parseInt(scoreDataElements.eq(2).text(), 10), + roll: parseInt(scoreDataElements.eq(3).text(), 10), + bad: parseInt(scoreDataElements.eq(4).text(), 10), + count: { + play: parseInt(scoreDataElements.eq(5).text(), 10), + clear: parseInt(scoreDataElements.eq(6).text(), 10), + fullcombo: parseInt(scoreDataElements.eq(7).text(), 10), + donderfullcombo: parseInt(scoreDataElements.eq(8).text(), 10) + } + } + + return { + songName, + difficulty, + scoreData + } +} + +function parseScoreDataFromHtml(html: string): RecentScoreData[] { + const $ = cheerio.load(html) + const scoreElements = $('.scoreUser') + + return scoreElements.map((_, element) => parseDifficultyScoreData(element)).get() +} + +export const getRecentScoreData = async (page: number): Promise => { + const url = `https://donderhiroba.jp/history_recent_score.php?page=${page}` + const res = await fetch(url) + const html = await res.text() + return parseScoreDataFromHtml(html) +} diff --git a/src/components/Rating/recentScoreStorage.ts b/src/components/Rating/recentScoreStorage.ts new file mode 100644 index 0000000..e027ff4 --- /dev/null +++ b/src/components/Rating/recentScoreStorage.ts @@ -0,0 +1,70 @@ +import type { ScoreData } from "./ratingTypes"; + +class RecentScoreStorage { + private scoreDataMap: Record = {} + private lastUpdated: string | null = null + + constructor() { + this.scoreDataMap = {} + this.lastUpdated = null + } + + public async loadFromChromeStorage() { + const result = await chrome.storage.local.get(['recentScores', 'lastUpdated']) + if (result.recentScores) { + this.scoreDataMap = result.recentScores + } + if (result.lastUpdated) { + this.lastUpdated = result.lastUpdated + } + } + + private formatDate(date: Date): string { + const year = date.getFullYear().toString().slice(-2) + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + } + + private async saveToStorage() { + await chrome.storage.local.set({ + recentScores: this.scoreDataMap, + lastUpdated: this.formatDate(new Date()) + }) + } + + async mergeMap(scoreDataMap: Record) { + for (const [songNo, scoreData] of Object.entries(scoreDataMap)) { + await this.mergeSingle(songNo, scoreData) + } + } + + async mergeSingle(songNo: string, scoreData: ScoreData) { + if (this.scoreDataMap[songNo] === undefined) { + this.scoreDataMap[songNo] = scoreData + } else { + this.scoreDataMap[songNo] = { + ...this.scoreDataMap[songNo], + difficulty: { + ...this.scoreDataMap[songNo].difficulty, + ...scoreData.difficulty + } + } + } + await this.saveToStorage() + } + + getMap() { + return this.scoreDataMap + } + + getLastUpdated() { + return this.lastUpdated + } +} + +export default RecentScoreStorage diff --git a/src/components/Table/styles.css b/src/components/Table/styles.css new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/components/Table/styles.css @@ -0,0 +1 @@ + \ No newline at end of file From 6ac62f171862f46c0bb2c5e9aae65afdda89e417 Mon Sep 17 00:00:00 2001 From: exqt <8168124+exqt@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:38:50 +0900 Subject: [PATCH 3/7] refactor: disable taiko wiki injection --- src/injection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/injection.ts b/src/injection.ts index 3fac8d6..a1bdf8a 100644 --- a/src/injection.ts +++ b/src/injection.ts @@ -37,7 +37,7 @@ const runHiroba = async (): Promise => { } const runTaikoWiki = async (): Promise => { - void diffchart() + // void diffchart() } if (window.location.href.includes('taiko.wiki')) { From 6f0d9302d21d59303791294dca16d08e2e21c666 Mon Sep 17 00:00:00 2001 From: exqt <8168124+exqt@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:38:58 +0900 Subject: [PATCH 4/7] version: 0.2.8.2 --- CHANGELOG | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 414ff85..29c09a1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +# 0.2.8.2 +- 업로드 속도 개선 + # 0.2.8 - crxjs CSP 이슈 해결? - 레이티 업로드 추가 diff --git a/package.json b/package.json index 9642a94..495cc68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "donder-hiroba-plus", "private": true, - "version": "0.2.8.1", + "version": "0.2.8.2", "type": "module", "scripts": { "dev": "vite dev", From 0dbc3863254d080c1339c5564bb86848ac49f159 Mon Sep 17 00:00:00 2001 From: exqt <8168124+exqt@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:45:59 +0900 Subject: [PATCH 5/7] fix: lint --- src/components/Rating/RatingApp.svelte | 25 +++++----- src/components/Rating/recentScore.ts | 55 +++++++++++---------- src/components/Rating/recentScoreStorage.ts | 24 ++++----- src/injection.ts | 2 +- 4 files changed, 52 insertions(+), 54 deletions(-) diff --git a/src/components/Rating/RatingApp.svelte b/src/components/Rating/RatingApp.svelte index 9910212..4d90c12 100644 --- a/src/components/Rating/RatingApp.svelte +++ b/src/components/Rating/RatingApp.svelte @@ -5,9 +5,9 @@ import lzutf8 from 'lzutf8' import type { ClearData, DifficultyScoreData } from './ratingTypes' import { waitFor } from '../../lib/utils' - import { onMount } from 'svelte'; - import { getRecentScoreData, type RecentScoreData } from './recentScore'; - import RecentScoreStorage from './recentScoreStorage'; + import { onMount } from 'svelte' + import { getRecentScoreData, type RecentScoreData } from './recentScore' + import RecentScoreStorage from './recentScoreStorage' const wikiOrigin = 'https://taiko.wiki' @@ -23,7 +23,7 @@ const storage = new RecentScoreStorage() let storageLoaded = false let lastUpdated: string | null = null - let scoreDataSorted: {songName: string, difficulty: string, score: DifficultyScoreData}[] = [] + let scoreDataSorted: Array<{ songName: string, difficulty: string, score: DifficultyScoreData }> = [] let totalPlayCount: string = '0 / 0 / 0 / 0' onMount(async () => { @@ -33,13 +33,13 @@ updateScoreDataSorted() }) - const updateScoreDataSorted = () => { - scoreDataSorted = [] + const updateScoreDataSorted = (): void => { + scoreDataSorted = [] let totalPlay = 0 let totalClear = 0 let totalFullcombo = 0 let totalDonderfullcombo = 0 - for (const [songNo, scoreData] of Object.entries(storage.getMap())) { + for (const [, scoreData] of Object.entries(storage.getMap())) { for (const [difficulty, score] of Object.entries(scoreData.difficulty)) { scoreDataSorted.push({ songName: scoreData.title, @@ -56,7 +56,6 @@ totalPlayCount = `${totalPlay} / ${totalClear} / ${totalFullcombo} / ${totalDonderfullcombo}` } - // ready let sendType: 'clear' | 'score' | 'recent' = 'clear' @@ -105,7 +104,6 @@ } async function uploadToWiki (donderData: CardData, clearData: ClearData[], scoreDataMap: Record): Promise { - const data = JSON.stringify({ donderData, clearData, @@ -126,7 +124,7 @@ }) await storage.mergeMap(scoreDataMap) - updateScoreDataSorted() + updateScoreDataSorted() } // 리팩토링된 send 함수 @@ -151,12 +149,11 @@ uploadMessage = 'Fetching clear data...' await hiroba.updateScore(null) - let clearData = await hiroba.getClearData(null) - + const clearData = await hiroba.getClearData(null) + if (sendType === 'clear') { uploadMessage = 'Uploading clear data...' await uploadToWiki(cardData, clearData, {}) - } else if (sendType === 'recent') { uploadMessage = 'Fetching recent score data...' const songNameToSongNo = new Map() @@ -345,7 +342,7 @@ table-layout: fixed; } - .play-count-table th, + .play-count-table th, .play-count-table td { border: 1px solid black; padding: 5px; diff --git a/src/components/Rating/recentScore.ts b/src/components/Rating/recentScore.ts index b41ebf3..a2b6fc9 100644 --- a/src/components/Rating/recentScore.ts +++ b/src/components/Rating/recentScore.ts @@ -1,6 +1,6 @@ import { load } from 'cheerio' import * as cheerio from 'cheerio' -import type { Difficulty, ScoreData, ScoreResponseData, DifficultyScoreData, Crown, Badge } from './ratingTypes' +import type { Difficulty, DifficultyScoreData, Crown, Badge } from './ratingTypes' const CROWN_MAP: Record = { '01': 'played', @@ -9,63 +9,64 @@ const CROWN_MAP: Record = { '04': 'donderfull' } -const getCrown = (src: string | undefined) => { - return src ? CROWN_MAP[src] ?? null : null +const getCrown = (src: string | undefined): Crown | null => { + return src !== undefined ? CROWN_MAP[src] ?? null : null } const BADGE_MAP: Record = { - '8': 'rainbow', - '7': 'purple', - '6': 'pink', - '5': 'gold', - '4': 'silver', - '3': 'bronze', - '2': 'white' + 8: 'rainbow', + 7: 'purple', + 6: 'pink', + 5: 'gold', + 4: 'silver', + 3: 'bronze', + 2: 'white' } -const getBadge = (element: any) => { - if (!element?.length) return null +const getBadge = (element: any): Badge | null => { + if (element === undefined || element == null || element.length === 0) return null const badgeId = element.attr('src')?.replace('image/sp/640/best_score_rank_', '').replace('_640.png', '') return BADGE_MAP[badgeId] ?? null } const diffCodeToDifficulty = (code: string): Difficulty => { - if (code === '5') return 'ura' - if (code === '4') return 'oni' - if (code === '3') return 'hard' - if (code === '2') return 'normal' - return 'easy' + if (code === '5') return 'ura' + if (code === '4') return 'oni' + if (code === '3') return 'hard' + if (code === '2') return 'normal' + return 'easy' } -export type RecentScoreData = { +export interface RecentScoreData { songName: string difficulty: Difficulty - scoreData: DifficultyScoreData + scoreData: DifficultyScoreData } -function parseDifficultyScoreData(body: any): RecentScoreData | null { +function parseDifficultyScoreData (body: any): RecentScoreData | null { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const $ = load(body) - const songName = $('.songNameTitleScore h2').text().trim() + const songName = $('.songNameTitleScore h2').text().trim() const diffCode = $('.levelIcon').first().attr('src')?.match(/icon_course02_(\d+)_640\.png/)?.[1] const difficulty = diffCodeToDifficulty(diffCode ?? '1') - const crownElements = $('.crownIcon') + const crownElements = $('.crownIcon') const crownCode = crownElements.first().attr('src')?.match(/crown_(\d+)_640\.png/)?.[1] const crown = getCrown(crownCode) const badge = getBadge(crownElements.eq(1)) - + // 점수 파싱 const score = parseInt($('.scoreScore').text().replace('点', ''), 10) // 상세 데이터 파싱 const scoreDataElements = $('.scoreDataArea .playDataScore') - + const scoreData: DifficultyScoreData = { crown, badge, score, - ranking: 0, + ranking: 0, good: parseInt(scoreDataElements.eq(0).text(), 10), maxCombo: parseInt(scoreDataElements.eq(1).text(), 10), ok: parseInt(scoreDataElements.eq(2).text(), 10), @@ -86,10 +87,10 @@ function parseDifficultyScoreData(body: any): RecentScoreData | null { } } -function parseScoreDataFromHtml(html: string): RecentScoreData[] { +function parseScoreDataFromHtml (html: string): RecentScoreData[] { const $ = cheerio.load(html) const scoreElements = $('.scoreUser') - + return scoreElements.map((_, element) => parseDifficultyScoreData(element)).get() } diff --git a/src/components/Rating/recentScoreStorage.ts b/src/components/Rating/recentScoreStorage.ts index e027ff4..c09d278 100644 --- a/src/components/Rating/recentScoreStorage.ts +++ b/src/components/Rating/recentScoreStorage.ts @@ -1,49 +1,49 @@ -import type { ScoreData } from "./ratingTypes"; +import type { ScoreData } from './ratingTypes' class RecentScoreStorage { private scoreDataMap: Record = {} private lastUpdated: string | null = null - constructor() { + constructor () { this.scoreDataMap = {} this.lastUpdated = null } - public async loadFromChromeStorage() { + public async loadFromChromeStorage (): Promise { const result = await chrome.storage.local.get(['recentScores', 'lastUpdated']) - if (result.recentScores) { + if (result.recentScores !== undefined) { this.scoreDataMap = result.recentScores } - if (result.lastUpdated) { + if (result.lastUpdated !== undefined) { this.lastUpdated = result.lastUpdated } } - private formatDate(date: Date): string { + private formatDate (date: Date): string { const year = date.getFullYear().toString().slice(-2) const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') const seconds = String(date.getSeconds()).padStart(2, '0') - + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } - private async saveToStorage() { + private async saveToStorage (): Promise { await chrome.storage.local.set({ recentScores: this.scoreDataMap, lastUpdated: this.formatDate(new Date()) }) } - async mergeMap(scoreDataMap: Record) { + async mergeMap (scoreDataMap: Record): Promise { for (const [songNo, scoreData] of Object.entries(scoreDataMap)) { await this.mergeSingle(songNo, scoreData) } } - async mergeSingle(songNo: string, scoreData: ScoreData) { + async mergeSingle (songNo: string, scoreData: ScoreData): Promise { if (this.scoreDataMap[songNo] === undefined) { this.scoreDataMap[songNo] = scoreData } else { @@ -58,11 +58,11 @@ class RecentScoreStorage { await this.saveToStorage() } - getMap() { + getMap (): Record { return this.scoreDataMap } - getLastUpdated() { + getLastUpdated (): string | null { return this.lastUpdated } } diff --git a/src/injection.ts b/src/injection.ts index a1bdf8a..9e0de85 100644 --- a/src/injection.ts +++ b/src/injection.ts @@ -2,7 +2,7 @@ import mypage_top from './injections/mypage_top' import score_list from './injections/score_list' import index from './injections/index' import favorite_song_select from './injections/favorite_song_select' -import diffchart from './injections/diffchart' +// import diffchart from './injections/diffchart' import score_detail from './injections/score_detail' import select_song from './injections/select_song' import i18n from './injections/i18n' From 28fb43e9ddc46a42b91f5145196443988217fbe2 Mon Sep 17 00:00:00 2001 From: exqt <8168124+exqt@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:10:34 +0900 Subject: [PATCH 6/7] feat: fetch songs using other method --- src/components/Rating/RatingApp.svelte | 67 ++++++++++++++++++++++++-- src/components/Rating/recentScore.ts | 18 +++++-- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/components/Rating/RatingApp.svelte b/src/components/Rating/RatingApp.svelte index 4d90c12..beb6837 100644 --- a/src/components/Rating/RatingApp.svelte +++ b/src/components/Rating/RatingApp.svelte @@ -57,7 +57,7 @@ } // ready - let sendType: 'clear' | 'score' | 'recent' = 'clear' + let sendType: 'clear' | 'score' | 'all' | 'recent' = 'clear' // 새로운 유틸리티 함수들 async function checkWikiLogin (): Promise { @@ -128,7 +128,7 @@ } // 리팩토링된 send 함수 - async function send (cardData: CardData, sendType: 'clear' | 'score' | 'recent'): Promise { + async function send (cardData: CardData, sendType: 'clear' | 'score' | 'all' | 'recent'): Promise { if (!confirm(`Send your donderhiroba datas to ${wikiOrigin}. It will be deleted together when you delete your account. Do you agree?`)) { alert('Canceled.') message = '' @@ -196,6 +196,57 @@ await uploadToWiki(cardData, clearData, scoreDataMap) + message = 'Upload completed' + scene = 'ready' + } else if (sendType === 'all') { + uploadMessage = 'Fetching recent score data...' + const songNameToSongNo = new Map() + for (const song of clearData) { + songNameToSongNo.set(song.title, song.songNo) + } + + complete = 0 + uploadMessage = 'Fetch score data... (0/?)' + + const recentScoreData: RecentScoreData[] = [] + const firstPage = await getRecentScoreData(1) + recentScoreData.push(...firstPage) + complete++ + uploadMessage = `Fetch score data... (${complete}/?)` + + // parse until fetched page is same as first page + while (true) { + const nextPage = await getRecentScoreData(complete + 1) + if (nextPage[0].songName === firstPage[0].songName && + nextPage[0].difficulty === firstPage[0].difficulty + ) break + recentScoreData.push(...nextPage) + complete++ + uploadMessage = `Fetch score data... (${complete}/?)` + } + + const scoreDataMap: Record = {} + for (const recentScore of recentScoreData) { + const songNo = songNameToSongNo.get(recentScore.songName) + if (songNo === undefined) continue + const score = recentScore.scoreData + if (scoreDataMap[songNo] === undefined) { + scoreDataMap[songNo] = { + title: recentScore.songName, + songNo, + difficulty: {} + } + } + scoreDataMap[songNo].difficulty[recentScore.difficulty] = score + } + + console.log(nRecentPageToFetch) + console.log(recentScoreData) + console.log(songNameToSongNo) + console.log(scoreDataMap) + + await uploadToWiki(cardData, clearData, scoreDataMap) + message = 'Upload completed' scene = 'ready' } else if (sendType === 'score') { @@ -253,15 +304,19 @@ + {#if storageLoaded} +
Last Score Updated:
{lastUpdated}
+ Total Play Count: {totalPlayCount}
List of Scores @@ -357,7 +373,7 @@ {#each scoreDataSorted as score} - {score.songName} + ({score.songNo}) {score.songName} {score.difficulty} {score.score.count.play} / {score.score.count.clear} / {score.score.count.fullcombo} / {score.score.count.donderfullcombo} diff --git a/src/components/Rating/recentScoreStorage.ts b/src/components/Rating/recentScoreStorage.ts index c09d278..33de602 100644 --- a/src/components/Rating/recentScoreStorage.ts +++ b/src/components/Rating/recentScoreStorage.ts @@ -9,6 +9,12 @@ class RecentScoreStorage { this.lastUpdated = null } + async clear (): Promise { + this.scoreDataMap = {} + this.lastUpdated = 'null' + await this.saveToStorage() + } + public async loadFromChromeStorage (): Promise { const result = await chrome.storage.local.get(['recentScores', 'lastUpdated']) if (result.recentScores !== undefined) { @@ -39,11 +45,15 @@ class RecentScoreStorage { async mergeMap (scoreDataMap: Record): Promise { for (const [songNo, scoreData] of Object.entries(scoreDataMap)) { + delete scoreData.difficulty.hard + delete scoreData.difficulty.normal + delete scoreData.difficulty.easy await this.mergeSingle(songNo, scoreData) } + await this.saveToStorage() } - async mergeSingle (songNo: string, scoreData: ScoreData): Promise { + private async mergeSingle (songNo: string, scoreData: ScoreData): Promise { if (this.scoreDataMap[songNo] === undefined) { this.scoreDataMap[songNo] = scoreData } else { @@ -55,7 +65,6 @@ class RecentScoreStorage { } } } - await this.saveToStorage() } getMap (): Record {