Gitea Test
First Push to giTea
This commit is contained in:
@@ -13,6 +13,9 @@ import vaultwarden from './routes/vaultwarden'
|
||||
import kuma from './routes/kuma'
|
||||
import arr from './routes/arr'
|
||||
import qbt from './routes/qbittorrent'
|
||||
import jellyfin from './routes/jellyfin'
|
||||
import navidrome from './routes/navidrome'
|
||||
import romm from './routes/romm'
|
||||
|
||||
const app = express()
|
||||
const PORT = Number(process.env.PORT ?? 3001)
|
||||
@@ -31,6 +34,9 @@ app.use('/api/vaultwarden', vaultwarden)
|
||||
app.use('/api/kuma', kuma)
|
||||
app.use('/api/arr', arr)
|
||||
app.use('/api/qbt', qbt)
|
||||
app.use('/api/jellyfin', jellyfin)
|
||||
app.use('/api/navidrome', navidrome)
|
||||
app.use('/api/romm', romm)
|
||||
|
||||
// Serve built frontend in production only
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Router } from 'express'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/status', async (_req, res) => {
|
||||
const host = process.env.JELLYFIN_HOST
|
||||
const key = process.env.JELLYFIN_API_KEY
|
||||
if (!host || !key) return res.status(503).json({ error: 'Jellyfin not configured' })
|
||||
|
||||
const headers = { 'X-Emby-Token': key }
|
||||
|
||||
try {
|
||||
const apiKey = { api_key: key }
|
||||
const [sessionsRes, countsRes] = await Promise.all([
|
||||
axios.get(`${host}/Sessions`, { headers, params: { ...apiKey, ActiveWithinSeconds: 180 } }),
|
||||
axios.get(`${host}/Items/Counts`, { headers, params: apiKey }),
|
||||
])
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const sessions = (sessionsRes.data as any[])
|
||||
.filter((s: any) => s.NowPlayingItem)
|
||||
.map((s: any) => {
|
||||
const item = s.NowPlayingItem
|
||||
const isEpisode = item?.Type === 'Episode'
|
||||
const title = isEpisode ? (item?.SeriesName ?? item?.Name ?? '') : (item?.Name ?? '')
|
||||
const subtitle = isEpisode
|
||||
? `S${String(item?.ParentIndexNumber ?? 0).padStart(2,'0')}E${String(item?.IndexNumber ?? 0).padStart(2,'0')} · ${item?.Name ?? ''}`
|
||||
: null
|
||||
return {
|
||||
user: s.UserName ?? 'Unknown',
|
||||
title,
|
||||
subtitle,
|
||||
type: item?.Type ?? '',
|
||||
progress: s.PlayState?.PositionTicks && item?.RunTimeTicks
|
||||
? Math.round((s.PlayState.PositionTicks / item.RunTimeTicks) * 100)
|
||||
: null,
|
||||
paused: s.PlayState?.IsPaused ?? false,
|
||||
client: s.Client ?? '',
|
||||
}
|
||||
})
|
||||
|
||||
const counts = countsRes.data as any
|
||||
res.json({
|
||||
sessions,
|
||||
library: {
|
||||
movies: counts.MovieCount ?? 0,
|
||||
episodes: counts.EpisodeCount ?? 0,
|
||||
songs: counts.SongCount ?? 0,
|
||||
albums: counts.AlbumCount ?? 0,
|
||||
},
|
||||
})
|
||||
} catch (err: any) {
|
||||
res.status(502).json({ error: err.message ?? 'Jellyfin error' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Router } from 'express'
|
||||
import axios from 'axios'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const router = Router()
|
||||
|
||||
let cachedToken: string | null = null
|
||||
let tokenExpiry = 0
|
||||
let tokenPromise: Promise<string> | null = null
|
||||
|
||||
async function getToken(host: string, user: string, pass: string): Promise<string> {
|
||||
if (cachedToken && Date.now() < tokenExpiry) return cachedToken
|
||||
// Deduplicate concurrent login attempts
|
||||
if (tokenPromise) return tokenPromise
|
||||
tokenPromise = axios.post(`${host}/auth/login`, { username: user, password: pass })
|
||||
.then(res => {
|
||||
cachedToken = res.data.token
|
||||
tokenExpiry = Date.now() + 55 * 60 * 1000
|
||||
tokenPromise = null
|
||||
return cachedToken!
|
||||
})
|
||||
.catch(err => {
|
||||
tokenPromise = null
|
||||
throw err
|
||||
})
|
||||
return tokenPromise
|
||||
}
|
||||
|
||||
router.get('/status', async (_req, res) => {
|
||||
const host = process.env.NAVIDROME_HOST
|
||||
const user = process.env.NAVIDROME_USER
|
||||
const pass = process.env.NAVIDROME_PASSWORD
|
||||
if (!host || !user || !pass)
|
||||
return res.status(503).json({ error: 'Navidrome not configured' })
|
||||
|
||||
try {
|
||||
const jwtToken = await getToken(host, user, pass)
|
||||
const headers = { 'X-ND-Authorization': `Bearer ${jwtToken}` }
|
||||
|
||||
// Use Subsonic API for now playing (standard endpoint)
|
||||
const salt = 'sycoDash'
|
||||
const md5Token = crypto.createHash('md5').update(pass + salt).digest('hex')
|
||||
const subParams = { u: user, t: md5Token, s: salt, v: '1.16.1', c: 'syco-dashboard', f: 'json' }
|
||||
|
||||
const [artistRes, albumRes, songRes, nowPlayingRes] = await Promise.all([
|
||||
axios.get(`${host}/api/artist`, { headers, params: { _start: 0, _end: 1 } }),
|
||||
axios.get(`${host}/api/album`, { headers, params: { _start: 0, _end: 1 } }),
|
||||
axios.get(`${host}/api/song`, { headers, params: { _start: 0, _end: 1 } }),
|
||||
axios.get(`${host}/rest/getNowPlaying.view`, { params: subParams }).catch(() => null),
|
||||
])
|
||||
|
||||
const artistCount = Number(artistRes.headers['x-total-count'] ?? 0)
|
||||
const albumCount = Number(albumRes.headers['x-total-count'] ?? 0)
|
||||
const songCount = Number(songRes.headers['x-total-count'] ?? 0)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const nowPlayingEntries: any[] = nowPlayingRes?.data?.['subsonic-response']?.nowPlaying?.entry ?? []
|
||||
const entries = Array.isArray(nowPlayingEntries) ? nowPlayingEntries : [nowPlayingEntries]
|
||||
const nowPlaying = entries.filter(Boolean).map((e: any) => ({
|
||||
user: e.username ?? 'Unknown',
|
||||
title: e.title ?? '',
|
||||
artist: e.artist ?? '',
|
||||
album: e.album ?? '',
|
||||
}))
|
||||
|
||||
res.json({ artistCount, albumCount, songCount, nowPlaying })
|
||||
} catch (err: any) {
|
||||
res.status(502).json({ error: err.message ?? 'Navidrome error' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Router } from 'express'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/status', async (_req, res) => {
|
||||
const host = process.env.ROMM_HOST
|
||||
const user = process.env.ROMM_USER
|
||||
const pass = process.env.ROMM_PASSWORD
|
||||
if (!host || !user || !pass) return res.status(503).json({ error: 'RomM not configured' })
|
||||
|
||||
const auth = { username: user, password: pass }
|
||||
|
||||
try {
|
||||
const [statsRes, platformsRes] = await Promise.all([
|
||||
axios.get(`${host}/api/stats`, { auth }),
|
||||
axios.get(`${host}/api/platforms`, { auth }),
|
||||
])
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const stats: any = statsRes.data ?? {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const platforms: any[] = platformsRes.data ?? []
|
||||
|
||||
res.json({
|
||||
platformCount: stats.PLATFORMS ?? platforms.length,
|
||||
romCount: stats.ROMS ?? 0,
|
||||
platforms: platforms.map((p: any) => ({
|
||||
name: p.name ?? p.fs_slug ?? p.slug ?? '',
|
||||
romCount: p.rom_count ?? p.roms_count ?? null,
|
||||
})),
|
||||
})
|
||||
} catch (err: any) {
|
||||
res.status(502).json({ error: err.message ?? 'RomM error' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user