chore: update public repo

This commit is contained in:
desu-bot 2025-01-14 02:38:00 +00:00
parent e0109980c0
commit e7c9507247
No known key found for this signature in database
25 changed files with 5364 additions and 0 deletions

View file

@ -0,0 +1,20 @@
import qrTerminal from 'qrcode-terminal'
import { createTg } from '../../utils/telegram.ts'
const sessionName = process.argv[2]
if (!sessionName) {
console.error('Usage: mtcute-login.ts <session name>')
process.exit(1)
}
const tg = createTg(sessionName)
const self = await tg.start({
qrCodeHandler(url, expires) {
console.log(qrTerminal.generate(url, { small: true }))
},
})
console.log(`Logged in as ${self.displayName} (${self.id})`)
await tg.close()

View file

@ -0,0 +1,105 @@
import type { NavidromeSong } from '../../utils/navidrome.ts'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import kuromoji from 'kuromoji'
import { isKana, toRomaji } from 'wanakana'
import { fetchSongs, navidromeFfetch as ffetch } from '../../utils/navidrome.ts'
const WHITELIST_KEYS = new Set([
// actual different tracks with the same title
'["sorry about my face","untitled track"]',
'["kooeetekumogeemusu","neko bushou sengoku emaki"]',
'["eve","merufuakutorii"]',
// todo
'["arm","legend of zelda"]',
'["arm","tomorrow heart beat ~ ashita anata ni dokkidoki☆ ~"]',
'["dwat","rotladatormarf"]',
'["fujiwara mari sai","zenbuatashinokawaiino"]',
])
const moji = await new Promise<any>((resolve, reject) => {
kuromoji.builder({
dicPath: join(createRequire(import.meta.url).resolve('kuromoji/'), '../../dict'),
}).build((err, tokenizer) => {
if (err) return reject(err)
resolve(tokenizer)
})
})
function clean(s: string) {
const str = s.toLowerCase()
.replace(/\(Explicit\)/i, '')
.replace(/[!@#$%^&*()_+=[\]{}\\|/,.;':"<>`~-]/g, '')
if (str.match(/[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/)) {
// has japanese
const tokens = moji.tokenize(str)
let res = ''
for (const token of tokens) {
if (token.word_type === 'UNKNOWN') {
res += isKana(token.surface_form) ? toRomaji(token.surface_form) : token.surface_form
} else if (token.word_type === 'KNOWN') {
res += `${toRomaji(token.reading)} `
}
}
return res.trimEnd()
}
return str
}
const CHUNK_SIZE = 1000
function getSongKey(song: NavidromeSong) {
return JSON.stringify([
clean(song.artist),
clean(song.title),
])
}
const seen = new Map<string, NavidromeSong[]>()
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)
}
console.log('⌛ fetched chunk %d (%d items)', Math.floor(offset / CHUNK_SIZE), songs.length)
}
const keysSorted = Array.from(seen.keys()).sort()
let duplicates = 0
for (const key of keysSorted) {
const arr = seen.get(key)!
if (arr.length === 1) continue
duplicates += 1
console.log()
console.log('found duplicates for %s:', key)
for (const song of arr) {
console.log(' %s - %s (from %s - %s) (at %s)', song.artist, song.title, song.albumArtist, song.album, song.path)
}
}
if (duplicates === 0) {
console.log('✅ no duplicates found')
} else {
console.log('🚨 %d duplicates found', duplicates)
}

View file

@ -0,0 +1,66 @@
import { readFile, rm } from 'node:fs/promises'
import { join } from 'node:path'
import { $ } from 'zx'
import { downloadStream } from '../../utils/fetch.ts'
import { getEnv } from '../../utils/misc.ts'
import { fetchSongs } from '../../utils/navidrome.ts'
import { WebdavClient } from '../../utils/webdav.ts'
const webdav = new WebdavClient({
baseUrl: getEnv('NAVIDROME_WEBDAV_ENDPOINT'),
username: getEnv('NAVIDROME_WEBDAV_USERNAME'),
password: getEnv('NAVIDROME_WEBDAV_PASSWORD'),
})
const CHUNK_SIZE = 1000
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 ext = song.path.split('.').pop()!
if (ext !== 'm4a') continue
console.log('❌ song %s is m4a, remuxing...', song.path)
const webdavPath = song.path.replace('/music/s3/', '/')
const res = await webdav.get(webdavPath).catch(() => null)
if (!res) {
console.log(' ❌ failed to get %s', webdavPath)
continue
}
const tmpfile = join('assets', `${song.id}.m4a`)
await downloadStream(res.body!, tmpfile)
console.log(' - downloaded to %s', tmpfile)
const probe = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${tmpfile}`.json()
const audioStream = probe.streams.find(stream => stream.codec_type === 'audio')
if (!audioStream) {
console.log(' ❌ no audio stream found')
await rm(tmpfile)
continue
}
const codec = audioStream.codec_name
if (codec !== 'flac') {
console.log(` ❌ audio stream is ${codec}, not flac, skipping`)
await rm(tmpfile)
continue
}
console.log(' - audio stream is flac, remuxing')
// remux
const remuxed = join('assets', `${song.id}.flac`)
await rm(remuxed, { force: true })
await $`ffmpeg -i ${tmpfile} -c:a copy ${remuxed}`.quiet(true)
console.log(' - remuxed to %s', remuxed)
await rm(tmpfile)
await webdav.put(webdavPath.replace('.m4a', '.flac'), await readFile(remuxed))
await webdav.delete(webdavPath)
console.log(' - uploaded to %s', webdavPath.replace('.m4a', '.flac'))
await rm(remuxed)
}
}

View file

@ -0,0 +1,39 @@
import { filesize } from 'filesize'
import { z } from 'zod'
import { ffetch } from '../../utils/fetch.ts'
import { getEnv } from '../../utils/misc.ts'
const res = await ffetch('/api/v0/transfers/uploads', {
baseUrl: getEnv('SLSKD_ENDPOINT'),
headers: {
cookie: getEnv('SLSKD_COOKIE'),
},
}).parsedJson(z.array(
z.object({
username: z.string(),
directories: z.array(z.object({
directory: z.string(),
fileCount: z.number(),
files: z.array(z.object({
id: z.string(),
filename: z.string(),
state: z.string(),
bytesTransferred: z.number(),
})),
})),
}),
))
let total = 0
for (const user of res) {
for (const dir of user.directories) {
for (const file of dir.files) {
if (file.state !== 'Completed, Succeeded') continue
total += file.bytesTransferred
}
}
}
console.log(filesize(total))

View file

@ -0,0 +1,58 @@
import { iter } from '@fuman/utils'
import { z } from 'zod'
import { minimist, question } from 'zx'
import { downloadFile, ffetch } from '../../utils/fetch.ts'
const args = minimist(process.argv.slice(2), {
string: ['filename'],
})
const query = args._[0] ?? await question('Search query (Artist - Album): ')
const data = await ffetch('https://api.deezer.com/search', {
query: {
q: query,
limit: 15,
},
}).parsedJson(z.object({
data: z.array(z.object({
type: z.literal('track'),
title: z.string(),
artist: z.object({
name: z.string(),
}),
album: z.object({
id: z.number(),
title: z.string(),
cover_xl: z.string(),
}),
})),
}))
const groupedByAlbum = new Map<number, typeof data['data']>()
for (const result of data.data) {
const albumId = result.album.id
if (!groupedByAlbum.has(albumId)) {
groupedByAlbum.set(albumId, [])
}
groupedByAlbum.get(albumId)!.push(result)
}
const idxToAlbum = new Map<number, number>()
for (const [idx, [id, tracks]] of iter.enumerate(groupedByAlbum.entries())) {
idxToAlbum.set(idx, id)
console.log(`${idx + 1}. ${tracks[0].artist.name} - ${tracks[0].album.title}`)
for (const track of tracks) {
console.log(` ${track.title}`)
}
}
console.log('Enter number to download album art:')
const number = Number.parseInt(await question('[1] > ') || '1')
const artworkUrl = groupedByAlbum.get(idxToAlbum.get(number - 1)!)![0].album.cover_xl
await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`)

View file

@ -0,0 +1,129 @@
import { rm } from 'node:fs/promises'
import { $, question } from 'zx'
import { fileExists } from '../../utils/fs.ts'
let filename = await question('filename >')!
const startTs = await question('start timestamp >')
const endTs = await question('end timestamp >')
const outputFilename = await question('output filename [output.mp4] >') || 'assets/output.mp4'
if (filename[0] === '\'' && filename[filename.length - 1] === '\'') {
filename = filename.slice(1, -1)
}
const ffprobe = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${filename}`.json()
async function chooseStream(type: string, options: any[], allowNone = false) {
console.log(`Found ${type} streams:`)
for (let i = 0; i < options.length; i++) {
const stream = options[i]
console.log(`[${i + 1}] (${stream.codec_name}, ${stream.tags.language}) ${stream.tags.title}`)
}
if (allowNone) {
console.log(`[0] No ${type}`)
}
const res = await question(`select ${type} >`) || '0'
if (res === '0' && allowNone) {
return null
}
const streamIndex = Number.parseInt(res)
if (Number.isNaN(streamIndex) || streamIndex < 1 || streamIndex > options.length) {
console.error('Invalid input')
process.exit(1)
}
return streamIndex - 1
}
const allVideos = ffprobe.streams.filter(stream => stream.codec_type === 'video')
const allAudios = ffprobe.streams.filter(stream => stream.codec_type === 'audio')
const allSubtitles = ffprobe.streams.filter(stream => stream.codec_type === 'subtitle')
let videoStream: number | null = null
let audioStream: number | null = null
let subtitleStream: number | null = null
if (allVideos.length > 1) {
videoStream = await chooseStream('video', allVideos)
} else if (allVideos.length > 0) {
videoStream = 0
} else {
console.error('No video streams found')
process.exit(1)
}
if (allAudios.length > 1) {
audioStream = await chooseStream('audio', allAudios)
} else if (allAudios.length > 0) {
audioStream = 0
} else {
console.warn('No audio streams found, proceeding without audio')
}
if (allSubtitles.length > 0) {
subtitleStream = await chooseStream('subtitle', allSubtitles, true)
}
const args: string[] = [
'-i',
filename,
'-c:v',
'libx264',
'-map',
`0:v:${videoStream}`,
'-c:v',
'libx264',
]
if (audioStream !== null) {
args.push('-map', `0:a:${audioStream}`)
}
if (subtitleStream !== null) {
const filenameEscaped = filename.replace(/'/g, "'\\\\\\''")
args.push('-vf', `format=yuv420p,subtitles='${filenameEscaped}':si=${subtitleStream}`)
} else {
args.push('-vf', 'format=yuv420p')
}
if (audioStream !== null) {
args.push('-c:a', 'libopus')
if (allAudios[audioStream].codec_name === 'flac') {
args.push('-b:a', '320k')
}
}
args.push(
'-ss',
startTs!,
'-to',
endTs!,
outputFilename,
)
if (await fileExists(outputFilename)) {
const overwrite = await question('Output file already exists, overwrite? [y/N] >')
if (overwrite?.toLowerCase() !== 'y') {
process.exit(0)
}
await rm(outputFilename)
}
try {
$.env.AV_LOG_FORCE_COLOR = 'true'
await $`ffmpeg ${args}`
} catch (e) {
process.exit(1)
}
const openDir = await question('open output directory? [Y/n] >')
if (!openDir || openDir?.toLowerCase() === 'y') {
await $`open -R ${outputFilename}`
}

View file

@ -0,0 +1,46 @@
import { iter } from '@fuman/utils'
import { z } from 'zod'
import { minimist, question } from 'zx'
import { downloadFile, ffetch } from '../../utils/fetch.ts'
const args = minimist(process.argv.slice(2), {
string: ['entity', 'filename'],
})
const entity = args.entity ?? 'album'
const query = args._[0] ?? await question('Search query (Artist - Album): ')
const data = await ffetch('https://itunes.apple.com/search', {
query: {
term: query,
entity,
limit: 15,
},
}).parsedJson(z.object({
results: z.array(z.object({
kind: z.literal('song').optional(),
artistName: z.string(),
collectionName: z.string(),
artworkUrl100: z.string(),
releaseDate: z.string(),
trackName: z.string().optional(),
}).passthrough()),
}))
for (const [i, result] of iter.enumerate(data.results)) {
if (result.kind === 'song') {
console.log(`${i + 1}. ${result.artistName} - ${result.trackName} (${result.collectionName}, ${new Date(result.releaseDate).toLocaleDateString('ru-RU')})`)
continue
}
console.log(`${i + 1}. ${result.artistName} - ${result.collectionName} (${new Date(result.releaseDate).toLocaleDateString('ru-RU')})`)
}
console.log('Enter number to download album art:')
const number = Number.parseInt(await question('[1] > ') || '1')
const artworkUrl = data.results[number - 1].artworkUrl100.replace('100x100', '1500x1500')
await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`)

