mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
55d1aca9b6
* refactor: cleanup routes * style: smaller base font * chore: upgrade dependencies * chore: lock node version to electron * refactor: pass HTTP to integration controller (#652) * refactor: deprecate onair control * refactor: remove playback router * Several project files user folder (#617) * chore: automated screenshots (#667) * feat: app settings (#658) * refactor: remove deprecated event data (#674) * Studio clock (#663) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Feat: reorder events with alt+ctrl + arrow up/down (#645) * Warning and danger per event (#677) --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * refactor: stabilise actionHandler (#683) Co-authored-by: Fabian Posenau <fabian@fphome.de> * improvement: hide seconds (#675) * wip: overview (#688) * fix: focus cursor (#695) * refactor: update lower third (#665) * Refactor/time formatting (#696) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * feat: multiple selection (#703) --------- Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Alex <ac@omnivox.dk> * fix: test - go to `Edit mode` befor tying to click `Event options` button (#708) * refactor: runtime service (#715) * fix: issue with loosing cursor position on message (#719) * remove info panel (#721) * Event editor continue (#722) * update API - part (#709) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * refactor: update timers (#729) * feat: many timers (#706) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * refactor: excel cleanup (#734) * refactor: allow import of blocks and skip import (#735) * Project manager (#697) * refactor: UI for linking events (#763) * upgraded pipeline actions (#777) * Over under (#771) * custom fields (#744) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Sheets settings (#774) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * style: tweaks to lower thirds (#785) * refactor: delays account for gaps (#784) * refactor: partial state updates (#780) * feat: generate crash report (#787) * Sheet use limited input device auth flow (#782) --------- Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Custom fields views (#789) * refactor: deprecate presenter and subtitle (#795) * refactor: organise API around resources (#798) --------- Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> * Time to end (#804) * Skip fixes (#805) * fix: onair derives from playback * Param nav (#822) --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> * refactor: download files from interface (#831) * Quick options (#814) * End pause (#832) * chore: bump node version in docker (#834) * refactor: follow in run mode (#840) * fix: uncaught error in http integration (#837) * Apply project (#843) Co-authored-by: Matteo Gheza <matteo.gheza07@gmail.com> Co-authored-by: Ary <arylmoraesn@gmail.com> Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Alex Rohleder <alexrohleder96@gmail.com> Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> Co-authored-by: Fabian Posenau <fabianpos99+github@gmail.com>
267 lines
7.7 KiB
TypeScript
267 lines
7.7 KiB
TypeScript
import { DatabaseModel, GetInfo, ProjectData, ProjectFile, ProjectFileListResponse } from 'ontime-types';
|
|
|
|
import { copyFile, rename, stat, writeFile } from 'fs/promises';
|
|
import { existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
import { initRundown } from '../rundown-service/RundownService.js';
|
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
|
import { getNetworkInterfaces } from '../../utils/networkInterfaces.js';
|
|
import { resolveProjectsDirectory, resolveStylesPath } from '../../setup/index.js';
|
|
import { filterProjectFiles, parseProjectFile } from './projectFileUtils.js';
|
|
import { appStateService } from '../app-state-service/AppStateService.js';
|
|
import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
|
|
import { dbModel } from '../../models/dataModel.js';
|
|
import { deleteFile } from '../../utils/parserUtils.js';
|
|
import { switchDb } from '../../setup/loadDb.js';
|
|
|
|
// init dependencies
|
|
init();
|
|
|
|
/**
|
|
* Ensure services has its dependencies initialized
|
|
*/
|
|
function init() {
|
|
ensureDirectory(resolveProjectsDirectory);
|
|
}
|
|
|
|
type Options = {
|
|
onlyRundown?: 'true' | 'false';
|
|
};
|
|
|
|
/**
|
|
* Handles a file from the upload folder and applies its data
|
|
*/
|
|
export async function applyProjectFile(name: string, options?: Options) {
|
|
const filePath = join(resolveProjectsDirectory, name);
|
|
const data = parseProjectFile(filePath);
|
|
|
|
// change LowDB to point to new file
|
|
await switchDb(name);
|
|
|
|
// apply data model
|
|
await applyDataModel(data, options);
|
|
|
|
// persist the project selection
|
|
await appStateService.updateDatabaseConfig(name);
|
|
}
|
|
|
|
/**
|
|
* Copies a file from upload folder to the projects folder
|
|
* @param filePath
|
|
* @param name
|
|
* @returns
|
|
*/
|
|
export async function handleUploadedFile(filePath: string, name: string) {
|
|
const newFilePath = join(resolveProjectsDirectory, name);
|
|
await rename(filePath, newFilePath);
|
|
await deleteFile(filePath);
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Asynchronously retrieves and returns an array of project files from the 'uploads' folder.
|
|
* Each file in the 'uploads' folder is checked, and only those with a '.json' extension are processed.
|
|
* For each qualifying file, its metadata is retrieved, including filename, creation time, and last modification time.
|
|
*
|
|
* @returns {Promise<Array<ProjectFile>>} A promise that resolves to an array of ProjectFile objects,
|
|
* each representing a file in the 'uploads' folder with its metadata.
|
|
* The metadata includes the filename, creation or overwriting time (updatedAt)
|
|
*
|
|
* @throws {Error} Throws an error if there is an issue in reading the directory or fetching file statistics.
|
|
*/
|
|
export async function getProjectFiles(): Promise<ProjectFile[]> {
|
|
const allFiles = await getFilesFromFolder(resolveProjectsDirectory);
|
|
const filteredFiles = filterProjectFiles(allFiles);
|
|
|
|
const projectFiles: ProjectFile[] = [];
|
|
for (const file of filteredFiles) {
|
|
const filePath = join(resolveProjectsDirectory, file);
|
|
const stats = await stat(filePath);
|
|
|
|
projectFiles.push({
|
|
filename: removeFileExtension(file),
|
|
updatedAt: stats.mtime.toISOString(),
|
|
});
|
|
}
|
|
|
|
return projectFiles;
|
|
}
|
|
|
|
/**
|
|
* Gathers data related to the project list
|
|
*/
|
|
export async function getProjectList(): Promise<ProjectFileListResponse> {
|
|
const files = await getProjectFiles();
|
|
const appState = await appStateService.get();
|
|
const lastLoadedProject = removeFileExtension(appState.lastLoadedProject);
|
|
|
|
return {
|
|
files,
|
|
lastLoadedProject,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Duplicates an existing project file
|
|
*/
|
|
export async function duplicateProjectFile(existingProjectFile: string, newProjectFile: string) {
|
|
const projectFilePath = join(resolveProjectsDirectory, existingProjectFile);
|
|
const duplicateProjectFilePath = join(resolveProjectsDirectory, newProjectFile);
|
|
|
|
return copyFile(projectFilePath, duplicateProjectFilePath);
|
|
}
|
|
|
|
/**
|
|
* Renames an existing project file
|
|
*/
|
|
export async function renameProjectFile(existingProjectFile: string, newName: string) {
|
|
const projectFilePath = join(resolveProjectsDirectory, existingProjectFile);
|
|
const newProjectFilePath = join(resolveProjectsDirectory, newName);
|
|
|
|
await rename(projectFilePath, newProjectFilePath);
|
|
|
|
// Update the last loaded project config if current loaded project is the one being renamed
|
|
const { lastLoadedProject } = await appStateService.get();
|
|
|
|
if (lastLoadedProject === existingProjectFile) {
|
|
await appStateService.updateDatabaseConfig(newName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new project file and applies its result
|
|
*/
|
|
export async function createProjectFile(filename: string, projectData: ProjectData) {
|
|
const data = {
|
|
...dbModel,
|
|
project: {
|
|
...dbModel.project,
|
|
...projectData,
|
|
},
|
|
};
|
|
|
|
// create new file
|
|
const newFile = join(resolveProjectsDirectory, filename);
|
|
await writeFile(newFile, JSON.stringify(data));
|
|
|
|
// change LowDB to point to new file
|
|
await switchDb(filename);
|
|
|
|
// apply its data
|
|
await applyDataModel(data);
|
|
|
|
appStateService.updateDatabaseConfig(filename);
|
|
}
|
|
|
|
/**
|
|
* Deletes a project file
|
|
*/
|
|
export async function deleteProjectFile(filename: string) {
|
|
const projectFilePath = join(resolveProjectsDirectory, filename);
|
|
await deleteFile(projectFilePath);
|
|
}
|
|
|
|
/**
|
|
* Adds business logic to gathering data for the info endpoint
|
|
*/
|
|
export async function getInfo(): Promise<GetInfo> {
|
|
const { version, serverPort } = DataProvider.getSettings();
|
|
const osc = DataProvider.getOsc();
|
|
|
|
// get nif and inject localhost
|
|
const ni = getNetworkInterfaces();
|
|
ni.unshift({ name: 'localhost', address: '127.0.0.1' });
|
|
const cssOverride = resolveStylesPath;
|
|
|
|
return {
|
|
networkInterfaces: ni,
|
|
version,
|
|
serverPort,
|
|
osc,
|
|
cssOverride,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Business logic for resolving a string
|
|
*/
|
|
export function extractPin(value: string | undefined | null, fallback: string | null): string | null {
|
|
if (value === null) {
|
|
return value;
|
|
}
|
|
if (typeof value === 'undefined') {
|
|
return fallback;
|
|
}
|
|
if (value.length === 0) {
|
|
return null;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* applies a partial database model
|
|
*/
|
|
export async function applyDataModel(data: Partial<DatabaseModel>, _options?: Options) {
|
|
runtimeService.stop();
|
|
|
|
// TODO: allow partial project merge from options
|
|
const { rundown, customFields, ...rest } = data;
|
|
const newData = await DataProvider.mergeIntoData(rest);
|
|
|
|
if (rundown != null) {
|
|
initRundown(rundown, customFields ?? {});
|
|
}
|
|
|
|
return newData;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a project of a given name exists
|
|
* @param name
|
|
*/
|
|
export function doesProjectExist(name: string): boolean {
|
|
const projectFilePath = join(resolveProjectsDirectory, name);
|
|
return existsSync(projectFilePath);
|
|
}
|
|
|
|
/**
|
|
* @description Validates the existence of project files.
|
|
* @param {object} projectFiles
|
|
* @param {string} projectFiles.projectFilename
|
|
* @param {string} projectFiles.newFilename
|
|
*
|
|
* @returns {Promise<Array<string>>} Array of errors
|
|
*
|
|
*/
|
|
export const validateProjectFiles = (projectFiles: { filename?: string; newFilename?: string }): Array<string> => {
|
|
const errors: string[] = [];
|
|
|
|
if (projectFiles.filename) {
|
|
const projectFilePath = join(resolveProjectsDirectory, projectFiles.filename);
|
|
|
|
if (!existsSync(projectFilePath)) {
|
|
errors.push('Project file does not exist');
|
|
}
|
|
}
|
|
|
|
if (projectFiles.newFilename) {
|
|
const projectFilePath = join(resolveProjectsDirectory, projectFiles.newFilename);
|
|
|
|
if (existsSync(projectFilePath)) {
|
|
errors.push('New project file already exists');
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
/**
|
|
* Get current project title or fallback
|
|
*/
|
|
export function getProjectTitle(): string {
|
|
const { title } = DataProvider.getProjectData();
|
|
return title || 'ontime data';
|
|
}
|