feat(*.liquid): apply prettier to liquid templates
- offer to create tag when none is found while adding a link from cli - fix tag display in search
This commit is contained in:
parent
49e21d574e
commit
efe701f939
112 changed files with 1319 additions and 1134 deletions
|
@ -1,22 +1,22 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import inquirer from 'inquirer';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
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 rootDir = path.resolve(__dirname, '..');
|
||||
const CACHE_DIR = path.join(rootDir, '.cache');
|
||||
const CONFIG_PATH = path.join(CACHE_DIR, 'config.json');
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const CACHE_DIR = path.join(rootDir, ".cache");
|
||||
const CONFIG_PATH = path.join(CACHE_DIR, "config.json");
|
||||
|
||||
export const MEDIA_TYPES = [
|
||||
{ key: 'movie', label: 'movie', folder: 'movies' },
|
||||
{ key: 'show', label: 'tv show', folder: 'shows' }
|
||||
{ key: "movie", label: "movie", folder: "movies" },
|
||||
{ key: "show", label: "tv show", folder: "shows" }
|
||||
];
|
||||
|
||||
const ASSET_TYPES = ['poster', 'backdrop'];
|
||||
const ASSET_TYPES = ["poster", "backdrop"];
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env') });
|
||||
dotenv.config({ path: path.resolve(__dirname, "..", "..", ".env") });
|
||||
|
||||
export const initConfig = async () => {
|
||||
const existingConfig = (await fs.pathExists(CONFIG_PATH)) ? await fs.readJson(CONFIG_PATH) : {};
|
||||
|
@ -25,8 +25,8 @@ export const initConfig = async () => {
|
|||
if (config.storageDir) {
|
||||
const { updateStorage } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'updateStorage',
|
||||
type: "confirm",
|
||||
name: "updateStorage",
|
||||
message: `Storage directory is already set to "${config.storageDir}". Do you want to update it?`,
|
||||
default: false
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ export const initConfig = async () => {
|
|||
if (updateStorage) {
|
||||
const { storageDir } = await inquirer.prompt([
|
||||
{
|
||||
name: 'storageDir',
|
||||
message: 'Where is your storage root directory?',
|
||||
name: "storageDir",
|
||||
message: "Where is your storage root directory?",
|
||||
validate: fs.pathExists
|
||||
}
|
||||
]);
|
||||
|
@ -46,8 +46,8 @@ export const initConfig = async () => {
|
|||
} else {
|
||||
const { storageDir } = await inquirer.prompt([
|
||||
{
|
||||
name: 'storageDir',
|
||||
message: 'Where is your storage root directory?',
|
||||
name: "storageDir",
|
||||
message: "Where is your storage root directory?",
|
||||
validate: fs.pathExists
|
||||
}
|
||||
]);
|
||||
|
@ -57,9 +57,9 @@ export const initConfig = async () => {
|
|||
|
||||
const { customize } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'customize',
|
||||
message: 'Do you want to customize default media paths?',
|
||||
type: "confirm",
|
||||
name: "customize",
|
||||
message: "Do you want to customize default media paths?",
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
@ -70,14 +70,14 @@ export const initConfig = async () => {
|
|||
config.mediaPaths[key] = {};
|
||||
|
||||
for (const assetType of ASSET_TYPES) {
|
||||
const assetFolder = assetType === 'poster' ? '' : `/${assetType}s`;
|
||||
const defaultPath = `Media assets/${folder}${assetFolder}`.replace(/\/$/, '');
|
||||
const assetFolder = assetType === "poster" ? "" : `/${assetType}s`;
|
||||
const defaultPath = `Media assets/${folder}${assetFolder}`.replace(/\/$/, "");
|
||||
let subpath = defaultPath;
|
||||
|
||||
if (customize) {
|
||||
const response = await inquirer.prompt([
|
||||
{
|
||||
name: 'subpath',
|
||||
name: "subpath",
|
||||
message: `Subpath for ${label}/${assetType} (relative to storage root):`,
|
||||
default: defaultPath
|
||||
}
|
||||
|
@ -94,43 +94,43 @@ export const initConfig = async () => {
|
|||
? (
|
||||
await inquirer.prompt([
|
||||
{
|
||||
name: 'artistPath',
|
||||
message: 'Subpath for artist images (relative to storage root):',
|
||||
default: 'Media assets/artists'
|
||||
name: "artistPath",
|
||||
message: "Subpath for artist images (relative to storage root):",
|
||||
default: "Media assets/artists"
|
||||
}
|
||||
])
|
||||
).artistPath
|
||||
: 'Media assets/artists';
|
||||
: "Media assets/artists";
|
||||
|
||||
config.albumPath = customize
|
||||
? (
|
||||
await inquirer.prompt([
|
||||
{
|
||||
name: 'albumPath',
|
||||
message: 'Subpath for album images (relative to storage root):',
|
||||
default: 'Media assets/albums'
|
||||
name: "albumPath",
|
||||
message: "Subpath for album images (relative to storage root):",
|
||||
default: "Media assets/albums"
|
||||
}
|
||||
])
|
||||
).albumPath
|
||||
: 'Media assets/albums';
|
||||
: "Media assets/albums";
|
||||
|
||||
config.bookPath = customize
|
||||
? (
|
||||
await inquirer.prompt([
|
||||
{
|
||||
name: 'bookPath',
|
||||
message: 'Subpath for book images (relative to storage root):',
|
||||
default: 'Media assets/books'
|
||||
name: "bookPath",
|
||||
message: "Subpath for book images (relative to storage root):",
|
||||
default: "Media assets/books"
|
||||
}
|
||||
])
|
||||
).bookPath
|
||||
: 'Media assets/books';
|
||||
: "Media assets/books";
|
||||
|
||||
if (config.directus?.apiUrl) {
|
||||
const { updateApiUrl } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'updateApiUrl',
|
||||
type: "confirm",
|
||||
name: "updateApiUrl",
|
||||
message: `Directus API URL is already set to "${config.directus.apiUrl}". Do you want to update it?`,
|
||||
default: false
|
||||
}
|
||||
|
@ -139,9 +139,9 @@ export const initConfig = async () => {
|
|||
if (updateApiUrl) {
|
||||
const { apiUrl } = await inquirer.prompt([
|
||||
{
|
||||
name: 'apiUrl',
|
||||
message: 'Enter your Directus instance URL:',
|
||||
validate: (input) => input.startsWith('http') || 'Must be a valid URL'
|
||||
name: "apiUrl",
|
||||
message: "Enter your Directus instance URL:",
|
||||
validate: (input) => input.startsWith("http") || "Must be a valid URL"
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -150,9 +150,9 @@ export const initConfig = async () => {
|
|||
} else {
|
||||
const { apiUrl } = await inquirer.prompt([
|
||||
{
|
||||
name: 'apiUrl',
|
||||
message: 'Enter your Directus URL:',
|
||||
validate: (input) => input.startsWith('http') || 'Must be a valid URL'
|
||||
name: "apiUrl",
|
||||
message: "Enter your Directus URL:",
|
||||
validate: (input) => input.startsWith("http") || "Must be a valid URL"
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -171,7 +171,7 @@ export const initConfig = async () => {
|
|||
|
||||
export const loadConfig = async () => {
|
||||
if (!(await fs.pathExists(CONFIG_PATH))) {
|
||||
console.error('❌ Config not found. Run \`coryd init\` first.');
|
||||
console.error("❌ Config not found. Run \`coryd init\` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -183,15 +183,15 @@ const fetchGlobals = async () => {
|
|||
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.');
|
||||
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',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${POSTGREST_API_KEY}`
|
||||
}
|
||||
});
|
||||
|
@ -201,7 +201,7 @@ const fetchGlobals = async () => {
|
|||
const data = await res.json();
|
||||
return data[0] || {};
|
||||
} catch (err) {
|
||||
console.error('❌ Error fetching globals:', err.message);
|
||||
console.error("❌ Error fetching globals:", err.message);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import { createDirectus, staticToken, rest } from '@directus/sdk';
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import dotenv from "dotenv";
|
||||
import { createDirectus, staticToken, rest } from "@directus/sdk";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '..', '..', '..', '.env') });
|
||||
dotenv.config({ path: path.resolve(__dirname, "..", "..", "..", ".env") });
|
||||
|
||||
let directus;
|
||||
let API_URL;
|
||||
|
@ -15,13 +15,13 @@ export const initDirectusClient = (config) => {
|
|||
|
||||
const token = process.env.DIRECTUS_API_TOKEN;
|
||||
|
||||
if (!API_URL || !token) throw new Error('Missing Directus API URL or token.');
|
||||
if (!API_URL || !token) throw new Error("Missing Directus API URL or token.");
|
||||
|
||||
directus = createDirectus(API_URL).with(staticToken(process.env.DIRECTUS_API_TOKEN)).with(rest());
|
||||
};
|
||||
|
||||
export const getDirectusClient = () => {
|
||||
if (!directus) throw new Error('Directus client not initialized.');
|
||||
if (!directus) throw new Error("Directus client not initialized.");
|
||||
|
||||
return directus;
|
||||
};
|
||||
|
@ -31,7 +31,7 @@ const request = async (method, endpoint, body = null) => {
|
|||
const res = await fetch(`${API_URL}/items/${endpoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${API_TOKEN}`
|
||||
},
|
||||
body: body ? JSON.stringify(body) : null
|
||||
|
@ -46,13 +46,13 @@ const request = async (method, endpoint, body = null) => {
|
|||
return await res.json();
|
||||
};
|
||||
|
||||
export const searchItems = async (collection, query = '', filters = {}) => {
|
||||
export const searchItems = async (collection, query = "", filters = {}) => {
|
||||
const API_TOKEN = process.env.DIRECTUS_API_TOKEN;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (query) params.append('search', query);
|
||||
if (query) params.append("search", query);
|
||||
|
||||
params.append('limit', '50');
|
||||
params.append("limit", "50");
|
||||
|
||||
for (const [field, value] of Object.entries(filters)) {
|
||||
params.append(`filter[${field}][_eq]`, value);
|
||||
|
@ -60,9 +60,9 @@ export const searchItems = async (collection, query = '', filters = {}) => {
|
|||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/items/${collection}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${API_TOKEN}`
|
||||
}
|
||||
});
|
||||
|
@ -77,6 +77,6 @@ export const searchItems = async (collection, query = '', filters = {}) => {
|
|||
};
|
||||
|
||||
export const updateItem = async (collection, id, values) =>
|
||||
await request('PATCH', `${collection}/${id}`, values);
|
||||
await request("PATCH", `${collection}/${id}`, values);
|
||||
|
||||
export const createItem = async (collection, values) => await request('POST', collection, values);
|
||||
export const createItem = async (collection, values) => await request("POST", collection, values);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { searchItems } from '../directus/client.js';
|
||||
import inquirer from "inquirer";
|
||||
import { searchItems } from "../directus/client.js";
|
||||
|
||||
export const promptForMultipleRelations = async (collection, label = collection) => {
|
||||
const selectedIds = new Set();
|
||||
|
||||
while (true) {
|
||||
const { query } = await inquirer.prompt({
|
||||
name: 'query',
|
||||
name: "query",
|
||||
message: `🔍 Search ${label} (or leave blank to finish):`
|
||||
});
|
||||
const trimmed = query.trim();
|
||||
|
@ -22,8 +22,8 @@ export const promptForMultipleRelations = async (collection, label = collection)
|
|||
}
|
||||
|
||||
const { selected } = await inquirer.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'selected',
|
||||
type: "checkbox",
|
||||
name: "selected",
|
||||
message: `✔ Select ${label} to add:`,
|
||||
choices: results.map((item) => ({
|
||||
name: item.name || item.title || item.id,
|
||||
|
@ -34,8 +34,8 @@ export const promptForMultipleRelations = async (collection, label = collection)
|
|||
selected.forEach((id) => selectedIds.add(id));
|
||||
|
||||
const { again } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'again',
|
||||
type: "confirm",
|
||||
name: "again",
|
||||
message: `Search and add more ${label}?`,
|
||||
default: false
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import https from 'https';
|
||||
import inquirer from 'inquirer';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { loadConfig, MEDIA_TYPES } from './config.js';
|
||||
import { sanitizeMediaString } from './sanitize.js';
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import https from "https";
|
||||
import inquirer from "inquirer";
|
||||
import { fileURLToPath } from "url";
|
||||
import { loadConfig, MEDIA_TYPES } from "./config.js";
|
||||
import { sanitizeMediaString } from "./sanitize.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const downloadImage = (url, dest) =>
|
||||
|
@ -17,19 +17,19 @@ const downloadImage = (url, dest) =>
|
|||
return reject(new Error(`Failed to download. Status: ${response.statusCode}`));
|
||||
|
||||
response.pipe(file);
|
||||
file.on('finish', () => file.close(resolve));
|
||||
file.on("finish", () => file.close(resolve));
|
||||
})
|
||||
.on('error', reject);
|
||||
.on("error", reject);
|
||||
});
|
||||
const isValidTMDBUrl = (val) =>
|
||||
/^https:\/\/image\.tmdb\.org\/t\/p\//.test(val) || '❌ Must be a valid TMDB image url';
|
||||
/^https:\/\/image\.tmdb\.org\/t\/p\//.test(val) || "❌ Must be a valid TMDB image url";
|
||||
const overwriteImageDownloadPrompt = async (url, finalPath, fileName) => {
|
||||
await fs.ensureDir(path.dirname(finalPath));
|
||||
|
||||
if (await fs.pathExists(finalPath)) {
|
||||
const { overwrite } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'overwrite',
|
||||
type: "confirm",
|
||||
name: "overwrite",
|
||||
message: `${fileName} already exists. Overwrite?`,
|
||||
default: false
|
||||
});
|
||||
|
@ -49,29 +49,29 @@ const overwriteImageDownloadPrompt = async (url, finalPath, fileName) => {
|
|||
export const downloadWatchingImages = async () => {
|
||||
const config = await loadConfig();
|
||||
const { mediaType } = await inquirer.prompt({
|
||||
type: 'list',
|
||||
name: 'mediaType',
|
||||
message: 'Movie or a tv show?',
|
||||
type: "list",
|
||||
name: "mediaType",
|
||||
message: "Movie or a tv show?",
|
||||
choices: MEDIA_TYPES.map(({ key, label }) => ({
|
||||
name: label,
|
||||
value: key
|
||||
}))
|
||||
});
|
||||
const { tmdbId } = await inquirer.prompt({
|
||||
name: 'tmdbId',
|
||||
message: 'Enter the TMDB ID:'
|
||||
name: "tmdbId",
|
||||
message: "Enter the TMDB ID:"
|
||||
});
|
||||
|
||||
if (!tmdbId) {
|
||||
console.warn('⚠️ TMDB ID is required.');
|
||||
console.warn("⚠️ TMDB ID is required.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { posterUrl, backdropUrl } = await inquirer.prompt([
|
||||
{
|
||||
name: 'posterUrl',
|
||||
message: 'Enter the poster url:',
|
||||
name: "posterUrl",
|
||||
message: "Enter the poster url:",
|
||||
validate: (val) => {
|
||||
if (!val) return true;
|
||||
|
||||
|
@ -79,8 +79,8 @@ export const downloadWatchingImages = async () => {
|
|||
}
|
||||
},
|
||||
{
|
||||
name: 'backdropUrl',
|
||||
message: 'Enter the backdrop url:',
|
||||
name: "backdropUrl",
|
||||
message: "Enter the backdrop url:",
|
||||
validate: (val) => {
|
||||
if (!val) return true;
|
||||
|
||||
|
@ -89,14 +89,14 @@ export const downloadWatchingImages = async () => {
|
|||
}
|
||||
]);
|
||||
const types = [
|
||||
{ type: 'poster', url: posterUrl },
|
||||
{ type: 'backdrop', url: backdropUrl }
|
||||
{ type: "poster", url: posterUrl },
|
||||
{ type: "backdrop", url: backdropUrl }
|
||||
];
|
||||
|
||||
for (const { type, url } of types) {
|
||||
if (!url) continue;
|
||||
|
||||
const ext = path.extname(new URL(url).pathname) || '.jpg';
|
||||
const ext = path.extname(new URL(url).pathname) || ".jpg";
|
||||
const fileName = `${type}-${tmdbId}${ext}`;
|
||||
const targetSubPath = config.mediaPaths[mediaType][type];
|
||||
|
||||
|
@ -116,34 +116,34 @@ export const downloadWatchingImages = async () => {
|
|||
export const downloadArtistImage = async () => {
|
||||
const config = await loadConfig();
|
||||
const { artistName } = await inquirer.prompt({
|
||||
name: 'artistName',
|
||||
message: 'Enter the artist name:'
|
||||
name: "artistName",
|
||||
message: "Enter the artist name:"
|
||||
});
|
||||
|
||||
if (!artistName) {
|
||||
console.warn('⚠️ Artist name is required.');
|
||||
console.warn("⚠️ Artist name is required.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageUrl } = await inquirer.prompt({
|
||||
name: 'imageUrl',
|
||||
message: 'Enter the artist image url:',
|
||||
name: "imageUrl",
|
||||
message: "Enter the artist image url:",
|
||||
validate: (val) => {
|
||||
try {
|
||||
new URL(val);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return '❌ Must be a valid url.';
|
||||
return "❌ Must be a valid url.";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sanitizedName = sanitizeMediaString(artistName);
|
||||
const ext = path.extname(new URL(imageUrl).pathname) || '.jpg';
|
||||
const ext = path.extname(new URL(imageUrl).pathname) || ".jpg";
|
||||
const fileName = `${sanitizedName}${ext}`;
|
||||
const targetDir = path.join(config.storageDir, 'artists');
|
||||
const targetDir = path.join(config.storageDir, "artists");
|
||||
const finalPath = path.join(targetDir, fileName);
|
||||
|
||||
await overwriteImageDownloadPrompt(imageUrl, finalPath, fileName);
|
||||
|
@ -153,44 +153,44 @@ export const downloadAlbumImage = async () => {
|
|||
const config = await loadConfig();
|
||||
|
||||
const { artistName } = await inquirer.prompt({
|
||||
name: 'artistName',
|
||||
message: 'Enter the artist name:'
|
||||
name: "artistName",
|
||||
message: "Enter the artist name:"
|
||||
});
|
||||
|
||||
if (!artistName) {
|
||||
console.warn('⚠️ Artist name is required.');
|
||||
console.warn("⚠️ Artist name is required.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { albumName } = await inquirer.prompt({
|
||||
name: 'albumName',
|
||||
message: 'Enter the album name:'
|
||||
name: "albumName",
|
||||
message: "Enter the album name:"
|
||||
});
|
||||
|
||||
if (!albumName) {
|
||||
console.warn('⚠️ Album name is required.');
|
||||
console.warn("⚠️ Album name is required.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageUrl } = await inquirer.prompt({
|
||||
name: 'imageUrl',
|
||||
message: 'Enter the album image url:',
|
||||
name: "imageUrl",
|
||||
message: "Enter the album image url:",
|
||||
validate: (val) => {
|
||||
try {
|
||||
new URL(val);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return '❌ Must be a valid url.';
|
||||
return "❌ Must be a valid url.";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const artistSlug = sanitizeMediaString(artistName);
|
||||
const albumSlug = sanitizeMediaString(albumName);
|
||||
const ext = path.extname(new URL(imageUrl).pathname) || '.jpg';
|
||||
const ext = path.extname(new URL(imageUrl).pathname) || ".jpg";
|
||||
const fileName = `${artistSlug}-${albumSlug}${ext}`;
|
||||
const targetDir = path.join(config.storageDir, config.albumPath);
|
||||
const finalPath = path.join(targetDir, fileName);
|
||||
|
@ -201,37 +201,37 @@ export const downloadAlbumImage = async () => {
|
|||
export const downloadBookImage = async () => {
|
||||
const config = await loadConfig();
|
||||
const { isbn } = await inquirer.prompt({
|
||||
name: 'isbn',
|
||||
message: 'Enter the ISBN (no spaces):',
|
||||
name: "isbn",
|
||||
message: "Enter the ISBN (no spaces):",
|
||||
validate: (val) =>
|
||||
/^[a-zA-Z0-9-]+$/.test(val) || 'ISBN must contain only letters, numbers, or hyphens'
|
||||
/^[a-zA-Z0-9-]+$/.test(val) || "ISBN must contain only letters, numbers, or hyphens"
|
||||
});
|
||||
const { bookTitle } = await inquirer.prompt({
|
||||
name: 'bookTitle',
|
||||
message: 'Enter the book title:'
|
||||
name: "bookTitle",
|
||||
message: "Enter the book title:"
|
||||
});
|
||||
|
||||
if (!bookTitle) {
|
||||
console.warn('⚠️ Book title is required.');
|
||||
console.warn("⚠️ Book title is required.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageUrl } = await inquirer.prompt({
|
||||
name: 'imageUrl',
|
||||
message: 'Enter the book cover image URL:',
|
||||
name: "imageUrl",
|
||||
message: "Enter the book cover image URL:",
|
||||
validate: (val) => {
|
||||
try {
|
||||
new URL(val);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return 'Must be a valid URL';
|
||||
return "Must be a valid URL";
|
||||
}
|
||||
}
|
||||
});
|
||||
const titleSlug = sanitizeMediaString(bookTitle);
|
||||
const ext = path.extname(new URL(imageUrl).pathname) || '.jpg';
|
||||
const ext = path.extname(new URL(imageUrl).pathname) || ".jpg";
|
||||
const fileName = `${isbn}-${titleSlug}${ext}`;
|
||||
const targetDir = path.join(config.storageDir, config.bookPath);
|
||||
const finalPath = path.join(targetDir, fileName);
|
||||
|
@ -241,17 +241,17 @@ export const downloadBookImage = async () => {
|
|||
|
||||
export const downloadAsset = async () => {
|
||||
const { type } = await inquirer.prompt({
|
||||
type: 'list',
|
||||
name: 'type',
|
||||
message: 'What type of asset are you downloading?',
|
||||
choices: ['movie/tv show', 'artist', 'album', 'book']
|
||||
type: "list",
|
||||
name: "type",
|
||||
message: "What type of asset are you downloading?",
|
||||
choices: ["movie/tv show", "artist", "album", "book"]
|
||||
});
|
||||
|
||||
if (type === 'artist') {
|
||||
if (type === "artist") {
|
||||
await downloadArtistImage();
|
||||
} else if (type === 'album') {
|
||||
} else if (type === "album") {
|
||||
await downloadAlbumImage();
|
||||
} else if (type === 'book') {
|
||||
} else if (type === "book") {
|
||||
await downloadBookImage();
|
||||
} else {
|
||||
await downloadWatchingImages();
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
export const handleExitError = (err, type = 'Unhandled error') => {
|
||||
export const handleExitError = (err, type = "Unhandled error") => {
|
||||
const isExit =
|
||||
err?.name === 'ExitPromptError' ||
|
||||
err?.code === 'ERR_CANCELED' ||
|
||||
err?.message?.includes('SIGINT');
|
||||
err?.name === "ExitPromptError" ||
|
||||
err?.code === "ERR_CANCELED" ||
|
||||
err?.message?.includes("SIGINT");
|
||||
|
||||
if (isExit) {
|
||||
console.log('\n👋 Exiting. Cya!\n');
|
||||
console.log("\n👋 Exiting. Cya!\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
|
124
cli/lib/jobs.js
124
cli/lib/jobs.js
|
@ -1,110 +1,110 @@
|
|||
import inquirer from 'inquirer';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import { loadConfig } from './config.js';
|
||||
import inquirer from "inquirer";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import dotenv from "dotenv";
|
||||
import { loadConfig } from "./config.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env') });
|
||||
dotenv.config({ path: path.resolve(__dirname, "..", "..", ".env") });
|
||||
|
||||
export const runJobsMenu = async () => {
|
||||
const config = await loadConfig();
|
||||
const JOBS = [
|
||||
{
|
||||
name: '🛠 Rebuild site',
|
||||
type: 'curl',
|
||||
urlEnvVar: 'SITE_REBUILD_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',
|
||||
name: "💿 Scrobble listens from Navidrome",
|
||||
type: "curl",
|
||||
apiUrl: `${config.url}/api/scrobble.php`,
|
||||
tokenEnvVar: 'NAVIDROME_SCROBBLE_TOKEN',
|
||||
method: 'POST'
|
||||
tokenEnvVar: "NAVIDROME_SCROBBLE_TOKEN",
|
||||
method: "POST"
|
||||
},
|
||||
{
|
||||
name: '🎧 Update total plays',
|
||||
type: 'curl',
|
||||
urlEnvVar: 'TOTAL_PLAYS_WEBHOOK',
|
||||
tokenEnvVar: 'DIRECTUS_API_TOKEN',
|
||||
method: 'GET'
|
||||
name: "🎧 Update total plays",
|
||||
type: "curl",
|
||||
urlEnvVar: "TOTAL_PLAYS_WEBHOOK",
|
||||
tokenEnvVar: "DIRECTUS_API_TOKEN",
|
||||
method: "GET"
|
||||
},
|
||||
{
|
||||
name: '🐘 Send posts to Mastodon',
|
||||
type: 'curl',
|
||||
name: "🐘 Send posts to Mastodon",
|
||||
type: "curl",
|
||||
apiUrl: `${config.url}/api/mastodon.php`,
|
||||
tokenEnvVar: 'MASTODON_SYNDICATION_TOKEN',
|
||||
method: 'POST'
|
||||
tokenEnvVar: "MASTODON_SYNDICATION_TOKEN",
|
||||
method: "POST"
|
||||
},
|
||||
{
|
||||
name: '🎤 Import artist from Navidrome',
|
||||
type: 'curl',
|
||||
name: "🎤 Import artist from Navidrome",
|
||||
type: "curl",
|
||||
apiUrl: `${config.url}/api/artist-import.php`,
|
||||
tokenEnvVar: 'ARTIST_IMPORT_TOKEN',
|
||||
method: 'POST',
|
||||
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')
|
||||
type: "input",
|
||||
name: "artistId",
|
||||
message: "Enter the Navidrome artist ID:",
|
||||
validate: (input) => (input ? true : "Artist ID is required")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '📖 Import book',
|
||||
type: 'curl',
|
||||
name: "📖 Import book",
|
||||
type: "curl",
|
||||
apiUrl: `${config.url}/api/book-import.php`,
|
||||
tokenEnvVar: 'BOOK_IMPORT_TOKEN',
|
||||
method: 'POST',
|
||||
tokenEnvVar: "BOOK_IMPORT_TOKEN",
|
||||
method: "POST",
|
||||
paramsPrompt: [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'isbn',
|
||||
type: "input",
|
||||
name: "isbn",
|
||||
message: "Enter the book's ISBN:",
|
||||
validate: (input) => (input ? true : 'ISBN is required')
|
||||
validate: (input) => (input ? true : "ISBN is required")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '📽 Import a movie or tv show',
|
||||
type: 'curl',
|
||||
name: "📽 Import a movie or tv show",
|
||||
type: "curl",
|
||||
apiUrl: `${config.url}/api/watching-import.php`,
|
||||
tokenEnvVar: 'WATCHING_IMPORT_TOKEN',
|
||||
method: 'POST',
|
||||
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: "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 tv show?',
|
||||
choices: ['movie', 'tv']
|
||||
type: "list",
|
||||
name: "media_type",
|
||||
message: "Is this a movie or tv show?",
|
||||
choices: ["movie", "tv"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '📺 Import upcoming TV seasons',
|
||||
type: 'curl',
|
||||
name: "📺 Import upcoming TV seasons",
|
||||
type: "curl",
|
||||
apiUrl: `${config.url}/api/seasons-import.php`,
|
||||
tokenEnvVar: 'SEASONS_IMPORT_TOKEN',
|
||||
method: 'POST'
|
||||
tokenEnvVar: "SEASONS_IMPORT_TOKEN",
|
||||
method: "POST"
|
||||
}
|
||||
];
|
||||
|
||||
const { selectedJob } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'selectedJob',
|
||||
message: 'Select a job to run:',
|
||||
type: "list",
|
||||
name: "selectedJob",
|
||||
message: "Select a job to run:",
|
||||
choices: JOBS.map((job, index) => ({
|
||||
name: job.name,
|
||||
value: index
|
||||
|
@ -114,7 +114,7 @@ export const runJobsMenu = async () => {
|
|||
|
||||
const job = JOBS[selectedJob];
|
||||
|
||||
if (job.type === 'curl') {
|
||||
if (job.type === "curl") {
|
||||
let params = null;
|
||||
|
||||
if (job.paramsPrompt) {
|
||||
|
@ -132,9 +132,9 @@ export const runJobsMenu = async () => {
|
|||
|
||||
const runCurl = async ({
|
||||
urlEnvVar,
|
||||
apiUrl = '',
|
||||
apiUrl = "",
|
||||
tokenEnvVar,
|
||||
method = 'POST',
|
||||
method = "POST",
|
||||
name,
|
||||
params = null
|
||||
}) => {
|
||||
|
@ -151,7 +151,7 @@ const runCurl = async ({
|
|||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
...(token && { Authorization: `Bearer ${token}` })
|
||||
},
|
||||
...(params && { body: JSON.stringify(params) })
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import inquirer from 'inquirer';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import inquirer from "inquirer";
|
||||
import { execSync } from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootDir = path.resolve(__dirname, '..', '..');
|
||||
const packageJsonPath = path.join(rootDir, 'package.json');
|
||||
const rootDir = path.resolve(__dirname, "..", "..");
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
|
||||
export const runRootScript = async (scriptArg) => {
|
||||
const pkg = await fs.readJson(packageJsonPath);
|
||||
|
@ -17,9 +17,9 @@ export const runRootScript = async (scriptArg) => {
|
|||
if (!script) {
|
||||
const { selected } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'selected',
|
||||
message: 'Select a script to run:',
|
||||
type: "list",
|
||||
name: "selected",
|
||||
message: "Select a script to run:",
|
||||
choices: Object.keys(scripts)
|
||||
}
|
||||
]);
|
||||
|
@ -34,7 +34,7 @@ export const runRootScript = async (scriptArg) => {
|
|||
|
||||
try {
|
||||
execSync(`npm run ${script}`, {
|
||||
stdio: 'inherit',
|
||||
stdio: "inherit",
|
||||
cwd: rootDir
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { transliterate } from 'transliteration';
|
||||
import { transliterate } from "transliteration";
|
||||
|
||||
export const sanitizeMediaString = (input) => {
|
||||
const ascii = transliterate(input);
|
||||
const cleaned = ascii.replace(/[^a-zA-Z0-9\s-]/g, '');
|
||||
const slugified = cleaned.replace(/[\s-]+/g, '-').toLowerCase();
|
||||
const cleaned = ascii.replace(/[^a-zA-Z0-9\s-]/g, "");
|
||||
const slugified = cleaned.replace(/[\s-]+/g, "-").toLowerCase();
|
||||
|
||||
return slugified.replace(/^-+|-+$/g, '');
|
||||
return slugified.replace(/^-+|-+$/g, "");
|
||||
};
|
||||
|
||||
export const removeUrlProtocol = (url) => (url ? url.replace(/^https?:\/\//, '') : '');
|
||||
export const removeUrlProtocol = (url) => (url ? url.replace(/^https?:\/\//, "") : "");
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { loadConfig } from '../config.js';
|
||||
import { initDirectusClient, searchItems, createItem } from '../directus/client.js';
|
||||
import inquirer from "inquirer";
|
||||
import { loadConfig } from "../config.js";
|
||||
import { initDirectusClient, searchItems, createItem } from "../directus/client.js";
|
||||
|
||||
export const addBlockedRobot = async () => {
|
||||
const config = await loadConfig();
|
||||
|
||||
initDirectusClient(config);
|
||||
|
||||
const robots = await searchItems('robots', '/');
|
||||
let rootRobot = robots.find((r) => r.path === '/');
|
||||
const robots = await searchItems("robots", "/");
|
||||
let rootRobot = robots.find((r) => r.path === "/");
|
||||
|
||||
if (!rootRobot) {
|
||||
console.log('ℹ️ No robots entry for `/` found. Creating one...');
|
||||
console.log("ℹ️ No robots entry for `/` found. Creating one...");
|
||||
|
||||
const newRobot = await createItem('robots', { path: '/' });
|
||||
const newRobot = await createItem("robots", { path: "/" });
|
||||
|
||||
rootRobot = newRobot.data || newRobot;
|
||||
|
||||
console.log('✅ Created robots rule for `/`');
|
||||
console.log("✅ Created robots rule for `/`");
|
||||
}
|
||||
|
||||
const { userAgent } = await inquirer.prompt({
|
||||
name: 'userAgent',
|
||||
message: '🤖 Enter the user-agent string to block:',
|
||||
validate: (input) => !!input || 'User-agent cannot be empty'
|
||||
name: "userAgent",
|
||||
message: "🤖 Enter the user-agent string to block:",
|
||||
validate: (input) => !!input || "User-agent cannot be empty"
|
||||
});
|
||||
|
||||
const createdAgent = await createItem('user_agents', {
|
||||
const createdAgent = await createItem("user_agents", {
|
||||
user_agent: userAgent
|
||||
});
|
||||
|
||||
const agentId = createdAgent.data?.id || createdAgent.id;
|
||||
|
||||
await createItem('robots_user_agents', {
|
||||
await createItem("robots_user_agents", {
|
||||
robots_id: rootRobot.id,
|
||||
user_agents_id: agentId
|
||||
});
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { loadConfig } from '../config.js';
|
||||
import inquirer from "inquirer";
|
||||
import { loadConfig } from "../config.js";
|
||||
import {
|
||||
initDirectusClient,
|
||||
getDirectusClient,
|
||||
searchItems,
|
||||
createItem,
|
||||
updateItem
|
||||
} from '../directus/client.js';
|
||||
} from "../directus/client.js";
|
||||
|
||||
export const addEpisodeToShow = async () => {
|
||||
const config = await loadConfig();
|
||||
|
@ -15,21 +15,21 @@ export const addEpisodeToShow = async () => {
|
|||
|
||||
const directus = getDirectusClient();
|
||||
const showResults = await inquirer.prompt({
|
||||
name: 'query',
|
||||
message: 'Search for a show:'
|
||||
name: "query",
|
||||
message: "Search for a show:"
|
||||
});
|
||||
const matches = await searchItems('shows', showResults.query);
|
||||
const matches = await searchItems("shows", showResults.query);
|
||||
|
||||
if (!matches.length) {
|
||||
console.warn('⚠️ No matching shows found.');
|
||||
console.warn("⚠️ No matching shows found.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { showId } = await inquirer.prompt({
|
||||
type: 'list',
|
||||
name: 'showId',
|
||||
message: 'Select a show:',
|
||||
type: "list",
|
||||
name: "showId",
|
||||
message: "Select a show:",
|
||||
choices: matches.map((s) => ({
|
||||
name: s.title || s.name || s.id,
|
||||
value: s.id
|
||||
|
@ -37,23 +37,23 @@ export const addEpisodeToShow = async () => {
|
|||
});
|
||||
const { season_number, episode_number, plays } = await inquirer.prompt([
|
||||
{
|
||||
name: 'season_number',
|
||||
message: 'Season number:',
|
||||
name: "season_number",
|
||||
message: "Season number:",
|
||||
validate: (val) => !isNaN(val)
|
||||
},
|
||||
{
|
||||
name: 'episode_number',
|
||||
message: 'Episode number:',
|
||||
name: "episode_number",
|
||||
message: "Episode number:",
|
||||
validate: (val) => !isNaN(val)
|
||||
},
|
||||
{
|
||||
name: 'plays',
|
||||
message: 'Play count:',
|
||||
name: "plays",
|
||||
message: "Play count:",
|
||||
default: 0,
|
||||
validate: (val) => !isNaN(val)
|
||||
}
|
||||
]);
|
||||
const existing = await searchItems('episodes', `${season_number} ${episode_number}`);
|
||||
const existing = await searchItems("episodes", `${season_number} ${episode_number}`);
|
||||
const match = existing.find(
|
||||
(e) =>
|
||||
Number(e.season_number) === Number(season_number) &&
|
||||
|
@ -63,21 +63,21 @@ export const addEpisodeToShow = async () => {
|
|||
|
||||
if (match) {
|
||||
const { update } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'update',
|
||||
type: "confirm",
|
||||
name: "update",
|
||||
message: `Episode exists. Update play count from ${match.plays ?? 0} to ${plays}?`,
|
||||
default: true
|
||||
});
|
||||
|
||||
if (update) {
|
||||
await updateItem('episodes', match.id, { plays });
|
||||
await updateItem("episodes", match.id, { plays });
|
||||
|
||||
console.log(`✅ Updated episode: S${season_number}E${episode_number}`);
|
||||
} else {
|
||||
console.warn('⚠️ Skipped update.');
|
||||
console.warn("⚠️ Skipped update.");
|
||||
}
|
||||
} else {
|
||||
await createItem('episodes', {
|
||||
await createItem("episodes", {
|
||||
season_number: Number(season_number),
|
||||
episode_number: Number(episode_number),
|
||||
plays: Number(plays),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { loadConfig } from '../config.js';
|
||||
import { initDirectusClient, searchItems, createItem } from '../directus/client.js';
|
||||
import { removeUrlProtocol } from '../sanitize.js';
|
||||
import inquirer from "inquirer";
|
||||
import { loadConfig } from "../config.js";
|
||||
import { initDirectusClient, searchItems, createItem } from "../directus/client.js";
|
||||
import { removeUrlProtocol } from "../sanitize.js";
|
||||
|
||||
export const addLinkToShare = async () => {
|
||||
const config = await loadConfig();
|
||||
|
@ -10,34 +10,34 @@ export const addLinkToShare = async () => {
|
|||
|
||||
const { title, link, description, authorQuery } = await inquirer.prompt([
|
||||
{
|
||||
name: 'title',
|
||||
message: '📝 Title for the link:',
|
||||
validate: (input) => !!input || 'Title is required'
|
||||
name: "title",
|
||||
message: "📝 Title for the link:",
|
||||
validate: (input) => !!input || "Title is required"
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
message: '🔗 URL to share:',
|
||||
validate: (input) => input.startsWith('http') || 'Must be a valid URL'
|
||||
name: "link",
|
||||
message: "🔗 URL to share:",
|
||||
validate: (input) => input.startsWith("http") || "Must be a valid URL"
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
message: '🗒 Description (optional):',
|
||||
default: ''
|
||||
name: "description",
|
||||
message: "🗒 Description (optional):",
|
||||
default: ""
|
||||
},
|
||||
{
|
||||
name: 'authorQuery',
|
||||
message: '👤 Search for an author:'
|
||||
name: "authorQuery",
|
||||
message: "👤 Search for an author:"
|
||||
}
|
||||
]);
|
||||
|
||||
const authorMatches = await searchItems('authors', authorQuery);
|
||||
const authorMatches = await searchItems("authors", authorQuery);
|
||||
let author;
|
||||
|
||||
if (!authorMatches.length) {
|
||||
const { shouldCreate } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'shouldCreate',
|
||||
message: '❌ No authors found. Do you want to create a new one?',
|
||||
type: "confirm",
|
||||
name: "shouldCreate",
|
||||
message: "❌ No authors found. Do you want to create a new one?",
|
||||
default: true
|
||||
});
|
||||
|
||||
|
@ -45,19 +45,19 @@ export const addLinkToShare = async () => {
|
|||
|
||||
const { name, url, mastodon, rss, json, newsletter, blogroll } = await inquirer.prompt([
|
||||
{
|
||||
name: 'name',
|
||||
message: '👤 Author name:',
|
||||
validate: (input) => !!input || 'Name is required'
|
||||
name: "name",
|
||||
message: "👤 Author name:",
|
||||
validate: (input) => !!input || "Name is required"
|
||||
},
|
||||
{ name: 'url', message: '🔗 URL (optional):', default: '' },
|
||||
{ name: 'mastodon', message: '🐘 Mastodon handle (optional):', default: '' },
|
||||
{ name: 'rss', message: '📡 RSS feed (optional):', default: '' },
|
||||
{ name: 'json', message: '🧾 JSON feed (optional):', default: '' },
|
||||
{ name: 'newsletter', message: '📰 Newsletter URL (optional):', default: '' },
|
||||
{ type: 'confirm', name: 'blogroll', message: '📌 Add to blogroll?', default: false }
|
||||
{ name: "url", message: "🔗 URL (optional):", default: "" },
|
||||
{ name: "mastodon", message: "🐘 Mastodon handle (optional):", default: "" },
|
||||
{ name: "rss", message: "📡 RSS feed (optional):", default: "" },
|
||||
{ name: "json", message: "🧾 JSON feed (optional):", default: "" },
|
||||
{ name: "newsletter", message: "📰 Newsletter URL (optional):", default: "" },
|
||||
{ type: "confirm", name: "blogroll", message: "📌 Add to blogroll?", default: false }
|
||||
]);
|
||||
|
||||
const created = await createItem('authors', {
|
||||
const created = await createItem("authors", {
|
||||
name,
|
||||
url,
|
||||
mastodon,
|
||||
|
@ -70,9 +70,9 @@ export const addLinkToShare = async () => {
|
|||
author = created.data?.id || created.id;
|
||||
} else {
|
||||
const response = await inquirer.prompt({
|
||||
type: 'list',
|
||||
name: 'author',
|
||||
message: 'Select an author:',
|
||||
type: "list",
|
||||
name: "author",
|
||||
message: "Select an author:",
|
||||
choices: authorMatches.map((a) => {
|
||||
const cleanUrl = removeUrlProtocol(a.url);
|
||||
const display = cleanUrl ? `${a.name} (${cleanUrl})` : a.name;
|
||||
|
@ -91,41 +91,64 @@ export const addLinkToShare = async () => {
|
|||
|
||||
while (true) {
|
||||
const { query } = await inquirer.prompt({
|
||||
name: 'query',
|
||||
message: '🏷 Search for tags (or leave blank to finish):'
|
||||
name: "query",
|
||||
message: "🏷 Search for tags (or leave blank to finish):"
|
||||
});
|
||||
|
||||
const trimmedQuery = query.trim();
|
||||
if (!trimmedQuery) break;
|
||||
|
||||
const tags = await searchItems('tags', trimmedQuery);
|
||||
const tags = await searchItems("tags", trimmedQuery);
|
||||
|
||||
if (!tags.length) {
|
||||
console.warn(`⚠️ No tags found matching "${trimmedQuery}"`);
|
||||
|
||||
const { shouldCreateTag } = await inquirer.prompt({
|
||||
type: "confirm",
|
||||
name: "shouldCreateTag",
|
||||
message: `Do you want to create a new tag named "${trimmedQuery}"?`,
|
||||
default: true
|
||||
});
|
||||
|
||||
if (shouldCreateTag) {
|
||||
const createdTag = await createItem("tags", { name: trimmedQuery });
|
||||
const newTagId = createdTag.data?.id || createdTag.id;
|
||||
|
||||
tagIds.push(newTagId);
|
||||
}
|
||||
|
||||
const { again } = await inquirer.prompt({
|
||||
type: "confirm",
|
||||
name: "again",
|
||||
message: "Search and select more tags?",
|
||||
default: false
|
||||
});
|
||||
|
||||
if (!again) break;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const { selected } = await inquirer.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'selected',
|
||||
message: '✔ Select tags to add:',
|
||||
type: "checkbox",
|
||||
name: "selected",
|
||||
message: "✔ Select tags to add:",
|
||||
choices: tags.map((tag) => ({ name: tag.name, value: tag.id }))
|
||||
});
|
||||
|
||||
tagIds.push(...selected);
|
||||
|
||||
const { again } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'again',
|
||||
message: 'Search and select more tags?',
|
||||
type: "confirm",
|
||||
name: "again",
|
||||
message: "Search and select more tags?",
|
||||
default: false
|
||||
});
|
||||
|
||||
if (!again) break;
|
||||
}
|
||||
|
||||
await createItem('links', {
|
||||
await createItem("links", {
|
||||
title,
|
||||
link,
|
||||
description,
|
||||
|
@ -134,5 +157,5 @@ export const addLinkToShare = async () => {
|
|||
date: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log('✅ Link created successfully.');
|
||||
console.log("✅ Link created successfully.");
|
||||
};
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { loadConfig } from '../config.js';
|
||||
import { initDirectusClient, createItem, searchItems } from '../directus/client.js';
|
||||
import { promptForMultipleRelations } from '../directus/relationHelpers.js';
|
||||
import inquirer from "inquirer";
|
||||
import { loadConfig } from "../config.js";
|
||||
import { initDirectusClient, createItem, searchItems } from "../directus/client.js";
|
||||
import { promptForMultipleRelations } from "../directus/relationHelpers.js";
|
||||
|
||||
const ASSOCIATED_MEDIA_TYPES = ['artists', 'books', 'movies', 'shows', 'genres'];
|
||||
const ASSOCIATED_MEDIA_TYPES = ["artists", "books", "movies", "shows", "genres"];
|
||||
const BLOCK_COLLECTIONS = [
|
||||
'youtube_player',
|
||||
'github_banner',
|
||||
'npm_banner',
|
||||
'rss_banner',
|
||||
'calendar_banner',
|
||||
'forgejo_banner'
|
||||
"youtube_player",
|
||||
"github_banner",
|
||||
"npm_banner",
|
||||
"rss_banner",
|
||||
"calendar_banner",
|
||||
"forgejo_banner"
|
||||
];
|
||||
|
||||
export const addPost = async () => {
|
||||
|
@ -20,24 +20,24 @@ export const addPost = async () => {
|
|||
|
||||
const { title, description, content, featured } = await inquirer.prompt([
|
||||
{
|
||||
name: 'title',
|
||||
message: '📝 Title:',
|
||||
validate: (input) => !!input || 'Title is required'
|
||||
name: "title",
|
||||
message: "📝 Title:",
|
||||
validate: (input) => !!input || "Title is required"
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
message: '🗒 Description:',
|
||||
default: ''
|
||||
name: "description",
|
||||
message: "🗒 Description:",
|
||||
default: ""
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
message: '📄 Content:',
|
||||
default: ''
|
||||
name: "content",
|
||||
message: "📄 Content:",
|
||||
default: ""
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'featured',
|
||||
message: '⭐ Featured?',
|
||||
type: "confirm",
|
||||
name: "featured",
|
||||
message: "⭐ Featured?",
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
@ -46,14 +46,14 @@ export const addPost = async () => {
|
|||
|
||||
while (true) {
|
||||
const { query } = await inquirer.prompt({
|
||||
name: 'query',
|
||||
message: '🏷 Search for tags (or leave blank to finish):'
|
||||
name: "query",
|
||||
message: "🏷 Search for tags (or leave blank to finish):"
|
||||
});
|
||||
const trimmedQuery = query.trim();
|
||||
|
||||
if (!trimmedQuery) break;
|
||||
|
||||
const tags = await searchItems('tags', trimmedQuery);
|
||||
const tags = await searchItems("tags", trimmedQuery);
|
||||
|
||||
if (!tags.length) {
|
||||
console.warn(`⚠️ No tags found matching "${trimmedQuery}"`);
|
||||
|
@ -62,18 +62,18 @@ export const addPost = async () => {
|
|||
}
|
||||
|
||||
const { selected } = await inquirer.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'selected',
|
||||
message: '✔ Select tags to add:',
|
||||
type: "checkbox",
|
||||
name: "selected",
|
||||
message: "✔ Select tags to add:",
|
||||
choices: tags.map((tag) => ({ name: tag.name, value: tag.id }))
|
||||
});
|
||||
|
||||
tagIds.push(...selected);
|
||||
|
||||
const { again } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'again',
|
||||
message: 'Search and select more tags?',
|
||||
type: "confirm",
|
||||
name: "again",
|
||||
message: "Search and select more tags?",
|
||||
default: false
|
||||
});
|
||||
|
||||
|
@ -82,25 +82,25 @@ export const addPost = async () => {
|
|||
|
||||
const selectedBlocks = [];
|
||||
const { includeBlocks } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'includeBlocks',
|
||||
message: '➕ Add blocks?',
|
||||
type: "confirm",
|
||||
name: "includeBlocks",
|
||||
message: "➕ Add blocks?",
|
||||
default: false
|
||||
});
|
||||
|
||||
if (includeBlocks) {
|
||||
while (true) {
|
||||
const { collection } = await inquirer.prompt({
|
||||
type: 'list',
|
||||
name: 'collection',
|
||||
message: '🧱 Choose a block collection (or Cancel to finish):',
|
||||
choices: [...BLOCK_COLLECTIONS, new inquirer.Separator(), 'Cancel']
|
||||
type: "list",
|
||||
name: "collection",
|
||||
message: "🧱 Choose a block collection (or Cancel to finish):",
|
||||
choices: [...BLOCK_COLLECTIONS, new inquirer.Separator(), "Cancel"]
|
||||
});
|
||||
|
||||
if (collection === 'Cancel') break;
|
||||
if (collection === "Cancel") break;
|
||||
|
||||
const { query } = await inquirer.prompt({
|
||||
name: 'query',
|
||||
name: "query",
|
||||
message: `🔍 Search ${collection}:`
|
||||
});
|
||||
const results = await searchItems(collection, query);
|
||||
|
@ -112,8 +112,8 @@ export const addPost = async () => {
|
|||
}
|
||||
|
||||
const { itemId } = await inquirer.prompt({
|
||||
type: 'list',
|
||||
name: 'itemId',
|
||||
type: "list",
|
||||
name: "itemId",
|
||||
message: `Select an item from ${collection}:`,
|
||||
choices: results.map((item) => ({
|
||||
name: item.title || item.name || item.id,
|
||||
|
@ -124,9 +124,9 @@ export const addPost = async () => {
|
|||
selectedBlocks.push({ collection, item: itemId });
|
||||
|
||||
const { again } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'again',
|
||||
message: '➕ Add another block?',
|
||||
type: "confirm",
|
||||
name: "again",
|
||||
message: "➕ Add another block?",
|
||||
default: false
|
||||
});
|
||||
|
||||
|
@ -136,16 +136,16 @@ export const addPost = async () => {
|
|||
|
||||
const associatedMediaPayload = {};
|
||||
const { includeMedia } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'includeMedia',
|
||||
message: '➕ Add associated media?',
|
||||
type: "confirm",
|
||||
name: "includeMedia",
|
||||
message: "➕ Add associated media?",
|
||||
default: false
|
||||
});
|
||||
|
||||
if (includeMedia) {
|
||||
for (const mediaType of ASSOCIATED_MEDIA_TYPES) {
|
||||
const { query } = await inquirer.prompt({
|
||||
name: 'query',
|
||||
name: "query",
|
||||
message: `🔎 Search for ${mediaType} to associate (or leave blank to skip):`
|
||||
});
|
||||
|
||||
|
@ -160,8 +160,8 @@ export const addPost = async () => {
|
|||
}
|
||||
|
||||
const { selected } = await inquirer.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'selected',
|
||||
type: "checkbox",
|
||||
name: "selected",
|
||||
message: `✔ Select ${mediaType} to associate:`,
|
||||
choices: matches.map((m) => ({
|
||||
name: m.name_string || m.title || m.name || m.label || m.id,
|
||||
|
@ -176,7 +176,7 @@ export const addPost = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const media = await promptForMultipleRelations('media', 'Associated media');
|
||||
const media = await promptForMultipleRelations("media", "Associated media");
|
||||
const payload = {
|
||||
title,
|
||||
description,
|
||||
|
@ -188,7 +188,7 @@ export const addPost = async () => {
|
|||
...associatedMediaPayload
|
||||
};
|
||||
|
||||
await createItem('posts', payload);
|
||||
await createItem("posts", payload);
|
||||
|
||||
console.log('✅ Post created successfully.');
|
||||
console.log("✅ Post created successfully.");
|
||||
};
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { addPost } from './addPost.js';
|
||||
import { addLinkToShare } from './addLinkToShare.js';
|
||||
import { addEpisodeToShow } from './addEpisodeToShow.js';
|
||||
import { updateReadingProgress } from './updateReadingProgress.js';
|
||||
import { addBlockedRobot } from './addBlockedRobot.js';
|
||||
import inquirer from "inquirer";
|
||||
import { addPost } from "./addPost.js";
|
||||
import { addLinkToShare } from "./addLinkToShare.js";
|
||||
import { addEpisodeToShow } from "./addEpisodeToShow.js";
|
||||
import { updateReadingProgress } from "./updateReadingProgress.js";
|
||||
import { addBlockedRobot } from "./addBlockedRobot.js";
|
||||
|
||||
const TASKS = [
|
||||
{ name: '📄 Add post', handler: addPost },
|
||||
{ name: '🔗 Add link to share', handler: addLinkToShare },
|
||||
{ name: '➕ Add episode to show', handler: addEpisodeToShow },
|
||||
{ name: '📚 Update reading progress', handler: updateReadingProgress },
|
||||
{ name: '🤖 Block robot', handler: addBlockedRobot }
|
||||
{ name: "📄 Add post", handler: addPost },
|
||||
{ name: "🔗 Add link to share", handler: addLinkToShare },
|
||||
{ name: "➕ Add episode to show", handler: addEpisodeToShow },
|
||||
{ name: "📚 Update reading progress", handler: updateReadingProgress },
|
||||
{ name: "🤖 Block robot", handler: addBlockedRobot }
|
||||
];
|
||||
|
||||
export const runTasksMenu = async () => {
|
||||
const { task } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'task',
|
||||
message: 'Select a task to perform:',
|
||||
type: "list",
|
||||
name: "task",
|
||||
message: "Select a task to perform:",
|
||||
choices: TASKS.map((t) => ({ name: t.name, value: t.handler }))
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import inquirer from 'inquirer';
|
||||
import { loadConfig } from '../config.js';
|
||||
import { initDirectusClient, searchItems, updateItem } from '../directus/client.js';
|
||||
import inquirer from "inquirer";
|
||||
import { loadConfig } from "../config.js";
|
||||
import { initDirectusClient, searchItems, updateItem } from "../directus/client.js";
|
||||
|
||||
export const updateReadingProgress = async () => {
|
||||
const config = await loadConfig();
|
||||
|
||||
initDirectusClient(config);
|
||||
|
||||
const readingBooks = await searchItems('books', '', { read_status: 'started' });
|
||||
const readingBooks = await searchItems("books", "", { read_status: "started" });
|
||||
|
||||
if (!readingBooks.length) {
|
||||
console.log('📖 No books currently marked as "started".');
|
||||
|
@ -16,9 +16,9 @@ export const updateReadingProgress = async () => {
|
|||
}
|
||||
|
||||
const { bookId } = await inquirer.prompt({
|
||||
type: 'list',
|
||||
name: 'bookId',
|
||||
message: '📚 Select a book to update progress:',
|
||||
type: "list",
|
||||
name: "bookId",
|
||||
message: "📚 Select a book to update progress:",
|
||||
choices: readingBooks.map((book) => {
|
||||
const title = book.title || book.name || `Book #${book.id}`;
|
||||
const progress = book.progress ?? 0;
|
||||
|
@ -30,16 +30,16 @@ export const updateReadingProgress = async () => {
|
|||
})
|
||||
});
|
||||
const { progress } = await inquirer.prompt({
|
||||
name: 'progress',
|
||||
message: '📕 New progress percentage (0–100):',
|
||||
name: "progress",
|
||||
message: "📕 New progress percentage (0–100):",
|
||||
validate: (input) => {
|
||||
const num = Number(input);
|
||||
|
||||
return (!isNaN(num) && num >= 0 && num <= 100) || 'Enter a number from 0 to 100';
|
||||
return (!isNaN(num) && num >= 0 && num <= 100) || "Enter a number from 0 to 100";
|
||||
}
|
||||
});
|
||||
|
||||
await updateItem('books', bookId, { progress: Number(progress) });
|
||||
await updateItem("books", bookId, { progress: Number(progress) });
|
||||
|
||||
console.log(`✅ Updated book progress to ${progress}%`);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue