Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ The format is detected by content, not file extension, so a renamed export is
still recognized. Invalid files are rejected with a message naming what was
wrong (not a zip, missing manifest, non-SQLite database, or missing tables).

### Build a working directory

Download every referenced video at the chosen resolution, pre-cut segment clips,
and write a self-contained per-playlist working directory:

```bash
vbs plt build meeting.playlist
```

This produces `<playlist-slug>/` containing ordered play-ready `clips/`, the
untouched source downloads in `media/`, extracted `thumbs/`, a `playlist.json`
cue sheet, and a Typst `cuesheet.typ` (compiled to `cuesheet.pdf` when `typst`
is installed). Downloads are cached and verified by checksum, so re-running is
fast and deterministic. `ffmpeg` and `ffprobe` are required.

The media API endpoint is read from the config key `plt.mediaapi` (never
committed); override it per run with `--media-api`. Other flags: `--out` (where
to create the working directory), `--resolution` (default `720p`), and `--lang`
(override the written-language code).

## installation for homebrew (MacOS/Linux)

brew install kindlyops/tap/vbs
Expand Down
11 changes: 11 additions & 0 deletions cmd/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ go_library(
"play_unix.go",
"play_windows.go",
"plt.go",
"plt_build.go",
"plt_clips.go",
"plt_cuesheet.go",
"plt_helpers.go",
"plt_media.go",
"plt_parse.go",
"root.go",
],
Expand Down Expand Up @@ -54,8 +58,15 @@ go_test(
srcs = [
"chapters_test.go",
"lighting_test.go",
"plt_build_integration_test.go",
"plt_cache_test.go",
"plt_client_test.go",
"plt_clips_integration_test.go",
"plt_clips_test.go",
"plt_cuesheet_test.go",
"plt_fixture_test.go",
"plt_helpers_test.go",
"plt_media_test.go",
"plt_parse_test.go",
"plt_print_test.go",
"plt_sniff_test.go",
Expand Down
84 changes: 74 additions & 10 deletions cmd/plt.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"text/tabwriter"

"github.com/muesli/coral"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
import 'github.com/spf13/viper' is not allowed from list 'Main' (depguard)

)

var pltPrintJSON bool
Expand Down Expand Up @@ -63,7 +65,7 @@ func runPltPrint(_ *coral.Command, args []string) {
log.Fatal().Err(err).Msg("Could not parse playlist")
}

view := buildPrintView(pl)
view := buildPrintView(pl, viper.GetString("plt.mediaapi"))

if pltPrintJSON {
err = renderJSON(os.Stdout, view)
Expand Down Expand Up @@ -132,6 +134,7 @@ type printItem struct {
Kind string `json:"kind"`
DurationSec float64 `json:"durationSec"`
EndAction int `json:"endAction"`
MediaURL string `json:"mediaURL,omitempty"`
Markers []printMarker `json:"markers,omitempty"`
}

Expand All @@ -141,8 +144,11 @@ type printMarker struct {
DurationSec float64 `json:"durationSec"`
}

// buildPrintView projects the parsed playlist into the print summary.
func buildPrintView(pl *Playlist) printView {
// buildPrintView projects the parsed playlist into the print summary. base is
// the configured media API endpoint (plt.mediaapi); when empty, media URLs are
// shown with a "<plt.mediaapi>" placeholder so the playlist-derived query is
// still visible offline.
func buildPrintView(pl *Playlist, base string) printView {
view := printView{Name: pl.Name, Items: make([]printItem, 0, len(pl.Items))}

for _, it := range pl.Items {
Expand All @@ -153,6 +159,7 @@ func buildPrintView(pl *Playlist) printView {
Kind: itemKind(it),
DurationSec: itemDurationSec(it),
EndAction: it.EndAction,
MediaURL: itemMediaURL(it, base),
}
for _, m := range it.Markers {
pi.Markers = append(pi.Markers, printMarker{
Expand All @@ -166,6 +173,33 @@ func buildPrintView(pl *Playlist) printView {
return view
}

// itemMediaURL builds the media API query URL an item resolves to, derived from
// its catalog keys. Image cues and unsupported shapes have no URL. When base is
// empty the placeholder "<plt.mediaapi>" stands in for the configured endpoint.
func itemMediaURL(it Item, base string) string {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
parameter name 'it' is too short for the scope of its usage (varnamelen)

if it.Location == nil {
return ""
}
if base == "" {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
if statements should only be cuddled with assignments (wsl)

base = "<plt.mediaapi>"
}

url, err := buildMediaURL(base, displayLangCode(it.Location.MepsLanguage), it.Location)
if err != nil {
return ""
}
return url
}

// displayLangCode returns the written-language code for a MepsLanguage ID, or
// the numeric ID when it is not mapped (best effort for display only).
func displayLangCode(id int) string {
if code, err := resolveLanguage(id, ""); err == nil {
return code
}
return strconv.Itoa(id)
}

// describeSource renders a one-line description of where an item's media comes
// from, following the catalog resolution rule (KeySymbol+Track, book/chapter, docid).
func describeSource(it Item) string {
Expand All @@ -178,12 +212,17 @@ func describeSource(it Item) string {
return "unknown source"
}

switch {
case loc.KeySymbol == "nwt" && loc.BookNumber > 0:
shape, err := classifyLocation(loc)
if err != nil {
return fmt.Sprintf("unsupported location (type %d)", loc.Type)
}

switch shape {
case shapeBookChapter:
return fmt.Sprintf("book %d:%d", loc.BookNumber, loc.ChapterNumber)
case loc.KeySymbol != "" && loc.Track > 0:
case shapePub:
return fmt.Sprintf("pub %s track %d", loc.KeySymbol, loc.Track)
case loc.Type == 3 && loc.DocumentID > 0:
case shapeDocid:
return fmt.Sprintf("docid %d", loc.DocumentID)
default:
return fmt.Sprintf("unsupported location (type %d)", loc.Type)
Expand Down Expand Up @@ -226,7 +265,7 @@ func renderText(w io.Writer, view printView) error {
}

tw := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0)
if _, err := fmt.Fprintln(tw, "#\tLABEL\tSOURCE\tDURATION\tMARKERS\tEND"); err != nil {
if _, err := fmt.Fprintln(tw, "#\tLABEL\tSOURCE\tDURATION\tMARKERS\tAFTER"); err != nil {
return fmt.Errorf("could not write table header: %w", err)
}

Expand All @@ -235,15 +274,40 @@ func renderText(w io.Writer, view printView) error {
if len(it.Markers) > 0 {
markers = fmt.Sprintf("%d", len(it.Markers))
}
if _, err := fmt.Fprintf(tw, "%d\t%s\t%s\t%.1fs\t%s\t%d\n",
it.Position, it.Label, it.Source, it.DurationSec, markers, it.EndAction); err != nil {
if _, err := fmt.Fprintf(tw, "%d\t%s\t%s\t%.1fs\t%s\t%s\n",
it.Position, it.Label, it.Source, it.DurationSec, markers, endActionLabel(it.EndAction)); err != nil {
return fmt.Errorf("could not write table row: %w", err)
}
}

if err := tw.Flush(); err != nil {
return fmt.Errorf("could not flush table: %w", err)
}

return renderMediaURLs(w, view)
}

// renderMediaURLs prints the media API query URL each item resolves to, derived
// from the playlist's catalog keys.
func renderMediaURLs(w io.Writer, view printView) error {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
parameter name 'w' is too short for the scope of its usage (varnamelen)

var withURL []printItem
for _, it := range view.Items {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
ranges should only be cuddled with assignments used in the iteration (wsl)

if it.MediaURL != "" {
withURL = append(withURL, it)
}
}
if len(withURL) == 0 {
return nil
}

if _, err := fmt.Fprint(w, "\nMedia URLs:\n"); err != nil {
return fmt.Errorf("could not write media URL header: %w", err)
}
for _, it := range withURL {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
ranges should only be cuddled with assignments used in the iteration (wsl)

if _, err := fmt.Fprintf(w, " %d %s\n", it.Position, it.MediaURL); err != nil {
return fmt.Errorf("could not write media URL: %w", err)
}
}
return nil
}

Expand Down
Loading
Loading