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