#!/usr/bin/env node
/**
* Web UI Server for Postiz Media Manager
*
* Simple Express server that provides a web interface for managing Postiz media
*/
import express, { Request, Response } from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { PostizClient } from './postizClient.js';
import { MediaService } from './mediaService.js';
import { config } from './config.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, '../public')));
// Initialize services
const client = new PostizClient();
const mediaService = new MediaService(client);
/**
* API Routes
*/
// Get media statistics
app.get('/api/stats', async (req: Request, res: Response) => {
try {
const stats = await mediaService.getMediaStats();
res.json(stats);
} catch (error) {
console.error('Error getting stats:', error);
res.status(500).json({
error: 'Failed to get statistics',
message: (error as Error).message
});
}
});
// Get all media with optional filters
app.get('/api/media', async (req: Request, res: Response) => {
try {
const { startDate, endDate, limit } = req.query;
let allMedia = await mediaService.getAllMedia();
// Filter by date range if provided
if (startDate || endDate) {
allMedia = allMedia.filter((media) => {
if (!media.createdAt) return true;
const createdDate = new Date(media.createdAt);
if (startDate && createdDate < new Date(startDate as string)) {
return false;
}
if (endDate && createdDate > new Date(endDate as string)) {
return false;
}
return true;
});
}
// Apply limit if provided
const limitNum = limit ? parseInt(limit as string) : undefined;
const mediaToReturn = limitNum ? allMedia.slice(0, limitNum) : allMedia;
res.json({
items: mediaToReturn,
total: allMedia.length,
returned: mediaToReturn.length,
});
} catch (error) {
console.error('Error getting media:', error);
res.status(500).json({
error: 'Failed to get media',
message: (error as Error).message
});
}
});
// Get orphan media (not used in future posts)
app.get('/api/media/orphans', async (req: Request, res: Response) => {
try {
const { limit } = req.query;
const limitNum = limit ? parseInt(limit as string) : undefined;
const orphans = await mediaService.findOrphanMedia(limitNum);
res.json({
items: orphans,
total: orphans.length,
});
} catch (error) {
console.error('Error getting orphan media:', error);
res.status(500).json({
error: 'Failed to get orphan media',
message: (error as Error).message
});
}
});
// Get protected media IDs (used in future posts)
app.get('/api/media/protected', async (req: Request, res: Response) => {
try {
const protectedIds = await mediaService.getProtectedMediaIds();
res.json({
protectedMediaIds: Array.from(protectedIds),
count: protectedIds.size,
});
} catch (error) {
console.error('Error getting protected media:', error);
res.status(500).json({
error: 'Failed to get protected media',
message: (error as Error).message
});
}
});
// Get media used in published posts
app.get('/api/media/published', async (req: Request, res: Response) => {
try {
const { startDate, endDate } = req.query;
// Get posts with 'published' or 'completed' status
const publishedPosts = await client.listPosts({
statuses: ['published', 'completed', 'posted'],
});
// Extract media IDs from published posts
const publishedMediaIds = new Set<string>();
for (const post of publishedPosts) {
// Extract media IDs using the same logic as MediaService
const mediaIds: string[] = [];
if (post.mediaIds && Array.isArray(post.mediaIds)) {
mediaIds.push(...post.mediaIds);
}
if (post.files && Array.isArray(post.files)) {
mediaIds.push(...post.files);
}
if (post.attachments && Array.isArray(post.attachments)) {
mediaIds.push(...post.attachments);
}
if (post.media && Array.isArray(post.media)) {
const ids = post.media
.filter((m: any) => m && typeof m === 'object' && m.id)
.map((m: any) => m.id);
mediaIds.push(...ids);
}
mediaIds.forEach((id) => publishedMediaIds.add(id));
}
// Get all media and filter by published media IDs
let allMedia = await mediaService.getAllMedia();
const publishedMedia = allMedia.filter((media) =>
publishedMediaIds.has(media.id)
);
// Filter by date range if provided
let filteredMedia = publishedMedia;
if (startDate || endDate) {
filteredMedia = publishedMedia.filter((media) => {
if (!media.createdAt) return true;
const createdDate = new Date(media.createdAt);
if (startDate && createdDate < new Date(startDate as string)) {
return false;
}
if (endDate && createdDate > new Date(endDate as string)) {
return false;
}
return true;
});
}
res.json({
items: filteredMedia,
total: filteredMedia.length,
});
} catch (error) {
console.error('Error getting published media:', error);
res.status(500).json({
error: 'Failed to get published media',
message: (error as Error).message
});
}
});
// Delete media by IDs
app.post('/api/media/delete', async (req: Request, res: Response) => {
try {
const { mediaIds } = req.body;
if (!mediaIds || !Array.isArray(mediaIds) || mediaIds.length === 0) {
return res.status(400).json({
error: 'mediaIds array is required'
});
}
const results = {
total: mediaIds.length,
deleted: [] as string[],
failed: [] as Array<{ id: string; reason: string }>,
};
// Delete each media
for (const mediaId of mediaIds) {
const result = await mediaService.deleteMediaById(mediaId);
if (result.success) {
results.deleted.push(mediaId);
} else {
results.failed.push({
id: mediaId,
reason: result.error || 'Unknown error',
});
}
}
res.json(results);
} catch (error) {
console.error('Error deleting media:', error);
res.status(500).json({
error: 'Failed to delete media',
message: (error as Error).message
});
}
});
// Cleanup orphan media
app.post('/api/media/cleanup', async (req: Request, res: Response) => {
try {
const { dryRun = true, limit } = req.body;
const report = await mediaService.cleanupOrphanMedia({
dryRun,
limit,
});
res.json(report);
} catch (error) {
console.error('Error cleaning up media:', error);
res.status(500).json({
error: 'Failed to cleanup media',
message: (error as Error).message
});
}
});
/**
* Serve the main HTML page
*/
app.get('/', (req: Request, res: Response) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
/**
* Start server
*/
app.listen(PORT, () => {
console.log(`
╔════════════════════════════════════════════════════════╗
║ Postiz Media Manager - Web UI ║
╚════════════════════════════════════════════════════════╝
✓ Server running at: http://localhost:${PORT}
✓ Postiz API URL: ${config.postizBaseUrl}
✓ API Token: ${config.postizApiToken.substring(0, 20)}...
Open your browser and visit: http://localhost:${PORT}
`);
});