test-card/.dagger/src/index.ts

393 lines
12 KiB
TypeScript

/**
* A generated module for TestCard functions
*
* This module has been generated via dagger init and serves as a reference to
* basic module structure as you get started with Dagger.
*
* Two functions have been pre-created. You can modify, delete, or add to them,
* as needed. They demonstrate usage of arguments and return types using simple
* echo and grep commands. The functions can be called from the dagger CLI or
* from one of the SDKs.
*
* The first line in this comment block is a short description line and the
* rest is a long description with more detail on the module's purpose or usage,
* if appropriate. All modules should have a short description.
*/
import {
dag,
Container,
Directory,
File,
Secret,
object,
func,
argument,
Platform
} from '@dagger.io/dagger';
@object()
export class TestCard {
// ============ Helpers ============
private auxMediaBase(): Container {
// Equivalent of Earthly target: aux-media
return dag
.container()
.from('debian:bookworm')
.withExec([
'bash',
'-lc',
'apt-get update && apt-get install -y ffmpeg sox && rm -rf /var/lib/apt/lists/*'
]);
}
private nodeWithBunBase(platform?: Platform): Container {
return dag.container({ platform }).from('node:lts').withExec(['npm', 'install', '-g', 'bun']);
}
// ============ site ============
/**
* Build the website (equivalent to Earthly target `site`).
* Returns the built `build/` directory as a Dagger Directory.
*/
@func()
async site(@argument({ defaultPath: '/' }) source: Directory): Promise<Directory> {
const assets = await this.assetsGenerated(source);
const ctr = this.nodeWithBunBase()
.withMountedDirectory('/src', source)
.withMountedDirectory('/assets-generated', assets)
.withWorkdir('/src')
.withExec(['bun', 'install', '--frozen-lockfile'])
.withExec([
'bash',
'-lc',
'mkdir -p /src/assets/generated && cp -a /assets-generated/. /src/assets/generated'
])
.withExec([
'sh',
'-lc',
'export VITE_BUILD_DATE=$(date -Iminutes -u | sed "s/+00:00//") && bun x svelte-kit sync && bun run build'
]);
return ctr.directory('/src/build');
}
// ============ deploy ============
/**
* Deploy the built site via rsync over SSH (equivalent to Earthly target `deploy`).
* Provide secrets corresponding to SSH config and target.
*/
@func()
async deploy(
sshConfig: Secret,
sshUploadKey: Secret,
sshKnownHosts: Secret,
sshTarget: Secret,
@argument({ defaultPath: '/build' }) build: Directory
): Promise<string> {
const ctr = dag
.container()
.from('alpine:latest')
.withExec(['sh', '-lc', 'apk add --no-cache openssh-client rsync'])
.withMountedDirectory('/build', build)
.withSecretVariable('SSH_CONFIG', sshConfig)
.withSecretVariable('SSH_UPLOAD_KEY', sshUploadKey)
.withSecretVariable('SSH_KNOWN_HOSTS', sshKnownHosts)
.withSecretVariable('SSH_TARGET', sshTarget)
.withExec([
'sh',
'-lc',
'mkdir -p "$HOME/.ssh" && echo "$SSH_CONFIG" > $HOME/.ssh/config && echo "$SSH_UPLOAD_KEY" > $HOME/.ssh/id_rsa && echo "$SSH_KNOWN_HOSTS" > $HOME/.ssh/known_hosts && chmod 600 $HOME/.ssh/*'
])
.withExec(['sh', '-lc', 'rsync -cvrz --delete /build/ "$SSH_TARGET"']);
return ctr.stdout();
}
// ============ avsync-video-components ============
/**
* Render AV sync frames with Bun. Returns the frames directory.
* Mirrors the `avsync-video-components` video part.
*/
@func()
async avsyncFrames(
fps: number = 60,
size: number = 1200,
@argument({ defaultPath: '/' }) source: Directory
): Promise<Directory> {
const ctr = this.nodeWithBunBase()
// pptr troubleshooting libs
.withExec([
'bash',
'-lc',
'apt-get update && apt-get -y install chromium libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 && rm -rf /var/lib/apt/lists/*'
])
.withExec([
'bash',
'-lc',
'groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser && mkdir -p /home/pptruser && chown -R pptruser:pptruser /home/pptruser'
])
.withUser('pptruser')
.withDirectory('/site', source, { owner: 'pptruser', include: ['package.json', 'bun.lock'] })
.withWorkdir('/site')
.withExec(['bun', 'install', '--frozen-lockfile', '--verbose'])
.withEnvVariable('PUPPETEER_EXECUTABLE_PATH', '/usr/bin/chromium')
.withEnvVariable('PUPPETEER_SKIP_DOWNLOAD', '1')
.withDirectory('/site', source, { owner: 'pptruser' })
.withExec([
'bun',
'av:render:video',
'--fps',
String(fps),
'--cycles',
'1',
'--size',
String(size),
'--output',
'/var/tmp/frames'
]);
return ctr.directory('/var/tmp/frames');
}
/**
* Render AV sync audio track with Bun. Returns the WAV track file.
* Mirrors the `avsync-video-components` audio part.
*/
@func()
async avsyncTrack(
cycles: number = 16,
@argument({ defaultPath: '/' }) source: Directory
): Promise<File> {
const ctr = this.nodeWithBunBase()
.withDirectory('/site', source)
.withWorkdir('/site')
.withExec(['bun', 'install', '--frozen-lockfile'])
.withExec([
'bun',
'av:render:audio',
'-i',
'beep.wav',
'-o',
'/var/tmp/track.wav',
'--repeats',
String(cycles)
]);
return ctr.file('/var/tmp/track.wav');
}
// ============ avsync-video ============
/**
* Compose frames and audio track into avsync.webm. Returns the resulting file.
* Mirrors the Earthly `avsync-video` target.
*/
@func()
async avsyncVideo(
fps: number = 60,
cycles: number = 16,
@argument({ defaultPath: '/' }) source: Directory
): Promise<File> {
const frames = await this.avsyncFrames(fps, 1200, source);
const track = await this.avsyncTrack(cycles, source);
const ctr = this.auxMediaBase()
.withMountedDirectory('/frames', frames)
.withMountedFile('/track.wav', track)
.withExec(['sh', '-lc', 'find /frames -type f | sort | sed "s#^#file #" > /frames.txt'])
.withEnvVariable('CYCLES', String(cycles))
.withExec([
'sh',
'-lc',
'for i in $(seq 1 $CYCLES); do cat /frames.txt >> /final-frames.txt; done'
])
.withEnvVariable('FPS', String(fps))
.withExec([
'sh',
'-lc',
'ffmpeg -r "$FPS" -f concat -safe 0 -i /final-frames.txt -i /track.wav -c:v libvpx-vp9 -pix_fmt yuva420p -shortest /avsync.webm'
]);
return ctr.file('/avsync.webm');
}
// ============ audio-channel-tracks (WAV) ============
/**
* Generate stereo/5.1/7.1 WAV channel tracks from assets/audio/channels input.
* Mirrors the Earthly `audio-channel-tracks` target.
*/
@func()
async audioChannelTracksWav(@argument({ defaultPath: '/' }) src: Directory): Promise<Directory> {
const ctr = this.auxMediaBase()
.withMountedDirectory('/raw', src.directory('/assets/audio/channels'))
.withExec([
'bash',
'-lc',
'mkdir -p /input /output && cd /raw && for file in *.wav; do sox "$file" "/input/$file" silence 1 0.1 0.1% reverse silence 1 0.1 0.1% reverse; done'
])
.withWorkdir('/input')
.withExec(['bash', '-lc', 'mkdir -p /output/wav/stereo /output/wav/5.1 /output/wav/7.1'])
// stereo
.withExec([
'bash',
'-lc',
'ffmpeg -i Left.wav -af "pan=stereo|FL=c0" /output/wav/stereo/Left.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Right.wav -af "pan=stereo|FR=c0" /output/wav/stereo/Right.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Center.wav -af "pan=stereo|FL=c0|FR=c0" /output/wav/stereo/Center.wav -hide_banner -loglevel error'
])
// 5.1
.withExec([
'bash',
'-lc',
'ffmpeg -i Front_Left.wav -af "pan=5.1|FL=c0" /output/wav/5.1/Front_Left.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Front_Right.wav -af "pan=5.1|FR=c0" /output/wav/5.1/Front_Right.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Front_Center.wav -af "pan=5.1|FC=c0" /output/wav/5.1/Front_Center.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Noise.wav -af "pan=5.1|LFE=c0" /output/wav/5.1/LFE_Noise.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Rear_Left.wav -af "pan=5.1|BL=c0" /output/wav/5.1/Rear_Left.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Rear_Right.wav -af "pan=5.1|BR=c0" /output/wav/5.1/Rear_Right.wav -hide_banner -loglevel error'
])
// 7.1
.withExec([
'bash',
'-lc',
'ffmpeg -i Front_Left.wav -af "pan=7.1|FL=c0" /output/wav/7.1/Front_Left.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Front_Right.wav -af "pan=7.1|FR=c0" /output/wav/7.1/Front_Right.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Front_Center.wav -af "pan=7.1|FC=c0" /output/wav/7.1/Front_Center.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Noise.wav -af "pan=7.1|LFE=c0" /output/wav/7.1/LFE_Noise.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Side_Left.wav -af "pan=7.1|SL=c0" /output/wav/7.1/Side_Left.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Side_Right.wav -af "pan=7.1|SR=c0" /output/wav/7.1/Side_Right.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Rear_Left.wav -af "pan=7.1|BL=c0" /output/wav/7.1/Rear_Left.wav -hide_banner -loglevel error'
])
.withExec([
'bash',
'-lc',
'ffmpeg -i Rear_Right.wav -af "pan=7.1|BR=c0" /output/wav/7.1/Rear_Right.wav -hide_banner -loglevel error'
]);
return ctr.directory('/output/wav');
}
// ============ audio-channel-tracks-ogg ============
@func()
async audioChannelTracksOgg(
@argument({ defaultPath: '/' }) source: Directory
): Promise<Directory> {
const wavDir = await this.audioChannelTracksWav(source);
const ctr = this.auxMediaBase()
.withMountedDirectory('/output/wav', wavDir)
.withExec(['bash', '-lc', 'mkdir -p /output/ogg/stereo /output/ogg/5.1 /output/ogg/7.1'])
.withExec([
'bash',
'-lc',
'for file in /output/wav/stereo/*.wav; do ffmpeg -i "$file" -c:a libvorbis "/output/ogg/stereo/$(basename "$file" .wav).ogg" -hide_banner -loglevel error; done'
])
.withExec([
'bash',
'-lc',
'for file in /output/wav/5.1/*.wav; do ffmpeg -i "$file" -c:a libvorbis "/output/ogg/5.1/$(basename "$file" .wav).ogg" -hide_banner -loglevel error; done'
])
.withExec([
'bash',
'-lc',
'for file in /output/wav/7.1/*.wav; do ffmpeg -i "$file" -c:a libvorbis "/output/ogg/7.1/$(basename "$file" .wav).ogg" -hide_banner -loglevel error; done'
]);
return ctr.directory('/output/ogg');
}
// ============ audio-channel-tracks-mp3 ============
@func()
async audioChannelTracksMp3(
@argument({ defaultPath: '/' }) source: Directory
): Promise<Directory> {
const wavDir = await this.audioChannelTracksWav(source);
const ctr = this.auxMediaBase()
.withMountedDirectory('/output/wav', wavDir)
.withExec(['bash', '-lc', 'mkdir -p /output/mp3/stereo /output/mp3/5.1 /output/mp3/7.1'])
.withExec([
'bash',
'-lc',
'for file in /output/wav/stereo/*.wav; do ffmpeg -i "$file" -c:a libmp3lame "/output/mp3/stereo/$(basename "$file" .wav).mp3" -hide_banner -loglevel error; done'
])
.withExec([
'bash',
'-lc',
'for file in /output/wav/5.1/*.wav; do ffmpeg -i "$file" -c:a libmp3lame "/output/mp3/5.1/$(basename "$file" .wav).mp3" -hide_banner -loglevel error; done'
])
.withExec([
'bash',
'-lc',
'for file in /output/wav/7.1/*.wav; do ffmpeg -i "$file" -c:a libmp3lame "/output/mp3/7.1/$(basename "$file" .wav).mp3" -hide_banner -loglevel error; done'
]);
return ctr.directory('/output/mp3');
}
// ============ assets-generated ============
/**
* Aggregate generated assets into a directory containing:
* - avsync.webm
* - audio/ (MP3 variants)
*/
@func()
async assetsGenerated(@argument({ defaultPath: '/' }) source: Directory): Promise<Directory> {
const avsync = await this.avsyncVideo(60, 16, source);
const audioMp3 = await this.audioChannelTracksMp3(source);
return dag.directory().withFile('avsync.webm', avsync).withDirectory('audio', audioMp3);
}
}