Skip to content

Commit

Permalink
make puublisher self-contained with local compose, add CTOC frame
Browse files Browse the repository at this point in the history
  • Loading branch information
umputun committed Jan 1, 2024
1 parent 9ac8e16 commit f8e5f37
Show file tree
Hide file tree
Showing 54 changed files with 1,558 additions and 521 deletions.
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ docker-compose run --rm hugo

Скрипты публикации могут быть вызваны при помощи make в директории `./publisher`:

- `make new` — создает шаблон нового выпуска, темы берутся с news.radio-t.com
- `make prep` — создает шаблон "Темы для ..." следующего выпуска
- `make print-mp3-tags EPISODE=685` - выводит mp3 теги файла эпизода подскаста
- `make new` — создает шаблон нового выпуска, темы берутся с news.radio-t.com, номер выпуска берется из api сайта
- `make prep` — создает шаблон "Темы для ..." следующего выпуска, номер выпуска берется из api сайта
- `make upload-mp3 EPISODE=685` - добавляет mp3 теги и картинку в файл эпизода подкаста, после чего разносит его по нодам через внешний ansible контейнер. Для выполнения необходимо подключить в docker-compose конфиге директорию с mp3 файлами подкаста как volume в сервис publisher
- `make deploy` — добавляет в гит и запускает push + build на мастер. После этого строит лог чата и очищает темы

