tweaks
This commit is contained in:
parent
7e35d6a68e
commit
13cce6cd87
7 changed files with 183 additions and 59 deletions
1
go.mod
1
go.mod
|
|
@ -3,6 +3,7 @@ module media-roller
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/go-chi/chi v4.0.3+incompatible
|
github.com/go-chi/chi v4.0.3+incompatible
|
||||||
github.com/go-chi/valve v0.0.0-20170920024740-9e45288364f4
|
github.com/go-chi/valve v0.0.0-20170920024740-9e45288364f4
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -1,4 +1,6 @@
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
|
github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
|
||||||
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
github.com/go-chi/valve v0.0.0-20170920024740-9e45288364f4 h1:JYZmrkBDj6LwUbsRysF9tpLnz59npoZSI3KG2XHqvHw=
|
github.com/go-chi/valve v0.0.0-20170920024740-9e45288364f4 h1:JYZmrkBDj6LwUbsRysF9tpLnz59npoZSI3KG2XHqvHw=
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ func main() {
|
||||||
// Setup routes
|
// Setup routes
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("Welcome!"))
|
_, _ = w.Write([]byte("<a href='/media'>media roller</a>"))
|
||||||
})
|
})
|
||||||
r.Route("/media", func(r chi.Router) {
|
r.Route("/media", func(r chi.Router) {
|
||||||
r.Get("/", media.Index)
|
r.Get("/", media.Index)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
package media
|
package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,7 +17,6 @@ This file will download the media from a URL and save it to disk.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -21,8 +26,15 @@ import (
|
||||||
|
|
||||||
const downloadDir = "downloads/"
|
const downloadDir = "downloads/"
|
||||||
|
|
||||||
type ResponseData struct {
|
type Media struct {
|
||||||
Id string
|
Id string
|
||||||
|
Name string
|
||||||
|
SizeInBytes int64
|
||||||
|
HumanSize string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaResults struct {
|
||||||
|
Medias []Media
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use something better than this. It's too tedious to map
|
// TODO: Use something better than this. It's too tedious to map
|
||||||
|
|
@ -43,16 +55,32 @@ func FetchMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := fetch(url)
|
// NOTE: This system is for a simple use case, meant to run at home. This is not a great design for a robust system.
|
||||||
|
// We are hashing the URL here and writing files to disk to a consistent directory based on the ID. You can imagine
|
||||||
|
// concurrent users would break this for the same URL. That's fine given this is for a simple home system.
|
||||||
|
// Future work can make this more sophisticated.
|
||||||
|
id := GetMD5Hash(url)
|
||||||
|
// Look to see if we already have the media on disk
|
||||||
|
medias, err := getAllFilesForId(id)
|
||||||
|
if len(medias) == 0 {
|
||||||
|
// We don't, so go fetch it
|
||||||
|
id, err = fetch(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
medias, err = getAllFilesForId(id)
|
||||||
data := ResponseData{
|
if err != nil {
|
||||||
Id: id,
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := fetchResponseTmpl.Execute(w, data); err != nil {
|
}
|
||||||
|
|
||||||
|
response := MediaResults{
|
||||||
|
Medias: medias,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fetchResponseTmpl.Execute(w, response); err != nil {
|
||||||
log.Error().Msgf("Error rendering template: %v", err)
|
log.Error().Msgf("Error rendering template: %v", err)
|
||||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +89,7 @@ func FetchMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
// returns the ID of the file
|
// returns the ID of the file
|
||||||
func fetch(url string) (string, error) {
|
func fetch(url string) (string, error) {
|
||||||
// The id will be used as the name of the parent directory of the output files
|
// The id will be used as the name of the parent directory of the output files
|
||||||
id := uuid.New().String()
|
id := GetMD5Hash(url)
|
||||||
name := getMediaDirectory(id) + "%(title)s.%(ext)s"
|
name := getMediaDirectory(id) + "%(title)s.%(ext)s"
|
||||||
|
|
||||||
log.Info().Msgf("Downloading %s to %s", url, id)
|
log.Info().Msgf("Downloading %s to %s", url, id)
|
||||||
|
|
@ -118,3 +146,73 @@ func fetch(url string) (string, error) {
|
||||||
func getMediaDirectory(id string) string {
|
func getMediaDirectory(id string) string {
|
||||||
return downloadDir + id + "/"
|
return downloadDir + id + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// id is expected to be validated prior to calling this func
|
||||||
|
func getAllFilesForId(id string) ([]Media, error) {
|
||||||
|
root := getMediaDirectory(id)
|
||||||
|
file, err := os.Open(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files, _ := file.Readdirnames(0) // 0 to read all files and folders
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil, errors.New("ID not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var medias []Media
|
||||||
|
|
||||||
|
// We expect two files to be produced for each video, a json manifest and an mp4.
|
||||||
|
for _, f := range files {
|
||||||
|
if !strings.HasSuffix(f, ".json") {
|
||||||
|
fi, err := os.Stat(root + f)
|
||||||
|
var size int64 = 0
|
||||||
|
if err == nil {
|
||||||
|
size = fi.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
media := Media{
|
||||||
|
Id: id,
|
||||||
|
Name: filepath.Base(f),
|
||||||
|
SizeInBytes: size,
|
||||||
|
HumanSize: humanize.Bytes(uint64(size)),
|
||||||
|
}
|
||||||
|
medias = append(medias, media)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return medias, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// id is expected to be validated prior to calling this func
|
||||||
|
// TODO: This needs to handle multiple files in the directory
|
||||||
|
func getFileFromId(id string) (string, error) {
|
||||||
|
root := getMediaDirectory(id)
|
||||||
|
file, err := os.Open(root)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
files, _ := file.Readdirnames(0) // 0 to read all files and folders
|
||||||
|
if len(files) == 0 {
|
||||||
|
return "", errors.New("ID not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect two files to be produced, a json manifest and an mp4. We want to return the mp4
|
||||||
|
// Sometimes the video file might not have an mp4 extension, so filter out the json file
|
||||||
|
for _, f := range files {
|
||||||
|
if !strings.HasSuffix(f, ".json") {
|
||||||
|
// TODO: This is just returning the first file found. We need to handle multiple
|
||||||
|
return root + f, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("unable to find file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMD5Hash(url string) string {
|
||||||
|
return fmt.Sprintf("%x", md5.Sum([]byte(url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidId(id string) bool {
|
||||||
|
// TODO: Finish this
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,24 @@ package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This will serve the fetched files to the client
|
This will serve the fetched files to the client
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ServeMedia(w http.ResponseWriter, r *http.Request) {
|
func ServeMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get("id")
|
id := r.URL.Query().Get("id")
|
||||||
log.Info().Msgf("Serving file %s", id)
|
log.Info().Msgf("Serving file %s", id)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
http.Error(w, "Missing file ID", http.StatusBadRequest)
|
http.Error(w, "Missing file ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
} else if _, err := uuid.Parse(id); err != nil {
|
} else if !isValidId(id) {
|
||||||
// Try to parse it just to avoid any type of directory traversal attacks
|
// Try to parse it just to avoid any type of directory traversal attacks
|
||||||
http.Error(w, "Invalid file ID", http.StatusBadRequest)
|
http.Error(w, "Invalid file ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -39,32 +33,6 @@ func ServeMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
streamFileToClient(w, filename)
|
streamFileToClient(w, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// id is expected to be validated prior to calling this func
|
|
||||||
func getFileFromId(id string) (string, error) {
|
|
||||||
root := getMediaDirectory(id)
|
|
||||||
file, err := os.Open(root)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
files, _ := file.Readdirnames(0) // 0 to read all files and folders
|
|
||||||
if len(files) == 0 {
|
|
||||||
return "", errors.New("ID not found")
|
|
||||||
} else if len(files) > 2 {
|
|
||||||
// We should only have 2 media file produced, the mp4 and the json file
|
|
||||||
return "", errors.New("internal error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect two files to be produced, a json manifest and an mp4. We want to return the mp4
|
|
||||||
// Sometimes the video file might not have an mp4 extension, so filter out the json file
|
|
||||||
for _, f := range files {
|
|
||||||
if !strings.HasSuffix(f, ".json") {
|
|
||||||
return root + f, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("unable to find file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamFileToClient(writer http.ResponseWriter, filename string) {
|
func streamFileToClient(writer http.ResponseWriter, filename string) {
|
||||||
// Check if file exists and open
|
// Check if file exists and open
|
||||||
Openfile, err := os.Open(filename)
|
Openfile, err := os.Open(filename)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,36 @@
|
||||||
<html lang="">
|
<!doctype html>
|
||||||
<head><title>media-roller</title></head>
|
<html lang="en">
|
||||||
<h1>Download media file</h1>
|
<head>
|
||||||
<form action="media/fetch">
|
<title>media-roller</title>
|
||||||
<input type="text" name="url"><input type="submit" value="Fetch"/>
|
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"
|
||||||
</form>
|
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body style="background-color: #43464a">
|
||||||
|
<div class="container d-flex flex-column text-light text-center">
|
||||||
|
<div class="flex-grow-1"></div>
|
||||||
|
<div class="jumbotron bg-transparent flex-grow-1">
|
||||||
|
<h1 class="display-4"><a class="text-light" href="/media">media roller</a></h1>
|
||||||
|
<p>
|
||||||
|
Mobile friendly tool for downloading videos from social media
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<form action="media/fetch" method="GET">
|
||||||
|
<div class="input-group">
|
||||||
|
<input name="url" type="url" class="form-control" placeholder="URL" aria-label="URL"
|
||||||
|
aria-describedby="button-submit" autofocus>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-primary" type="submit" id="button-submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<p class="text-muted">Source at <a class="text-light" href="https://github.com/rroller/media-roller">https://github.com/rroller/media-roller</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,31 @@
|
||||||
<html lang="">
|
<!doctype html>
|
||||||
<head></head>
|
<html lang="en">
|
||||||
<h1>Download media file</h1>
|
<head>
|
||||||
<a href="download?id={{.Id}}">download</a>
|
<title>media-roller</title>
|
||||||
|
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"
|
||||||
|
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body style="background-color: #43464a">
|
||||||
|
<div class="container d-flex flex-column text-light text-center">
|
||||||
|
<div class="flex-grow-1"></div>
|
||||||
|
<div class="jumbotron bg-transparent flex-grow-1">
|
||||||
|
<h1 class="display-4"><a class="text-light" href="/media">media roller</a></h1>
|
||||||
|
<p>
|
||||||
|
Mobile friendly tool for downloading videos from social media
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<h2>Done!</h2>
|
||||||
|
{{range .Medias}}
|
||||||
|
<a style="color: dodgerblue" href="download?id={{.Id}}">{{.Name}}</a> <small>{{.HumanSize}}</small>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<p class="text-muted">Source at <a class="text-light" href="https://github.com/rroller/media-roller">https://github.com/rroller/media-roller</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in a new issue