diff --git a/publisher/Dockerfile b/publisher/Dockerfile index 489e3a06..aa0671fa 100644 --- a/publisher/Dockerfile +++ b/publisher/Dockerfile @@ -37,7 +37,7 @@ RUN \ spot --version COPY --from=build /build/bin/publisher /usr/local/bin/publisher -COPY spot.yml /srv/spot.yml +COPY spot.yml /etc/spot.yml WORKDIR /srv/hugo USER app diff --git a/publisher/Makefile b/publisher/Makefile index 887751bd..5977804f 100644 --- a/publisher/Makefile +++ b/publisher/Makefile @@ -1,7 +1,7 @@ .DEFAULT_GOAL := help SHELL:=/bin/bash .ONESHELL: -.PHONY: new prep print-mp3-tags upload-mp3 deploy build +.PHONY: new-show-post prep-show-post proc-mp3 deploy-site build TMPFILE := $(shell mktemp) makefile_dir := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) @@ -13,26 +13,26 @@ 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-show-post, prep-show-post, proc-mp3, deploy-site' # generate new episode post markdown file and open it using SublimeText -new: +new-episode: @docker-compose run --rm -it publisher new | tee ${TMPFILE}; @${subl} ${makefile_dir}/../hugo/`tail -n 1 ${TMPFILE} | tr -d '\r'`; # generate new prep post markdown file and open it using SublimeText -prep: +prep-show-post: @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 to master and all nodes -proc: - @docker-compose run --rm -it publisher proc --episode="$$EPISODE" --dbg +proc-mp3: + @docker-compose run --rm -it publisher proc --file="$$FILE" --dbg # deploy new podcast episode page to https://radio-t.com and regenerate site -deploy: +deploy-site: @docker-compose run --rm -it publisher deploy build: @echo $(REV) - @docker build -t publisher --build-arg REV=$(REV) . \ No newline at end of file + @docker build -t radio-t/publisher --build-arg REV=$(REV) . \ No newline at end of file diff --git a/publisher/app/cmd/proc_mp3.go b/publisher/app/cmd/proc_mp3.go index da0bfb0f..2e4464f6 100644 --- a/publisher/app/cmd/proc_mp3.go +++ b/publisher/app/cmd/proc_mp3.go @@ -24,7 +24,6 @@ var artifactsFS embed.FS // Proc handles podcast upload to all destinations. It sets mp3 tags first and then deploys to master and nodes via spot tool. type Proc struct { Executor - LocationMp3 string LocationPosts string Dry bool SkipTransfer bool @@ -32,12 +31,15 @@ type Proc struct { var authors = []string{"Umputun", "Bobuk", "Gray", "Ksenks", "Alek.sys"} -// Do uploads an episode to all destinations. It takes an episode number as input and returns an error if any of the actions fail. -// deploy performed by spot tool, see spot.yml -func (p *Proc) Do(episodeNum int) error { - log.Printf("[INFO] upload episode %d, mp3 location:%q, posts location:%q", episodeNum, p.LocationMp3, p.LocationPosts) - mp3file := filepath.Join(p.LocationMp3, fmt.Sprintf("rt_podcast%d", episodeNum), fmt.Sprintf("rt_podcast%d.mp3", episodeNum)) - log.Printf("[DEBUG] mp3 file %s", mp3file) +// Do uploads an episode to all destinations. It takes the filename and extracts episode from this filename. +// Set all the mp3 tags and add chapters. Then deploy to master and nodes. Deploy performed by spot tool, see spot.yml +func (p *Proc) Do(mp3file string) error { + episodeNum, err := episodeFromFile(mp3file) + if err != nil { + return fmt.Errorf("can't get episode number from file %s, %w", mp3file, err) + } + + log.Printf("[INFO] process file %s, episode %d, posts location:%q", mp3file, episodeNum, p.LocationPosts) hugoPost := fmt.Sprintf("%s/podcast-%d.md", p.LocationPosts, episodeNum) log.Printf("[DEBUG] hugo post file %s", hugoPost) posstContent, err := os.ReadFile(hugoPost) @@ -50,7 +52,7 @@ func (p *Proc) Do(episodeNum int) error { } log.Printf("[DEBUG] chapters %v", chapters) - err = p.setMp3Tags(episodeNum, chapters) + err = p.setMp3Tags(mp3file, episodeNum, chapters) if err != nil { log.Printf("[WARN] can't set mp3 tags for %s, %v", mp3file, err) } @@ -60,8 +62,8 @@ func (p *Proc) Do(episodeNum int) error { return nil } - p.Run("spot", "-e mp3:"+mp3file, `--task="deploy to master"`, "-v") - p.Run("spot", "-e mp3:"+mp3file, `--task="deploy to nodes"`, "-v") + p.Run("spot", "-p /etc/spot.yml", "-e mp3:"+mp3file, `--task="deploy to master"`, "-v") + p.Run("spot", "-p /etc/spot.yml", "-e mp3:"+mp3file, `--task="deploy to nodes"`, "-v") return nil } @@ -74,8 +76,7 @@ type chapter struct { // setMp3Tags sets mp3 tags for a given episode. It uses artifactsFS to read cover.jpg // and uses the chapter information to set the chapter tags. -func (p *Proc) setMp3Tags(episodeNum int, chapters []chapter) error { - mp3file := fmt.Sprintf("%s/rt_podcast%d/rt_podcast%d.mp3", p.LocationMp3, episodeNum, episodeNum) +func (p *Proc) setMp3Tags(mp3file string, episodeNum int, chapters []chapter) error { log.Printf("[INFO] set mp3 tags for %s", mp3file) if p.Dry { return nil @@ -304,3 +305,14 @@ func (p *Proc) ShowAllTags(fname string) { } } } + +// episodeFromFile takes full path to mp3 file and returns episode number +func episodeFromFile(mp3Location string) (int, error) { + name := filepath.Base(mp3Location) + re := regexp.MustCompile(`rt_podcast(\d+)\.mp3`) + matches := re.FindStringSubmatch(name) + if len(matches) != 2 { + return 0, fmt.Errorf("can't find episode number in %s", mp3Location) + } + return strconv.Atoi(matches[1]) +} diff --git a/publisher/app/cmd/proc_mp3_test.go b/publisher/app/cmd/proc_mp3_test.go index b5ca8874..db81c41d 100644 --- a/publisher/app/cmd/proc_mp3_test.go +++ b/publisher/app/cmd/proc_mp3_test.go @@ -38,21 +38,20 @@ func TestProc_Do(t *testing.T) { d := Proc{ Executor: ex, - LocationMp3: tempDir, LocationPosts: "testdata", } - err = d.Do(123) + err = d.Do("/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3") require.NoError(t, err) 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"}, - ex.RunCalls()[0].Params) + assert.Equal(t, []string{"-p /etc/spot.yml", "-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, []string{"-e mp3:/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3", "--task=\"deploy to nodes\"", "-v"}, - ex.RunCalls()[1].Params) + assert.Equal(t, []string{"-p /etc/spot.yml", "-e mp3:/tmp/publisher_test/rt_podcast123/rt_podcast123.mp3", + "--task=\"deploy to nodes\"", "-v"}, ex.RunCalls()[1].Params) } func TestProc_setMp3Tags(t *testing.T) { @@ -74,9 +73,10 @@ func TestProc_setMp3Tags(t *testing.T) { _, err = io.Copy(dst, src) require.NoError(t, err) + u := Proc{} + t.Run("without chapters", func(t *testing.T) { - u := Proc{LocationMp3: tempDir} - err = u.setMp3Tags(123, nil) + err = u.setMp3Tags(dst.Name(), 123, nil) require.NoError(t, err) tag, err := id3v2.Open(dst.Name(), id3v2.Options{Parse: true}) @@ -89,8 +89,7 @@ func TestProc_setMp3Tags(t *testing.T) { }) t.Run("with chapters", func(t *testing.T) { - u := Proc{LocationMp3: tempDir} - err = u.setMp3Tags(123, []chapter{ + err = u.setMp3Tags(dst.Name(), 123, []chapter{ {"Chapter One", "http://example.com/one", time.Second}, {"Chapter Two", "http://example.com/two", time.Second * 5}, }) @@ -213,3 +212,43 @@ filename = "rt_podcast686" assert.NoError(t, err) assert.Equal(t, expectedChapters, result) } + +func TestEpisodeFromFile(t *testing.T) { + testCases := []struct { + name string + input string + expected int + expectErr bool + }{ + { + name: "valid episode number", + input: "/path/to/rt_podcast123.mp3", + expected: 123, + expectErr: false, + }, + { + name: "missing episode number", + input: "/another/path/to/rt_podcast.mp3", + expected: 0, + expectErr: true, + }, + { + name: "non-numeric episode number", + input: "/yet/another/path/to/rt_podcastXYZ.mp3", + expected: 0, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := episodeFromFile(tc.input) + if tc.expectErr { + assert.Error(t, err, "Expected an error but didn't get one") + } else { + assert.NoError(t, err, "Expected no error but got one") + assert.Equal(t, tc.expected, result, "Mismatch in expected and actual result") + } + }) + } +} diff --git a/publisher/app/main.go b/publisher/app/main.go index f2792cc3..fd5503b5 100644 --- a/publisher/app/main.go +++ b/publisher/app/main.go @@ -28,7 +28,7 @@ var opts struct { } `command:"prep" description:"make new prep podcast post"` ProcessCmd struct { - Location string `long:"location" env:"LOCATION" default:"/episodes" description:"podcast location"` + File string `long:"file" env:"FILE" description:"mp3 file name"` 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:"proc" description:"proces podcast - tag mp3 and upload"` @@ -78,7 +78,7 @@ func main() { } if p.Active != nil && p.Command.Find("proc") == p.Active { - runProc(episodeNum()) + runProc() } if p.Active != nil && p.Command.Find("deploy") == p.Active { @@ -134,18 +134,17 @@ func runPrep(episodeNum int) { fmt.Printf("%s/prep-%d.md", opts.PrepShowCmd.Dest, episodeNum) // don't delete! used by external callers } -func runProc(episodeNum int) { +func runProc() { proc := cmd.Proc{ Executor: &cmd.ShellExecutor{Dry: opts.Dry}, - LocationMp3: opts.ProcessCmd.Location, LocationPosts: opts.ProcessCmd.HugoPosts, SkipTransfer: opts.ProcessCmd.SkipTransfer, Dry: opts.Dry, } - if err := proc.Do(episodeNum); err != nil { - log.Fatalf("[ERROR] failed to proc #%d, %v", episodeNum, err) + if err := proc.Do(opts.ProcessCmd.File); err != nil { + log.Fatalf("[ERROR] failed to proc %s, %v", opts.ProcessCmd.File, err) } - log.Printf("[INFO] deployed #%d", episodeNum) + log.Printf("[INFO] processsed %s", opts.ProcessCmd.File) } func runTags() { diff --git a/publisher/docker-compose.yml b/publisher/docker-compose.yml index a3fed504..db6d8a9c 100644 --- a/publisher/docker-compose.yml +++ b/publisher/docker-compose.yml @@ -12,10 +12,10 @@ services: max-size: "10m" max-file: "5" environment: - PYTHONPATH: /srv/publisher - RT_NEWS_ADMIN: + - 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 + - /Volumes/Podcasts/radio-t:/Volumes/Podcasts/radio-t + - /Users/umputun/.ssh/id_rsa.pub:/home/app/.ssh/id_rsa.pub + - /Users/umputun/.ssh/id_rsa:/home/app/.ssh/id_rsa +# entrypoint: tail -f /dev/null \ No newline at end of file diff --git a/publisher/spot.yml b/publisher/spot.yml index 14a790dc..045147f2 100644 --- a/publisher/spot.yml +++ b/publisher/spot.yml @@ -1,3 +1,4 @@ +user: umputun targets: nodes: hosts: [{host: "n10.radio-t.com"}, {host: "n11.radio-t.com"}]