Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
rosmo committed Jun 8, 2017
1 parent 3e91874 commit 93755e2
Show file tree
Hide file tree
Showing 5 changed files with 438 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
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)
61 changes: 61 additions & 0 deletions config.yml
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 src/github.com/rosmo/go-mp3-podcast/cmd/go-mp3-podcast/main.go
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)
}
60 changes: 60 additions & 0 deletions src/github.com/rosmo/go-mp3-podcast/config/config.go
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
}
Loading

0 comments on commit 93755e2

Please sign in to comment.