-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
438 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# go-mp3-podcast | ||
|
||
This is an utility to take a directory containing MP3s and turn them into an | ||
RSS feed. The utility is configured using a simple YAML file (see config.yml | ||
for example). | ||
|
||
## How to run | ||
|
||
### RSS mode | ||
|
||
``` | ||
go-mp3-podcast -config config.yml -dir /path/to/mp3s -mode rss | ||
``` | ||
|
||
### Directory index mode | ||
|
||
``` | ||
go-mp3-podcast -config config.yml -dir /path/to/mp3s -mode index | ||
``` | ||
|
||
## License | ||
|
||
Licensed under MIT. Copyright 2017 Taneli Leppä <[email protected]> | ||
|
||
## Acknowledgements | ||
|
||
Uses great libraries from: | ||
- [eduncan911/podcast](https://github.com/eduncan911/podcast) | ||
- [mikkyang/id3-go](https://github.com/mikkyang/id3-go) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
--- | ||
channel: | ||
title: Your Channel Name | ||
link: http://link/to/your/channel | ||
description: Description of your channel | ||
copyright: Your copyright | ||
url: http://link/to/your/channel | ||
language: en-us | ||
|
||
image: | ||
url: http://address/to/an/image.jpg | ||
|
||
items: | ||
guid: | ||
baseUrl: http://base/url/ | ||
link: | ||
baseUrl: http://base/url/ | ||
enclosure: | ||
baseUrl: http://base/url/ | ||
date: # optional, allows extracting date from title | ||
from: title | ||
format: dd.mm.yyyy # for supported formats, see process.go | ||
filter: # optional, allows filtering files based on criteria | ||
minimumSize: 102400 # 100 KB | ||
|
||
# template when running in index mode | ||
index: | ||
template: | | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>{{ .Config.Channel.Title }}</title> | ||
<link rel="stylesheet" href="podcast.css"> | ||
</head> | ||
<body> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th>Title</th> | ||
<th>File</th> | ||
<th>Date</th> | ||
<th>Size</th> | ||
<th>Last modified</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{{ range .Podcasts }} | ||
<tr> | ||
<td><a href="{{ $.Config.Items.Enclosure.BaseUrl }}{{ .Filename }}">{{ .Title }}</a></td> | ||
<td><a href="{{ $.Config.Items.Enclosure.BaseUrl }}{{ .Filename }}">{{ .Filename }}</a></td> | ||
<td>{{ .PublishDate }}</td> | ||
<td>{{ .Length }}</td> | ||
<td>{{ .Timestamp }}</td> | ||
</tr> | ||
{{ end }} | ||
</tbody> | ||
</table> | ||
<!-- page content --> | ||
</body> | ||
</html> |
189 changes: 189 additions & 0 deletions
189
src/github.com/rosmo/go-mp3-podcast/cmd/go-mp3-podcast/main.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"github.com/eduncan911/podcast" | ||
"github.com/rosmo/go-mp3-podcast/config" | ||
"github.com/rosmo/go-mp3-podcast/process" | ||
"html/template" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"strings" | ||
"sync" | ||
"time" | ||
) | ||
|
||
type Index struct { | ||
Config config.Configuration | ||
Podcasts []*process.AudioFile | ||
} | ||
|
||
type ByPublishDate []*process.AudioFile | ||
|
||
func (s ByPublishDate) Len() int { | ||
return len(s) | ||
} | ||
func (s ByPublishDate) Swap(i, j int) { | ||
s[i], s[j] = s[j], s[i] | ||
} | ||
func (s ByPublishDate) Less(i, j int) bool { | ||
return s[i].PublishDate.Unix() > s[j].PublishDate.Unix() | ||
} | ||
|
||
func generateIndex(cfg *config.Configuration, files []*process.AudioFile) { | ||
t := template.New("index") | ||
t.Parse(cfg.Index.Template) | ||
index := Index{ | ||
Config: *cfg, | ||
Podcasts: files, | ||
} | ||
err := t.Execute(os.Stdout, index) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "Failed to render template: %v\n", err.Error()) | ||
} | ||
} | ||
|
||
func generateFeed(cfg *config.Configuration, files []*process.AudioFile) { | ||
var extensionToEnclosureType = map[string]podcast.EnclosureType{ | ||
".mp3": podcast.MP3, | ||
".m4a": podcast.M4A, | ||
".m4v": podcast.M4V, | ||
".mp4": podcast.MP4, | ||
".mov": podcast.MOV, | ||
".pdf": podcast.PDF, | ||
".epub": podcast.EPUB} | ||
|
||
now := time.Now() | ||
feed := podcast.New( | ||
cfg.Channel.Title, | ||
cfg.Channel.Link, | ||
cfg.Channel.Description, | ||
nil, | ||
&now) | ||
if cfg.Image.Url != "" { | ||
feed.AddImage(cfg.Image.Url) | ||
} | ||
if cfg.Channel.Language != "" { | ||
feed.Language = cfg.Channel.Language | ||
} | ||
feed.Generator = "" | ||
|
||
for _, file := range files { | ||
link := cfg.Items.Link.BaseUrl + file.Filename | ||
guid := cfg.Items.Guid.BaseUrl + file.Filename | ||
enclosure := cfg.Items.Enclosure.BaseUrl + file.Filename | ||
|
||
item := podcast.Item{ | ||
Title: file.Title, | ||
Description: file.Title, | ||
Link: link, | ||
GUID: guid, | ||
} | ||
|
||
enclosureType := extensionToEnclosureType[strings.ToLower(filepath.Ext(file.Filename))] | ||
|
||
item.AddEnclosure(enclosure, enclosureType, file.Length) | ||
item.AddPubDate(&file.PublishDate) | ||
|
||
_, err := feed.AddItem(item) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "Failed to add file %v: %v\n", file, err) | ||
} | ||
} | ||
fmt.Print(feed.String()) | ||
} | ||
|
||
func processFiles(cfg *config.Configuration, directory string) ([]*process.AudioFile, error) { | ||
var supportedExtensions = [...]string{".aac", ".flac", ".m4a", ".mp3", ".ogg", ".ogm"} | ||
|
||
if directory[len(directory)-1] != '/' { | ||
directory = directory + "/" | ||
} | ||
fmt.Fprintf(os.Stderr, "Scanning directory: %v\n", directory) | ||
|
||
foundFiles, err := filepath.Glob(directory + "*") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
done := make(chan bool, 1) | ||
var wg sync.WaitGroup | ||
|
||
var podcasts []*process.AudioFile | ||
|
||
numberOfExtensions := len(supportedExtensions) | ||
go func() { | ||
results := make(chan *process.AudioFile) | ||
for _, file := range foundFiles { | ||
finfo, err := os.Stat(file) | ||
if err != nil { | ||
continue | ||
} | ||
fileSize := finfo.Size() | ||
ext := strings.ToLower(filepath.Ext(file)) | ||
i := sort.SearchStrings(supportedExtensions[:], ext) | ||
|
||
if fileSize >= cfg.Items.Filter.MinimumSize && | ||
i < numberOfExtensions && supportedExtensions[i] == ext { | ||
wg.Add(1) | ||
go func(file string) { | ||
defer wg.Done() | ||
|
||
audioFile, err := process.ProcessAudioFile(cfg, file) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "Error processing: %v: %v\n", file, err) | ||
} else { | ||
results <- audioFile | ||
} | ||
}(file) | ||
} | ||
} | ||
go func() { | ||
for res := range results { | ||
podcasts = append(podcasts, res) | ||
} | ||
}() | ||
wg.Wait() | ||
|
||
done <- true | ||
}() | ||
|
||
<-done | ||
sort.Sort(ByPublishDate(podcasts)) | ||
fmt.Fprintf(os.Stderr, "Scan complete.\n") | ||
return podcasts, nil | ||
} | ||
|
||
func main() { | ||
configFile := flag.String("config", "", "configuration file (yaml)") | ||
directory := flag.String("dir", ".", "directory containing mp3 files") | ||
mode := flag.String("mode", "rss", "select mode (rss or index)") | ||
|
||
flag.Parse() | ||
if *configFile == "" { | ||
fmt.Println("Usage: go-mp3-podcast -config file.yml") | ||
os.Exit(1) | ||
} | ||
|
||
cfg, err := config.Parse(*configFile) | ||
if err != nil { | ||
fmt.Printf("Error reading %s: %v\n", *configFile, err) | ||
os.Exit(2) | ||
} | ||
|
||
files, err := processFiles(cfg, *directory) | ||
if err != nil { | ||
fmt.Printf("Error processing files: %v\n", err) | ||
os.Exit(2) | ||
} | ||
|
||
if *mode == "index" { | ||
generateIndex(cfg, files) | ||
} else { | ||
generateFeed(cfg, files) | ||
} | ||
|
||
os.Exit(0) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package config | ||
|
||
import ( | ||
"errors" | ||
"gopkg.in/yaml.v2" | ||
"io/ioutil" | ||
) | ||
|
||
type Configuration struct { | ||
Channel struct { | ||
Title string `yaml:"title"` | ||
Link string `yaml:"link"` | ||
Description string `yaml:"description"` | ||
Copyright string `yaml:"copyright"` | ||
Url string `yaml:"url"` | ||
Language string `yaml:"language"` | ||
} | ||
Image struct { | ||
Title string `yaml:"title"` | ||
Url string `yaml:"url"` | ||
Itunes string `yaml:"itunes"` | ||
} | ||
Items struct { | ||
Guid struct { | ||
BaseUrl string `yaml:"baseUrl"` | ||
IsPermalink bool `yaml:"isPermaLink"` | ||
} | ||
Link struct { | ||
BaseUrl string `yaml:"baseUrl"` | ||
} | ||
Enclosure struct { | ||
BaseUrl string `yaml:"baseUrl"` | ||
} | ||
Date struct { | ||
From string `yaml:"from"` | ||
Format string `yaml:"format"` | ||
} | ||
Filter struct { | ||
MinimumSize int64 `yaml:"minimumSize"` | ||
} | ||
} | ||
Index struct { | ||
Template string `yaml:"template"` | ||
} | ||
} | ||
|
||
func Parse(file string) (*Configuration, error) { | ||
data, err := ioutil.ReadFile(file) | ||
if err != nil { | ||
return nil, errors.New("could not read configuration file") | ||
} | ||
|
||
cfg := Configuration{} | ||
err = yaml.Unmarshal(data, &cfg) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &cfg, nil | ||
} |
Oops, something went wrong.