From ce9f435ef201f9f8c25932474484f8f7023ea930 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 9 Apr 2025 04:20:08 +0000 Subject: [PATCH] chore: update public repo --- scripts/infra/navidrome-find-duplicates.ts | 31 ++++++-------- scripts/infra/navidrome-stats.ts | 18 +++++++++ utils/navidrome.ts | 47 ++++++++++++++++++---- 3 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 scripts/infra/navidrome-stats.ts diff --git a/scripts/infra/navidrome-find-duplicates.ts b/scripts/infra/navidrome-find-duplicates.ts index 3c89971..c5af85b 100644 --- a/scripts/infra/navidrome-find-duplicates.ts +++ b/scripts/infra/navidrome-find-duplicates.ts @@ -5,7 +5,7 @@ import { join } from 'node:path' import kuromoji from 'kuromoji' import { isKana, toRomaji } from 'wanakana' -import { fetchSongs, navidromeFfetch as ffetch } from '../../utils/navidrome.ts' +import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' const WHITELIST_KEYS = new Set([ // actual different tracks with the same title @@ -53,8 +53,6 @@ function clean(s: string) { return str } -const CHUNK_SIZE = 1000 - function getSongKey(song: NavidromeSong) { return JSON.stringify([ clean(song.artist), @@ -64,23 +62,20 @@ function getSongKey(song: NavidromeSong) { const seen = new Map() -for (let offset = 0; ; offset += CHUNK_SIZE) { - const songs = await fetchSongs(offset, CHUNK_SIZE) - if (songs.length === 0) break - - for (const song of songs) { - const key = getSongKey(song) - if (WHITELIST_KEYS.has(key)) continue - let arr = seen.get(key) - if (!arr) { - arr = [] - seen.set(key, arr) - } - - arr.push(song) +for await (const song of fetchSongsIter({ + onChunkProcessed: (page, items) => { + console.log('⌛ fetched chunk %d (%d items)', page, items) + }, +})) { + const key = getSongKey(song) + if (WHITELIST_KEYS.has(key)) continue + let arr = seen.get(key) + if (!arr) { + arr = [] + seen.set(key, arr) } - console.log('⌛ fetched chunk %d (%d items)', Math.floor(offset / CHUNK_SIZE), songs.length) + arr.push(song) } const keysSorted = Array.from(seen.keys()).sort() diff --git a/scripts/infra/navidrome-stats.ts b/scripts/infra/navidrome-stats.ts new file mode 100644 index 0000000..e4bb3fa --- /dev/null +++ b/scripts/infra/navidrome-stats.ts @@ -0,0 +1,18 @@ +import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' + +let count = 0 +let totalSize = 0 +let totalDuration = 0 + +console.log('⌛ fetching songs...') + +for await (const song of fetchSongsIter()) { + count += 1 + totalSize += song.size + totalDuration += song.duration +} + +console.log('---') +console.log('total songs: %d', count) +console.log('total size: %d GiB', (totalSize / 1024 / 1024 / 1024).toFixed(3)) +console.log('total duration: %d min (%d h)', (totalDuration / 60).toFixed(3), (totalDuration / 60 / 60).toFixed(3)) diff --git a/utils/navidrome.ts b/utils/navidrome.ts index be66efb..5314252 100644 --- a/utils/navidrome.ts +++ b/utils/navidrome.ts @@ -2,12 +2,26 @@ import { z } from 'zod' import { ffetch as ffetchBase } from './fetch.ts' import { getEnv } from './misc.ts' -export const navidromeFfetch = ffetchBase.extend({ - baseUrl: getEnv('NAVIDROME_ENDPOINT'), - headers: { - 'x-nd-authorization': `Bearer ${getEnv('NAVIDROME_TOKEN')}`, - }, -}) +let _cachedFfetch: typeof ffetchBase | undefined +export async function getNavidromeFfetch() { + if (_cachedFfetch) return _cachedFfetch + const baseUrl = getEnv('NAVIDROME_ENDPOINT') + const authRes = await ffetchBase.post('/auth/login', { + baseUrl, + json: { + username: getEnv('NAVIDROME_USERNAME'), + password: getEnv('NAVIDROME_PASSWORD'), + }, + }).parsedJson(z.object({ token: z.string() })) + + _cachedFfetch = ffetchBase.extend({ + baseUrl, + headers: { + 'x-nd-authorization': `Bearer ${authRes.token}`, + }, + }) + return _cachedFfetch +} export const NavidromeSong = z.object({ id: z.string(), @@ -17,11 +31,13 @@ export const NavidromeSong = z.object({ artist: z.string(), path: z.string(), duration: z.number(), + size: z.number(), }) export type NavidromeSong = z.infer -export function fetchSongs(offset: number, pageSize: number) { - return navidromeFfetch('/api/song', { +export async function fetchSongs(offset: number, pageSize: number) { + const api = await getNavidromeFfetch() + return api('/api/song', { query: { _start: offset, _end: offset + pageSize, @@ -30,3 +46,18 @@ export function fetchSongs(offset: number, pageSize: number) { }, }).parsedJson(z.array(NavidromeSong)) } + +export async function* fetchSongsIter(params?: { + chunkSize?: number + onChunkProcessed?: (page: number, items: number) => void +}) { + const { chunkSize = 1000, onChunkProcessed } = params ?? {} + for (let offset = 0; ; offset += chunkSize) { + const songs = await fetchSongs(offset, chunkSize) + if (songs.length === 0) return + + yield * songs + + onChunkProcessed?.(Math.floor(offset / chunkSize), songs.length) + } +}