Skip to content

Commit 01a1cac

Browse files
authored
Create new templates! (#13)
* Use main node image & upgrade fabric * prettier * create new templates * fix merge conflict
1 parent c36272b commit 01a1cac

21 files changed

Lines changed: 383 additions & 75 deletions

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3+
}

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"editor.defaultFormatter": "esbenp.prettier-vscode",
3+
"editor.formatOnSave": true,
4+
"editor.codeActionsOnSave": {
5+
"source.fixAll.eslint": "explicit"
6+
}
7+
}

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Bumper content are clips which can be used inbetween the main content of your TV
44

55
A file is output for each channel on your service, and which is replaced each time a new show starts (with an updated bumper based on what will be playing next). This content can then be imported in your chosen IPTV service.
66

7+
---
8+
9+
<h2 align=center>🛠️ Project under active development 🛠️</h2>
10+
<p align=center>There is no official release yet (though dev releases are available on <code>ollyscoding/bumpgen-test:&lt;commit-sha&gt;</code>), and breaking changes will be actively made until v0.1 is released</p>
11+
12+
---
13+
714
## Getting Started
815

916
You can run bumpgen through docker compose, create a docker compose file from the [the template](./docs/compose.yml).
@@ -13,3 +20,36 @@ You'll need to update the `/path/to/your/output` and `/path/to/your/background-c
1320
If you haven't already, create the config directory, then create a file called `bumpgen.config.json` within it, and copy across the contents of [bumpgen.config.example.json](./configs/bumpgen.config.example.json). Then add the URL for your XML TV file, and change any other settings as desired.
1421

1522
You should now be good to go, run `docker compose up -d` to get started!
23+
24+
### Templates
25+
26+
Bumpgen offers a number of default templates to use for your channels, as well as exerimental plugin support if you want to develop your own templates.
27+
28+
<table>
29+
<tr>
30+
<th>Name</th>
31+
<th>Example</th>
32+
</tr>
33+
<tr>
34+
<td><code>centre-title-and-time</code></td>
35+
<td><img src="./docs/screenshots/template_centre-title-and-time.png" width=500px></td>
36+
</tr>
37+
<tr>
38+
<td><code>left-panel-info</code></td>
39+
<td><img src="./docs/screenshots/template_left-panel-info.png" width=500px></td>
40+
</td>
41+
<tr>
42+
<td><code>left-panel-next-five</code></td>
43+
<td><img src="./docs/screenshots/template_left-panel-next-five.png" width=500px></td>
44+
</td>
45+
</tr>
46+
</table>
47+
48+
## What's still needed for MVP?
49+
50+
- Give templates all upcoming shows not just the next one & build 'left-panel-next-5' template
51+
- Options for resolution (per channel)
52+
- Option for length including \* as 'fill'
53+
- Plugins: some kind of versioning + publish types + example repo + language/locale in template
54+
- Frontend for configuring
55+
- Allow animations?

apps/backend/src/canvas2video/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type makeSceneFunction = (
3232
canvas: fabric.StaticCanvas,
3333
anim: gsap.core.Timeline,
3434
compose: () => void,
35-
) => void;
35+
) => Promise<void>;
3636

