S3 Media Server: Pt. 3

Where we left off

Last time we left off being able to serve up a basic list of shows that exist. Today, we will extend that code to allow a user to browse the shows, seasons and episodes.

Links

In the HTML template last time, we set up each show to be a link to {{ . }}. When executed, this template will then generate links to <url>/<show name>. While I’m a bit concerned about how this might work out in practice, I’m not going to worry about this just yet. I’m just going to click on it to see what happens. I expect that because there are no slashes in the show name, any spaces will just be encoded by the browser and my handler getSeasons() will get called. Let’s find out!

404

Well, I was wrong. When I clicked the link, I was served up a 404 error. The URL is http://localhost:8080/Scrubs/, which to me should cause the r.HandleFunc("/{Show}") to handle this. Digging into this a bit, I notice that the handler doesn’t have a trailing slash, whereas the URL does.

Enter Dr. Google. A quick search brought me to the StrictSlash function call for mux link. This has some promise. However, I can also change my code to be a bit more strict. I note that my Go function getShows() could strip off that trailing slash as well, which appears to be there because it is the delimiter in the query I just ran.

Since it seems to make more sense to me not to be strict about the trailing slash, I’m going to make a minor change to the initialization code, connecting the getSeasons handler to both patterns of URL:

r.HandleFunc("/", getShows)
r.HandleFunc("/{Show}", getSeasons)
r.HandleFunc("/{Show}/", getSeasons)
r.HandleFunc("/{Show}/{Season}", getEpisodes)
r.HandleFunc("/{Show}/{Season}/", getEpisodes)
r.HandleFunc("/{Show}/{Season}/{Episode}", getEpisode)

Making this change does indeed result in my getSeasons() function getting called to handle the above URL.

Listing Seasons

This function call is very similar to the listing of shows; the only difference is that the prefix is now /<Show>/, while the delimiter continues to be /.

I’ve chosen to mostly re-use the HTML template for now, since most of the functionality here is the same: display a list of items. I’ve updated it slightly to allow me to set a different Title on each page; the way I’ve done that is to introduce a struct type to pass data into the template execution, and reference it from the HTML template side (e.g. .Title and .Items). I don’t like the way I’ve done it, and should pull out the manually-defined type to a package-scoped typed struct. I will do that in the future.

One thing I noticed was that for this to work, the Prefix field of the AWS query must have a trailing / in order to work. This seems to be because otherwise, the separator field will seemingly cause the query to return an empty set.

Since the code is almost the same for listing seasons and listening episodes, I’ve gone ahead and implemented both.

Working Code

package main

import (
	"html/template"
	"log"
	"net/http"
	"os"
	"path"
	"strings"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/gorilla/mux"
)

const (
	MEDIA_BUCKET_NAME = "jfisher-media-library"
)

var (
	showsTemplate *template.Template
)

const (
	showsTemplateHTML = `
<html>
<head>
	<title>{{ .Title }}</title>
	<style>
		body {
			font-family: sans-serif;
		}
	</style>
</head>
<body>
	<table border=0>
		{{ range .List }}
			<tr>
				<td>
					<a href="{{.}}">
					{{ . }}
					</a>
				</td>
			</tr>
		{{ end }}
	</table>
</body>
`
)

func getEpisode(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	show := vars["Show"]
	season := vars["Season"]
	episode := vars["Episode"]
	log.Printf("Getting episode %v of %v, %v", episode, show, season)
}

func getEpisodes(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	show := vars["Show"]
	season := vars["Season"]
	log.Printf("Getting a list of episodes for season %v of %v", season, show)

	sess, err := session.NewSession()
	if err != nil {
		log.Fatalf("Unable to create an AWS session: %v", err)
	}

	// S3 service client
	svc := s3.New(sess)

	prefix := path.Join(show, season) + string(os.PathSeparator)

	resp, err := svc.ListObjects(&s3.ListObjectsInput{
		Bucket: aws.String(MEDIA_BUCKET_NAME),
		Delimiter: aws.String("/"),
		Prefix: aws.String(prefix),
	})
	if err != nil {
		log.Fatalf("Unable to list the objects in the bucket: %v", err)
	}

	episodes := make([]string, len(resp.Contents))
	for _, item := range resp.Contents {
		parts := strings.Split(*item.Key, string(os.PathSeparator))
		episodes = append(episodes, parts[len(parts)-1])
	}

	data := struct {
		Title string
		List []string
	}{
		Title: "List of Episodes",
		List: episodes,
	}
	showsTemplate.Execute(w, data)
}

func getSeasons(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	show := vars["Show"] + string(os.PathSeparator)
	log.Printf("Getting a list of seasons available for %v", show)

	sess, err := session.NewSession()
	if err != nil {
		log.Fatalf("Unable to create an AWS session: %v", err)
	}

	// S3 service client
	svc := s3.New(sess)

	resp, err := svc.ListObjects(&s3.ListObjectsInput{
		Bucket: aws.String(MEDIA_BUCKET_NAME),
		Delimiter: aws.String("/"),
		Prefix: aws.String(show),
	})
	if err != nil {
		log.Fatalf("Unable to list the objects in the bucket: %v", err)
	}

	seasons := make([]string, len(resp.CommonPrefixes))
	for _, prefix := range resp.CommonPrefixes {
		parts := strings.Split(*prefix.Prefix, string(os.PathSeparator))
		seasons = append(seasons, parts[1])
	}

	data := struct {
		Title string
		List []string
	}{
		Title: "List of Seasons",
		List: seasons,
	}
	showsTemplate.Execute(w, data)
}

func getShows(w http.ResponseWriter, r *http.Request) {
	log.Printf("Getting a list of shows available")

	sess, err := session.NewSession()
	if err != nil {
		log.Fatalf("Unable to create an AWS session: %v", err)
	}

	// S3 service client
	svc := s3.New(sess)

	resp, err := svc.ListObjects(&s3.ListObjectsInput{
		Bucket: aws.String(MEDIA_BUCKET_NAME),
		Delimiter: aws.String("/"),
	})
	if err != nil {
		log.Fatalf("Unable to list the objects in the bucket: %v", err)
	}

	shows := make([]string, 0, len(resp.CommonPrefixes))
	for _, prefix := range resp.CommonPrefixes {
		shows = append(shows, *prefix.Prefix)
	}

	data := struct {
		Title string
		List []string
	}{
		Title: "List of Shows",
		List: shows,
	}
	showsTemplate.Execute(w, data)
}

func main() {
	// Pre-compile the HTML templates
	showsTemplate = template.Must(template.New("showList").Parse(showsTemplateHTML))

	r := mux.NewRouter()
	r.HandleFunc("/", getShows)
	r.HandleFunc("/{Show}", getSeasons)
	r.HandleFunc("/{Show}/", getSeasons)
	r.HandleFunc("/{Show}/{Season}", getEpisodes)
	r.HandleFunc("/{Show}/{Season}/", getEpisodes)
	r.HandleFunc("/{Show}/{Season}/{Episode}", getEpisode)
	log.Fatal(http.ListenAndServe(":8080", r))
}

Results

When I run this, I am able to browse through the various shows I have, including the seasons and the episodes of each. However, I cannot watch them yet – this will require another HTML template and a way to serve up the video.

Since I am currently waiting for a wedding to start, I will leave this exercise for a later date.