Code Rewrite
parent
746882138e
commit
2d397d1382
1
go.mod
1
go.mod
|
@ -5,4 +5,5 @@ go 1.21.0
|
||||||
require (
|
require (
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/gorilla/feeds v1.2.0
|
||||||
)
|
)
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -2,3 +2,11 @@ github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4m
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
||||||
|
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
|
32
main.go
32
main.go
|
@ -10,11 +10,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
port = os.Getenv("PODCAST_PORT") // Es: ":8080"
|
port = os.Getenv("PODCAST_PORT") // Es: ":8080"
|
||||||
baseURL = os.Getenv("PODCAST_BASE_URL") // Es: "http://mioserver.com"
|
baseURL = os.Getenv("PODCAST_BASE_URL") // Es: "http://mioserver.com"
|
||||||
audioDir = os.Getenv("PODCAST_AUDIO_DIR") // Es: "/data/podcast/audio"
|
audioDir = os.Getenv("PODCAST_AUDIO_DIR") // Es: "/data/podcast/audio"
|
||||||
coversDir = os.Getenv("PODCAST_COVERS_DIR") // Es: "/data/podcast/covers"
|
coversDir = os.Getenv("PODCAST_COVERS_DIR") // Es: "/data/podcast/covers"
|
||||||
podTitle = os.Getenv("PODCAST_TITLE") // Es: "Il Mio Podcast"
|
podTitle = os.Getenv("PODCAST_TITLE") // Es: "Il Mio Podcast"
|
||||||
|
podAuthor = os.Getenv("PODCAST_AUTHOR") // Es: "Il Mio Podcast"
|
||||||
|
podRights = os.Getenv("PODCAST_COPYRIGHT") // Es: "Il Mio Podcast"
|
||||||
|
podLogo = os.Getenv("PODCAST_LOGO") // Es: "Il Mio Podcast"
|
||||||
|
podDesc = os.Getenv("PODCAST_DESCRIPTION") // Es: "Il Mio Podcast"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -28,6 +32,10 @@ func init() {
|
||||||
{"PODCAST_AUDIO_DIR", audioDir},
|
{"PODCAST_AUDIO_DIR", audioDir},
|
||||||
{"PODCAST_COVERS_DIR", coversDir},
|
{"PODCAST_COVERS_DIR", coversDir},
|
||||||
{"PODCAST_TITLE", podTitle},
|
{"PODCAST_TITLE", podTitle},
|
||||||
|
{"PODCAST_AUTHOR", podAuthor},
|
||||||
|
{"PODCAST_COPYRIGHT", podRights},
|
||||||
|
{"PODCAST_LOGO", podLogo},
|
||||||
|
{"PODCAST_DESCRIPTION", podDesc},
|
||||||
} {
|
} {
|
||||||
if v.value == "" {
|
if v.value == "" {
|
||||||
log.Fatalf("Variabile d'ambiente mancante: %s", v.name)
|
log.Fatalf("Variabile d'ambiente mancante: %s", v.name)
|
||||||
|
@ -40,19 +48,23 @@ func main() {
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Verifica se la richiesta è per un file audio o cover
|
// Verifica se la richiesta è per un file audio o cover
|
||||||
requestedFile := filepath.Clean(r.URL.Path)
|
requestedFile := filepath.Clean(r.URL.Path)
|
||||||
isAudio := strings.HasPrefix(requestedFile, "/audio/")
|
isAudio := strings.HasPrefix(requestedFile, "/audio/") && strings.HasSuffix(requestedFile, ".mp3")
|
||||||
isCover := strings.HasPrefix(requestedFile, "/covers/")
|
isCover := strings.HasPrefix(requestedFile, "/covers/") && strings.HasSuffix(requestedFile, ".jpg")
|
||||||
// logghiamo che succede
|
isLogo := strings.HasPrefix(requestedFile, "/cover.jpg") && strings.HasSuffix(requestedFile, ".jpg")
|
||||||
log.Println("Richiesta da: ", r.UserAgent(), " per ", r.RequestURI)
|
|
||||||
if isAudio || isCover {
|
if isAudio || isCover || isLogo {
|
||||||
// Servi il file richiesto
|
// Servi il file richiesto
|
||||||
http.ServeFile(w, r, filepath.Join(".", requestedFile))
|
http.ServeFile(w, r, filepath.Join(".", requestedFile))
|
||||||
|
// logghiamo che succede
|
||||||
|
log.Println("Richiesta da: ", r.UserAgent(), " per ", r.RequestURI)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Altrimenti servi sempre l'RSS
|
// Altrimenti servi sempre l'RSS
|
||||||
if err := generateRSS(); err != nil {
|
if err := generateRSS(); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
// logghiamo che succede
|
||||||
|
log.Println("Richiesta da: ", r.UserAgent(), " per ", r.RequestURI, " --> feeds.xml ")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.ServeFile(w, r, "feed.xml")
|
http.ServeFile(w, r, "feed.xml")
|
||||||
|
|
79
pod.go
79
pod.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -14,12 +13,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Episode struct {
|
type Episode struct {
|
||||||
Title string
|
Title string
|
||||||
File string
|
File string
|
||||||
Cover string
|
Cover string
|
||||||
PubDate string
|
PubDate string
|
||||||
Artist string
|
Description string
|
||||||
Size int64
|
Artist string
|
||||||
|
Size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -32,54 +32,6 @@ func generateUUIDFromString(input string) uuid.UUID {
|
||||||
return uuid.NewSHA1(namespace, []byte(input))
|
return uuid.NewSHA1(namespace, []byte(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRSS() error {
|
|
||||||
rssLock.Lock()
|
|
||||||
defer rssLock.Unlock()
|
|
||||||
|
|
||||||
episodes, err := scanEpisodes()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rss := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<rss version="2.0"
|
|
||||||
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
|
|
||||||
xmlns:podcast="https://podcastindex.org/namespace/1.0"
|
|
||||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
||||||
xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
||||||
<channel>
|
|
||||||
<description>%s</description>
|
|
||||||
<title>%s</title>
|
|
||||||
<link>%s</link>`, podTitle, podTitle, baseURL)
|
|
||||||
|
|
||||||
for _, ep := range episodes {
|
|
||||||
rss += fmt.Sprintf(`
|
|
||||||
<item>
|
|
||||||
<title>%s</title>
|
|
||||||
<enclosure url="%s/audio/%s" type="audio/mpeg" length="%d"/>
|
|
||||||
<pubDate>%s</pubDate>
|
|
||||||
`, ep.Title, baseURL, filepath.Base(ep.File), ep.Size, ep.PubDate)
|
|
||||||
|
|
||||||
if ep.Cover != "" {
|
|
||||||
rss += fmt.Sprintf(`<itunes:image href="%s/covers/%s"/>`, baseURL, ep.Cover)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ep.Artist != "" {
|
|
||||||
rss += fmt.Sprintf(`
|
|
||||||
<itunes:author>%s</itunes:author>`, ep.Artist)
|
|
||||||
}
|
|
||||||
|
|
||||||
rss += fmt.Sprintf(`<link>%s/audio/%s</link>`, baseURL, filepath.Base(ep.File))
|
|
||||||
rss += fmt.Sprintf(`<guid isPermaLink="false">%s</guid>`, generateUUIDFromString(ep.Title).String())
|
|
||||||
rss += fmt.Sprintf(`<description>%s</description>`, ep.Title)
|
|
||||||
rss += "\n </item>"
|
|
||||||
}
|
|
||||||
|
|
||||||
rss += "\n</channel>\n</rss>"
|
|
||||||
|
|
||||||
return os.WriteFile("feed.xml", []byte(rss), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanEpisodes() ([]Episode, error) {
|
func scanEpisodes() ([]Episode, error) {
|
||||||
var episodes []Episode
|
var episodes []Episode
|
||||||
|
|
||||||
|
@ -105,12 +57,17 @@ func scanEpisodes() ([]Episode, error) {
|
||||||
// coverPath := filepath.Join(coversDir, baseName+".jpg")
|
// coverPath := filepath.Join(coversDir, baseName+".jpg")
|
||||||
|
|
||||||
ep := Episode{
|
ep := Episode{
|
||||||
Title: meta.Title(),
|
Title: meta.Title(),
|
||||||
Artist: meta.Artist(),
|
Artist: meta.Artist(),
|
||||||
File: baseName + ".mp3",
|
Description: meta.Comment(),
|
||||||
Cover: baseName + ".jpg",
|
File: baseName + ".mp3",
|
||||||
PubDate: info.ModTime().Format(time.RFC1123),
|
Cover: baseName + ".jpg",
|
||||||
Size: info.Size(),
|
PubDate: info.ModTime().Format(time.RFC1123),
|
||||||
|
Size: info.Size(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.Description == "" {
|
||||||
|
ep.Description = ep.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
episodes = append(episodes, ep)
|
episodes = append(episodes, ep)
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateRSS() error {
|
||||||
|
rssLock.Lock()
|
||||||
|
defer rssLock.Unlock()
|
||||||
|
|
||||||
|
episodes, err := scanEpisodes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// rss := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
// <rss version="2.0"
|
||||||
|
// xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
|
||||||
|
// xmlns:podcast="https://podcastindex.org/namespace/1.0"
|
||||||
|
// xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
// xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||||
|
// <channel>
|
||||||
|
// <description>%s</description>
|
||||||
|
// <title>%s</title>
|
||||||
|
// <link>%s</link>`, podTitle, podTitle, baseURL)
|
||||||
|
|
||||||
|
feed := &feeds.Feed{
|
||||||
|
Title: podTitle,
|
||||||
|
Link: &feeds.Link{Href: baseURL},
|
||||||
|
Description: podDesc,
|
||||||
|
Author: &feeds.Author{Name: podAuthor},
|
||||||
|
Created: time.Now(),
|
||||||
|
Copyright: podRights,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ep := range episodes {
|
||||||
|
|
||||||
|
epBaseUrl := baseURL + "/audio/" + filepath.Base(ep.File)
|
||||||
|
|
||||||
|
feed.Add(&feeds.Item{
|
||||||
|
Title: ep.Title,
|
||||||
|
Link: &feeds.Link{Href: epBaseUrl},
|
||||||
|
Description: ep.Description,
|
||||||
|
Enclosure: &feeds.Enclosure{
|
||||||
|
Url: epBaseUrl,
|
||||||
|
Length: fmt.Sprintf("%d", ep.Size),
|
||||||
|
Type: "audio/mpeg",
|
||||||
|
},
|
||||||
|
Created: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genera RSS
|
||||||
|
rss, _ := feed.ToRss()
|
||||||
|
return os.WriteFile("feed.xml", []byte(rss), 0644)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
; https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
eclint_indent_style = unset
|
|
@ -0,0 +1 @@
|
||||||
|
coverage.coverprofile
|
|
@ -0,0 +1,28 @@
|
||||||
|
Copyright (c) 2023 The Gorilla Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '')
|
||||||
|
GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
GO_SEC=$(shell which gosec 2> /dev/null || echo '')
|
||||||
|
GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest
|
||||||
|
|
||||||
|
GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '')
|
||||||
|
GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
|
||||||
|
.PHONY: golangci-lint
|
||||||
|
golangci-lint:
|
||||||
|
$(if $(GO_LINT), ,go install $(GO_LINT_URI))
|
||||||
|
@echo "##### Running golangci-lint"
|
||||||
|
golangci-lint run -v
|
||||||
|
|
||||||
|
.PHONY: gosec
|
||||||
|
gosec:
|
||||||
|
$(if $(GO_SEC), ,go install $(GO_SEC_URI))
|
||||||
|
@echo "##### Running gosec"
|
||||||
|
gosec ./...
|
||||||
|
|
||||||
|
.PHONY: govulncheck
|
||||||
|
govulncheck:
|
||||||
|
$(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI))
|
||||||
|
@echo "##### Running govulncheck"
|
||||||
|
govulncheck ./...
|
||||||
|
|
||||||
|
.PHONY: verify
|
||||||
|
verify: golangci-lint gosec govulncheck
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@echo "##### Running tests"
|
||||||
|
go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./...
|
|
@ -0,0 +1,198 @@
|
||||||
|
## gorilla/feeds
|
||||||
|

|
||||||
|
[](https://codecov.io/github/gorilla/feeds)
|
||||||
|
[](https://godoc.org/github.com/gorilla/feeds)
|
||||||
|
[](https://sourcegraph.com/github.com/gorilla/feeds?badge)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
feeds is a web feed generator library for generating RSS, Atom and JSON feeds from Go
|
||||||
|
applications.
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
* Provide a simple interface to create both Atom & RSS 2.0 feeds
|
||||||
|
* Full support for [Atom][atom], [RSS 2.0][rss], and [JSON Feed Version 1][jsonfeed] spec elements
|
||||||
|
* Ability to modify particulars for each spec
|
||||||
|
|
||||||
|
[atom]: https://tools.ietf.org/html/rfc4287
|
||||||
|
[rss]: http://www.rssboard.org/rss-specification
|
||||||
|
[jsonfeed]: https://jsonfeed.org/version/1.1
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
now := time.Now()
|
||||||
|
feed := &feeds.Feed{
|
||||||
|
Title: "jmoiron.net blog",
|
||||||
|
Link: &feeds.Link{Href: "http://jmoiron.net/blog"},
|
||||||
|
Description: "discussion about tech, footie, photos",
|
||||||
|
Author: &feeds.Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
|
||||||
|
Created: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Items = []*feeds.Item{
|
||||||
|
&feeds.Item{
|
||||||
|
Title: "Limiting Concurrency in Go",
|
||||||
|
Link: &feeds.Link{Href: "http://jmoiron.net/blog/limiting-concurrency-in-go/"},
|
||||||
|
Description: "A discussion on controlled parallelism in golang",
|
||||||
|
Author: &feeds.Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
&feeds.Item{
|
||||||
|
Title: "Logic-less Template Redux",
|
||||||
|
Link: &feeds.Link{Href: "http://jmoiron.net/blog/logicless-template-redux/"},
|
||||||
|
Description: "More thoughts on logicless templates",
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
&feeds.Item{
|
||||||
|
Title: "Idiomatic Code Reuse in Go",
|
||||||
|
Link: &feeds.Link{Href: "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"},
|
||||||
|
Description: "How to use interfaces <em>effectively</em>",
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
atom, err := feed.ToAtom()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rss, err := feed.ToRss()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := feed.ToJSON()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(atom, "\n", rss, "\n", json)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>jmoiron.net blog</title>
|
||||||
|
<link href="http://jmoiron.net/blog"></link>
|
||||||
|
<id>http://jmoiron.net/blog</id>
|
||||||
|
<updated>2013-01-16T03:26:01-05:00</updated>
|
||||||
|
<summary>discussion about tech, footie, photos</summary>
|
||||||
|
<entry>
|
||||||
|
<title>Limiting Concurrency in Go</title>
|
||||||
|
<link href="http://jmoiron.net/blog/limiting-concurrency-in-go/"></link>
|
||||||
|
<updated>2013-01-16T03:26:01-05:00</updated>
|
||||||
|
<id>tag:jmoiron.net,2013-01-16:/blog/limiting-concurrency-in-go/</id>
|
||||||
|
<summary type="html">A discussion on controlled parallelism in golang</summary>
|
||||||
|
<author>
|
||||||
|
<name>Jason Moiron</name>
|
||||||
|
<email>jmoiron@jmoiron.net</email>
|
||||||
|
</author>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>Logic-less Template Redux</title>
|
||||||
|
<link href="http://jmoiron.net/blog/logicless-template-redux/"></link>
|
||||||
|
<updated>2013-01-16T03:26:01-05:00</updated>
|
||||||
|
<id>tag:jmoiron.net,2013-01-16:/blog/logicless-template-redux/</id>
|
||||||
|
<summary type="html">More thoughts on logicless templates</summary>
|
||||||
|
<author></author>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>Idiomatic Code Reuse in Go</title>
|
||||||
|
<link href="http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"></link>
|
||||||
|
<updated>2013-01-16T03:26:01-05:00</updated>
|
||||||
|
<id>tag:jmoiron.net,2013-01-16:/blog/idiomatic-code-reuse-in-go/</id>
|
||||||
|
<summary type="html">How to use interfaces <em>effectively</em></summary>
|
||||||
|
<author></author>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>jmoiron.net blog</title>
|
||||||
|
<link>http://jmoiron.net/blog</link>
|
||||||
|
<description>discussion about tech, footie, photos</description>
|
||||||
|
<managingEditor>jmoiron@jmoiron.net (Jason Moiron)</managingEditor>
|
||||||
|
<pubDate>2013-01-16T03:22:24-05:00</pubDate>
|
||||||
|
<item>
|
||||||
|
<title>Limiting Concurrency in Go</title>
|
||||||
|
<link>http://jmoiron.net/blog/limiting-concurrency-in-go/</link>
|
||||||
|
<description>A discussion on controlled parallelism in golang</description>
|
||||||
|
<pubDate>2013-01-16T03:22:24-05:00</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Logic-less Template Redux</title>
|
||||||
|
<link>http://jmoiron.net/blog/logicless-template-redux/</link>
|
||||||
|
<description>More thoughts on logicless templates</description>
|
||||||
|
<pubDate>2013-01-16T03:22:24-05:00</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Idiomatic Code Reuse in Go</title>
|
||||||
|
<link>http://jmoiron.net/blog/idiomatic-code-reuse-in-go/</link>
|
||||||
|
<description>How to use interfaces <em>effectively</em></description>
|
||||||
|
<pubDate>2013-01-16T03:22:24-05:00</pubDate>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
|
||||||
|
{
|
||||||
|
"version": "https://jsonfeed.org/version/1.1",
|
||||||
|
"title": "jmoiron.net blog",
|
||||||
|
"home_page_url": "http://jmoiron.net/blog",
|
||||||
|
"description": "discussion about tech, footie, photos",
|
||||||
|
"author": {
|
||||||
|
"name": "Jason Moiron"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jason Moiron"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"url": "http://jmoiron.net/blog/limiting-concurrency-in-go/",
|
||||||
|
"title": "Limiting Concurrency in Go",
|
||||||
|
"summary": "A discussion on controlled parallelism in golang",
|
||||||
|
"date_published": "2013-01-16T03:22:24.530817846-05:00",
|
||||||
|
"author": {
|
||||||
|
"name": "Jason Moiron"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jason Moiron"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"url": "http://jmoiron.net/blog/logicless-template-redux/",
|
||||||
|
"title": "Logic-less Template Redux",
|
||||||
|
"summary": "More thoughts on logicless templates",
|
||||||
|
"date_published": "2013-01-16T03:22:24.530817846-05:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"url": "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/",
|
||||||
|
"title": "Idiomatic Code Reuse in Go",
|
||||||
|
"summary": "How to use interfaces \u003cem\u003eeffectively\u003c/em\u003e",
|
||||||
|
"date_published": "2013-01-16T03:22:24.530817846-05:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,178 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generates Atom feed as XML
|
||||||
|
|
||||||
|
const ns = "http://www.w3.org/2005/Atom"
|
||||||
|
|
||||||
|
type AtomPerson struct {
|
||||||
|
Name string `xml:"name,omitempty"`
|
||||||
|
Uri string `xml:"uri,omitempty"`
|
||||||
|
Email string `xml:"email,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomSummary struct {
|
||||||
|
XMLName xml.Name `xml:"summary"`
|
||||||
|
Content string `xml:",chardata"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomContent struct {
|
||||||
|
XMLName xml.Name `xml:"content"`
|
||||||
|
Content string `xml:",chardata"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomAuthor struct {
|
||||||
|
XMLName xml.Name `xml:"author"`
|
||||||
|
AtomPerson
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomContributor struct {
|
||||||
|
XMLName xml.Name `xml:"contributor"`
|
||||||
|
AtomPerson
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomEntry struct {
|
||||||
|
XMLName xml.Name `xml:"entry"`
|
||||||
|
Xmlns string `xml:"xmlns,attr,omitempty"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Updated string `xml:"updated"` // required
|
||||||
|
Id string `xml:"id"` // required
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Content *AtomContent
|
||||||
|
Rights string `xml:"rights,omitempty"`
|
||||||
|
Source string `xml:"source,omitempty"`
|
||||||
|
Published string `xml:"published,omitempty"`
|
||||||
|
Contributor *AtomContributor
|
||||||
|
Links []AtomLink // required if no child 'content' elements
|
||||||
|
Summary *AtomSummary // required if content has src or content is base64
|
||||||
|
Author *AtomAuthor // required if feed lacks an author
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple links with different rel can coexist
|
||||||
|
type AtomLink struct {
|
||||||
|
//Atom 1.0 <link rel="enclosure" type="audio/mpeg" title="MP3" href="http://www.example.org/myaudiofile.mp3" length="1234" />
|
||||||
|
XMLName xml.Name `xml:"link"`
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
Rel string `xml:"rel,attr,omitempty"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
Length string `xml:"length,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomFeed struct {
|
||||||
|
XMLName xml.Name `xml:"feed"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Id string `xml:"id"` // required
|
||||||
|
Updated string `xml:"updated"` // required
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Icon string `xml:"icon,omitempty"`
|
||||||
|
Logo string `xml:"logo,omitempty"`
|
||||||
|
Rights string `xml:"rights,omitempty"` // copyright used
|
||||||
|
Subtitle string `xml:"subtitle,omitempty"`
|
||||||
|
Link *AtomLink
|
||||||
|
Author *AtomAuthor `xml:"author,omitempty"`
|
||||||
|
Contributor *AtomContributor
|
||||||
|
Entries []*AtomEntry `xml:"entry"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Atom struct {
|
||||||
|
*Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAtomEntry(i *Item) *AtomEntry {
|
||||||
|
id := i.Id
|
||||||
|
link := i.Link
|
||||||
|
if link == nil {
|
||||||
|
link = &Link{}
|
||||||
|
}
|
||||||
|
if len(id) == 0 {
|
||||||
|
// if there's no id set, try to create one, either from data or just a uuid
|
||||||
|
if len(link.Href) > 0 && (!i.Created.IsZero() || !i.Updated.IsZero()) {
|
||||||
|
dateStr := anyTimeFormat("2006-01-02", i.Updated, i.Created)
|
||||||
|
host, path := link.Href, "/invalid.html"
|
||||||
|
if url, err := url.Parse(link.Href); err == nil {
|
||||||
|
host, path = url.Host, url.Path
|
||||||
|
}
|
||||||
|
id = fmt.Sprintf("tag:%s,%s:%s", host, dateStr, path)
|
||||||
|
} else {
|
||||||
|
id = "urn:uuid:" + NewUUID().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var name, email string
|
||||||
|
if i.Author != nil {
|
||||||
|
name, email = i.Author.Name, i.Author.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
link_rel := link.Rel
|
||||||
|
if link_rel == "" {
|
||||||
|
link_rel = "alternate"
|
||||||
|
}
|
||||||
|
x := &AtomEntry{
|
||||||
|
Title: i.Title,
|
||||||
|
Links: []AtomLink{{Href: link.Href, Rel: link_rel, Type: link.Type}},
|
||||||
|
Id: id,
|
||||||
|
Updated: anyTimeFormat(time.RFC3339, i.Updated, i.Created),
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there's a description, assume it's html
|
||||||
|
if len(i.Description) > 0 {
|
||||||
|
x.Summary = &AtomSummary{Content: i.Description, Type: "html"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there's a content, assume it's html
|
||||||
|
if len(i.Content) > 0 {
|
||||||
|
x.Content = &AtomContent{Content: i.Content, Type: "html"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Enclosure != nil && link_rel != "enclosure" {
|
||||||
|
x.Links = append(x.Links, AtomLink{Href: i.Enclosure.Url, Rel: "enclosure", Type: i.Enclosure.Type, Length: i.Enclosure.Length})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(name) > 0 || len(email) > 0 {
|
||||||
|
x.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: name, Email: email}}
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new AtomFeed with a generic Feed struct's data
|
||||||
|
func (a *Atom) AtomFeed() *AtomFeed {
|
||||||
|
updated := anyTimeFormat(time.RFC3339, a.Updated, a.Created)
|
||||||
|
link := a.Link
|
||||||
|
if link == nil {
|
||||||
|
link = &Link{}
|
||||||
|
}
|
||||||
|
feed := &AtomFeed{
|
||||||
|
Xmlns: ns,
|
||||||
|
Title: a.Title,
|
||||||
|
Link: &AtomLink{Href: link.Href, Rel: link.Rel},
|
||||||
|
Subtitle: a.Description,
|
||||||
|
Id: link.Href,
|
||||||
|
Updated: updated,
|
||||||
|
Rights: a.Copyright,
|
||||||
|
}
|
||||||
|
if a.Author != nil {
|
||||||
|
feed.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: a.Author.Name, Email: a.Author.Email}}
|
||||||
|
}
|
||||||
|
for _, e := range a.Items {
|
||||||
|
feed.Entries = append(feed.Entries, newAtomEntry(e))
|
||||||
|
}
|
||||||
|
return feed
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedXml returns an XML-Ready object for an Atom object
|
||||||
|
func (a *Atom) FeedXml() interface{} {
|
||||||
|
return a.AtomFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedXml returns an XML-ready object for an AtomFeed object
|
||||||
|
func (a *AtomFeed) FeedXml() interface{} {
|
||||||
|
return a
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Syndication (feed) generator library for golang.
|
||||||
|
|
||||||
|
Installing
|
||||||
|
|
||||||
|
go get github.com/gorilla/feeds
|
||||||
|
|
||||||
|
Feeds provides a simple, generic Feed interface with a generic Item object as well as RSS, Atom and JSON Feed specific RssFeed, AtomFeed and JSONFeed objects which allow access to all of each spec's defined elements.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
|
||||||
|
Create a Feed and some Items in that feed using the generic interfaces:
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
. "github.com/gorilla/feeds"
|
||||||
|
)
|
||||||
|
|
||||||
|
now = time.Now()
|
||||||
|
|
||||||
|
feed := &Feed{
|
||||||
|
Title: "jmoiron.net blog",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog"},
|
||||||
|
Description: "discussion about tech, footie, photos",
|
||||||
|
Author: &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
|
||||||
|
Created: now,
|
||||||
|
Copyright: "This work is copyright © Benjamin Button",
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Items = []*Item{
|
||||||
|
&Item{
|
||||||
|
Title: "Limiting Concurrency in Go",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog/limiting-concurrency-in-go/"},
|
||||||
|
Description: "A discussion on controlled parallelism in golang",
|
||||||
|
Author: &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
&Item{
|
||||||
|
Title: "Logic-less Template Redux",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog/logicless-template-redux/"},
|
||||||
|
Description: "More thoughts on logicless templates",
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
&Item{
|
||||||
|
Title: "Idiomatic Code Reuse in Go",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"},
|
||||||
|
Description: "How to use interfaces <em>effectively</em>",
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
From here, you can output Atom, RSS, or JSON Feed versions of this feed easily
|
||||||
|
|
||||||
|
atom, err := feed.ToAtom()
|
||||||
|
rss, err := feed.ToRss()
|
||||||
|
json, err := feed.ToJSON()
|
||||||
|
|
||||||
|
You can also get access to the underlying objects that feeds uses to export its XML
|
||||||
|
|
||||||
|
atomFeed := (&Atom{Feed: feed}).AtomFeed()
|
||||||
|
rssFeed := (&Rss{Feed: feed}).RssFeed()
|
||||||
|
jsonFeed := (&JSON{Feed: feed}).JSONFeed()
|
||||||
|
|
||||||
|
From here, you can modify or add each syndication's specific fields before outputting
|
||||||
|
|
||||||
|
atomFeed.Subtitle = "plays the blues"
|
||||||
|
atom, err := ToXML(atomFeed)
|
||||||
|
rssFeed.Generator = "gorilla/feeds v1.0 (github.com/gorilla/feeds)"
|
||||||
|
rss, err := ToXML(rssFeed)
|
||||||
|
jsonFeed.NextUrl = "https://www.example.com/feed.json?page=2"
|
||||||
|
json, err := jsonFeed.ToJSON()
|
||||||
|
*/
|
||||||
|
package feeds
|
|
@ -0,0 +1,146 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Href, Rel, Type, Length string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Name, Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
Url, Title, Link string
|
||||||
|
Width, Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Enclosure struct {
|
||||||
|
Url, Length, Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Title string
|
||||||
|
Link *Link
|
||||||
|
Source *Link
|
||||||
|
Author *Author
|
||||||
|
Description string // used as description in rss, summary in atom
|
||||||
|
Id string // used as guid in rss, id in atom
|
||||||
|
IsPermaLink string // an optional parameter for guid in rss
|
||||||
|
Updated time.Time
|
||||||
|
Created time.Time
|
||||||
|
Enclosure *Enclosure
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
Title string
|
||||||
|
Link *Link
|
||||||
|
Description string
|
||||||
|
Author *Author
|
||||||
|
Updated time.Time
|
||||||
|
Created time.Time
|
||||||
|
Id string
|
||||||
|
Subtitle string
|
||||||
|
Items []*Item
|
||||||
|
Copyright string
|
||||||
|
Image *Image
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a new Item to a Feed
|
||||||
|
func (f *Feed) Add(item *Item) {
|
||||||
|
f.Items = append(f.Items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the first non-zero time formatted as a string or ""
|
||||||
|
func anyTimeFormat(format string, times ...time.Time) string {
|
||||||
|
for _, t := range times {
|
||||||
|
if !t.IsZero() {
|
||||||
|
return t.Format(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface used by ToXML to get a object suitable for exporting XML.
|
||||||
|
type XmlFeed interface {
|
||||||
|
FeedXml() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn a feed object (either a Feed, AtomFeed, or RssFeed) into xml
|
||||||
|
// returns an error if xml marshaling fails
|
||||||
|
func ToXML(feed XmlFeed) (string, error) {
|
||||||
|
x := feed.FeedXml()
|
||||||
|
data, err := xml.MarshalIndent(x, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// strip empty line from default xml header
|
||||||
|
s := xml.Header[:len(xml.Header)-1] + string(data)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteXML writes a feed object (either a Feed, AtomFeed, or RssFeed) as XML into
|
||||||
|
// the writer. Returns an error if XML marshaling fails.
|
||||||
|
func WriteXML(feed XmlFeed, w io.Writer) error {
|
||||||
|
x := feed.FeedXml()
|
||||||
|
// write default xml header, without the newline
|
||||||
|
if _, err := w.Write([]byte(xml.Header[:len(xml.Header)-1])); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e := xml.NewEncoder(w)
|
||||||
|
e.Indent("", " ")
|
||||||
|
return e.Encode(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates an Atom representation of this feed
|
||||||
|
func (f *Feed) ToAtom() (string, error) {
|
||||||
|
a := &Atom{f}
|
||||||
|
return ToXML(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAtom writes an Atom representation of this feed to the writer.
|
||||||
|
func (f *Feed) WriteAtom(w io.Writer) error {
|
||||||
|
return WriteXML(&Atom{f}, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates an Rss representation of this feed
|
||||||
|
func (f *Feed) ToRss() (string, error) {
|
||||||
|
r := &Rss{f}
|
||||||
|
return ToXML(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteRss writes an RSS representation of this feed to the writer.
|
||||||
|
func (f *Feed) WriteRss(w io.Writer) error {
|
||||||
|
return WriteXML(&Rss{f}, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON creates a JSON Feed representation of this feed
|
||||||
|
func (f *Feed) ToJSON() (string, error) {
|
||||||
|
j := &JSON{f}
|
||||||
|
return j.ToJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSON writes an JSON representation of this feed to the writer.
|
||||||
|
func (f *Feed) WriteJSON(w io.Writer) error {
|
||||||
|
j := &JSON{f}
|
||||||
|
feed := j.JSONFeed()
|
||||||
|
|
||||||
|
e := json.NewEncoder(w)
|
||||||
|
e.SetIndent("", " ")
|
||||||
|
return e.Encode(feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort sorts the Items in the feed with the given less function.
|
||||||
|
func (f *Feed) Sort(less func(a, b *Item) bool) {
|
||||||
|
lessFunc := func(i, j int) bool {
|
||||||
|
return less(f.Items[i], f.Items[j])
|
||||||
|
}
|
||||||
|
sort.SliceStable(f.Items, lessFunc)
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jsonFeedVersion = "https://jsonfeed.org/version/1.1"
|
||||||
|
|
||||||
|
// JSONAuthor represents the author of the feed or of an individual item
|
||||||
|
// in the feed
|
||||||
|
type JSONAuthor struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Url string `json:"url,omitempty"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONAttachment represents a related resource. Podcasts, for instance, would
|
||||||
|
// include an attachment that’s an audio or video file.
|
||||||
|
type JSONAttachment struct {
|
||||||
|
Url string `json:"url,omitempty"`
|
||||||
|
MIMEType string `json:"mime_type,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Size int32 `json:"size,omitempty"`
|
||||||
|
Duration time.Duration `json:"duration_in_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
// The Duration field is marshaled in seconds, all other fields are marshaled
|
||||||
|
// based upon the definitions in struct tags.
|
||||||
|
func (a *JSONAttachment) MarshalJSON() ([]byte, error) {
|
||||||
|
type EmbeddedJSONAttachment JSONAttachment
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
Duration float64 `json:"duration_in_seconds,omitempty"`
|
||||||
|
*EmbeddedJSONAttachment
|
||||||
|
}{
|
||||||
|
EmbeddedJSONAttachment: (*EmbeddedJSONAttachment)(a),
|
||||||
|
Duration: a.Duration.Seconds(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||||
|
// The Duration field is expected to be in seconds, all other field types
|
||||||
|
// match the struct definition.
|
||||||
|
func (a *JSONAttachment) UnmarshalJSON(data []byte) error {
|
||||||
|
type EmbeddedJSONAttachment JSONAttachment
|
||||||
|
var raw struct {
|
||||||
|
Duration float64 `json:"duration_in_seconds,omitempty"`
|
||||||
|
*EmbeddedJSONAttachment
|
||||||
|
}
|
||||||
|
raw.EmbeddedJSONAttachment = (*EmbeddedJSONAttachment)(a)
|
||||||
|
|
||||||
|
err := json.Unmarshal(data, &raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.Duration > 0 {
|
||||||
|
nsec := int64(raw.Duration * float64(time.Second))
|
||||||
|
raw.EmbeddedJSONAttachment.Duration = time.Duration(nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONItem represents a single entry/post for the feed.
|
||||||
|
type JSONItem struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Url string `json:"url,omitempty"`
|
||||||
|
ExternalUrl string `json:"external_url,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
ContentHTML string `json:"content_html,omitempty"`
|
||||||
|
ContentText string `json:"content_text,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
BannerImage string `json:"banner_,omitempty"`
|
||||||
|
PublishedDate *time.Time `json:"date_published,omitempty"`
|
||||||
|
ModifiedDate *time.Time `json:"date_modified,omitempty"`
|
||||||
|
Author *JSONAuthor `json:"author,omitempty"` // deprecated in JSON Feed v1.1, keeping for backwards compatibility
|
||||||
|
Authors []*JSONAuthor `json:"authors,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
Attachments []JSONAttachment `json:"attachments,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONHub describes an endpoint that can be used to subscribe to real-time
|
||||||
|
// notifications from the publisher of this feed.
|
||||||
|
type JSONHub struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONFeed represents a syndication feed in the JSON Feed Version 1 format.
|
||||||
|
// Matching the specification found here: https://jsonfeed.org/version/1.
|
||||||
|
type JSONFeed struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Language string `json:"language,omitempty"`
|
||||||
|
HomePageUrl string `json:"home_page_url,omitempty"`
|
||||||
|
FeedUrl string `json:"feed_url,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
UserComment string `json:"user_comment,omitempty"`
|
||||||
|
NextUrl string `json:"next_url,omitempty"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
|
Favicon string `json:"favicon,omitempty"`
|
||||||
|
Author *JSONAuthor `json:"author,omitempty"` // deprecated in JSON Feed v1.1, keeping for backwards compatibility
|
||||||
|
Authors []*JSONAuthor `json:"authors,omitempty"`
|
||||||
|
Expired *bool `json:"expired,omitempty"`
|
||||||
|
Hubs []*JSONHub `json:"hubs,omitempty"`
|
||||||
|
Items []*JSONItem `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON is used to convert a generic Feed to a JSONFeed.
|
||||||
|
type JSON struct {
|
||||||
|
*Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
|
||||||
|
func (f *JSON) ToJSON() (string, error) {
|
||||||
|
return f.JSONFeed().ToJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
|
||||||
|
func (f *JSONFeed) ToJSON() (string, error) {
|
||||||
|
data, err := json.MarshalIndent(f, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONFeed creates a new JSONFeed with a generic Feed struct's data.
|
||||||
|
func (f *JSON) JSONFeed() *JSONFeed {
|
||||||
|
feed := &JSONFeed{
|
||||||
|
Version: jsonFeedVersion,
|
||||||
|
Title: f.Title,
|
||||||
|
Description: f.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Link != nil {
|
||||||
|
feed.HomePageUrl = f.Link.Href
|
||||||
|
}
|
||||||
|
if f.Author != nil {
|
||||||
|
author := &JSONAuthor{
|
||||||
|
Name: f.Author.Name,
|
||||||
|
}
|
||||||
|
feed.Author = author
|
||||||
|
feed.Authors = []*JSONAuthor{author}
|
||||||
|
}
|
||||||
|
for _, e := range f.Items {
|
||||||
|
feed.Items = append(feed.Items, newJSONItem(e))
|
||||||
|
}
|
||||||
|
return feed
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJSONItem(i *Item) *JSONItem {
|
||||||
|
item := &JSONItem{
|
||||||
|
Id: i.Id,
|
||||||
|
Title: i.Title,
|
||||||
|
Summary: i.Description,
|
||||||
|
|
||||||
|
ContentHTML: i.Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Link != nil {
|
||||||
|
item.Url = i.Link.Href
|
||||||
|
}
|
||||||
|
if i.Source != nil {
|
||||||
|
item.ExternalUrl = i.Source.Href
|
||||||
|
}
|
||||||
|
if i.Author != nil {
|
||||||
|
author := &JSONAuthor{
|
||||||
|
Name: i.Author.Name,
|
||||||
|
}
|
||||||
|
item.Author = author
|
||||||
|
item.Authors = []*JSONAuthor{author}
|
||||||
|
}
|
||||||
|
if !i.Created.IsZero() {
|
||||||
|
item.PublishedDate = &i.Created
|
||||||
|
}
|
||||||
|
if !i.Updated.IsZero() {
|
||||||
|
item.ModifiedDate = &i.Updated
|
||||||
|
}
|
||||||
|
if i.Enclosure != nil && strings.HasPrefix(i.Enclosure.Type, "image/") {
|
||||||
|
item.Image = i.Enclosure.Url
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
// rss support
|
||||||
|
// validation done according to spec here:
|
||||||
|
// http://cyber.law.harvard.edu/rss/rss.html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// private wrapper around the RssFeed which gives us the <rss>..</rss> xml
|
||||||
|
type RssFeedXml struct {
|
||||||
|
XMLName xml.Name `xml:"rss"`
|
||||||
|
Version string `xml:"version,attr"`
|
||||||
|
ContentNamespace string `xml:"xmlns:content,attr"`
|
||||||
|
Channel *RssFeed
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssContent struct {
|
||||||
|
XMLName xml.Name `xml:"content:encoded"`
|
||||||
|
Content string `xml:",cdata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssImage struct {
|
||||||
|
XMLName xml.Name `xml:"image"`
|
||||||
|
Url string `xml:"url"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Width int `xml:"width,omitempty"`
|
||||||
|
Height int `xml:"height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssTextInput struct {
|
||||||
|
XMLName xml.Name `xml:"textInput"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssFeed struct {
|
||||||
|
XMLName xml.Name `xml:"channel"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Link string `xml:"link"` // required
|
||||||
|
Description string `xml:"description"` // required
|
||||||
|
Language string `xml:"language,omitempty"`
|
||||||
|
Copyright string `xml:"copyright,omitempty"`
|
||||||
|
ManagingEditor string `xml:"managingEditor,omitempty"` // Author used
|
||||||
|
WebMaster string `xml:"webMaster,omitempty"`
|
||||||
|
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||||
|
LastBuildDate string `xml:"lastBuildDate,omitempty"` // updated used
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Generator string `xml:"generator,omitempty"`
|
||||||
|
Docs string `xml:"docs,omitempty"`
|
||||||
|
Cloud string `xml:"cloud,omitempty"`
|
||||||
|
Ttl int `xml:"ttl,omitempty"`
|
||||||
|
Rating string `xml:"rating,omitempty"`
|
||||||
|
SkipHours string `xml:"skipHours,omitempty"`
|
||||||
|
SkipDays string `xml:"skipDays,omitempty"`
|
||||||
|
Image *RssImage
|
||||||
|
TextInput *RssTextInput
|
||||||
|
Items []*RssItem `xml:"item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssItem struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Link string `xml:"link"` // required
|
||||||
|
Description string `xml:"description"` // required
|
||||||
|
Content *RssContent
|
||||||
|
Author string `xml:"author,omitempty"`
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Comments string `xml:"comments,omitempty"`
|
||||||
|
Enclosure *RssEnclosure
|
||||||
|
Guid *RssGuid // Id used
|
||||||
|
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||||
|
Source string `xml:"source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssEnclosure struct {
|
||||||
|
//RSS 2.0 <enclosure url="http://example.com/file.mp3" length="123456789" type="audio/mpeg" />
|
||||||
|
XMLName xml.Name `xml:"enclosure"`
|
||||||
|
Url string `xml:"url,attr"`
|
||||||
|
Length string `xml:"length,attr"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssGuid struct {
|
||||||
|
//RSS 2.0 <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid>
|
||||||
|
XMLName xml.Name `xml:"guid"`
|
||||||
|
Id string `xml:",chardata"`
|
||||||
|
IsPermaLink string `xml:"isPermaLink,attr,omitempty"` // "true", "false", or an empty string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rss struct {
|
||||||
|
*Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new RssItem with a generic Item struct's data
|
||||||
|
func newRssItem(i *Item) *RssItem {
|
||||||
|
item := &RssItem{
|
||||||
|
Title: i.Title,
|
||||||
|
Description: i.Description,
|
||||||
|
PubDate: anyTimeFormat(time.RFC1123Z, i.Created, i.Updated),
|
||||||
|
}
|
||||||
|
if i.Id != "" {
|
||||||
|
item.Guid = &RssGuid{Id: i.Id, IsPermaLink: i.IsPermaLink}
|
||||||
|
}
|
||||||
|
if i.Link != nil {
|
||||||
|
item.Link = i.Link.Href
|
||||||
|
}
|
||||||
|
if len(i.Content) > 0 {
|
||||||
|
item.Content = &RssContent{Content: i.Content}
|
||||||
|
}
|
||||||
|
if i.Source != nil {
|
||||||
|
item.Source = i.Source.Href
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a closure
|
||||||
|
if i.Enclosure != nil && i.Enclosure.Type != "" && i.Enclosure.Length != "" {
|
||||||
|
item.Enclosure = &RssEnclosure{Url: i.Enclosure.Url, Type: i.Enclosure.Type, Length: i.Enclosure.Length}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Author != nil {
|
||||||
|
item.Author = i.Author.Name
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new RssFeed with a generic Feed struct's data
|
||||||
|
func (r *Rss) RssFeed() *RssFeed {
|
||||||
|
pub := anyTimeFormat(time.RFC1123Z, r.Created, r.Updated)
|
||||||
|
build := anyTimeFormat(time.RFC1123Z, r.Updated)
|
||||||
|
author := ""
|
||||||
|
if r.Author != nil {
|
||||||
|
author = r.Author.Email
|
||||||
|
if len(r.Author.Name) > 0 {
|
||||||
|
author = fmt.Sprintf("%s (%s)", r.Author.Email, r.Author.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var image *RssImage
|
||||||
|
if r.Image != nil {
|
||||||
|
image = &RssImage{Url: r.Image.Url, Title: r.Image.Title, Link: r.Image.Link, Width: r.Image.Width, Height: r.Image.Height}
|
||||||
|
}
|
||||||
|
|
||||||
|
var href string
|
||||||
|
if r.Link != nil {
|
||||||
|
href = r.Link.Href
|
||||||
|
}
|
||||||
|
channel := &RssFeed{
|
||||||
|
Title: r.Title,
|
||||||
|
Link: href,
|
||||||
|
Description: r.Description,
|
||||||
|
ManagingEditor: author,
|
||||||
|
PubDate: pub,
|
||||||
|
LastBuildDate: build,
|
||||||
|
Copyright: r.Copyright,
|
||||||
|
Image: image,
|
||||||
|
}
|
||||||
|
for _, i := range r.Items {
|
||||||
|
channel.Items = append(channel.Items, newRssItem(i))
|
||||||
|
}
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedXml returns an XML-Ready object for an Rss object
|
||||||
|
func (r *Rss) FeedXml() interface{} {
|
||||||
|
// only generate version 2.0 feeds for now
|
||||||
|
return r.RssFeed().FeedXml()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedXml returns an XML-ready object for an RssFeed object
|
||||||
|
func (r *RssFeed) FeedXml() interface{} {
|
||||||
|
return &RssFeedXml{
|
||||||
|
Version: "2.0",
|
||||||
|
Channel: r,
|
||||||
|
ContentNamespace: "http://purl.org/rss/1.0/modules/content/",
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<title><![CDATA[Lorem ipsum feed for an interval of 1 minutes]]></title>
|
||||||
|
<description><![CDATA[This is a constantly updating lorem ipsum feed]]></description>
|
||||||
|
<link>http://example.com/</link>
|
||||||
|
<generator>RSS for Node</generator>
|
||||||
|
<lastBuildDate>Tue, 30 Oct 2018 23:22:37 GMT</lastBuildDate>
|
||||||
|
<author><![CDATA[John Smith]]></author>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
|
||||||
|
<copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright>
|
||||||
|
<ttl>60</ttl>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:22:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Exercitation ut Lorem sint proident.]]></description>
|
||||||
|
<link>http://example.com/test/1540941720</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941720</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:21:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Ea est do quis fugiat exercitation.]]></description>
|
||||||
|
<link>http://example.com/test/1540941660</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941660</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:21:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:20:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Ipsum velit cillum ad laborum sit nulla exercitation consequat sint veniam culpa veniam voluptate incididunt.]]></description>
|
||||||
|
<link>http://example.com/test/1540941600</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941600</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:20:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:19:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Ullamco pariatur aliqua consequat ea veniam id qui incididunt laborum.]]></description>
|
||||||
|
<link>http://example.com/test/1540941540</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941540</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:19:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:18:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Velit proident aliquip aliquip anim mollit voluptate laboris voluptate et occaecat occaecat laboris ea nulla.]]></description>
|
||||||
|
<link>http://example.com/test/1540941480</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941480</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:18:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:17:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Do in quis mollit consequat id in minim laborum sint exercitation laborum elit officia.]]></description>
|
||||||
|
<link>http://example.com/test/1540941420</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941420</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:17:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:16:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Irure id sint ullamco Lorem magna consectetur officia adipisicing duis incididunt.]]></description>
|
||||||
|
<link>http://example.com/test/1540941360</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941360</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:16:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:15:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Sunt anim excepteur esse nisi commodo culpa laborum exercitation ad anim ex elit.]]></description>
|
||||||
|
<link>http://example.com/test/1540941300</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941300</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:15:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:14:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Excepteur aliquip fugiat ex labore nisi.]]></description>
|
||||||
|
<link>http://example.com/test/1540941240</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941240</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:14:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:13:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Id proident adipisicing proident pariatur aute pariatur pariatur dolor dolor in voluptate dolor.]]></description>
|
||||||
|
<link>http://example.com/test/1540941180</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941180</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:13:00 GMT</pubDate>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title><![CDATA[Lorem ipsum feed for an interval of 1 minutes]]></title>
|
||||||
|
<description><![CDATA[This is a constantly updating lorem ipsum feed]]></description>
|
||||||
|
<link>http://example.com/</link>
|
||||||
|
<generator>RSS for Node</generator>
|
||||||
|
<lastBuildDate>Tue, 30 Oct 2018 23:22:37 GMT</lastBuildDate>
|
||||||
|
<author><![CDATA[John Smith]]></author>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
|
||||||
|
<copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright>
|
||||||
|
<ttl>60</ttl>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:22:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Exercitation ut Lorem sint proident.]]></description>
|
||||||
|
<link>http://example.com/test/1540941720</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941720</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:21:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Ea est do quis fugiat exercitation.]]></description>
|
||||||
|
<link>http://example.com/test/1540941660</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941660</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:21:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:20:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Ipsum velit cillum ad laborum sit nulla exercitation consequat sint veniam culpa veniam voluptate incididunt.]]></description>
|
||||||
|
<link>http://example.com/test/1540941600</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941600</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:20:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:19:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Ullamco pariatur aliqua consequat ea veniam id qui incididunt laborum.]]></description>
|
||||||
|
<link>http://example.com/test/1540941540</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941540</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:19:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:18:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Velit proident aliquip aliquip anim mollit voluptate laboris voluptate et occaecat occaecat laboris ea nulla.]]></description>
|
||||||
|
<link>http://example.com/test/1540941480</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941480</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:18:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:17:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Do in quis mollit consequat id in minim laborum sint exercitation laborum elit officia.]]></description>
|
||||||
|
<link>http://example.com/test/1540941420</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941420</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:17:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:16:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Irure id sint ullamco Lorem magna consectetur officia adipisicing duis incididunt.]]></description>
|
||||||
|
<link>http://example.com/test/1540941360</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941360</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:16:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:15:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Sunt anim excepteur esse nisi commodo culpa laborum exercitation ad anim ex elit.]]></description>
|
||||||
|
<link>http://example.com/test/1540941300</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941300</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:15:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:14:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Excepteur aliquip fugiat ex labore nisi.]]></description>
|
||||||
|
<link>http://example.com/test/1540941240</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941240</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:14:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title><![CDATA[Lorem ipsum 2018-10-30T23:13:00+00:00]]></title>
|
||||||
|
<description><![CDATA[Id proident adipisicing proident pariatur aute pariatur pariatur dolor dolor in voluptate dolor.]]></description>
|
||||||
|
<link>http://example.com/test/1540941180</link>
|
||||||
|
<guid isPermaLink="true">http://example.com/test/1540941180</guid>
|
||||||
|
<dc:creator><![CDATA[John Smith]]></dc:creator>
|
||||||
|
<pubDate>Tue, 30 Oct 2018 23:13:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
|
@ -0,0 +1,20 @@
|
||||||
|
[Full iTunes list](https://help.apple.com/itc/podcasts_connect/#/itcb54353390)
|
||||||
|
|
||||||
|
[Example of ideal iTunes RSS feed](https://help.apple.com/itc/podcasts_connect/#/itcbaf351599)
|
||||||
|
|
||||||
|
```
|
||||||
|
<itunes:author>
|
||||||
|
<itunes:block>
|
||||||
|
<itunes:catergory>
|
||||||
|
<itunes:image>
|
||||||
|
<itunes:duration>
|
||||||
|
<itunes:explicit>
|
||||||
|
<itunes:isClosedCaptioned>
|
||||||
|
<itunes:order>
|
||||||
|
<itunes:complete>
|
||||||
|
<itunes:new-feed-url>
|
||||||
|
<itunes:owner>
|
||||||
|
<itunes:subtitle>
|
||||||
|
<itunes:summary>
|
||||||
|
<language>
|
||||||
|
```
|
|
@ -0,0 +1,27 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
// relevant bits from https://github.com/abneptis/GoUUID/blob/master/uuid.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UUID [16]byte
|
||||||
|
|
||||||
|
// create a new uuid v4
|
||||||
|
func NewUUID() *UUID {
|
||||||
|
u := &UUID{}
|
||||||
|
_, err := rand.Read(u[:16])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u[8] = (u[8] | 0x80) & 0xBf
|
||||||
|
u[6] = (u[6] | 0x40) & 0x4f
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UUID) String() string {
|
||||||
|
return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:])
|
||||||
|
}
|
|
@ -4,3 +4,6 @@ github.com/dhowden/tag
|
||||||
# github.com/google/uuid v1.6.0
|
# github.com/google/uuid v1.6.0
|
||||||
## explicit
|
## explicit
|
||||||
github.com/google/uuid
|
github.com/google/uuid
|
||||||
|
# github.com/gorilla/feeds v1.2.0
|
||||||
|
## explicit; go 1.20
|
||||||
|
github.com/gorilla/feeds
|
||||||
|
|
Loading…
Reference in New Issue