Initial Commit
Some checks are pending
Build Docker Image / docker (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Lillith Rose (Device: Lucia) 2025-06-08 14:33:19 -04:00
commit fdeba8fd41
17 changed files with 2290 additions and 0 deletions

6
.dockerignore Normal file
View file

@ -0,0 +1,6 @@
*
!src
!package.json
!pnpm-lock.yaml
!build.mjs
!tsconfig.json

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
dist/

40
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: Build Docker Image
on:
push:
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: ubuntu-latest
if: github.repository != 'lillithkt/template'
env:
REPO: ${{ github.repository.name }}
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: downcase REPO
shell: bash
run: |
echo "REPO=${GITHUB_REPOSITORY##*/}" >>${GITHUB_ENV}
- name: Build and push
uses: docker/build-push-action@v6.15.0
with:
push: true
file: Dockerfile
platforms: linux/amd64
tags: ghcr.io/lillithkt/${{ env.REPO }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max

25
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Test
on:
push:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Setup Node
uses: actions/setup-node@v4.2.0
with:
node-version: 22.13.1
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Test
run: pnpm test

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
dist/
output/

13
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"configurations": [
{
"name": "Launch Program",
"program": "${workspaceFolder}/dist/index.js",
"request": "launch",
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"],
"type": "node",
"preLaunchTask": "${defaultBuildTask}"
}
]
}

12
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"files.eol": "\n",
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

15
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "pnpm build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
}
]
}

26
Dockerfile Normal file
View file

@ -0,0 +1,26 @@
FROM node:22.13.1-alpine
ENV NODE_ENV=development
RUN npm install -g pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
RUN pnpm prune --prod
FROM node:22.13.1-alpine
RUN apk add ffmpeg
ENV NODE_ENV=production
WORKDIR /dist
COPY --from=0 /src/dist/* ./
CMD ["node", "."]

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# Lillith's Template
My template for server projects
## Contains:
- Typescript
- Esbuild Single-File output
- ESLint
- Two Stage Docker build for minimum download size
- GHCR Upload for the docker image
- c/i For linting and testing compiling

13
build.mjs Normal file
View file

@ -0,0 +1,13 @@
import * as esbuild from "esbuild";
esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/index.js",
platform: "node",
target: ["node21"],
sourcemap: true,
loader: {
".node": "copy",
},
});

34
eslint.config.mjs Normal file
View file

@ -0,0 +1,34 @@
import { defineConfig, globalIgnores } from "eslint/config";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import prettier from "eslint-plugin-prettier";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default defineConfig([globalIgnores(["**/dist/"]), {
extends: compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"),
plugins: {
"@typescript-eslint": typescriptEslint,
prettier,
},
languageOptions: {
globals: {
...globals.node,
},
parser: tsParser,
},
}]);

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "template",
"description": "Starter template for lillith's projects",
"homepage": "https://github.com/lillithkt/template",
"main": "dist/index.js",
"engines": {
"node": ">=21.5.0"
},
"packageManager": "pnpm@9.0.1",
"scripts": {
"build": "node build.mjs",
"start": "node dist/index.js",
"test": "tsc --noEmit && eslint .",
"lint": "eslint ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.22.0",
"@types/fluent-ffmpeg": "^2.1.27",
"@types/node": "^22.13.10",
"esbuild": "^0.25.1",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.3",
"fluent-ffmpeg": "^2.1.3",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"prettier-eslint": "^16.3.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1"
}
}

1947
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

54
src/index.ts Normal file
View file

@ -0,0 +1,54 @@
import ffmpeg from "fluent-ffmpeg";
import { Event, getCurrentEvent, loadEvents } from "setlist";
const commandBase = ffmpeg()
.input("rtsp://stream.furality.online/live/club")
.seekInput("0:01")
.audioCodec("copy")
.videoCodec("copy")
.outputOption("-y");
async function runCommand(event: Event) {
const eventEnd = new Date(event.end);
const now = new Date();
const secondsUntilEventEnd = Math.max(
0,
Math.floor((eventEnd.getTime() - now.getTime()) / 1000)
);
const command = commandBase.clone().duration(5);
const filename = event.name.replace(/ /g, "_");
console.log(`Saving ${secondsUntilEventEnd}s of 1440p to ${filename}.mkv`);
console.log(process.cwd());
command.save(`./output/${filename}.mkv`);
await new Promise<void>((res) => {
command.on("start", console.log);
command.on("error", (err) => {
throw err;
});
command.on("end", (cmd) => {
console.log(cmd);
res();
});
});
console.log("Command Done");
}
async function main() {
await loadEvents();
while (true) {
// find next
const event = getCurrentEvent();
if (!event?.name) {
console.error("No more sets!");
process.exit(0);
}
await runCommand(event);
// sleep 5s
await new Promise((res) => setTimeout(res, 5000));
}
}
main();

43
src/setlist.ts Normal file
View file

@ -0,0 +1,43 @@
export interface Event {
id: string;
name: string;
start: string;
end: string;
description?: string;
subtitle?: string;
type: string;
imageUrl: string;
questCompatible: boolean;
pcCompatible: boolean;
livestream: boolean;
status: string;
supporter: boolean;
rsvp: boolean;
vote: any;
}
export let events: Event[] | null = null;
export async function loadEvents() {
console.log("Loading Events");
const { data }: { data: Event[] } = await fetch(
"https://api.fynn.ai/v2/event"
).then((i) => i.json());
events = data.filter((i) => i.type === "dj");
console.log(`Loaded ${events.length} DJ events`);
return events;
}
export function getCurrentEvent(): Event | null {
if (!events) return null;
const now = new Date();
return (
events.find((event) => {
const start = new Date(event.start);
const end = new Date(event.end);
return now >= start && now <= end;
}) || null
);
}

11
tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"baseUrl": "./src",
"noEmit": true
}
}