mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
fix(CuratedCategories): improve fetching from Github
This commit is contained in:
parent
111ad5aec8
commit
b6e6e10328
|
|
@ -1,10 +1,14 @@
|
||||||
import { SystemService } from '#services/system_service'
|
import { SystemService } from '#services/system_service'
|
||||||
|
import { ZimService } from '#services/zim_service'
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class EasySetupController {
|
export default class EasySetupController {
|
||||||
constructor(private systemService: SystemService) {}
|
constructor(
|
||||||
|
private systemService: SystemService,
|
||||||
|
private zimService: ZimService
|
||||||
|
) {}
|
||||||
|
|
||||||
async index({ inertia }: HttpContext) {
|
async index({ inertia }: HttpContext) {
|
||||||
const services = await this.systemService.getServices({ installedOnly: false })
|
const services = await this.systemService.getServices({ installedOnly: false })
|
||||||
|
|
@ -18,4 +22,8 @@ export default class EasySetupController {
|
||||||
async complete({ inertia }: HttpContext) {
|
async complete({ inertia }: HttpContext) {
|
||||||
return inertia.render('easy-setup/complete')
|
return inertia.render('easy-setup/complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listCuratedCategories({}: HttpContext) {
|
||||||
|
return await this.zimService.listCuratedCategories()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,21 @@ import {
|
||||||
ZIM_STORAGE_PATH,
|
ZIM_STORAGE_PATH,
|
||||||
} from '../utils/fs.js'
|
} from '../utils/fs.js'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { CuratedCollectionWithStatus, CuratedCollectionsFile } from '../../types/downloads.js'
|
import { CuratedCategory, CuratedCollectionWithStatus, CuratedCollectionsFile } from '../../types/downloads.js'
|
||||||
import vine from '@vinejs/vine'
|
import vine from '@vinejs/vine'
|
||||||
import { curatedCollectionsFileSchema } from '#validators/curated_collections'
|
import { curatedCategoriesFileSchema, curatedCollectionsFileSchema } from '#validators/curated_collections'
|
||||||
import CuratedCollection from '#models/curated_collection'
|
import CuratedCollection from '#models/curated_collection'
|
||||||
import CuratedCollectionResource from '#models/curated_collection_resource'
|
import CuratedCollectionResource from '#models/curated_collection_resource'
|
||||||
import { RunDownloadJob } from '#jobs/run_download_job'
|
import { RunDownloadJob } from '#jobs/run_download_job'
|
||||||
import { DownloadCollectionOperation, DownloadRemoteSuccessCallback } from '../../types/files.js'
|
import { DownloadCollectionOperation, DownloadRemoteSuccessCallback } from '../../types/files.js'
|
||||||
|
|
||||||
const ZIM_MIME_TYPES = ['application/x-zim', 'application/x-openzim', 'application/octet-stream']
|
const ZIM_MIME_TYPES = ['application/x-zim', 'application/x-openzim', 'application/octet-stream']
|
||||||
|
const CATEGORIES_URL = 'https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/collections/kiwix-categories.json'
|
||||||
const COLLECTIONS_URL =
|
const COLLECTIONS_URL =
|
||||||
'https://github.com/Crosstalk-Solutions/project-nomad/raw/refs/heads/master/collections/kiwix.json'
|
'https://github.com/Crosstalk-Solutions/project-nomad/raw/refs/heads/master/collections/kiwix.json'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface IZimService {
|
interface IZimService {
|
||||||
downloadCollection: DownloadCollectionOperation
|
downloadCollection: DownloadCollectionOperation
|
||||||
downloadRemoteSuccessCallback: DownloadRemoteSuccessCallback
|
downloadRemoteSuccessCallback: DownloadRemoteSuccessCallback
|
||||||
|
|
@ -245,6 +247,23 @@ export class ZimService implements IZimService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listCuratedCategories(): Promise<CuratedCategory[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(CATEGORIES_URL)
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
const validated = await vine.validate({
|
||||||
|
schema: curatedCategoriesFileSchema,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return validated.categories
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[ZimService] Failed to fetch curated categories:`, error)
|
||||||
|
throw new Error('Failed to fetch curated categories or invalid format was received')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async listCuratedCollections(): Promise<CuratedCollectionWithStatus[]> {
|
async listCuratedCollections(): Promise<CuratedCollectionWithStatus[]> {
|
||||||
const collections = await CuratedCollection.query().where('type', 'zim').preload('resources')
|
const collections = await CuratedCollection.query().where('type', 'zim').preload('resources')
|
||||||
return collections.map((collection) => ({
|
return collections.map((collection) => ({
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,29 @@ export const curatedCollectionValidator = vine.object({
|
||||||
export const curatedCollectionsFileSchema = vine.object({
|
export const curatedCollectionsFileSchema = vine.object({
|
||||||
collections: vine.array(curatedCollectionValidator).minLength(1),
|
collections: vine.array(curatedCollectionValidator).minLength(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For validating the categories file, which has a different structure than the collections file
|
||||||
|
* since it includes tiers within each category.
|
||||||
|
*/
|
||||||
|
export const curatedCategoriesFileSchema = vine.object({
|
||||||
|
categories: vine.array(
|
||||||
|
vine.object({
|
||||||
|
name: vine.string(),
|
||||||
|
slug: vine.string(),
|
||||||
|
icon: vine.string(),
|
||||||
|
description: vine.string(),
|
||||||
|
language: vine.string().minLength(2).maxLength(5),
|
||||||
|
tiers: vine.array(
|
||||||
|
vine.object({
|
||||||
|
name: vine.string(),
|
||||||
|
slug: vine.string(),
|
||||||
|
description: vine.string(),
|
||||||
|
recommended: vine.boolean().optional(),
|
||||||
|
includesTier: vine.string().optional(),
|
||||||
|
resources: vine.array(curatedCollectionResourceValidator),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@ import { SystemInformationResponse } from '../../types/system'
|
||||||
import api from '~/lib/api'
|
import api from '~/lib/api'
|
||||||
|
|
||||||
export type UseSystemInfoProps = Omit<
|
export type UseSystemInfoProps = Omit<
|
||||||
UseQueryOptions<SystemInformationResponse>,
|
UseQueryOptions<SystemInformationResponse | undefined>,
|
||||||
'queryKey' | 'queryFn'
|
'queryKey' | 'queryFn'
|
||||||
> & {}
|
> & {}
|
||||||
|
|
||||||
export const useSystemInfo = (props: UseSystemInfoProps) => {
|
export const useSystemInfo = (props: UseSystemInfoProps) => {
|
||||||
const queryData = useQuery<SystemInformationResponse>({
|
const queryData = useQuery<SystemInformationResponse | undefined>({
|
||||||
...props,
|
...props,
|
||||||
queryKey: ['system-info'],
|
queryKey: ['system-info'],
|
||||||
queryFn: () => api.getSystemInfo(),
|
queryFn: async () => await api.getSystemInfo(),
|
||||||
refetchInterval: 45000, // Refetch every 45 seconds
|
refetchInterval: 45000, // Refetch every 45 seconds
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { ListRemoteZimFilesResponse, ListZimFilesResponse } from '../../types/zi
|
||||||
import { ServiceSlim } from '../../types/services'
|
import { ServiceSlim } from '../../types/services'
|
||||||
import { FileEntry } from '../../types/files'
|
import { FileEntry } from '../../types/files'
|
||||||
import { SystemInformationResponse, SystemUpdateStatus } from '../../types/system'
|
import { SystemInformationResponse, SystemUpdateStatus } from '../../types/system'
|
||||||
import { CuratedCollectionWithStatus, DownloadJobWithProgress } from '../../types/downloads'
|
import { CuratedCategory, CuratedCollectionWithStatus, DownloadJobWithProgress } from '../../types/downloads'
|
||||||
import { catchInternal } from './util'
|
import { catchInternal } from './util'
|
||||||
|
|
||||||
class API {
|
class API {
|
||||||
|
|
@ -167,6 +167,15 @@ class API {
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listCuratedCategories() {
|
||||||
|
return catchInternal(async () => {
|
||||||
|
const response = await this.client.get<CuratedCategory[]>(
|
||||||
|
'/easy-setup/curated-categories'
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
async listDocs() {
|
async listDocs() {
|
||||||
return catchInternal(async () => {
|
return catchInternal(async () => {
|
||||||
const response = await this.client.get<Array<{ title: string; slug: string }>>('/docs/list')
|
const response = await this.client.get<Array<{ title: string; slug: string }>>('/docs/list')
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,6 @@ type WizardStep = 1 | 2 | 3 | 4
|
||||||
const CURATED_MAP_COLLECTIONS_KEY = 'curated-map-collections'
|
const CURATED_MAP_COLLECTIONS_KEY = 'curated-map-collections'
|
||||||
const CURATED_ZIM_COLLECTIONS_KEY = 'curated-zim-collections'
|
const CURATED_ZIM_COLLECTIONS_KEY = 'curated-zim-collections'
|
||||||
const CURATED_CATEGORIES_KEY = 'curated-categories'
|
const CURATED_CATEGORIES_KEY = 'curated-categories'
|
||||||
const CATEGORIES_URL =
|
|
||||||
'https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/feature/tiered-collections/collections/kiwix-categories.json'
|
|
||||||
|
|
||||||
// Helper to get all resources for a tier (including inherited resources)
|
// Helper to get all resources for a tier (including inherited resources)
|
||||||
const getAllResourcesForTier = (tier: CategoryTier, allTiers: CategoryTier[]): CategoryResource[] => {
|
const getAllResourcesForTier = (tier: CategoryTier, allTiers: CategoryTier[]): CategoryResource[] => {
|
||||||
|
|
@ -159,24 +157,16 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// All services for display purposes
|
|
||||||
const allServices = props.system.services
|
|
||||||
|
|
||||||
// Services that can still be installed (not already installed)
|
|
||||||
// Fetch curated categories with tiers
|
// Fetch curated categories with tiers
|
||||||
const { data: categories, isLoading: isLoadingCategories } = useQuery({
|
const { data: categories, isLoading: isLoadingCategories } = useQuery({
|
||||||
queryKey: [CURATED_CATEGORIES_KEY],
|
queryKey: [CURATED_CATEGORIES_KEY],
|
||||||
queryFn: async () => {
|
queryFn: () => api.listCuratedCategories(),
|
||||||
const response = await fetch(CATEGORIES_URL)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch categories')
|
|
||||||
}
|
|
||||||
const data = await response.json()
|
|
||||||
return data.categories as CuratedCategory[]
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// All services for display purposes
|
||||||
|
const allServices = props.system.services
|
||||||
|
|
||||||
const availableServices = props.system.services.filter(
|
const availableServices = props.system.services.filter(
|
||||||
(service) => !service.installed && service.installation_status !== 'installing'
|
(service) => !service.installed && service.installation_status !== 'installing'
|
||||||
)
|
)
|
||||||
|
|
@ -186,12 +176,6 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
|
||||||
(service) => service.installed
|
(service) => service.installed
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleServiceSelection = (serviceName: string) => {
|
|
||||||
setSelectedServices((prev) =>
|
|
||||||
prev.includes(serviceName) ? prev.filter((s) => s !== serviceName) : [...prev, serviceName]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleMapCollection = (slug: string) => {
|
const toggleMapCollection = (slug: string) => {
|
||||||
setSelectedMapCollections((prev) =>
|
setSelectedMapCollections((prev) =>
|
||||||
prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug]
|
prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug]
|
||||||
|
|
@ -248,7 +232,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
|
||||||
|
|
||||||
// Add tier resources
|
// Add tier resources
|
||||||
const tierResources = getSelectedTierResources()
|
const tierResources = getSelectedTierResources()
|
||||||
totalBytes += tierResources.reduce((sum, r) => sum + r.size_mb * 1024 * 1024, 0)
|
totalBytes += tierResources.reduce((sum, r) => sum + (r.size_mb ?? 0) * 1024 * 1024, 0)
|
||||||
|
|
||||||
// Add map collections
|
// Add map collections
|
||||||
if (mapCollections) {
|
if (mapCollections) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ router.on('/about').renderInertia('about')
|
||||||
|
|
||||||
router.get('/easy-setup', [EasySetupController, 'index'])
|
router.get('/easy-setup', [EasySetupController, 'index'])
|
||||||
router.get('/easy-setup/complete', [EasySetupController, 'complete'])
|
router.get('/easy-setup/complete', [EasySetupController, 'complete'])
|
||||||
|
router.get('/api/easy-setup/curated-categories', [EasySetupController, 'listCuratedCategories'])
|
||||||
|
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export type DownloadJobWithProgress = {
|
||||||
export type CategoryResource = {
|
export type CategoryResource = {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
size_mb: number
|
size_mb?: number
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user