diff --git a/config.go b/config.go new file mode 100644 index 0000000..a23317c --- /dev/null +++ b/config.go @@ -0,0 +1,81 @@ +package main + +import ( + "github.com/pelletier/go-toml/v2" + "os" + "io/ioutil" + "sync" + "log" + "strings" +) + +type GenericError struct { msg string } +func (e *GenericError) Error() string { return e.msg } + +type PodcastConfig struct { + Name string `toml:"name"` + URL string `toml:"url"` + UseAuth bool + User string `toml:"user"` + Pass string `toml:"pass"` + Convert string `toml:"convert"` +} + +type ServerConfig struct { + MediaProxyBaseURL string `toml:"media-proxy-base-url"` + Port int `toml:"port"` + Feeds []PodcastConfig `toml:"podcasts"` + FileRoot string `toml:"file-root"` + UpdateTimeout uint `toml:"update-timeout"` +} + +func (cfg *ServerConfig) Load(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return err; + } + file, err := os.Open(path) + if err!=nil { + return err + } + defer file.Close() + content, err := ioutil.ReadAll(file) + if err!=nil { + return err + } + if err := toml.Unmarshal(content, cfg); err!=nil { + return err + } + for _, feed := range cfg.Feeds { + if strings.Contains(feed.Name, "/") { + log.Fatal("invalid feed name contains '/': "+feed.Name) + } + if feed.User!="" && feed.Pass!="" { + feed.UseAuth = true + } else { + feed.UseAuth = false + } + } + return nil +} + +type FeedCacheEntry struct { + Mutex sync.Mutex + Text string +} + +type FeedCacheS struct { + Texts map[string]string + sync.Mutex +} + +type ServerContext struct { + Config ServerConfig + WG sync.WaitGroup + IsActive bool + FeedCache FeedCacheS +} + +func (ctx *ServerContext) LoadConfig(path string) error { + ctx.FeedCache.Texts = make(map[string]string) + return ctx.Config.Load(path); +} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..9160bdc --- /dev/null +++ b/config.toml @@ -0,0 +1,14 @@ +port = 3666 +media-proxy-base-url = "http://localhost:3666" +file-root = "set path here..." +update-timeout = 30 + +[[podcasts]] +name = "another_very_unique_name" +url = "https://myawesomepodcast2" +convert = "opus" + +[[podcasts]] +name = "a_unique_name" +url = "https://myawesomepodcast" +convert = "opus" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..154b2a5 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module go-podcast-proxy + +go 1.22.5 + +require ( + github.com/PuerkitoBio/goquery v1.8.0 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mmcdole/gofeed v1.3.0 // indirect + github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + golang.org/x/net v0.4.0 // indirect + golang.org/x/text v0.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a2f4f27 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= +github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= +github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk= +github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handle-feed.go b/handle-feed.go new file mode 100644 index 0000000..e7a8179 --- /dev/null +++ b/handle-feed.go @@ -0,0 +1,25 @@ +package main + +import ( + "net/http" + "log" + "fmt" +) + +func (ctx *ServerContext) HandleFeed(w http.ResponseWriter, req *http.Request) error { + ctx.WG.Add(1) + defer ctx.WG.Done() + name := req.URL.Path + name = name[len(name)-6:] + log.Println("requesting feed <", name, ">") + + ctx.FeedCache.Lock() + defer ctx.FeedCache.Unlock() + if e, has := ctx.FeedCache.Texts[name]; has { + fmt.Fprintf(w, e) + } else { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return nil + } + return nil +} diff --git a/handle-file.go b/handle-file.go new file mode 100644 index 0000000..8c84b71 --- /dev/null +++ b/handle-file.go @@ -0,0 +1,12 @@ +package main + +import ( + "net/http" +) + +func (ctx *ServerContext) HandleFile(w http.ResponseWriter, req *http.Request) error { + ctx.WG.Add(1) + defer ctx.WG.Done() + http.ServeFile(w, req, ctx.Config.FileRoot + "/" + req.URL.Path); + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8b27525 --- /dev/null +++ b/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "log" + "strconv" + "os" + "time" + "os/signal" + "syscall" + "net/http" +) + +func (ctx *ServerContext) Sleep(seconds uint) { + for i:=(uint)(0); i +// #include +// #include +// mpv_handle* mpvinit(){ +// return mpv_create(); +// } +// int mpvbegin(mpv_handle* mpv, char* src, char* dst, char* ext) { +// mpv_set_property_string(mpv, "o", dst); +// mpv_set_property_string(mpv, "of", ext); +// mpv_set_property_string(mpv, "oacopts", "b=96k"); +// mpv_initialize(mpv); +// const char *args[3] = {"loadfile", src, NULL}; +// return mpv_command(mpv, args); +// } +// mpv_event* mpvwait(mpv_handle* mpv){ +// return mpv_wait_event(mpv, 0.25); +// } +// void mpvend(mpv_handle* mpv){ +// mpv_destroy(mpv); +// } +import "C" + +func (ctx* ServerContext) MPVConvert(source string, destination string, ext string) { + defer ctx.WG.Done() + + dir := filepath.Dir(destination) + os.MkdirAll(dir, os.ModePerm) + + mpv := C.mpvinit() + + csrc := C.CString(source) + defer C.free(unsafe.Pointer(csrc)) + cdst := C.CString(destination) + defer C.free(unsafe.Pointer(cdst)) + cext := C.CString(ext) + oacopts := C.CString("oacopts") + defer C.free(unsafe.Pointer(oacopts)) + r := C.mpvbegin(mpv, csrc, cdst, cext) + if r!=0 { + log.Println("mpv error (", source, ", ", destination, ", ", ext, "): ", r) + } + for C.mpvwait(mpv).event_id!=C.MPV_EVENT_END_FILE { + if !ctx.IsActive { + C.mpvend(mpv) + os.Remove(destination) + log.Println("mpv cancelled (", source, ", ", destination, ", ", ext, ")") + return + } + } + log.Println("mpv success (", source, ", ", destination, ", ", ext, ")") + C.mpvend(mpv) +} diff --git a/update-cache.go b/update-cache.go new file mode 100644 index 0000000..462ce6f --- /dev/null +++ b/update-cache.go @@ -0,0 +1,89 @@ +package main + +import ( + "github.com/mmcdole/gofeed" + "context" + "time" + "strings" + "strconv" + "net/url" + "log" + "os" + "encoding/base64" +) + +func (ctx* ServerContext) UpdateCache() error { + defer ctx.WG.Done() + cache := make(map[string]string) + parser := gofeed.NewParser() + for _, feed := range ctx.Config.Feeds { + _ = feed + if feed.UseAuth { + parser.AuthConfig = &gofeed.Auth{ Username : feed.User, Password : feed.Pass } + } else { + parser.AuthConfig = nil + } + pctx, cancel := context.WithTimeout(context.Background(), time.Duration(ctx.Config.UpdateTimeout)*time.Second) + defer cancel() + if doc, err := parser.ParseURLWithContext(feed.URL, pctx); err!=nil { + log.Println(err) + } else { + for _, item := range doc.Items { + for _, enclosure := range item.Enclosures { + if !ctx.IsActive { + return nil + } + if strings.HasPrefix(enclosure.Type, "audio") { + log.Println("captured enclosure audio! @ " + enclosure.URL) + u, err := url.Parse(enclosure.URL) + if err!=nil { + log.Println(err) + continue + } + new_filename := feed.Name + "/" + base64.StdEncoding.EncodeToString([]byte(u.Path)) + "." + feed.Convert; + if _, err := os.Stat(ctx.Config.FileRoot + "/" + new_filename); os.IsNotExist(err) { + old_url := enclosure.URL + enclosure.URL = ctx.Config.MediaProxyBaseURL + "/" + new_filename + enclosure.Type = "audio/" + feed.Convert + ctx.WG.Add(1) + ctx.MPVConvert(old_url, ctx.Config.FileRoot + "/" + new_filename, feed.Convert) + } + } + } + if media, has := item.Extensions["media"]; has { + if content, has := media["content"]; has { + for _, ext := range content{ + if attr_type, has := ext.Attrs["type"]; has { + if strings.HasPrefix(attr_type, "audio") { + if source, has_url := ext.Attrs["url"]; has_url { + log.Println("captured extension audio! @ " + source) + new_filename := feed.Name + "/" + base64.StdEncoding.EncodeToString([]byte(source)) + "." + feed.Convert; + if _, err := os.Stat(ctx.Config.FileRoot + "/" + new_filename); os.IsNotExist(err) { + ext.Attrs["url"] = ctx.Config.MediaProxyBaseURL + "/" + new_filename + ext.Attrs["type"] = "audio/" + feed.Convert + ctx.WG.Add(1) + ctx.MPVConvert(source, ctx.Config.FileRoot + "/" + new_filename, feed.Convert) + if fi, err := os.Stat(ctx.Config.FileRoot + "/" + new_filename); err==nil { + ext.Attrs["fileSize"] = strconv.FormatInt(fi.Size(), 10) + } + } + } + } + } + } + } + } + } + cache[feed.Name] = doc.String() + } + } + ctx.FeedCache.Lock() + defer ctx.FeedCache.Unlock() + clear(ctx.FeedCache.Texts) + for k, v := range cache { + log.Println(k) + ctx.FeedCache.Texts[k] = v + } + log.Println("cache updated") + return nil +}