├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── commands ├── build.go ├── init.go ├── new.go ├── podgen.go ├── push.go └── server.go ├── main.go ├── scripts ├── build.sh ├── dist.sh ├── test.sh ├── verify_no_uuid.sh ├── website_push.sh └── windows │ ├── build.bat │ └── verify_no_uuid.bat └── utils ├── error.go ├── file.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.yml 26 | podgen 27 | .idea 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | opyright (c) 2015 Tyr Chen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DEPS = $(shell go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) 2 | PACKAGES = $(shell go list ./...) 3 | VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods \ 4 | -nilfunc -printf -rangeloops -shift -structtags -unsafeptr 5 | 6 | all: deps format 7 | @mkdir -p bin/ 8 | @bash --norc -i ./scripts/build.sh 9 | 10 | cov: 11 | gocov test ./... | gocov-html > /tmp/coverage.html 12 | open /tmp/coverage.html 13 | 14 | deps: 15 | @echo "--> Installing build dependencies" 16 | @go get -d -v ./... $(DEPS) 17 | 18 | updatedeps: deps 19 | @echo "--> Updating build dependencies" 20 | @go get -d -f -u ./... $(DEPS) 21 | 22 | test: deps 23 | @./scripts/verify_no_uuid.sh 24 | @./scripts/test.sh 25 | @$(MAKE) vet 26 | 27 | integ: 28 | go list ./... | INTEG_TESTS=yes xargs -n1 go test 29 | 30 | cover: deps 31 | ./scripts/verify_no_uuid.sh 32 | go list ./... | xargs -n1 go test --cover 33 | 34 | format: deps 35 | @echo "--> Running go fmt" 36 | @go fmt $(PACKAGES) 37 | 38 | vet: 39 | @go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ 40 | go get golang.org/x/tools/cmd/vet; \ 41 | fi 42 | @echo "--> Running go tool vet $(VETARGS) ." 43 | @go tool vet $(VETARGS) . ; if [ $$? -eq 1 ]; then \ 44 | echo ""; \ 45 | echo "Vet found suspicious constructs. Please check the reported constructs"; \ 46 | echo "and fix them if necessary before submitting the code for reviewal."; \ 47 | fi 48 | 49 | web: 50 | ./scripts/website_run.sh 51 | 52 | web-push: 53 | ./scripts/website_push.sh 54 | 55 | .PHONY: all cov deps integ test vet web web-push test-nodep 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # podgen 2 | 3 | Podgen is a tool for statically generate a podcast site with itunes enabled rss. After putting your generated online, you could register your rss against itunes podcast so that your awesome voice could be reached everywhere. 4 | 5 | This project is highly inspired by go projects from hashicorp. The ``./scripts`` is from **consul** and the CLI is modified based on **vault** (this is not true anymore, now the CLI is using github.com/spf13/cobra). 6 | 7 | ## Installation 8 | 9 | For Mac OSX, the easiest way to download the current [release](https://github.com/tyrchen/podgen/releases/latest) from [tyrchen/podgen release](https://github.com/tyrchen/podgen/releases). 10 | 11 | Later on we will support homebrew so that you could install directly with: 12 | 13 | ``` 14 | $ brew install podgen 15 | ``` 16 | 17 | For linux user, please download source code and compile it yourself. 18 | 19 | podgen doesn't support windows at this stage. I will finish all the functionalities then consider windows support. Sorry. 20 | 21 | ## Usage 22 | 23 | To use podgen, first of all, you need to init a site. Please create a repo in github and close it into your local directory: 24 | 25 | ``` 26 | $ git clone git@github.com:yourname/xyz.git 27 | $ cd xyz 28 | $ podgen init [--template github.com/tyrchen/podgen-basic] 29 | ``` 30 | 31 | [note] If you don't pass ``--template`` to the ``init`` command, it will use the ``tyrchen/podgen-basic`` template from github. 32 | 33 | It will create configuration files for you to use: 34 | 35 | ``` 36 | $ ls 37 | build CNAME channel.yml items.yml assets template 38 | ``` 39 | 40 | And it will create a ``gh-pages`` branch to store the build target. Just same as any other github powered static site. 41 | 42 | Then edit ``channel.yml`` to put your channel information and add your podcast items into ``items.yml``. Finally copy your mp3 into ``assets`` folder. You're almost set! Ah, if you want to use it against your customer domain, set the ``CNAME`` file! 43 | 44 | The last step is to build the project: 45 | 46 | ``` 47 | $ podgen build 48 | ``` 49 | 50 | Build should be done very quickly. It will generate the podcast site and rss, put them into ``build``, then push all the changes under ``build`` to ``gh-pages``. You shall then see the generated site in less than a minute. 51 | 52 | If everything looks fine. You just need to issue "podgen push" to push the source and generated html to ``master`` and ``gh-pages``. 53 | 54 | ``` 55 | $ podgen push -m "add a new episode" 56 | ``` 57 | 58 | Meanwhile, your itunes podcast app shall get the latest rss. Try it and have fun! 59 | 60 | If later on you want to add new episode, just run `podgen new` and it will create new items in your `items.yml`. Just change the title/description and put your music file into `assets` directory and you're all set! 61 | 62 | Feel free to view a live demo of my podcast site: [programmer life](http://podcast.tchen.me). It is also available in iTunes/podcast. 63 | -------------------------------------------------------------------------------- /commands/build.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "io/ioutil" 7 | "log" 8 | "math" 9 | "os" 10 | "strings" 11 | 12 | "gopkg.in/yaml.v2" 13 | 14 | "github.com/codeskyblue/go-sh" 15 | uuid "github.com/satori/go.uuid" 16 | "github.com/spf13/cobra" 17 | "github.com/tyrchen/gopod" 18 | 19 | "time" 20 | 21 | "github.com/tyrchen/podgen/utils" 22 | ) 23 | 24 | type Item struct { 25 | Title string 26 | Description string 27 | Link string 28 | Pubdate string 29 | Image string 30 | Guid string 31 | } 32 | 33 | type Channel struct { 34 | Title string 35 | Link string 36 | Description string 37 | Image string 38 | Copyright string 39 | Language string 40 | Author string 41 | Categories []string 42 | Page int 43 | Twitter string 44 | Linkedin string 45 | Github string 46 | } 47 | 48 | type PageTemplate struct { 49 | Info Channel 50 | Home string 51 | Current Item 52 | Podcasts []Item 53 | Paginator template.HTML 54 | } 55 | 56 | var generated_guid bool 57 | 58 | var buildCmd = &cobra.Command{ 59 | Use: "build", 60 | Short: "build the podcast site", 61 | Long: `build the podcast site, generate html files against template`, 62 | Run: func(cmd *cobra.Command, args []string) { 63 | execute() 64 | }, 65 | } 66 | 67 | // command implementation 68 | 69 | func execute() { 70 | generatePages() 71 | session := sh.NewSession() 72 | cpFiles(session, ".", TARGET_PATH, "assets", "CNAME") 73 | cpFiles(session, TEMPLATE_PATH, TARGET_PATH, "css", "font-awesome", "fonts", "img", "js") 74 | if generated_guid { 75 | log.Println("Build finished. Please copy guid: to your items.yml for each episode.") 76 | } else { 77 | log.Println("Build finished.") 78 | } 79 | } 80 | 81 | func generatePages() { 82 | channel := getChannelData("channel.yml") 83 | items := getItemData("items.yml") 84 | 85 | current := items[0] 86 | chopped_items := chopItems(items, channel.Page) 87 | len_chopped_items := len(chopped_items) 88 | 89 | generateRss(channel, items) 90 | 91 | pages := make([]int, len_chopped_items) 92 | for i := 1; i <= len_chopped_items; i++ { 93 | pages[i-1] = i 94 | } 95 | 96 | data, _ := ioutil.ReadFile(fmt.Sprintf("%s/index.tmpl", TEMPLATE_PATH)) 97 | content := string(data[:]) 98 | funcs := template.FuncMap{"alt": alt, "trunc": truncate} 99 | t := template.Must(template.New("Podgen").Funcs(funcs).Parse(content)) 100 | 101 | for i := 1; i <= len_chopped_items; i++ { 102 | var filename string 103 | var home string 104 | if i == 1 { 105 | filename = "index.html" 106 | home = "#current" 107 | } else { 108 | filename = fmt.Sprintf("page%d.html", i) 109 | home = "index.html" 110 | } 111 | f, err := os.Create(fmt.Sprintf("%s/%s", TARGET_PATH, filename)) 112 | utils.CheckError(err) 113 | defer f.Close() 114 | 115 | err = t.Execute(f, PageTemplate{ 116 | Info: channel, 117 | Home: home, 118 | Current: current, 119 | Podcasts: chopped_items[i-1], 120 | Paginator: generatePaginator(i, len_chopped_items), 121 | }) 122 | utils.CheckError(err) 123 | } 124 | } 125 | 126 | func generateRss(channel Channel, items []Item) { 127 | imageUrl := utils.Urljoin(channel.Link, ASSETS_PATH, channel.Image) 128 | c := gopod.ChannelFactory(channel.Title, channel.Link, channel.Description, imageUrl) 129 | c.SetiTunesExplicit("No") 130 | c.SetCopyright(channel.Copyright) 131 | c.SetiTunesAuthor(channel.Author) 132 | c.SetiTunesSummary(channel.Description) 133 | c.SetCategory(strings.Join(channel.Categories, ",")) 134 | c.SetLanguage(channel.Language) 135 | 136 | for _, item := range items { 137 | url := utils.Urljoin(c.Link, ASSETS_PATH, item.Link) 138 | enclosure := gopod.Enclosure{ 139 | Url: url, 140 | Length: "0", 141 | Type: "audio/mpeg", 142 | } 143 | pubdate, err := time.Parse(time.RFC3339, item.Pubdate) 144 | utils.CheckError(err) 145 | var guid string 146 | description := truncate(item.Description) 147 | if item.Guid != "" { 148 | guid = item.Guid 149 | } else { 150 | generated_guid = true 151 | id, _ := uuid.NewV4() 152 | guid = id.String() 153 | log.Printf("%s - guid: %s\n", item.Link, guid) 154 | 155 | } 156 | 157 | episode := gopod.Item{ 158 | Title: item.Title, 159 | Link: url, 160 | Description: description, 161 | PubDate: pubdate.Format(time.RFC1123), 162 | Guid: guid, 163 | TunesAuthor: channel.Author, 164 | TunesSubtitle: description, 165 | TunesSummary: description, 166 | Enclosure: []*gopod.Enclosure{&enclosure}, 167 | } 168 | 169 | episode.SetTunesImage(utils.Urljoin(channel.Link, ASSETS_PATH, item.Image)) 170 | 171 | c.AddItem(&episode) 172 | } 173 | f, err := os.Create(fmt.Sprintf("%s/%s", TARGET_PATH, "rss.xml")) 174 | utils.CheckError(err) 175 | defer f.Close() 176 | f.Write(c.Publish()) 177 | } 178 | 179 | func getChannelData(filename string) (channel Channel) { 180 | data, err := ioutil.ReadFile(filename) 181 | if err != nil { 182 | log.Fatalf("Cannot read file %s (%s)", filename, err) 183 | } 184 | err = yaml.Unmarshal(data, &channel) 185 | if err != nil { 186 | log.Fatalf("Cannot parse file %s (%s)", filename, err) 187 | } 188 | return 189 | } 190 | 191 | func getItemData(filename string) (items []Item) { 192 | 193 | data, err := ioutil.ReadFile(filename) 194 | if err != nil { 195 | log.Fatalf("Cannot read file %s (%s)", filename, err) 196 | } 197 | err = yaml.Unmarshal(data, &items) 198 | if err != nil { 199 | log.Fatalf("Cannot parse file %s (%s)", filename, err) 200 | } 201 | 202 | // ugly reverse - I'm seeking something like list.reverse() in python but not found. 203 | for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 { 204 | items[i], items[j] = items[j], items[i] 205 | } 206 | 207 | return 208 | } 209 | 210 | func chopItems(items []Item, page int) (chopped_items [][]Item) { 211 | 212 | length := len(items) 213 | j := 0 214 | for i := 1; i < length; i += page { 215 | chopped_items = append(chopped_items, items[i:int(math.Min(float64(i+page), float64(length)))]) 216 | j += 1 217 | } 218 | return 219 | } 220 | 221 | func alt(x int) string { 222 | if x%2 == 0 { 223 | return "a" 224 | } else { 225 | return "b" 226 | } 227 | } 228 | 229 | func truncate(str string) string { 230 | data := []rune(str) 231 | if len(data) <= MAX_DESCRIPTION { 232 | return str 233 | } else { 234 | return string(data[:MAX_DESCRIPTION-1]) + "..." 235 | } 236 | } 237 | 238 | // I cannot bear the golang template to do such a not-that-complicated paginator, 239 | // thus I just do string concat myself...please tell me an elegant way to do so!! 240 | func generatePaginator(curPage int, maxPage int) template.HTML { 241 | var data []string 242 | var pageName string 243 | var css_class string 244 | if curPage == 1 { 245 | data = append(data, "
  • «
  • ") 246 | } else { 247 | if curPage-1 == 1 { 248 | pageName = "index.html" 249 | } else { 250 | pageName = fmt.Sprintf("page%d.html", (curPage - 1)) 251 | } 252 | data = append(data, fmt.Sprintf("
  • «
  • ", pageName)) 253 | } 254 | for i := 1; i <= maxPage; i++ { 255 | if i == curPage { 256 | css_class = "active" 257 | } else { 258 | css_class = "" 259 | } 260 | 261 | if i == 1 { 262 | pageName = "index.html" 263 | } else { 264 | pageName = fmt.Sprintf("page%d.html", i) 265 | } 266 | data = append(data, fmt.Sprintf("
  • %d
  • ", css_class, pageName, i)) 267 | } 268 | 269 | if curPage == maxPage { 270 | data = append(data, "
  • »
  • ") 271 | } else { 272 | data = append(data, fmt.Sprintf("
  • »
  • ", (curPage+1))) 273 | } 274 | 275 | return template.HTML(strings.Join(data, "\n")) 276 | } 277 | -------------------------------------------------------------------------------- /commands/init.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/codeskyblue/go-sh" 10 | "github.com/spf13/cobra" 11 | "github.com/tcnksm/go-gitconfig" 12 | 13 | "github.com/tyrchen/podgen/utils" 14 | ) 15 | 16 | var ( 17 | template_repo string 18 | originUrl string 19 | FILES_TO_CHECK = []string{"channel.yml", "items.yml", "build", ASSETS_PATH, TEMPLATE_PATH, "CNAME"} 20 | ) 21 | 22 | var initCmd = &cobra.Command{ 23 | Use: "init", 24 | Short: "Initialize a new podcast site on current directory", 25 | Long: `Initialize a new podcast site on current directiory. 26 | Configuration files and site `, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | originUrl = getOriginUrl() 29 | log.Printf("Current repo is %s, You're using template: %s\n", originUrl, template_repo) 30 | if !utils.Exists("./.git") { 31 | log.Printf("'.git' is not found. Please create an empty github repo, clone it to you local directory and then run this command under the directory.") 32 | os.Exit(-1) 33 | } 34 | 35 | for _, filename := range FILES_TO_CHECK { 36 | if utils.Exists(filename) { 37 | log.Printf("Hmm...found existing '%s' - seems you're on an already initialized podcast directory. I cannot init it again.", filename) 38 | os.Exit(-1) 39 | } 40 | } 41 | getTemplate() 42 | createGhPages() 43 | 44 | log.Println("\nCongratulations, your podcast site is ready to use. Please modify the *.yml files and try to 'podgen build' your site!\n") 45 | }, 46 | } 47 | 48 | func init() { 49 | initCmd.Flags().StringVarP(&template_repo, "template", "t", DEFAULT_TMPL, "Content type to create") 50 | } 51 | 52 | // command implementation 53 | 54 | func getTemplate() { 55 | session := sh.NewSession() 56 | session.Command("git", "clone", "--depth=1", template_repo, TEMPLATE_PATH).Run() 57 | removeFiles(session, ".git") 58 | mvFiles(session, "channel.yml", "items.yml", ASSETS_PATH, ".gitignore", "CNAME") 59 | gitCommit(session, "initial podcast site", "master", true) 60 | } 61 | 62 | func createGhPages() { 63 | session := sh.NewSession() 64 | session.Command("git", "checkout", "--orphan", GH_PAGES).Run() 65 | session.Command("git", "rm", "-rf", ".").Run() 66 | session.Command("touch", "index.html").Run() 67 | 68 | gitCommit(session, "initial podcast site", "gh-pages", true) 69 | 70 | session.Command("git", "checkout", "master").Run() 71 | 72 | session.Command("git", "clone", "-b", GH_PAGES, originUrl, "build").Run() 73 | 74 | cpFiles(session, TEMPLATE_PATH, TARGET_PATH, "css", "font-awesome", "fonts", "img", "js") 75 | } 76 | 77 | func removeFiles(session *sh.Session, files ...string) { 78 | for _, filename := range files { 79 | session.Command("rm", "-rf", fmt.Sprintf("%s/%s", TEMPLATE_PATH, filename)).Run() 80 | } 81 | } 82 | 83 | func mvFiles(session *sh.Session, files ...string) { 84 | for _, filename := range files { 85 | session.Command("mv", fmt.Sprintf("%s/%s", TEMPLATE_PATH, filename), ".").Run() 86 | } 87 | } 88 | 89 | func cpFiles(session *sh.Session, src string, dest string, files ...string) { 90 | for _, filename := range files { 91 | session.Command("cp", "-r", fmt.Sprintf("%s/%s", src, filename), dest).Run() 92 | } 93 | } 94 | 95 | func gitCommit(session *sh.Session, message string, branch string, setUpstream bool) { 96 | session.Command("git", "add", ".").Run() 97 | session.Command("git", "commit", "-a", "-m", message).Run() 98 | if setUpstream { 99 | session.Command("git", "push", "-u", "origin", branch).Run() 100 | } else { 101 | session.Command("git", "push", "origin", branch).Run() 102 | } 103 | 104 | } 105 | 106 | func getOriginUrl() string { 107 | originUrl, err := gitconfig.OriginURL() 108 | utils.CheckError(err, "Please make sure you're in the correct directory, see 'podgen help' for more information.") 109 | if !strings.HasPrefix(originUrl, "git@") { 110 | log.Printf("Please clone the repo with SSL clone URL. Otherwise the repo cannot be modified (Origin url is %s in .git/config)\n", originUrl) 111 | os.Exit(-1) 112 | } 113 | return originUrl 114 | } 115 | -------------------------------------------------------------------------------- /commands/new.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | uuid "github.com/satori/go.uuid" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/tyrchen/podgen/utils" 13 | ) 14 | 15 | const ( 16 | ITEM_TEMPLATE = ` 17 | - title: change_me 18 | link: chang_me.mp3 19 | image: change_me.png 20 | description: > 21 | change_me 22 | pubdate: %s 23 | guid: %s 24 | ` 25 | ) 26 | 27 | var newCmd = &cobra.Command{ 28 | Use: "new", 29 | Short: "Generate a new item into items.yml", 30 | Long: "Create a new episode from the template, appending it to items.yml. Please edit that file", 31 | Run: func(cmd *cobra.Command, args []string) { 32 | f, err := os.OpenFile("items.yml", os.O_APPEND|os.O_WRONLY, 0600) 33 | utils.CheckError(err) 34 | 35 | defer f.Close() 36 | id, _ := uuid.NewV4() 37 | guid := id.String() 38 | pubdate := time.Now().Format(time.RFC3339) 39 | 40 | content := fmt.Sprintf(ITEM_TEMPLATE, pubdate, guid) 41 | _, err = f.WriteString(content) 42 | utils.CheckError(err) 43 | 44 | log.Println("Item info generated. Please modify items.yml.") 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /commands/podgen.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const ( 8 | DEFAULT_TMPL = "https://github.com/tyrchen/podgen-basic" 9 | TEMPLATE_PATH = "template" 10 | GH_PAGES = "gh-pages" 11 | TARGET_PATH = "build" 12 | ASSETS_PATH = "assets" 13 | MAX_DESCRIPTION = 96 14 | DEFAULT_PORT = 6060 15 | ) 16 | 17 | // Root command 18 | var PodgenCmd = &cobra.Command{ 19 | Use: "podgen", 20 | Short: "podgen builds your podcast site", 21 | Long: `podgen is the command to build your awesome podcast site 22 | 23 | podgen is a fast and flexible static site generator for podcast. If you'd like to publish a 24 | podcast in iTunes you need to host your media files yourself and provide a rss to iTunes. podgen 25 | helps you to do it quite easy with just a few commands. You don't need a server to host the files, 26 | podgen leverages the powerful github pages. 27 | 28 | Steps to create a podcast site: 29 | 30 | 1. Create a public repo in github and clone it to a directory. 31 | 2. Init a podcast site by using "podgen init", under that directory. 32 | 3. Modify the "*.yml" files and copy the images/mp3 to desired sub directory, modify CNAME file for custom domain. See https://help.github.com/articles/setting-up-a-custom-domain-with-github-pages/. 33 | 4. Build the site by using "podgen build". 34 | 5. Look and feel the site by using "podgen server" (optional). 35 | 6. Push the site by using "podgen push". 36 | 7. You're all site. Now you can browse your site and register the rss in iTunes. 37 | 38 | Next when you have new podcast you just modify "items.yml" and copy the related files. Then do 4-6. 39 | 40 | Complete documentation is available at http://github.com/tyrchen/podgen.`, 41 | } 42 | 43 | func Execute() { 44 | PodgenCmd.AddCommand(initCmd, buildCmd, serverCmd, pushCmd, newCmd) 45 | PodgenCmd.Execute() 46 | } 47 | -------------------------------------------------------------------------------- /commands/push.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/codeskyblue/go-sh" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | const ( 9 | DEFAULT_PUSH_MSG = "add new podcast." 10 | ) 11 | 12 | var ( 13 | push_msg string 14 | ) 15 | 16 | var pushCmd = &cobra.Command{ 17 | Use: "push", 18 | Short: "push the podcast site to github pages", 19 | Long: `push the podcast site to github pages. Github will host the static files for you`, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | session := sh.NewSession() 22 | gitCommit(session, push_msg, "master", true) 23 | session.SetDir("./build") 24 | gitCommit(session, push_msg, "gh-pages", true) 25 | }, 26 | } 27 | 28 | func init() { 29 | pushCmd.Flags().StringVarP(&push_msg, "message", "m", DEFAULT_PUSH_MSG, "use the given message for git log") 30 | } 31 | -------------------------------------------------------------------------------- /commands/server.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "path/filepath" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/skratchdot/open-golang/open" 14 | "github.com/spf13/cobra" 15 | "gopkg.in/fsnotify.v1" 16 | 17 | "github.com/tyrchen/podgen/utils" 18 | ) 19 | 20 | var ( 21 | port int 22 | ) 23 | 24 | var serverCmd = &cobra.Command{ 25 | Use: "server", 26 | Short: "host the generated site locally", 27 | Long: `host the generated site locally so that you could look and feel it before pushing`, 28 | Run: func(cmd *cobra.Command, args []string) { 29 | 30 | addr := fmt.Sprintf(":%d", port) 31 | url := fmt.Sprintf("http://localhost:%d/index.html", port) 32 | 33 | go func() { 34 | time.Sleep(1) 35 | log.Printf("Starting your default browser with %s\n", url) 36 | open.Start(url) 37 | }() 38 | 39 | processSignal(watchFiles()) 40 | 41 | log.Printf("Serving static content on %s\n", addr) 42 | http.ListenAndServe(addr, http.FileServer(http.Dir("./build"))) 43 | 44 | }, 45 | } 46 | 47 | func init() { 48 | serverCmd.Flags().IntVarP(&port, "port", "p", DEFAULT_PORT, "port to listen") 49 | } 50 | 51 | func processSignal(watcher *fsnotify.Watcher) { 52 | sigs := make(chan os.Signal, 1) 53 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 54 | 55 | go func() { 56 | <-sigs 57 | log.Println("Shutting down...") 58 | watcher.Close() 59 | os.Exit(-1) 60 | }() 61 | 62 | } 63 | 64 | func watchFiles() (watcher *fsnotify.Watcher) { 65 | watcher, err := fsnotify.NewWatcher() 66 | utils.CheckError(err) 67 | changes := fsnotify.Write | fsnotify.Rename | fsnotify.Create 68 | 69 | needBuild := make(chan bool, 1) 70 | 71 | go func() { 72 | modified := false 73 | for { 74 | select { 75 | case event := <-watcher.Events: 76 | if event.Op&changes != 0 { 77 | modified = true 78 | } 79 | case <-time.After(time.Second * 1): 80 | 81 | if modified { 82 | modified = false 83 | needBuild <- true 84 | } 85 | 86 | case err := <-watcher.Errors: 87 | log.Println("error:", err) 88 | } 89 | } 90 | }() 91 | 92 | go func() { 93 | for { 94 | select { 95 | case build := <-needBuild: 96 | if build { 97 | log.Println("Rebuilding the project...") 98 | execute() 99 | } 100 | } 101 | } 102 | }() 103 | 104 | watchDirs := func(path string, info os.FileInfo, err error) error { 105 | stat, err := os.Stat(path) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if stat.IsDir() { 111 | watcher.Add(path) 112 | } 113 | return nil 114 | } 115 | 116 | err = filepath.Walk("template", watchDirs) 117 | utils.CheckError(err) 118 | 119 | watcher.Add(".") 120 | utils.CheckError(err) 121 | 122 | return 123 | } 124 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | // "fmt" 5 | "log" 6 | // "os" 7 | // "time" 8 | 9 | "io/ioutil" 10 | 11 | // 12 | "github.com/tyrchen/podgen/commands" 13 | "gopkg.in/yaml.v2" 14 | "runtime" 15 | ) 16 | 17 | type Item struct { 18 | Title string 19 | Description string 20 | Link string 21 | PubDate string 22 | } 23 | 24 | type Channel struct { 25 | Title string 26 | Link string 27 | Description string 28 | Image string 29 | Copyright string 30 | Language string 31 | Author string 32 | Categories []string 33 | } 34 | 35 | func GetChannelData(filename string) (channel Channel) { 36 | data, err := ioutil.ReadFile(filename) 37 | if err != nil { 38 | log.Fatalf("Cannot read file %s (%s)", filename, err) 39 | } 40 | err = yaml.Unmarshal(data, &channel) 41 | if err != nil { 42 | log.Fatalf("Cannot parse file %s (%s)", filename, err) 43 | } 44 | return 45 | } 46 | 47 | func main() { 48 | runtime.GOMAXPROCS(runtime.NumCPU()) 49 | commands.Execute() 50 | } 51 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script builds the application from source. 4 | set -e 5 | 6 | # Get the parent directory of where this script is. 7 | SOURCE="${BASH_SOURCE[0]}" 8 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 9 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 10 | 11 | # Change into that directory 12 | cd $DIR 13 | 14 | # Get the git commit 15 | GIT_COMMIT=$(git rev-parse HEAD) 16 | GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true) 17 | GIT_DESCRIBE=$(git describe --tags) 18 | 19 | # If we're building on Windows, specify an extension 20 | EXTENSION="" 21 | if [ "$(go env GOOS)" = "windows" ]; then 22 | EXTENSION=".exe" 23 | fi 24 | 25 | GOPATHSINGLE=${GOPATH%%:*} 26 | if [ "$(go env GOOS)" = "windows" ]; then 27 | GOPATHSINGLE=${GOPATH%%;*} 28 | fi 29 | 30 | if [ "$(go env GOOS)" = "freebsd" ]; then 31 | export CC="clang" 32 | fi 33 | 34 | # On OSX, we need to use an older target to ensure binaries are 35 | # compatible with older linkers 36 | if [ "$(go env GOOS)" = "darwin" ]; then 37 | export MACOSX_DEPLOYMENT_TARGET=10.6 38 | fi 39 | 40 | # Install dependencies 41 | echo "--> Installing dependencies to speed up builds..." 42 | go get \ 43 | -ldflags "${CGO_LDFLAGS}" \ 44 | ./... 45 | 46 | # Build! 47 | echo "--> Building..." 48 | go build \ 49 | -ldflags "${CGO_LDFLAGS} -X cli.GitCommit ${GIT_COMMIT}${GIT_DIRTY} -X cli.GitDescribe ${GIT_DESCRIBE}" \ 50 | -v \ 51 | -o bin/podgen${EXTENSION} 52 | cp bin/podgen${EXTENSION} ${GOPATHSINGLE}/bin 53 | -------------------------------------------------------------------------------- /scripts/dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Get the version from the command line 5 | VERSION=$1 6 | if [ -z $VERSION ]; then 7 | echo "Please specify a version." 8 | exit 1 9 | fi 10 | 11 | # Make sure we have a bintray API key 12 | if [ -z $BINTRAY_API_KEY ]; then 13 | echo "Please set your bintray API key in the BINTRAY_API_KEY env var." 14 | exit 1 15 | fi 16 | 17 | # Get the parent directory of where this script is. 18 | SOURCE="${BASH_SOURCE[0]}" 19 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 20 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 21 | 22 | # Change into that dir because we expect that 23 | cd $DIR 24 | 25 | # Zip all the files 26 | rm -rf ./dist/pkg 27 | mkdir -p ./dist/pkg 28 | for FILENAME in $(find ./dist -mindepth 1 -maxdepth 1 -type f); do 29 | FILENAME=$(basename $FILENAME) 30 | EXTENSION="${FILENAME##*.}" 31 | PLATFORM="${FILENAME%.*}" 32 | 33 | if [ "${EXTENSION}" != "exe" ]; then 34 | EXTENSION="" 35 | else 36 | EXTENSION=".${EXTENSION}" 37 | fi 38 | 39 | CONSULNAME="consul${EXTENSION}" 40 | 41 | pushd ./dist 42 | 43 | if [ "${FILENAME}" = "ui.zip" ]; then 44 | cp ${FILENAME} ./pkg/${VERSION}_web_ui.zip 45 | else 46 | if [ "${EXTENSION}" = "" ]; then 47 | chmod +x ${FILENAME} 48 | fi 49 | 50 | cp ${FILENAME} ${CONSULNAME} 51 | zip ./pkg/${VERSION}_${PLATFORM}.zip ${CONSULNAME} 52 | rm ${CONSULNAME} 53 | fi 54 | 55 | popd 56 | done 57 | 58 | # Make the checksums 59 | pushd ./dist/pkg 60 | shasum -a256 * > ./${VERSION}_SHA256SUMS 61 | popd 62 | 63 | # Upload 64 | for ARCHIVE in ./dist/pkg/*; do 65 | ARCHIVE_NAME=$(basename ${ARCHIVE}) 66 | 67 | echo Uploading: $ARCHIVE_NAME 68 | curl \ 69 | -T ${ARCHIVE} \ 70 | -umitchellh:${BINTRAY_API_KEY} \ 71 | "https://api.bintray.com/content/mitchellh/consul/consul/${VERSION}/${ARCHIVE_NAME}" 72 | done 73 | 74 | exit 0 75 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Create a temp dir and clean it up on exit 4 | TEMPDIR=`mktemp -d -t consul-test.XXX` 5 | trap "rm -rf $TEMPDIR" EXIT HUP INT QUIT TERM 6 | 7 | # Build the Consul binary for the API tests 8 | echo "--> Building consul" 9 | go build -o $TEMPDIR/consul || exit 1 10 | 11 | # Run the tests 12 | echo "--> Running tests" 13 | go list ./... | PATH=$TEMPDIR:$PATH xargs -n1 go test 14 | -------------------------------------------------------------------------------- /scripts/verify_no_uuid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep generateUUID consul/state_store.go 4 | RESULT=$? 5 | if [ $RESULT -eq 0 ]; then 6 | exit 1 7 | fi 8 | 9 | grep generateUUID consul/fsm.go 10 | RESULT=$? 11 | if [ $RESULT -eq 0 ]; then 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /scripts/website_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the parent directory of where this script is. 4 | SOURCE="${BASH_SOURCE[0]}" 5 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 6 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 7 | 8 | # Change into that directory 9 | cd $DIR 10 | 11 | # Add the git remote if it doesn't exist 12 | git remote | grep heroku || { 13 | git remote add heroku git@heroku.com:consul-www.git 14 | } 15 | 16 | # Push the subtree (force) 17 | git push heroku `git subtree split --prefix website master`:master --force 18 | -------------------------------------------------------------------------------- /scripts/windows/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal 4 | 5 | if not exist %1 exit /B 1 6 | cd %1 7 | 8 | :: Get the git commit 9 | set _GIT_COMMIT_FILE=%TEMP%\consul-git_commit.txt 10 | set _GIT_DIRTY_FILE=%TEMP%\consul-git_dirty.txt 11 | set _GIT_DESCRIBE_FILE=%TEMP%\consul-git_describe.txt 12 | 13 | set _NUL_CMP_FILE=%TEMP%\consul-nul_cmp.txt 14 | type NUL >%_NUL_CMP_FILE% 15 | 16 | git rev-parse HEAD >%_GIT_COMMIT_FILE% 17 | set /p _GIT_COMMIT=<%_GIT_COMMIT_FILE% 18 | del /F "%_GIT_COMMIT_FILE%" 2>NUL 19 | 20 | set _GIT_DIRTY= 21 | git status --porcelain >%_GIT_DIRTY_FILE% 22 | fc %_GIT_DIRTY_FILE% %_NUL_CMP_FILE% >NUL 23 | if errorlevel 1 set _GIT_DIRTY=+CHANGES 24 | del /F "%_GIT_DIRTY_FILE%" 2>NUL 25 | del /F "%_NUL_CMP_FILE%" 2>NUL 26 | 27 | git describe --tags >%_GIT_DESCRIBE_FILE% 28 | set /p _GIT_DESCRIBE=<%_GIT_DESCRIBE_FILE% 29 | del /F "%_GIT_DESCRIBE_FILE%" 2>NUL 30 | 31 | :: Install dependencies 32 | echo --^> Installing dependencies to speed up builds... 33 | go get .\... 34 | 35 | :: Build! 36 | echo --^> Building... 37 | go build^ 38 | -ldflags "-X main.GitCommit %_GIT_COMMIT%%_GIT_DIRTY% -X main.GitDescribe %_GIT_DESCRIBE%"^ 39 | -v^ 40 | -o bin\consul.exe . 41 | if errorlevel 1 exit /B 1 42 | copy /B /Y bin\consul.exe %GOPATH%\bin\consul.exe >NUL 43 | -------------------------------------------------------------------------------- /scripts/windows/verify_no_uuid.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal 4 | 5 | if not exist %1\consul\state_store.go exit /B 1 6 | if not exist %1\consul\fsm.go exit /B 1 7 | 8 | findstr /R generateUUID %1\consul\state_store.go 1>nul 9 | if not %ERRORLEVEL% EQU 1 exit /B 1 10 | 11 | findstr generateUUID %1\consul\fsm.go 1>nul 12 | if not %ERRORLEVEL% EQU 1 exit /B 1 13 | 14 | exit /B 0 15 | -------------------------------------------------------------------------------- /utils/error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func CheckError(err error, messages ...string) { 10 | if err != nil { 11 | log.Println(err) 12 | if len(messages) > 0 { 13 | log.Println(strings.Join(messages, " ")) 14 | } 15 | 16 | os.Exit(-1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func Exists(path string) bool { 9 | _, err := os.Stat(path) 10 | if err == nil { 11 | return true 12 | } 13 | 14 | if os.IsNotExist(err) { 15 | return false 16 | } 17 | 18 | log.Printf("Path %s exists but with error info: %s", err) 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /utils/util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "path" 5 | "strings" 6 | ) 7 | 8 | func Urljoin(prefix string, paths ...string) string { 9 | if !strings.HasSuffix(prefix, "/") { 10 | prefix += "/" 11 | } 12 | return prefix + path.Join(paths...) 13 | } 14 | --------------------------------------------------------------------------------