feat(cli): add support for running import and API jobs

This commit is contained in:
Cory Dransfeldt 2025-06-08 15:47:09 -07:00
parent 75df36acc3
commit 8a12e83b13
No known key found for this signature in database
8 changed files with 272 additions and 65 deletions

View file

@ -2,6 +2,7 @@ import fs from 'fs-extra';
import path from 'path';
import inquirer from 'inquirer';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CACHE_DIR = path.resolve(__dirname, '..', '.cache');
@ -9,15 +10,39 @@ const CONFIG_PATH = path.join(CACHE_DIR, 'config.json');
const MEDIA_TYPES = ['movie', 'show'];
const ASSET_TYPES = ['poster', 'backdrop'];
export const initConfig = async () => {
const config = {};
const { storageDir } = await inquirer.prompt([{
name: 'storageDir',
message: 'Where is your storage root directory?',
validate: fs.pathExists
}]);
dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env') });
config.storageDir = storageDir;
export const initConfig = async () => {
const existingConfig = await fs.pathExists(CONFIG_PATH)
? await fs.readJson(CONFIG_PATH)
: {};
const config = { ...existingConfig };
if (config.storageDir) {
const { updateStorage } = await inquirer.prompt([{
type: 'confirm',
name: 'updateStorage',
message: `Storage directory is already set to "${config.storageDir}". Do you want to update it?`,
default: false
}]);
if (updateStorage) {
const { storageDir } = await inquirer.prompt([{
name: 'storageDir',
message: 'Where is your storage root directory?',
validate: fs.pathExists
}]);
config.storageDir = storageDir;
}
} else {
const { storageDir } = await inquirer.prompt([{
name: 'storageDir',
message: 'Where is your storage root directory?',
validate: fs.pathExists
}]);
config.storageDir = storageDir;
}
const { customize } = await inquirer.prompt([{
type: 'confirm',
@ -43,7 +68,6 @@ export const initConfig = async () => {
message: `Subpath for ${mediaType}/${assetType} (relative to storage root):`,
default: defaultPath
}]);
subpath = response.subpath;
}
@ -62,6 +86,7 @@ export const initConfig = async () => {
])
).artistPath
: 'Media assets/artists';
config.albumPath = customize
? (
await inquirer.prompt([
@ -73,23 +98,26 @@ export const initConfig = async () => {
])
).albumPath
: 'Media assets/albums';
config.bookPath = customize
? (
await inquirer.prompt([
{
name: 'bookPath',
message: 'Subpath for book images (relative to storage root):',
default: 'Media assets/books'
}
])
).bookPath
: 'Media assets/books';
? (
await inquirer.prompt([
{
name: 'bookPath',
message: 'Subpath for book images (relative to storage root):',
default: 'Media assets/books'
}
])
).bookPath
: 'Media assets/books';
config.globals = await fetchGlobals();
await fs.ensureDir(CACHE_DIR);
await fs.writeJson(CONFIG_PATH, config, { spaces: 2 });
console.log(`✅ Config saved to ${CONFIG_PATH}`);
}
};
export const loadConfig = async () => {
if (!await fs.pathExists(CONFIG_PATH)) {
@ -99,3 +127,31 @@ export const loadConfig = async () => {
return await fs.readJson(CONFIG_PATH);
}
const fetchGlobals = async () => {
const POSTGREST_URL = process.env.POSTGREST_URL;
const POSTGREST_API_KEY = process.env.POSTGREST_API_KEY;
if (!POSTGREST_URL || !POSTGREST_API_KEY) {
console.warn('⚠️ Missing POSTGREST_URL or POSTGREST_API_KEY in env, skipping globals fetch.');
return {};
}
try {
const res = await fetch(`${POSTGREST_URL}/optimized_globals?select=*`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${POSTGREST_API_KEY}`
}
});
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
return data[0] || {};
} catch (err) {
console.error('❌ Error fetching globals:', err.message);
return {};
}
};

159
cli/lib/jobs.js Normal file
View file

@ -0,0 +1,159 @@
import inquirer from 'inquirer';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import { loadConfig } from './config.js';
const config = await loadConfig();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env') });
const JOBS = [{
name: '🎧 Update total plays',
type: 'curl',
urlEnvVar: 'TOTAL_PLAYS_WEBHOOK',
tokenEnvVar: 'DIRECTUS_API_TOKEN',
method: 'GET'
},
{
name: '🛠 Rebuild site',
type: 'curl',
urlEnvVar: 'SITE_REBUILD_WEBHOOK',
tokenEnvVar: 'DIRECTUS_API_TOKEN',
method: 'GET'
},
{
name: '💿 Scrobble listens from Navidrome',
type: 'curl',
apiUrl: `${config.globals.url}/api/scrobble.php`,
tokenEnvVar: 'NAVIDROME_SCROBBLE_TOKEN',
method: 'POST'
},
{
name: '🐘 Send posts to Mastodon',
type: 'curl',
apiUrl: `${config.globals.url}/api/mastodon.php`,
tokenEnvVar: 'MASTODON_SYNDICATION_TOKEN',
method: 'POST'
},
{
name: '🎤 Import artist from Navidrome',
type: 'curl',
apiUrl: `${config.globals.url}/api/artist-import.php`,
tokenEnvVar: 'ARTIST_IMPORT_TOKEN',
method: 'POST',
paramsPrompt: [{
type: 'input',
name: 'artistId',
message: 'Enter the Navidrome artist ID:',
validate: input => input ? true : 'Artist ID is required'
}]
},
{
name: '📖 Import book',
type: 'curl',
apiUrl: `${config.globals.url}/api/book-import.php`,
tokenEnvVar: 'BOOK_IMPORT_TOKEN',
method: 'POST',
paramsPrompt: [{
type: 'input',
name: 'isbn',
message: 'Enter the book\'s ISBN:',
validate: input => input ? true : 'ISBN is required'
}]
},
{
name: '📽 Import movie or show',
type: 'curl',
apiUrl: `${config.globals.url}/api/watching-import.php`,
tokenEnvVar: 'WATCHING_IMPORT_TOKEN',
method: 'POST',
tokenIncludeInParams: true,
paramsPrompt: [{
type: 'input',
name: 'tmdb_id',
message: 'Enter the TMDB ID:',
validate: (input) =>
/^\d+$/.test(input) ? true : 'Please enter a valid TMDB ID'
},
{
type: 'list',
name: 'media_type',
message: 'Is this a movie or a show?',
choices: ['movie', 'show']
}]
},
{
name: '📺 Import upcoming TV seasons',
type: 'curl',
apiUrl: `${config.globals.url}/api/seasons-import.php`,
tokenEnvVar: 'SEASONS_IMPORT_TOKEN',
method: 'POST'
}];
export const runJobsMenu = async () => {
const { selectedJob } = await inquirer.prompt([{
type: 'list',
name: 'selectedJob',
message: 'Select a job to run:',
choices: JOBS.map((job, index) => ({
name: job.name,
value: index
}))
}]);
const job = JOBS[selectedJob];
if (job.type === 'curl') {
let params = null;
if (job.paramsPrompt) {
const answers = await inquirer.prompt(job.paramsPrompt);
const token = process.env[job.tokenEnvVar];
params = { ...answers, ...(token ? { token } : {}) };
}
await runCurl({ ...job, params });
} else {
console.warn(`⚠️ Unsupported job type: ${job.type}`);
}
};
const runCurl = async ({
urlEnvVar,
apiUrl = "",
tokenEnvVar,
method = 'POST',
name,
params = null
}) => {
const url = process.env[urlEnvVar] || apiUrl;
const token = tokenEnvVar ? process.env[tokenEnvVar] : null;
if (!url) {
console.error(`❌ Missing URL for job. Check ${urlEnvVar} in your .env`);
return;
}
try {
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` })
},
...(params && { body: JSON.stringify(params) })
});
if (!res.ok) {
const errText = await res.text();
throw new Error(errText);
}
console.log(`${name} ran successfully.`);
} catch (err) {
console.error(`❌ Request failed: ${err.message}`);
}
};