View file

@ -0,0 +1,63 @@
import { iter } from '@fuman/utils'
import { z } from 'zod'
import { minimist, question } from 'zx'
import { downloadFile, ffetch } from '../../utils/fetch.ts'
const args = minimist(process.argv.slice(2), {
string: ['filename'],
})
const query = args._[0] ?? await question('Search query: ')
const data = await ffetch('https://itunes.apple.com/search', {
query: {
term: query,
entity: 'musicArtist',
limit: 15,
},
}).parsedJson(z.object({
results: z.array(z.object({
wrapperType: z.literal('artist'),
artistName: z.string(),
artistLinkUrl: z.string(),
primaryGenreName: z.string().default('Unknown'),
}).passthrough()),
}))
for (const [i, result] of iter.enumerate(data.results)) {
console.log(`${i + 1}. ${result.artistName} (${result.primaryGenreName})`)
continue
}
console.log('Enter number to download artist art:')
const number = Number.parseInt(await question('[1] > ') || '1')
const pageUrl = data.results[number - 1].artistLinkUrl
const $ = await ffetch(pageUrl).cheerio()
const pageData = JSON.parse($('#serialized-server-data').html()!)
const pageDataValidated = z.tuple([
z.object({
data: z.object({
seoData: z.object({
artworkUrl: z.string(),
}),
}),
}),
]).parse(pageData)
// {w}x{h}{c}.{f}
const artworkUrl = pageDataValidated[0].data.seoData.artworkUrl
.replace('{w}', '2500')
.replace('{h}', '2500')
.replace('{c}', 'cc')
.replace('{f}', 'jpg')
if (artworkUrl === '/assets/meta/apple-music.png') {
console.log('No artwork available')
process.exit(1)
}
await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`)

View file

@ -0,0 +1,51 @@
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import plist from 'plist'
import { z } from 'zod'
import { $ } from 'zx'
import { ffetch } from '../../utils/fetch.ts'
const latestVerInfo = await ffetch('https://api.github.com/repos/forkgram/tdesktop/releases/latest').parsedJson(
z.object({
tag_name: z.string().transform(v => v.replace(/^v/, '')),
assets: z.array(z.object({
name: z.string(),
browser_download_url: z.string(),
})),
}),
)
const INSTALL_PATH = '/Applications/Forkgram.app'
console.log('latest version:', latestVerInfo.tag_name)
const installedPlist = await readFile(join(INSTALL_PATH, 'Contents/Info.plist'), 'utf8')
const installedPlistParsed = z.object({
CFBundleShortVersionString: z.string(),
}).parse(plist.parse(installedPlist))
console.log('installed version:', installedPlistParsed.CFBundleShortVersionString)
if (installedPlistParsed.CFBundleShortVersionString === latestVerInfo.tag_name) {
console.log('✅ no update needed')
process.exit(0)
}
const arm64Asset = latestVerInfo.assets.find(asset => asset.name === 'Forkgram.macOS.no.auto-update_arm64.zip')
if (!arm64Asset) {
console.error('❌ no arm64 asset found')
process.exit(1)
}
console.log('installing new version...')
await $`curl -L ${arm64Asset.browser_download_url} -o /tmp/forkgram.zip`
await $`unzip -o /tmp/forkgram.zip -d /tmp/forkgram`
await $`kill -9 $(pgrep -f /Applications/Forkgram.app/Contents/MacOS/Telegram)`
await $`rm -rf ${INSTALL_PATH}`
await $`mv /tmp/forkgram/Telegram.app ${INSTALL_PATH}`
await $`rm -rf /tmp/forkgram`
await $`xattr -cr ${INSTALL_PATH}`
await $`open ${INSTALL_PATH}`
console.log('✅ done')