3737
export interface RendererConfig {
3838
width: number;

apps/backend/src/config/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ addFormats(ajv);
1212

1313
type ChannelId = string;
1414
export interface ChannelConfig {
15-
template: "centre-title-and-time";
15+
template: string;
1616
backgroundContent: "*" | string[];
1717
}
1818

@@ -54,7 +54,7 @@ const schema: JSONSchemaType<AppConfig> = {
5454
properties: {
5555
template: {
5656
type: "string",
57-
enum: ["centre-title-and-time"],
57+
// enum: ["centre-title-and-time"],
5858
},
5959
backgroundContent: {
6060
oneOf: [

apps/backend/src/jobs/main.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
fetchAndParseXmlTv,
3-
getNextProgrammeForChannel,
3+
getNextProgrammesForChannel,
44
getValueForConfiguredLang,
55
} from "../xmltv/index.js";
66
import {
@@ -12,7 +12,7 @@ import {
1212
type Result,
1313
} from "../result/index.js";
1414
import {
15-
createOverlayConfigFromProgramme,
15+
createProgrammeInfoFromProgrammes,
1616
makeVideo,
1717
} from "../video-generator/index.js";
1818
import { appConfig, type ChannelConfig } from "../config/app.js";
@@ -67,14 +67,17 @@ const channelTask = async (
6767
programmes: XmltvProgramme[],
6868
channelConfig: ChannelConfig,
6969
): ReturnType<typeof makeVideo> => {
70-
const currentProgramme = getNextProgrammeForChannel(channel, programmes);
71-
if (isFailure(currentProgramme)) {
72-
return failure("Failed to get next programme", currentProgramme.error);
70+
const nextProgrammes = getNextProgrammesForChannel(channel, programmes);
71+
if (isFailure(nextProgrammes)) {
72+
return failure("Failed to get next programme", nextProgrammes.error);
7373
}
7474

75-
const overlay = createOverlayConfigFromProgramme(currentProgramme.result);
76-
if (isFailure(overlay)) {
77-
return failure("Failed to get overlay config", overlay.error);
75+
const programmeInfo = createProgrammeInfoFromProgrammes(
76+
nextProgrammes.result,
77+
);
78+
79+
if (isFailure(programmeInfo)) {
80+
return failure("Failed to get overlay config", programmeInfo.error);
7881
}
7982

8083
const backgroundContentPath =
@@ -90,7 +93,7 @@ const channelTask = async (
9093

9194
return makeVideo({
9295
template: template.result,
93-
overlay: overlay.result,
96+
programmes: programmeInfo.result,
9497
outputDir: appConfig.outputFolder,
9598
outputFileName: `channel-${channel.id}.mp4`,
9699
length: 20,

apps/backend/src/video-generator/index.ts

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { XmltvProgramme } from "@iptv/xmltv";
2-
import type { FabricTemplate } from "bumpgen-shared/types";
2+
import type { FabricTemplate, ProgrammeInfo } from "bumpgen-shared/types";
3+
import { isNotUndefined } from "bumpgen-shared/utils";
4+
35
import {
46
failure,
57
isFailure,
@@ -22,15 +24,6 @@ import { logDebug, logError } from "../logger/index.js";
2224
import { existsSync } from "node:fs";
2325
import { Fonts } from "../fonts/index.js";
2426

25-
export interface VideoOverlay {
26-
title: string;
27-
subtitle?: string;
28-
episode?: string;
29-
description?: string;
30-
iconUrl?: string;
31-
start?: Date;
32-
}
33-
3427
export interface ChannelInfo {
3528
id: string;
3629
name?: string;
@@ -44,7 +37,7 @@ export interface VideoBackground {
4437

4538
export interface VideoOptions {
4639
channelInfo: ChannelInfo;
47-
overlay: VideoOverlay;
40+
programmes: [ProgrammeInfo, ...ProgrammeInfo[]];
4841
background?: VideoBackground;
4942
outputDir: string;
5043
outputFileName: string;
@@ -72,7 +65,7 @@ const getBackgroundVideoStartAndEnd = (
7265
};
7366

7467
const getProgrammeId = (options: VideoOptions) =>
75-
`${options.overlay.title}-${options.overlay.episode}`;
68+
`${options.programmes[0].title}-${options.programmes[0].episode}`;
7669

7770
const shouldGenerateVideo = async (
7871
options: VideoOptions,
@@ -134,8 +127,8 @@ export const makeVideo = async (
134127
width,
135128
height,
136129
fps: 1,
137-
makeScene: (fabric, canvas, anim, compose) => {
138-
options.template(options.overlay, {
130+
makeScene: async (fabric, canvas, anim, compose) => {
131+
await options.template(options.programmes, {
139132
getFontProperties: (...args) => Fonts.getFontProperties(...args),
140133
convertX: (val: number) => val * width,
141134
convertY: (val: number) => val * height,
@@ -183,25 +176,46 @@ export const makeVideo = async (
183176
}
184177
};
185178

186-
export const createOverlayConfigFromProgramme = (
187-
programme: XmltvProgramme,
188-
): Result<VideoOverlay> => {
189-
const title = getValueForConfiguredLang(programme.title);
190-
if (isFailure(title)) {
191-
return failure("Title required to create overlay");
179+
export const createProgrammeInfoFromProgrammes = (
180+
programmes: [XmltvProgramme, ...XmltvProgramme[]],
181+
): Result<[ProgrammeInfo, ...ProgrammeInfo[]]> => {
182+
const createProgrammeInfo = (
183+
programme: XmltvProgramme,
184+
): Result<ProgrammeInfo> => {
185+
const title = getValueForConfiguredLang(programme.title);
186+
if (isFailure(title)) {
187+
return failure("Title required to create overlay");
188+
}
189+
190+
const subtitle = getValueForConfiguredLang(programme.subTitle);
191+
const episode = getOnScreenEpisodeNumber(programme.episodeNum);
192+
const description = getValueForConfiguredLang(programme.desc);
193+
const iconUrl = getBestIcon(programme.icon);
194+
195+
return success({
196+
title: title.result,
197+
subtitle: unwrap(subtitle),
198+
episode: unwrap(episode),
199+
description: unwrap(description),
200+
start: programme.start,
201+
end: programme.stop,
202+
iconUrl: unwrap(iconUrl),
203+
});
204+
};
205+
206+
const firstItemResult = createProgrammeInfo(programmes[0]);
207+
208+
if (isFailure(firstItemResult)) {
209+
// Pass on the failure
210+
return firstItemResult;
192211
}
193212

194-
const subtitle = getValueForConfiguredLang(programme.subTitle);
195-
const episode = getOnScreenEpisodeNumber(programme.episodeNum);
196-
const description = getValueForConfiguredLang(programme.desc);
197-
const iconUrl = getBestIcon(programme.icon);
198-
199-
return success({
200-
title: title.result,
201-
subtitle: unwrap(subtitle),
202-
episode: unwrap(episode),
203-
description: unwrap(description),
204-
start: programme.start,
205-
iconUrl: unwrap(iconUrl),
206-
});
213+
return success([
214+
firstItemResult.result,
215+
...programmes
216+
.slice(1)
217+
.map(createProgrammeInfo)
218+
.map(unwrap)
219+
.filter(isNotUndefined),
220+
]);
207221
};

apps/backend/src/xmltv/index.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export const fetchAndParseXmlTv = async (
2121
try {
2222
const response = await fetch(path);
2323
const body = await response.text();
24-
const xmlTv = parseXmltv(body);
24+
// Fix a weird bug where the apostrophe specifically isn't decoded
25+
const bodyFixed = body.replaceAll("&#39;", "'");
26+
const xmlTv = parseXmltv(bodyFixed);
2527

2628
const { channels, programmes } = xmlTv;
2729
if (!channels) {
@@ -40,29 +42,32 @@ export const fetchAndParseXmlTv = async (
4042
}
4143
};
4244

43-
export const getNextProgrammeForChannel = (
45+
export const getNextProgrammesForChannel = (
4446
channel: XmltvChannel,
4547
programmes: XmltvProgramme[],
46-
): Result<XmltvProgramme> => {
48+
): Result<[XmltvProgramme, ...XmltvProgramme[]]> => {
4749
const forChannel = programmes.filter((p) => p.channel === channel.id);
4850
const sorted = [...forChannel].sort((a, b) => {
4951
return a.start.getTime() - b.start.getTime();
5052
});
5153

52-
const current = sorted.find((programme) => {
54+
const nextUpIndex = sorted.findIndex((programme) => {
5355
if (Date.now() < programme.start.getTime()) return true;
5456
else return false;
5557
});
5658

57-
if (current) {
58-
return success(current);
59-
} else {
59+
if (nextUpIndex === -1) {
6060
logDebug("Failed to find current program for channel " + channel.id);
6161
return failure(
6262
"Failed to find current programme playing on channel with id " +
6363
channel.id,
6464
);
6565
}
66+
67+
return success([
68+
programmes[nextUpIndex]!,
69+
...programmes.slice(nextUpIndex + 1),
70+
]);
6671
};
6772

6873
export const getOnScreenEpisodeNumber = (
8.58 MB
Loading
5.53 MB
Loading

0 commit comments

Comments
 (0)