Expand Down Expand Up @@ -45,8 +44,6 @@ npm run start-turbo
npm run production
```

лого в `src/images/`

фавиконки в `static/` и описаны в `layouts/partials/favicons.html`

обложки в `static/images/covers/` (для сохранения совместимости также оставлены обложки `static/images/cover.jpg` и `static/images/cover_rt_big_archive.png`)
- лого в `src/images/`
- фавиконки в `static/` и описаны в `layouts/partials/favicons.html`
- обложки в `static/images/covers/` (для сохранения совместимости также оставлены обложки `static/images/cover.jpg` и `static/images/cover_rt_big_archive.png`)
6 changes: 3 additions & 3 deletions publisher/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ GITREV=$(shell git describe --abbrev=7 --always --tags)
REV=$(GITREV)-$(BRANCH)-$(shell date +%Y%m%d-%H:%M:%S)

help:
@echo 'Available commands: new, prep, upload-mp3, deploy'
@echo 'available commands: new, prep, upload-mp3, deploy'

# generate new episode post markdown file and open it using SublimeText
new:
Expand All @@ -25,11 +25,11 @@ prep:
@docker-compose run --rm -it publisher prep | tee ${TMPFILE};
@${subl} ${makefile_dir}/../hugo/`tail -n 1 ${TMPFILE} | tr -d '\r'`;

# set necessary tags to episode mp3 file and upload it
# set necessary tags to episode mp3 file and upload it to master and all nodes
upload-mp3:
@docker-compose run --rm -it publisher upload --episode="$$EPISODE" --dbg

# deploy new podcast episode page to https://radio-t.com
# deploy new podcast episode page to https://radio-t.com and regenerate site
deploy:
@docker-compose run --rm -it publisher deploy

Expand Down
11 changes: 2 additions & 9 deletions publisher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,12 @@

## Как пользоваться скриптами в этой директории?

Перед использованием, необходимо собрать docker образ при помощи docker-compose (конфиг в руте репозитария), команда: `docker-compose build publisher`.
Перед использованием, необходимо собрать docker образ при помощи docker-compose (конфиг в `publisher` репозитария), команда: `docker-compose build`.

После сборки образа, скриптами публикации можно пользоваться как с помощью `make`:
После сборки образа, скриптами публикации можно пользоваться с помощью `make`:

- `make new` — создает шаблон нового выпуска, темы берутся с news.radio-t.com
- `make prep` — создает шаблон "Темы для ..." следующего выпуска
- `make print-mp3-tags EPISODE=685` - выводит mp3 теги файла эпизода подскаста
- `make upload-mp3 EPISODE=685` - добавляет mp3 теги и картинку в файл эпизода подкаста, после чего разносит его по нодам через внешний ansible контейнер. Для выполнения необходимо подключить в docker-compose конфиге директорию с mp3 файлами подкаста как volume в сервис publisher
- `make deploy` — добавляет в гит и запускает pull + build на мастер. После этого строит лог чата и очищает темы

## Для разработчика

После изменений python скриптов, желательно прогнать следующие линтеры и форматтеры:
- `isort --lines 119 -y`
- `black --line-length 120 --target-version py38 ./*/*.py`
- `flake8 . --max-line-length=120`
2 changes: 1 addition & 1 deletion publisher/app/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (c *ShellExecutor) Run(cmd string, params ...string) {

// Do executes command and returns error if failed
func (c *ShellExecutor) do(cmd string) error {
log.Printf("[DEBUG] execute: %s", cmd)
log.Printf("[INFO] execute: %s", cmd)
if c.Dry {
return nil
}
Expand Down
97 changes: 84 additions & 13 deletions publisher/app/cmd/upload.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package cmd

import (
"bytes"
"embed"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"

"github.com/bogem/id3v2/v2"
log "github.com/go-pkgz/lgr"
"github.com/tcolgate/mp3"
)

//go:embed artifacts/*
Expand All @@ -22,6 +26,7 @@ type Upload struct {
LocationMp3 string
LocationPosts string
Dry bool
SkipTransfer bool
}

// Do uploads an episode to all destinations. It takes an episode number as input and returns an error if any of the actions fail.
Expand Down Expand Up @@ -52,8 +57,13 @@ func (u *Upload) Do(episodeNum int) error {
log.Printf("[WARN] can't set mp3 tags for %s, %v", mp3file, err)
}

u.Run("spot", "-e mp3:"+mp3file, `--task="deploy to master`, "-v", mp3file)
u.Run("spot", "-e mp3:"+mp3file, `--task="deploy to nodes"`, "-v", mp3file)
if u.SkipTransfer {
log.Printf("[WARN] skip transfer of %s", mp3file)
return nil
}

u.Run("spot", "-e mp3:"+mp3file, `--task="deploy to master"`, "-v")
u.Run("spot", "-e mp3:"+mp3file, `--task="deploy to nodes"`, "-v")
return nil
}

Expand All @@ -72,24 +82,29 @@ func (u *Upload) setMp3Tags(episodeNum int, chapters []chapter) error {
if u.Dry {
return nil
}

tag, err := id3v2.Open(mp3file, id3v2.Options{Parse: true})
if err != nil {
return fmt.Errorf("can't open mp3 file %s, %w", mp3file, err)
}
defer tag.Close()

tag.SetTitle(fmt.Sprintf("Радио-Т %d", episodeNum))
tag.DeleteAllFrames() // clear all existing tags

tag.SetDefaultEncoding(id3v2.EncodingUTF8)

title := fmt.Sprintf("Радио-Т %d", episodeNum)
tag.SetTitle(title)
tag.SetArtist("Umputun, Bobuk, Gray, Ksenks, Alek.sys")
tag.SetAlbum("Радио-Т")
tag.SetYear(fmt.Sprintf("%d", time.Now().Year()))
tag.SetGenre("Podcast")
tag.SetDefaultEncoding(id3v2.EncodingUTF8)

// Set artwork
artwork, err := artifactsFS.ReadFile("artifacts/cover.png")
if err != nil {
return fmt.Errorf("can't read cover.jpg from artifacts, %w", err)
return fmt.Errorf("can't read cover.png from artifacts, %w", err)
}

pic := id3v2.PictureFrame{
Encoding: id3v2.EncodingUTF8,
MimeType: "image/png",
Expand All @@ -99,13 +114,27 @@ func (u *Upload) setMp3Tags(episodeNum int, chapters []chapter) error {
}
tag.AddAttachedPicture(pic)

// we need to get mp3 duration to set the correct end time for the last chapter
duration, err := u.getMP3Duration(mp3file)
if err != nil {
return fmt.Errorf("can't get mp3 duration, %w", err)
}

// create a CTOC frame manually
ctocFrame := u.createCTOCFrame(chapters)
tag.AddFrame(tag.CommonID("CTOC"), ctocFrame)

// add chapters
for i, chapter := range chapters {
var endTime time.Duration
if i < len(chapters)-1 {
// use the start time of the next chapter as the end time
endTime = chapters[i+1].Begin
} else {
endTime = 0
endTime = duration
}
chapterTitle := chapter.Title
if !utf8.ValidString(chapterTitle) {
return fmt.Errorf("chapter title contains invalid UTF-8 characters")
}
chapFrame := id3v2.ChapterFrame{
ElementID: strconv.Itoa(i + 1),
Expand All @@ -115,11 +144,11 @@ func (u *Upload) setMp3Tags(episodeNum int, chapters []chapter) error {
EndOffset: id3v2.IgnoredOffset,
Title: &id3v2.TextFrame{
Encoding: id3v2.EncodingUTF8,
Text: chapter.Title,
Text: chapterTitle,
},
Description: &id3v2.TextFrame{
Encoding: id3v2.EncodingUTF8,
Text: chapter.Title,
Text: chapterTitle,
},
}
tag.AddChapterFrame(chapFrame)
Expand Down Expand Up @@ -152,8 +181,11 @@ func (u *Upload) parseChapters(content string) ([]chapter, error) {
return time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second, nil
}

chapters := []chapter{}
// - [Chapter One](http://example.com/one) - *00:01:00*.
chapters := []chapter{
{Title: "Вступление", Begin: 0},
}

// get form md like this "- [Chapter One](http://example.com/one) - *00:01:00*."
chapterRegex := regexp.MustCompile(`-\s+\[(.*?)\]\((.*?)\)\s+-\s+\*(.*?)\*\.`)
matches := chapterRegex.FindAllStringSubmatch(content, -1)
for _, match := range matches {
Expand All @@ -174,6 +206,45 @@ func (u *Upload) parseChapters(content string) ([]chapter, error) {
})
}
}

if len(chapters) == 1 {
return []chapter{}, nil // no chapters found, don't return the introduction chapter
}
return chapters, nil
}

func (u *Upload) getMP3Duration(filePath string) (time.Duration, error) {
file, err := os.Open(filePath)
if err != nil {
return 0, err
}
defer file.Close()
d := mp3.NewDecoder(file)
var f mp3.Frame
var skipped int
var duration float64

for err == nil {
if err = d.Decode(&f, &skipped); err != nil && err != io.EOF {
log.Printf("[WARN] can't get duration for provided stream: %v", err)
return 0, nil
}
duration += f.Duration().Seconds()
}
return time.Second * time.Duration(duration), nil
}

func (u *Upload) createCTOCFrame(chapters []chapter) *id3v2.UnknownFrame {
var frameBody bytes.Buffer
frameBody.WriteByte(0x03) // write flags (e.g., 0x03 for top-level and ordered chapters)
frameBody.WriteByte(byte(len(chapters))) // write the number of child elements

// append child element IDs (chapter IDs)
for i, _ := range chapters {
elementID := fmt.Sprintf("%d", i+1)
frameBody.WriteString(elementID)
frameBody.WriteByte(0x00) // Null separator for IDs
}

// create and return an UnknownFrame with the constructed body
return &id3v2.UnknownFrame{Body: frameBody.Bytes()}
}
17 changes: 10 additions & 7 deletions publisher/app/cmd/upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ func TestUpload_Do(t *testing.T) {

require.Equal(t, 2, len(ex.RunCalls()))
assert.Equal(t, "spot", ex.RunCalls()[0].Cmd)
assert.Equal(t, []string{"-e mp3:/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3", "--task=\"deploy to master", "-v", "/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3"}, ex.RunCalls()[0].Params)
assert.Equal(t, []string{"-e mp3:/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3", "--task=\"deploy to master\"", "-v"},
ex.RunCalls()[0].Params)

assert.Equal(t, "spot", ex.RunCalls()[1].Cmd)
assert.Equal(t, 4, len(ex.RunCalls()[1].Params))
assert.Equal(t, []string{"-e mp3:/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3", "--task=\"deploy to nodes\"", "-v", "/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3"}, ex.RunCalls()[1].Params)
assert.Equal(t, []string{"-e mp3:/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3", "--task=\"deploy to nodes\"", "-v"},
ex.RunCalls()[1].Params)
}

func TestUpload_setMp3Tags(t *testing.T) {
Expand Down Expand Up @@ -124,12 +125,13 @@ func TestUpload_parseChapters(t *testing.T) {
{
name: "Valid chapters",
content: `
- [Chapter One](http://example.com/one) - *00:01:00*.
- [Chapter Two](http://example.com/two) - *00:02:30*.
- [Часть номер One](http://example.com/one) - *00:01:00*.
- [Часть номер Two](http://example.com/two) - *00:02:30*.
`,
expected: []chapter{
{"Chapter One", "http://example.com/one", time.Minute},
{"Chapter Two", "http://example.com/two", time.Minute*2 + time.Second*30},
{"Вступление", "", 0},
{"Часть номер One", "http://example.com/one", time.Minute},
{"Часть номер Two", "http://example.com/two", time.Minute*2 + time.Second*30},
},
expectError: false,
},
Expand Down Expand Up @@ -193,6 +195,7 @@ filename = "rt_podcast686"
`

expectedChapters := []chapter{
{"Вступление", "", 0},
{"Первому Macintosh 36 лет", "https://www.macrumors.com/2020/01/24/macintosh-36th-anniversary/", 4*time.Minute + 18*time.Second},
{"JetBrains придумает новую IntelliJ", "https://devclass.com/2020/01/21/jetbrains-reimagines-intellij-as-text-editor-machine-learning/", 11*time.Minute + 10*time.Second},
{"Мы учим не тому", "https://www.bloomberg.com/tosv2.html?vid=&uuid=59d32d10-31cd-11ea-a482-59e1177b04c0&url=L29waW5pb24vYXJ0aWNsZXMvMjAyMC0wMS0wNy9jb2RpbmctaXMtY29sbGFib3JhdGl2ZS1hbmQtc3RlbS1lZHVjYXRpb24tc2hvdWxkLWJlLXRvbw==", 28*time.Minute + 16*time.Second},
Expand Down
8 changes: 5 additions & 3 deletions publisher/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ var opts struct {
} `command:"prep" description:"make new prep podcast post"`

UploadCmd struct {
Location string `long:"location" env:"LOCATION" default:"/Volumes/Podcasts/radio-t/" description:"podcast location"`
HugoPosts string `long:"hugo-posts" env:"HUGO_POSTS" default:"/srv/hugo/content/posts" description:"hugo posts location"`
Location string `long:"location" env:"LOCATION" default:"/episodes" description:"podcast location"`
HugoPosts string `long:"hugo-posts" env:"HUGO_POSTS" default:"/srv/hugo/content/posts" description:"hugo posts location"`
SkipTransfer bool `long:"skip-transfer" env:"SKIP_TRANSFER" description:"skip transfer to remote locations"`
} `command:"upload" description:"upload podcast"`

DeployCmd struct {
Expand Down Expand Up @@ -58,7 +59,7 @@ func main() {
if err != nil {
log.Fatalf("[ERROR] can't get last podcast number, %v", err)
}
log.Printf("[DEBUG] episode %d", episodeNum)
log.Printf("[DEBUG] dtected episode: %d", episodeNum)

if p.Active != nil && p.Command.Find("new") == p.Active {
runNew(episodeNum)
Expand Down Expand Up @@ -126,6 +127,7 @@ func runUpload(episodeNum int) {
Executor: &cmd.ShellExecutor{Dry: opts.Dry},
LocationMp3: opts.UploadCmd.Location,
LocationPosts: opts.UploadCmd.HugoPosts,
SkipTransfer: opts.UploadCmd.SkipTransfer,
Dry: opts.Dry,
}
if err := upload.Do(episodeNum); err != nil {
Expand Down
21 changes: 21 additions & 0 deletions publisher/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

services:
publisher:
image: radio-t/publisher
build: .
hostname: publisher
container_name: publisher
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
environment:
PYTHONPATH: /srv/publisher
RT_NEWS_ADMIN:
volumes:
- ../:/srv/
- /Volumes/Podcasts/radio-t:/episodes
- /Users/umputun/.ssh/id_rsa.pub:/home/app/.ssh/id_rsa.pub:ro
- /Users/umputun/.ssh/id_rsa:/home/app/.ssh/id_rsa:ro
1 change: 1 addition & 0 deletions publisher/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/go-pkgz/lgr v0.11.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
github.com/umputun/go-flags v1.5.1
)

Expand Down
2 changes: 2 additions & 0 deletions publisher/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
github.com/umputun/go-flags v1.5.1 h1:vRauoXV3Ultt1HrxivSxowbintgZLJE+EcBy5ta3/mY=
github.com/umputun/go-flags v1.5.1/go.mod h1:nTbvsO/hKqe7Utri/NoyN18GR3+EWf+9RrmsdwdhrEc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
Expand Down
7 changes: 3 additions & 4 deletions publisher/vendor/github.com/bogem/id3v2/v2/common_ids.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion publisher/vendor/github.com/davecgh/go-spew/spew/bypass.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f8e5f37

Please sign in to comment.