mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat(ContentManager): Display friendly names instead of filenames
Content Manager now shows Title and Summary columns from Kiwix metadata instead of just raw filenames. Metadata is captured when files are downloaded from Content Explorer and stored in a new zim_file_metadata table. Existing files without metadata gracefully fall back to showing the filename. Changes: - Add zim_file_metadata table and model for storing title, summary, author - Update download flow to capture and store metadata from Kiwix library - Update Content Manager UI to display Title and Summary columns - Clean up metadata when ZIM files are deleted Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b31be66f9
commit
2c4fc59428
|
|
@ -2,7 +2,7 @@ import { ZimService } from '#services/zim_service'
|
|||
import {
|
||||
downloadCollectionValidator,
|
||||
filenameParamValidator,
|
||||
remoteDownloadValidator,
|
||||
remoteDownloadWithMetadataValidator,
|
||||
saveInstalledTierValidator,
|
||||
selectWikipediaValidator,
|
||||
} from '#validators/common'
|
||||
|
|
@ -25,8 +25,8 @@ export default class ZimController {
|
|||
}
|
||||
|
||||
async downloadRemote({ request }: HttpContext) {
|
||||
const payload = await request.validateUsing(remoteDownloadValidator)
|
||||
const { filename, jobId } = await this.zimService.downloadRemote(payload.url)
|
||||
const payload = await request.validateUsing(remoteDownloadWithMetadataValidator)
|
||||
const { filename, jobId } = await this.zimService.downloadRemote(payload.url, payload.metadata)
|
||||
|
||||
return {
|
||||
message: 'Download started successfully',
|
||||
|
|
|
|||
30
admin/app/models/zim_file_metadata.ts
Normal file
30
admin/app/models/zim_file_metadata.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { DateTime } from 'luxon'
|
||||
import { BaseModel, column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm'
|
||||
|
||||
export default class ZimFileMetadata extends BaseModel {
|
||||
static namingStrategy = new SnakeCaseNamingStrategy()
|
||||
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
|
||||
@column()
|
||||
declare filename: string
|
||||
|
||||
@column()
|
||||
declare title: string
|
||||
|
||||
@column()
|
||||
declare summary: string | null
|
||||
|
||||
@column()
|
||||
declare author: string | null
|
||||
|
||||
@column()
|
||||
declare size_bytes: number | null
|
||||
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare created_at: DateTime
|
||||
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updated_at: DateTime
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import CuratedCollection from '#models/curated_collection'
|
|||
import CuratedCollectionResource from '#models/curated_collection_resource'
|
||||
import InstalledTier from '#models/installed_tier'
|
||||
import WikipediaSelection from '#models/wikipedia_selection'
|
||||
import ZimFileMetadata from '#models/zim_file_metadata'
|
||||
import { RunDownloadJob } from '#jobs/run_download_job'
|
||||
import { DownloadCollectionOperation, DownloadRemoteSuccessCallback } from '../../types/files.js'
|
||||
import { SERVICE_NAMES } from '../../constants/service_names.js'
|
||||
|
|
@ -52,8 +53,24 @@ export class ZimService implements IZimService {
|
|||
const all = await listDirectoryContents(dirPath)
|
||||
const files = all.filter((item) => item.name.endsWith('.zim'))
|
||||
|
||||
// Fetch metadata for all files
|
||||
const metadataRecords = await ZimFileMetadata.all()
|
||||
const metadataMap = new Map(metadataRecords.map((m) => [m.filename, m]))
|
||||
|
||||
// Enrich files with metadata
|
||||
const enrichedFiles = files.map((file) => {
|
||||
const metadata = metadataMap.get(file.name)
|
||||
return {
|
||||
...file,
|
||||
title: metadata?.title || null,
|
||||
summary: metadata?.summary || null,
|
||||
author: metadata?.author || null,
|
||||
size_bytes: metadata?.size_bytes || null,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
files,
|
||||
files: enrichedFiles,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +165,10 @@ export class ZimService implements IZimService {
|
|||
}
|
||||
}
|
||||
|
||||
async downloadRemote(url: string): Promise<{ filename: string; jobId?: string }> {
|
||||
async downloadRemote(
|
||||
url: string,
|
||||
metadata?: { title: string; summary?: string; author?: string; size_bytes?: number }
|
||||
): Promise<{ filename: string; jobId?: string }> {
|
||||
const parsed = new URL(url)
|
||||
if (!parsed.pathname.endsWith('.zim')) {
|
||||
throw new Error(`Invalid ZIM file URL: ${url}. URL must end with .zim`)
|
||||
|
|
@ -167,6 +187,20 @@ export class ZimService implements IZimService {
|
|||
|
||||
const filepath = join(process.cwd(), ZIM_STORAGE_PATH, filename)
|
||||
|
||||
// Store metadata if provided
|
||||
if (metadata) {
|
||||
await ZimFileMetadata.updateOrCreate(
|
||||
{ filename },
|
||||
{
|
||||
title: metadata.title,
|
||||
summary: metadata.summary || null,
|
||||
author: metadata.author || null,
|
||||
size_bytes: metadata.size_bytes || null,
|
||||
}
|
||||
)
|
||||
logger.info(`[ZimService] Stored metadata for ZIM file: ${filename}`)
|
||||
}
|
||||
|
||||
// Dispatch a background download job
|
||||
const result = await RunDownloadJob.dispatch({
|
||||
url,
|
||||
|
|
@ -347,6 +381,10 @@ export class ZimService implements IZimService {
|
|||
}
|
||||
|
||||
await deleteFileIfExists(fullPath)
|
||||
|
||||
// Clean up metadata
|
||||
await ZimFileMetadata.query().where('filename', fileName).delete()
|
||||
logger.info(`[ZimService] Deleted metadata for ZIM file: ${fileName}`)
|
||||
}
|
||||
|
||||
// Wikipedia selector methods
|
||||
|
|
|
|||
|
|
@ -11,6 +11,25 @@ export const remoteDownloadValidator = vine.compile(
|
|||
})
|
||||
)
|
||||
|
||||
export const remoteDownloadWithMetadataValidator = vine.compile(
|
||||
vine.object({
|
||||
url: vine
|
||||
.string()
|
||||
.url({
|
||||
require_tld: false, // Allow local URLs
|
||||
})
|
||||
.trim(),
|
||||
metadata: vine
|
||||
.object({
|
||||
title: vine.string().trim().minLength(1),
|
||||
summary: vine.string().trim().optional(),
|
||||
author: vine.string().trim().optional(),
|
||||
size_bytes: vine.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export const remoteDownloadValidatorOptional = vine.compile(
|
||||
vine.object({
|
||||
url: vine
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'zim_file_metadata'
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id').primary()
|
||||
table.string('filename').notNullable().unique()
|
||||
table.string('title').notNullable()
|
||||
table.text('summary').nullable()
|
||||
table.string('author').nullable()
|
||||
table.bigInteger('size_bytes').nullable()
|
||||
table.timestamp('created_at')
|
||||
table.timestamp('updated_at')
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,11 +99,14 @@ class API {
|
|||
})()
|
||||
}
|
||||
|
||||
async downloadRemoteZimFile(url: string) {
|
||||
async downloadRemoteZimFile(
|
||||
url: string,
|
||||
metadata?: { title: string; summary?: string; author?: string; size_bytes?: number }
|
||||
) {
|
||||
return catchInternal(async () => {
|
||||
const response = await this.client.post<{ message: string; filename: string; url: string }>(
|
||||
'/zim/download-remote',
|
||||
{ url }
|
||||
{ url, metadata }
|
||||
)
|
||||
return response.data
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ import { useModals } from '~/context/ModalContext'
|
|||
import StyledModal from '~/components/StyledModal'
|
||||
import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus'
|
||||
import Alert from '~/components/Alert'
|
||||
import { FileEntry } from '../../../../types/files'
|
||||
import { ZimFileWithMetadata } from '../../../../types/zim'
|
||||
import { SERVICE_NAMES } from '../../../../constants/service_names'
|
||||
|
||||
export default function ZimPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const { openModal, closeAllModals } = useModals()
|
||||
const { isInstalled } = useServiceInstalledStatus(SERVICE_NAMES.KIWIX)
|
||||
const { data, isLoading } = useQuery<FileEntry[]>({
|
||||
const { data, isLoading } = useQuery<ZimFileWithMetadata[]>({
|
||||
queryKey: ['zim-files'],
|
||||
queryFn: getFiles,
|
||||
})
|
||||
|
|
@ -25,7 +25,7 @@ export default function ZimPage() {
|
|||
return res.data.files
|
||||
}
|
||||
|
||||
async function confirmDeleteFile(file: FileEntry) {
|
||||
async function confirmDeleteFile(file: ZimFileWithMetadata) {
|
||||
openModal(
|
||||
<StyledModal
|
||||
title="Confirm Delete?"
|
||||
|
|
@ -48,7 +48,7 @@ export default function ZimPage() {
|
|||
}
|
||||
|
||||
const deleteFileMutation = useMutation({
|
||||
mutationFn: async (file: FileEntry) => api.deleteZimFile(file.name.replace('.zim', '')),
|
||||
mutationFn: async (file: ZimFileWithMetadata) => api.deleteZimFile(file.name.replace('.zim', '')),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['zim-files'] })
|
||||
},
|
||||
|
|
@ -75,13 +75,30 @@ export default function ZimPage() {
|
|||
className="!mt-6"
|
||||
/>
|
||||
)}
|
||||
<StyledTable<FileEntry & { actions?: any }>
|
||||
<StyledTable<ZimFileWithMetadata & { actions?: any }>
|
||||
className="font-semibold mt-4"
|
||||
rowLines={true}
|
||||
loading={isLoading}
|
||||
compact
|
||||
columns={[
|
||||
{ accessor: 'name', title: 'Name' },
|
||||
{
|
||||
accessor: 'title',
|
||||
title: 'Title',
|
||||
render: (record) => (
|
||||
<span className="font-medium">
|
||||
{record.title || record.name}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'summary',
|
||||
title: 'Summary',
|
||||
render: (record) => (
|
||||
<span className="text-gray-600 text-sm line-clamp-2">
|
||||
{record.summary || '—'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
title: 'Actions',
|
||||
|
|
|
|||
|
|
@ -209,7 +209,12 @@ export default function ZimRemoteExplorer() {
|
|||
|
||||
async function downloadFile(record: RemoteZimFileEntry) {
|
||||
try {
|
||||
await api.downloadRemoteZimFile(record.download_url)
|
||||
await api.downloadRemoteZimFile(record.download_url, {
|
||||
title: record.title,
|
||||
summary: record.summary,
|
||||
author: record.author,
|
||||
size_bytes: record.size_bytes,
|
||||
})
|
||||
invalidateDownloads()
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { FileEntry } from './files.js'
|
||||
|
||||
export type ZimFileWithMetadata = FileEntry & {
|
||||
title: string | null
|
||||
summary: string | null
|
||||
author: string | null
|
||||
size_bytes: number | null
|
||||
}
|
||||
|
||||
export type ListZimFilesResponse = {
|
||||
files: FileEntry[]
|
||||
files: ZimFileWithMetadata[]
|
||||
next?: string
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user